🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

goatchain

Package Overview
Dependencies
Maintainers
2
Versions
38
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

goatchain

npmnpm
Version
0.0.29
Version published
Weekly downloads
51
-45.74%
Maintainers
2
Weekly downloads
 
Created
Source

GoatChain SDK Developer Guide 🐐

A TypeScript SDK for building AI agents with streaming support, tool calling, and middleware pattern.

npm version

⚠️ Breaking Change: preToolUse vs permissionRequest

preToolUse is now only for mutating tool calls (modifiedToolCall). Permission decisions (allow / deny) must be handled in permissionRequest.

Migration

Before (old pattern):

hooks: {
  preToolUse: async () => ({ allow: true }),
}

After (current pattern):

hooks: {
  permissionRequest: async () => ({ allow: true }),
}

If you need both, use:

  • preToolUse to rewrite tool arguments/tool call
  • permissionRequest to allow or block execution

📦 Installation

pnpm add goatchain
# or
npm install goatchain
# or
bun add goatchain

🎯 Core Concepts

GoatChain SDK is built around three core components:

  • Agent - The main orchestrator that manages the agent loop, middleware, and tools
  • Session - A conversation context that handles message history and streaming
  • ModelClient - Abstraction layer for LLM providers (OpenAI, Anthropic, etc.)

🚀 Quick Start

Basic Usage

import process from 'node:process'
import { Agent, createModel, createOpenAIAdapter } from 'goatchain'

// 1. Create model client
const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

// 2. Create agent
const agent = new Agent({
  name: 'Simple Assistant',
  systemPrompt: 'You are a helpful assistant.',
  model,
})

// 3. Create session and interact
const session = await agent.createSession()
session.send('Hello!')

// 4. Stream responses
for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  } else if (event.type === 'done') {
    console.log('\nDone:', event.stopReason)
  }
}

📡 Session Management

Session is the core component for managing conversations. It handles message history, state persistence, and event streaming.

Creating Sessions

// Create a new session
const session = await agent.createSession()

// Create session with custom ID
const session = await agent.createSession({ sessionId: 'my-session-id' })

// Create session with configuration overrides
const session = await agent.createSession({
  maxIterations: 10,
  requestParams: {
    temperature: 0.7,
    maxTokens: 2000,
  },
})

Session Configuration Options

interface CreateSessionOptions {
  sessionId?: string // Custom session ID
  model?: ModelRef // Optional model override
  maxIterations?: number // Max agent loop iterations (default: 1000)
  cwd?: string // Working directory for file operations
  requestParams?: {
    temperature?: number // Model temperature
    maxTokens?: number // Max output tokens
    topP?: number // Nucleus sampling parameter
  }
}

Working Directory (CWD) Configuration

You can set a working directory for the session, which will be automatically applied to all file operation tools (Read, Write, Edit, Glob, Grep, Bash, AstGrepSearch, AstGrepReplace):

// Set CWD when creating a session
const session = await agent.createSession({
  cwd: '/path/to/project',
})

// Get the current working directory
const cwd = session.getCwd()
console.log('Current directory:', cwd)

// Change the working directory at runtime
session.setCwd('/path/to/another/project')

// All file operations now use the new directory
session.send('Read the README.md file')

Benefits:

  • Automatically sets the working directory for all tools that support it
  • Persisted across session saves and restores
  • Can be changed at runtime with session.setCwd()
  • Simplifies file path management in multi-project environments

Sending Messages

// Simple text message
session.send('What is the weather today?')

// Multiple messages in sequence
session.send('First question')
// Wait for response...
session.send('Follow-up question')

Send Options

You can pass options to send() to control tool execution, approval, and more:

// Auto-approve all tools for this request
session.send('Create files and search the web', {
  toolContext: {
    approval: {
      autoApprove: true, // Skip approval for all tools
    },
  },
})

// Require approval only for high-risk tools
session.send('Analyze the codebase', {
  toolContext: {
    approval: {
      strategy: 'high_risk', // Only prompt for high/critical risk tools
    },
  },
})

// Combine with session-level CWD
session.setCwd('/path/to/project')
session.send('Read and analyze all TypeScript files', {
  toolContext: {
    approval: { autoApprove: true },
  },
})
// Tools will use /path/to/project as working directory
// and execute without requiring approval

Receiving Events

The receive() method returns an async generator that streams events:

for await (const event of session.receive()) {
  switch (event.type) {
    case 'text_delta':
      // Partial text from LLM
      process.stdout.write(event.delta)
      break

    case 'tool_call_start':
      console.log(`\nCalling tool: ${event.name}`)
      break

    case 'tool_result':
      console.log(`Tool result: ${event.result}`)
      break

    case 'iteration_end':
      console.log(`\nIteration ${event.iteration} complete`)
      console.log(`Tokens used: ${event.usage?.totalTokens}`)
      break

    case 'done':
      console.log(`\nConversation done: ${event.stopReason}`)
      console.log(`Total tokens: ${event.usage?.totalTokens}`)
      break

    case 'error':
      console.error(`Error: ${event.error}`)
      break
  }
}

Session Event Types

Event TypeDescriptionKey Fields
iteration_startAgent loop iteration beginsiteration
text_deltaPartial text responsedelta
thinking_startReasoning phase begins-
thinking_deltaReasoning contentdelta
thinking_endReasoning phase ends-
tool_call_startTool invocation beginsname, id
tool_call_deltaTool arguments streamdelta
tool_call_endTool call completename, args
tool_resultTool execution resultresult, error
iteration_endIteration completeusage, iteration
doneStream finishedstopReason, usage
errorError occurrederror

Session State Management

// Access message history
console.log(session.messages) // Message[]

// Check session status
console.log(session.status) // 'idle' | 'running' | 'completed' | 'error'

// Get token usage
console.log(session.usage)
// {
//   promptTokens: 150,
//   completionTokens: 80,
//   totalTokens: 230
// }

// Session metadata
console.log(session.id) // Session ID
console.log(session.createdAt) // Creation timestamp
console.log(session.updatedAt) // Last update timestamp

Session Persistence

Sessions can be saved and restored. All session state including message history, configuration, and working directory (cwd) are preserved:

// Save session to snapshot
const snapshot = session.toSnapshot()
// Store snapshot to database, file, etc.

// Restore session from snapshot
session.restoreFromSnapshot(snapshot)

// Or create a new session from snapshot
const restored = await agent.createSession()
restored.restoreFromSnapshot(snapshot)

// The restored session maintains all state including:
// - Message history
// - Configuration overrides
// - Working directory (cwd)
// - Usage statistics

Multi-turn Conversations

const session = await agent.createSession()

// Turn 1
session.send('What is 2 + 2?')
for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  }
}

// Turn 2 (maintains context)
session.send('What about multiplying that by 3?')
for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  }
}

// Session now contains full conversation history
console.log(session.messages.length) // 4 (2 user, 2 assistant)

Resuming Sessions

Resume interrupted sessions using checkpoints:

import { FileStateStore } from 'goatchain'

// Create agent with state store
const stateStore = new FileStateStore({
  dir: './checkpoints',
  deleteOnComplete: true,
})

const agent = new Agent({
  name: 'MyAgent',
  systemPrompt: 'You are helpful.',
  model,
  stateStore,
})

// Start session (automatically checkpointed)
const session = await agent.createSession({ sessionId: 'session-123' })
session.send('Start a long task')
for await (const event of session.receive()) {
  // Process events...
}

// Later, resume from checkpoint
const resumed = await agent.resumeSession('session-123')
for await (const event of resumed.receive()) {
  // Continue from where it left off
}

Session Lifecycle Hooks

// Add message manually
session.addMessage({
  role: 'user',
  content: 'Custom message',
})

// Save session state manually
await session.save()

// Clear session history
session.messages = []

🤖 Agent Configuration

Creating an Agent

import { Agent, createModel, createOpenAIAdapter } from 'goatchain'

const agent = new Agent({
  name: 'MyAgent',
  systemPrompt: 'You are a helpful assistant.',
  model,
  tools, // Optional: ToolRegistry instance
  stateStore, // Optional: for persistence
  middleware: [], // Optional: middlewares to register on startup
  mcpServers: [], // Optional: MCP server configs
  enableLogging: false, // Optional: internal logs
})

Agent Options

interface AgentOptions {
  id?: string // Optional custom agent ID
  name: string // Agent name
  systemPrompt: string // System instructions
  model: ModelClient // LLM client
  tools?: ToolRegistry // Tool registry (not an array)
  stateStore?: StateStore // Persistence layer
  middleware?: Middleware[] // Optional middleware list
  mcpServers?: MCPServerConfig[] // Optional MCP server configs
  enableLogging?: boolean // Internal debug logs
}

Runtime Model Switching

// Pin to specific model
agent.setModel({ provider: 'openai', modelId: 'gpt-4o-mini' })

// Switch to another concrete model client instance
const otherModelClient = createModel({ adapter: createOpenAIAdapter({ defaultModelId: 'gpt-4o' }) })
agent.setModel(otherModelClient)

Session Manager

Manage multiple sessions:

const sessionManager = agent.sessionManager

// List all sessions
const sessions = await sessionManager.list()

// Get specific session
const session = await sessionManager.get('session-id')

// Destroy session
await sessionManager.destroy('session-id')

🔧 Tool System

Using Built-in Tools

GoatChain SDK exports the following built-in tools:

Tool ClassRuntime NameCategoryPurpose
ReadToolReadFileRead file content (text, binary metadata, and selected converted formats)
WriteToolWriteFileCreate or overwrite files
EditToolEditFileIn-place text replacement edits
GlobToolGlobFile/SearchFind files by glob pattern
GrepToolGrepFile/SearchSearch file contents by pattern
BashToolBashCommandExecute shell commands
WebSearchToolWebSearchWebSearch the web (e.g. via Serper API)
WebFetchToolWebFetchWebFetch and extract content from a specific URL
TodoWriteToolTodoWritePlanningManage structured todo lists
TodoPlanToolTodoPlanPlanningCreate/update planning todos for plan flows
AskUserToolAskUserQuestionInteractionAsk the user structured follow-up questions
EnterPlanModeToolEnterPlanModeModeEnter plan mode
ExitPlanModeToolExitPlanModeModeExit plan mode
import {
  Agent,
  ToolRegistry,
  ReadTool,
  WriteTool,
  EditTool,
  BashTool,
  GrepTool,
  GlobTool,
  WebSearchTool,
  WebFetchTool,
} from 'goatchain'

// Create a tool registry and register tools
const tools = new ToolRegistry()
tools.register(new ReadTool())
tools.register(new WriteTool())
tools.register(new EditTool())
tools.register(new BashTool())
tools.register(new GrepTool())
tools.register(new GlobTool())
tools.register(new WebSearchTool({ apiKey: process.env.SERPER_API_KEY }))
tools.register(new WebFetchTool())

const agent = new Agent({
  name: 'MyAgent',
  systemPrompt: 'You are helpful.',
  model,
  tools,
})

MCP Servers (HTTP + stdio)

GoatChain can connect MCP servers and register their remote tools automatically:

const agent = new Agent({
  name: 'MCP Assistant',
  systemPrompt: 'You are helpful.',
  model,
  mcpServers: [
    {
      id: 'weather',
      name: 'Weather API',
      transport: 'http',
      url: 'https://example.com/mcp',
      auth: { type: 'bearer', token: process.env.WEATHER_API_KEY! },
    },
    {
      id: 'local-tools',
      name: 'Local Tools',
      transport: 'stdio',
      command: 'node',
      args: ['./mcp-servers/tools.js'],
    },
  ],
})

See docs/mcp.md for details.

Configuring File Tool Working Directory

File-related tools (ReadTool, WriteTool, EditTool, GlobTool, GrepTool, BashTool) support configuring the working directory:

import path from 'node:path'

// Define output directory
const OUTPUT_DIR = path.resolve(import.meta.dirname, 'output')

// Option 1: Set working directory only
const tools = new ToolRegistry()
tools.register(new ReadTool({ cwd: OUTPUT_DIR }))
tools.register(new WriteTool({ cwd: OUTPUT_DIR }))
tools.register(new EditTool({ cwd: OUTPUT_DIR }))
tools.register(new GlobTool({ cwd: OUTPUT_DIR }))
tools.register(new GrepTool({ cwd: OUTPUT_DIR }))
tools.register(new BashTool({ cwd: OUTPUT_DIR }))

// Option 2: Restrict to specific directory (security sandbox)
// This prevents access to files outside the allowed directory
const tools = new ToolRegistry()
tools.register(
  new ReadTool({
    cwd: OUTPUT_DIR,
    allowedDirectory: OUTPUT_DIR, // Only allow reads within OUTPUT_DIR
  }),
)
tools.register(
  new WriteTool({
    cwd: OUTPUT_DIR,
    allowedDirectory: OUTPUT_DIR, // Only allow writes within OUTPUT_DIR
  }),
)

How each file tool uses cwd:

ToolWhat it doesHow cwd is appliedPer-call overrideExtra sandbox options
ReadToolReads files (and some converted formats)Relative file_path resolves from cwdfile_path can be absoluteallowedDirectory, fileBlacklist, disableBlacklist
WriteToolWrites/overwrites filesRelative file_path resolves from cwdfile_path can be absoluteallowedDirectory, fileBlacklist, disableBlacklist
EditToolReplaces old_string with new_string in a fileRelative file_path resolves from cwdfile_path can be absolutefileBlacklist, disableBlacklist
GlobToolFinds files by patternSearch root defaults to cwdpath argument can change search rootfileBlacklist, disableBlacklist
GrepToolSearches text content in filesSearch runs under cwdpath argument narrows search scopefileBlacklist, disableBlacklist
BashToolRuns shell commandsCommands execute in cwdworkdir argument overrides per callNone

Directory & Protection Options:

OptionDescriptionExample
cwdWorking directory for resolving relative paths{ cwd: '/app/output' }
allowedDirectoryRestrict file access to this directory only (blocks path traversal){ allowedDirectory: '/app/output' }

When to use allowedDirectory:

  • When you want to sandbox the agent to a specific directory
  • To prevent accidental access to sensitive files
  • For production environments with security requirements

Creating Custom Tools

import { BaseTool, ToolRegistry } from 'goatchain'

class MyCustomTool extends BaseTool {
  name = 'my_tool'
  description = 'Does something useful'

  parameters = {
    type: 'object',
    properties: {
      input: {
        type: 'string',
        description: 'Input parameter',
      },
    },
    required: ['input'],
  }

  async execute(args: { input: string }) {
    // Your tool logic here
    return `Processed: ${args.input}`
  }
}

// Use it
const tools = new ToolRegistry()
tools.register(new MyCustomTool())

const agent = new Agent({
  name: 'MyAgent',
  systemPrompt: 'You are helpful.',
  model,
  tools,
})

Tool Registry

Dynamically manage tools:

const registry = agent.tools

// Register new tool
registry.register(new MyCustomTool())

// Unregister tool
registry.unregister('my_tool')

// Get tool
const tool = registry.get('my_tool')

// List all tools
const allTools = registry.list()

// Convert to OpenAI format
const openaiTools = registry.toOpenAIFormat()

🎣 Tool Approval & Hooks

Sessions support lifecycle hooks that let you intercept user input, tool calls, and session/subagent lifecycle events.

Key Concepts

GoatChain has three relevant mechanisms for tool execution control:

  • preToolUse Hook - For tool-call mutation before permission/approval

    • Can modify tool name/arguments with modifiedToolCall
    • Runs before permissionRequest
  • permissionRequest Hook - For programmatic auto-approval/blocking

    • Runs after preToolUse, so it sees the modified tool call
    • allow: true → skips approval flow (execution still goes through normal middleware/disabled checks)
    • allow: false → tool is blocked
    • Use this for automated scenarios where you want to programmatically approve/deny tools
  • Approval System (via toolContext.approval) - For interactive user approval

    • Pauses execution on high-risk tools
    • Shows requires_action event
    • Resumes with user's approval decisions
    • Use this for interactive UIs where users manually approve tools

These are independent: If permissionRequest returns allow: true, the approval system is bypassed.

Hook Types

interface AgentHooks {
  // Session lifecycle
  sessionStart?: (ctx: SessionStartContext) => Promise<void>
  sessionEnd?: (ctx: SessionEndContext) => Promise<void>
  stop?: (ctx: StopContext) => Promise<void>
  userPromptSubmit?: (ctx: UserPromptSubmitContext) => Promise<UserPromptSubmitResult>

  // Tool lifecycle
  // - Can modify tool call with modifiedToolCall
  preToolUse?: (ctx: ToolHookContext) => Promise<PreToolUseResult | void>
  permissionRequest?: (ctx: ToolHookContext) => Promise<PermissionRequestResult>
  postToolUse?: (ctx: ToolHookContext, result: unknown) => Promise<void>
  postToolUseFailure?: (ctx: ToolHookContext, error: Error) => Promise<void>

  // Subagent lifecycle (used by parallel task/subagent middleware)
  subagentStart?: (ctx: SubagentStartContext) => Promise<void>
  subagentStop?: (ctx: SubagentStopContext) => Promise<void>
}

// Backward-compatible alias
type ToolHooks = AgentHooks

interface BaseHookContext {
  sessionId: string
}

interface ToolHookContext extends BaseHookContext {
  toolCall: {
    id: string
    type: 'function'
    function: {
      name: string
      arguments: string
    }
  }
  toolContext: ToolExecutionContext
}

interface PermissionRequestResult {
  // If true, passes permission checks and skips approval flow
  // (tool still goes through normal middleware/disabled checks)
  // If false, blocks tool execution immediately
  allow: boolean
  // Optional: Modify the tool call before execution
  modifiedToolCall?: ToolCall
}

interface PreToolUseResult {
  // Optional: Modify the tool call before permission/approval/execution
  modifiedToolCall?: ToolCall
}

interface UserPromptSubmitResult {
  allow: boolean
  modifiedInput?: MessageContent
}

interface SessionStartContext extends BaseHookContext {
  startReason: 'new' | 'resume'
  messages: Message[]
}

interface StopContext extends BaseHookContext {
  stopReason:
    | 'max_iterations'
    | 'final_response'
    | 'error'
    | 'cancelled'
    | 'approval_required'
    | 'max_follow_ups'
  // Stop reason reported by the latest model response, when available
  // (e.g. 'tool_call' | 'final' | 'length' | 'error' | 'cancelled')
  modelStopReason?: 'tool_call' | 'final' | 'length' | 'error' | 'cancelled'
  finalResponse?: string
  usage: Usage
  error?: { code?: string; message: string }
  messages: Message[]
}

interface SessionEndContext extends BaseHookContext {
  stopReason: StopContext['stopReason']
  finalResponse?: string
  usage: Usage
  durationMs: number
  error?: { code?: string; message: string }
  messages: Message[]
}

interface UserPromptSubmitContext extends BaseHookContext {
  input: MessageContent
}

interface SubagentStartContext extends BaseHookContext {
  subagentId: string
  subagentType: string
  taskDescription?: string
  prompt: string
}

interface SubagentStopContext extends BaseHookContext {
  subagentId: string
  subagentType: string
  result?: unknown
  error?: Error
  durationMs: number
  usage?: Usage
  messages: Message[]
}

Hook Execution Order

Typical order in one run:

  • sessionStart (once, on the first receive() for this session)
  • userPromptSubmit (before a user message enters the loop; can block or rewrite input)
  • preToolUse (runs first for each tool call; can rewrite tool call)
  • permissionRequest (runs after preToolUse; allow/block decision before approval check)
  • Approval flow (toolContext.approval) if still required
  • postToolUse or postToolUseFailure
  • stop (after each LLM response completes; for no-LLM termination points, it runs before done)
  • sessionEnd (when a run fully finishes; not emitted at approval_required pause)

Basic Hook Usage

import { Agent, ToolRegistry, ReadTool, WriteTool } from 'goatchain'

const agent = new Agent({
  name: 'MyAgent',
  systemPrompt: 'You are helpful.',
  model,
  tools: new ToolRegistry().register(new ReadTool()).register(new WriteTool()),
})

// Create session with hooks
const session = await agent.createSession({
  hooks: {
    preToolUse: async (ctx) => {
      // Pre-mutate tool call before permission/approval
      const toolName = ctx.toolCall.function.name
      console.log(`PreToolUse: ${toolName}`)
    },

    permissionRequest: async (ctx) => {
      const toolName = ctx.toolCall.function.name
      console.log(`Tool requested: ${toolName}`)

      // Returning { allow: true } skips approval flow
      // (tool still goes through normal middleware/disabled checks before execution)
      // Returning { allow: false } blocks tool execution
      return { allow: true }
    },

    postToolUse: async (ctx, result) => {
      console.log(`Tool ${ctx.toolCall.function.name} completed successfully`)
    },

    postToolUseFailure: async (ctx, error) => {
      console.error(`Tool ${ctx.toolCall.function.name} failed:`, error)
    },
  },
})

Auto-Approval with permissionRequest Hook

The permissionRequest hook is powerful because allow: true bypasses the approval flow entirely, even for high-risk tools. This is useful for automated scenarios:

import type { RiskLevel } from 'goatchain'

// Define auto-approval policy
function shouldAutoApprove(toolName: string, riskLevel: RiskLevel): boolean {
  // Auto-approve safe and low risk tools
  if (riskLevel === 'safe' || riskLevel === 'low') {
    return true
  }

  // Block critical tools
  if (riskLevel === 'critical') {
    return false
  }

  // For medium/high risk, you can implement custom logic
  // e.g., check environment, user permissions, etc.
  return process.env.AUTO_APPROVE_ALL === 'true'
}

const session = await agent.createSession({
  hooks: {
    permissionRequest: async (ctx) => {
      const tool = agent.tools.get(ctx.toolCall.function.name)
      const riskLevel = tool?.riskLevel ?? 'safe'
      const allow = shouldAutoApprove(ctx.toolCall.function.name, riskLevel)

      if (allow) {
        console.log(`✓ Auto-approved: ${ctx.toolCall.function.name}`)
      } else {
        console.log(`✗ Blocked: ${ctx.toolCall.function.name}`)
      }

      return { allow }
    },
  },
})

Auto-Approve All Tools with toolContext

The simplest way to bypass approval for a specific request is using toolContext.approval.autoApprove:

// Auto-approve all tools for this specific send() call
session.send('Create a file and search the web', {
  toolContext: {
    approval: {
      autoApprove: true, // Bypasses approval for ALL tools in this request
    },
  },
})

for await (const event of session.receive()) {
  // All tools execute without requiring approval
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  }
}

Use cases:

  • Automated workflows: Scripts that run without user interaction
  • Testing: E2E tests that need tools to execute automatically
  • Trusted environments: When you trust the agent's tool usage completely

Approval strategies:

session.send('Your task', {
  toolContext: {
    approval: {
      autoApprove: true, // Approve all tools automatically
      // OR
      strategy: 'high_risk', // Only require approval for high/critical risk tools
      // OR
      strategy: 'all', // Require approval for every tool call
    },
  },
})

Interactive Approval with Pause/Resume

Important: The permissionRequest hook with allow: true skips approval. For interactive approval flows where you want to pause and ask the user, use toolContext.approval instead:

import type { AgentLoopCheckpoint } from 'goatchain'

// Step 1: Start session WITHOUT permissionRequest hook (to use approval system)
const session = await agent.createSession()

let checkpoint: AgentLoopCheckpoint | undefined

session.send('Create a file and delete it', {
  toolContext: {
    approval: { strategy: 'high_risk' }, // Pause on high-risk tools for user approval
  },
})

// Collect checkpoint when requires_action event is emitted
for await (const event of session.receive()) {
  if (event.type === 'requires_action') {
    // Save checkpoint
    checkpoint =
      event.checkpoint ||
      (await agent.stateStore?.loadCheckpoint(event.checkpointRef?.sessionId))
    break // Pause here
  }
}

// Step 2: Resume with approval decisions
if (checkpoint) {
  // Build approval decisions for pending tools
  const decisions = Object.fromEntries(
    checkpoint.pendingToolCalls.map((pending) => {
      const toolName = pending.toolCall.function.name
      const approved = confirm(`Approve ${toolName}?`)

      return [
        pending.toolCall.id,
        { approved, reason: approved ? undefined : 'User denied' },
      ]
    }),
  )

  // Resume session with decisions
  for await (const event of session.receive({
    toolContext: {
      approval: { decisions },
    },
  })) {
    // Process events...
  }
}

Complete Example: Interactive Approval System

See examples/tool-approval-session.ts for a full example demonstrating:

  • Sync approval - Real-time approval during tool execution
  • Async approval - Pause/resume pattern for user interaction
  • Blocked tools - Denying high-risk tools automatically
  • Risk-based policies - Different approval rules per risk level
# Run the example
bun run examples/tool-approval-session.ts

Key features shown:

  • Creating custom tools with risk levels
  • Implementing approval hooks with async delays
  • Pausing execution on requires_action events
  • Resuming sessions with approval decisions
  • Pretty-printed logging with colors and symbols

Modifying Tool Calls with preToolUse

The preToolUse hook can also modify tool calls before execution:

const session = await agent.createSession({
  hooks: {
    preToolUse: async (ctx) => {
      const toolName = ctx.toolCall.function.name

      // Example: Add safety constraints to file writes
      if (toolName === 'Write') {
        const args = JSON.parse(ctx.toolCall.function.arguments)

        // Modify the tool call to add restrictions
        const modifiedToolCall = {
          ...ctx.toolCall,
          function: {
            ...ctx.toolCall.function,
            arguments: JSON.stringify({
              ...args,
              // Force files to be written in a safe directory
              file_path: `/safe-dir/${args.file_path}`,
            }),
          },
        }

        return {
          modifiedToolCall,
        }
      }
    },
  },
})

Tool Context

The toolContext parameter in send() and receive() allows passing additional context:

session.send('Do something risky', {
  toolContext: {
    approval: {
      strategy: 'high_risk', // Pause on high-risk tools
      // or
      decisions: {
        tool_call_id_123: { approved: true },
        tool_call_id_456: { approved: false, reason: 'Too dangerous' },
      },
    },
    // Your custom context
    custom: { userId: '123', environment: 'production' },
  },
})

🧅 Middleware System

GoatChain uses a Koa-style onion model for middleware. Each middleware wraps around the core execution:

outer:before → inner:before → exec (model.stream) → inner:after → outer:after

Adding Middleware

// Add named middleware (recommended)
agent.use(async (state, next) => {
  const start = Date.now()
  console.log(`[${state.iteration}] Before model call`)

  const nextState = await next(state) // Execute next middleware/model

  console.log(`[${state.iteration}] After model call (${Date.now() - start}ms)`)
  return nextState
}, 'logging')

// Remove by name
agent.removeMiddleware('logging')

// View all middleware names
console.log(agent.middlewareNames) // ['logging', 'compression', ...]

// Use unsubscribe function
const unsubscribe = agent.use(middleware, 'temp')
unsubscribe() // Remove middleware

Built-in Middleware

Plan Mode Middleware

Adds planning phase before execution:

import { createPlanModeMiddleware } from 'goatchain'

// Automatically named 'plan_mode'
agent.use(createPlanModeMiddleware())

// With custom configuration
agent.use(
  createPlanModeMiddleware({
    name: 'my-plan', // Custom name
    planPrompt: 'Create a detailed plan...', // Custom prompt
  }),
)

Context Compression Middleware

Automatically compresses context when token limit is reached using a two-stage strategy:

import { createContextCompressionMiddleware } from 'goatchain'

// Automatically named 'context_compression'
agent.use(
  createContextCompressionMiddleware({
    maxTokens: 128000,
    protectedTurns: 2, // Keep last 2 conversation turns
    model: model,
    stateStore: agent.stateStore,
    toolCompressionTarget: 0.45, // Compress to 45% of maxTokens
    minKeepToolResults: 5, // Keep last 5 tool results
    // Optional: Enable detailed logging
    enableLogging: true,
    logFilePath: 'compression-logs.jsonl',
  }),
)

See Context Compression Logging Guide for details on monitoring compression behavior.

Custom Middleware Examples

Logging Middleware

agent.use(async (state, next) => {
  console.log(`Iteration ${state.iteration}:`, {
    messages: state.messages.length,
    pendingTools: state.pendingToolCalls.length,
  })

  const result = await next(state)

  console.log(`Completed iteration ${state.iteration}:`, {
    shouldContinue: result.shouldContinue,
    usage: result.usage,
  })

  return result
}, 'logger')

Error Handling Middleware

agent.use(async (state, next) => {
  try {
    return await next(state)
  } catch (error) {
    console.error('Agent error:', error)
    state.shouldContinue = false
    state.stopReason = 'error'
    state.error = error
    return state
  }
}, 'error-handler')

Rate Limiting Middleware

import { RateLimiter } from 'some-rate-limiter'

const limiter = new RateLimiter({ requestsPerMinute: 60 })

agent.use(async (state, next) => {
  await limiter.acquire()
  return next(state)
}, 'rate-limiter')

Custom Retry Middleware

agent.use(async (state, next) => {
  let retries = 3

  while (retries > 0) {
    try {
      return await next(state)
    } catch (error) {
      retries--
      if (retries === 0) throw error

      console.log(`Retrying... (${retries} attempts left)`)
      await new Promise((resolve) => setTimeout(resolve, 1000))
    }
  }

  return state
}, 'retry')

Middleware State

The AgentLoopState object passed to middleware:

interface AgentLoopState {
  sessionId: string // Current session ID
  messages: Message[] // Conversation history
  iteration: number // Current iteration number
  pendingToolCalls: ToolCallWithResult[] // Pending tool executions
  currentResponse: string // Current LLM response
  shouldContinue: boolean // Whether to continue loop
  stopReason?: string // Reason for stopping
  usage?: Usage // Token usage
  error?: Error // Error if any
}

🔌 Model Client

Creating a Model Client

import { createModel, createOpenAIAdapter } from 'goatchain'

const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
    baseUrl: 'https://api.openai.com/v1', // Optional
    organization: 'org-xxx', // Optional
  }),
})

OpenAI Adapter Options

interface OpenAIAdapterOptions {
  defaultModelId?: string // Default model ID
  apiKey?: string // OpenAI API key
  baseUrl?: string // Custom API endpoint (note: lowercase 'u')
  organization?: string // OpenAI organization ID
  defaultHeaders?: Record<string, string> // Custom headers
  timeout?: number // Request timeout (ms)
  maxRetries?: number // Max retry attempts (default: 2)
}

Using Different Models

// OpenAI
const openai = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

// OpenAI-compatible endpoints
const deepseek = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'deepseek-chat',
    apiKey: process.env.DEEPSEEK_API_KEY!,
    baseUrl: 'https://api.deepseek.com/v1',
  }),
})

Model Interface

Implement custom model adapters:

interface ModelClient {
  modelId: string

  // Streaming API (required)
  stream(request: ModelRequest): AsyncIterable<ModelStreamEvent>

  // Non-streaming API (optional)
  run?(request: ModelRequest): Promise<ModelRunResult>
}

interface ModelRequest {
  messages: Message[]
  tools?: OpenAITool[]
  temperature?: number
  maxTokens?: number
  topP?: number
  stopSequences?: string[]
}

💾 State Management

File State Store

Persist agent state to filesystem:

import { FileStateStore } from 'goatchain'

const stateStore = new FileStateStore({
  dir: './checkpoints', // Storage directory
  deleteOnComplete: true, // Auto-delete successful completions
})

const agent = new Agent({
  name: 'MyAgent',
  systemPrompt: 'You are helpful.',
  model,
  stateStore,
})

In-Memory State Store

For testing or temporary state:

import { InMemoryStateStore } from 'goatchain'

const stateStore = new InMemoryStateStore({
  deleteOnComplete: false,
})

State Store Interface

Implement custom state stores:

interface StateStore {
  deleteOnComplete: boolean

  saveCheckpoint(checkpoint: AgentLoopCheckpoint): Promise<void>
  loadCheckpoint(sessionId: string): Promise<AgentLoopCheckpoint | null>
  deleteCheckpoint(sessionId: string): Promise<void>
  listCheckpoints(): Promise<AgentLoopCheckpoint[]>
}

interface AgentLoopCheckpoint {
  sessionId: string
  messages: Message[]
  iteration: number
  usage: Usage
  createdAt: number
  updatedAt: number
}

Manual Checkpoint Management

// Save checkpoint manually
await stateStore.saveCheckpoint({
  sessionId: session.id,
  messages: session.messages,
  iteration: 3,
  usage: session.usage,
  createdAt: Date.now(),
  updatedAt: Date.now(),
})

// Load checkpoint
const checkpoint = await stateStore.loadCheckpoint('session-id')

// List all checkpoints
const checkpoints = await stateStore.listCheckpoints()

// Delete checkpoint
await stateStore.deleteCheckpoint('session-id')

📚 Complete Examples

Example 1: Simple Q&A Bot

import process from 'node:process'
import { Agent, createModel, createOpenAIAdapter } from 'goatchain'

const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

const agent = new Agent({
  name: 'Q&A Bot',
  systemPrompt: 'You are a helpful assistant that answers questions concisely.',
  model,
})

const session = await agent.createSession()
session.send('What is the capital of France?')

for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  }
}

Example 2: Agent with Tools

import {
  Agent,
  createModel,
  createOpenAIAdapter,
  ToolRegistry,
  ReadTool,
  WriteTool,
  BashTool,
} from 'goatchain'

const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

const tools = new ToolRegistry()
tools.register(new ReadTool())
tools.register(new WriteTool())
tools.register(new BashTool())

const agent = new Agent({
  name: 'File Assistant',
  systemPrompt: 'You help users manage their files.',
  model,
  tools,
})

const session = await agent.createSession()
session.send('Read the package.json file and tell me the version')

for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  } else if (event.type === 'tool_call_start') {
    console.log(`\nCalling: ${event.name}`)
  } else if (event.type === 'tool_result') {
    console.log(`Result: ${JSON.stringify(event.result).slice(0, 100)}...`)
  }
}

Example 3: Persistent Sessions

import {
  Agent,
  createModel,
  createOpenAIAdapter,
  FileStateStore,
} from 'goatchain'

const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

const stateStore = new FileStateStore({
  dir: './agent-state',
  deleteOnComplete: false, // Keep history
})

const agent = new Agent({
  name: 'Persistent Agent',
  systemPrompt: 'You are a helpful assistant.',
  model,
  stateStore,
})

// Create or resume session
let session
const sessionId = 'my-conversation'

try {
  session = await agent.resumeSession(sessionId)
  console.log('Resumed existing session')
} catch {
  session = await agent.createSession({ sessionId })
  console.log('Created new session')
}

session.send('Remember this: My favorite color is blue')

for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  }
}

Example 4: Session with Middleware

import {
  Agent,
  createModel,
  createOpenAIAdapter,
  createPlanModeMiddleware,
} from 'goatchain'

const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

const agent = new Agent({
  name: 'Planning Agent',
  systemPrompt: 'You are a helpful assistant.',
  model,
})

// Add logging middleware
agent.use(async (state, next) => {
  console.log(`\n=== Iteration ${state.iteration} ===`)
  const result = await next(state)
  console.log(`Tokens used: ${result.usage?.totalTokens || 0}`)
  return result
}, 'logger')

// Add plan mode
agent.use(createPlanModeMiddleware())

const session = await agent.createSession()
session.send('Create a todo list app with React and TypeScript')

for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  }
}

Example 5: Multi-turn Conversation

import { Agent, createModel, createOpenAIAdapter } from 'goatchain'

const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

const agent = new Agent({
  name: 'Conversational Agent',
  systemPrompt: 'You are a helpful assistant.',
  model,
})

const session = await agent.createSession()

async function chat(message: string) {
  console.log(`\nUser: ${message}`)
  console.log('Assistant: ')

  session.send(message)

  for await (const event of session.receive()) {
    if (event.type === 'text_delta') {
      process.stdout.write(event.delta)
    }
  }

  console.log('\n')
}

// Multi-turn conversation
await chat('My name is Alice')
await chat('What is 2 + 2?')
await chat('What is my name?') // Should remember "Alice"
await chat('Multiply the previous result by 3') // Should remember "4"

console.log(`Total messages: ${session.messages.length}`)
console.log(`Total tokens: ${session.usage.totalTokens}`)

Example 6: Working Directory with Auto-Approval

Combine session-level working directory with auto-approval for automated file operations:

import {
  Agent,
  createModel,
  createOpenAIAdapter,
  createBuiltinTools,
} from 'goatchain'

const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

const agent = new Agent({
  name: 'File Agent',
  systemPrompt: 'You are a file management assistant.',
  model,
  tools: createBuiltinTools(), // All file tools included
})

// Set working directory at session creation
const session = await agent.createSession({
  cwd: '/path/to/project',
})

// Auto-approve all file operations for automated workflow
session.send('List all TypeScript files, then create a summary.md file', {
  toolContext: {
    approval: {
      autoApprove: true, // No approval prompts - fully automated
    },
  },
})

for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  } else if (event.type === 'tool_call_start') {
    console.log(`\nExecuting: ${event.toolName}`)
  }
}

// Change directory and continue with auto-approval
session.setCwd('/path/to/another/project')
session.send('Analyze the project structure and create a report', {
  toolContext: {
    approval: { autoApprove: true },
  },
})

for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  }
}

Perfect for:

  • CI/CD pipelines that need file analysis
  • Automated code review bots
  • Project documentation generators
  • Bulk file operations without manual intervention

Example 7: Tool-level Working Directory (Advanced)

For more control, you can configure individual tools with specific directories and restrictions:

import path from 'node:path'
import {
  Agent,
  createModel,
  createOpenAIAdapter,
  ToolRegistry,
  ReadTool,
  WriteTool,
  GlobTool,
  GrepTool,
} from 'goatchain'

const model = createModel({
  adapter: createOpenAIAdapter({
    defaultModelId: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY!,
  }),
})

// Define output directory for agent's file operations
const OUTPUT_DIR = path.resolve(process.cwd(), 'output')

// Configure tools with working directory and restrictions
const tools = new ToolRegistry()
tools.register(
  new ReadTool({
    cwd: OUTPUT_DIR,
    allowedDirectory: OUTPUT_DIR, // Sandbox: only allow reads in OUTPUT_DIR
  }),
)
tools.register(
  new WriteTool({
    cwd: OUTPUT_DIR,
    allowedDirectory: OUTPUT_DIR, // Sandbox: only allow writes in OUTPUT_DIR
  }),
)
tools.register(new GlobTool({ cwd: OUTPUT_DIR }))
tools.register(new GrepTool({ cwd: OUTPUT_DIR }))

const agent = new Agent({
  name: 'Sandboxed File Agent',
  systemPrompt: `You are a file management assistant.
All your file operations are restricted to the output directory.
You can create, read, and modify files within this sandbox.`,
  model,
  tools,
})

const session = await agent.createSession()
session.send("Create a report.md file with a summary of today's tasks")

for await (const event of session.receive()) {
  if (event.type === 'text_delta') {
    process.stdout.write(event.delta)
  } else if (event.type === 'tool_call_start') {
    console.log(`\n[Tool] ${event.name}`)
  }
}

📖 API Reference

Agent Class

Constructor

new Agent(options: AgentOptions)

Options:

  • name: string - Agent name (required)
  • systemPrompt: string - System instructions (required)
  • model: ModelClient - LLM client (required)
  • tools?: ToolRegistry - Tool registry (not an array)
  • stateStore?: StateStore - Persistence layer
  • middleware?: Middleware[] - Middleware list to register at startup
  • mcpServers?: MCPServerConfig[] - MCP server configuration list
  • enableLogging?: boolean - Enable internal logs

Methods

createSession(options?): Promise<Session>

Create a new session.

const session = await agent.createSession({
  sessionId: 'custom-id', // Optional custom session ID
  maxIterations: 10, // Optional max iterations
  requestParams: {
    // Optional request parameters
    temperature: 0.7,
    maxTokens: 2000,
  },
})

resumeSession(sessionId, options?): Promise<Session>

Resume an existing session from checkpoint.

const session = await agent.resumeSession('session-123')

use(middleware, name?): Promise<() => void>

Add middleware. Returns unsubscribe function.

const unsubscribe = await agent.use(myMiddleware, 'my_middleware')
unsubscribe() // Remove middleware

removeMiddleware(nameOrFn): boolean

Remove middleware by name or function reference.

agent.removeMiddleware('my_middleware')

setModel(modelOrRef): void

Switch or pin model at runtime.

agent.setModel({ provider: 'openai', modelId: 'gpt-4o-mini' })

Properties

  • id: string - Agent ID
  • name: string - Agent name
  • systemPrompt: string - System prompt
  • model: ModelClient - Current model client
  • tools?: ToolRegistry - Tool registry
  • stateStore?: StateStore - State store
  • sessionManager?: BaseSessionManager - Session manager
  • middlewareNames: string[] - List of middleware names

Session Class

Methods

send(input): void

Send a message to the session.

session.send('Hello!')

receive(options?): AsyncGenerator<AgentEvent>

Stream agent events.

for await (const event of session.receive()) {
  console.log(event)
}

addMessage(message): void

Manually add a message.

session.addMessage({
  role: 'user',
  content: 'Hello',
})

save(): Promise<void>

Manually save session state.

await session.save()

toSnapshot(): SessionSnapshot

Export session to snapshot.

const snapshot = session.toSnapshot()

restoreFromSnapshot(snapshot): void

Restore session from snapshot.

getCwd(): string | undefined

Get the current working directory for this session.

const cwd = session.getCwd()
console.log('Working directory:', cwd)

setCwd(cwd: string): void

Set the current working directory for this session. This automatically syncs the new directory to all tools that support it.

session.setCwd('/path/to/project')
// All file operation tools now use this directory

Properties

  • id: string - Session ID
  • status: SessionStatus - Session status ('idle' | 'running' | 'completed' | 'error')
  • messages: Message[] - Message history
  • usage: Usage - Token usage statistics
  • createdAt: number - Creation timestamp
  • updatedAt: number - Last update timestamp

Message Type

interface Message {
  role: 'system' | 'user' | 'assistant' | 'tool'
  content: string | ToolCall[] | ToolResult[]
  name?: string // For tool messages
  toolCallId?: string // For tool messages
}

Usage Type

interface Usage {
  promptTokens: number
  completionTokens: number
  totalTokens: number
}

AgentEvent Types

type AgentEvent =
  | TextDeltaEvent
  | ToolCallStartEvent
  | ToolCallDeltaEvent
  | ToolCallEndEvent
  | ToolResultEvent
  | ThinkingStartEvent
  | ThinkingDeltaEvent
  | ThinkingEndEvent
  | IterationStartEvent
  | IterationEndEvent
  | DoneEvent
  | ErrorEvent

interface TextDeltaEvent {
  type: 'text_delta'
  delta: string
}

interface ToolCallStartEvent {
  type: 'tool_call_start'
  id: string
  name: string
}

interface ToolResultEvent {
  type: 'tool_result'
  tool_call_id: string
  result: unknown
  isError?: boolean
}

interface DoneEvent {
  type: 'done'
  stopReason:
    | 'max_iterations'
    | 'final_response'
    | 'error'
    | 'cancelled'
    | 'approval_required'
    | 'max_follow_ups'
  usage?: Usage
}

🏗️ Architecture

classDiagram
    direction TB

    class Agent {
        +id: string
        +name: string
        +systemPrompt: string
        +model: ModelClient
        +tools: ToolRegistry?
        +stateStore: StateStore?
        +sessionManager: BaseSessionManager?
        +use(middleware): Promise~function~
        +createSession(): Promise~Session~
        +resumeSession(id): Promise~Session~
    }

    class ModelClient {
        <<interface>>
        +modelId: string
        +stream(request): AsyncIterable~ModelStreamEvent~
    }

    class StateStore {
        <<interface>>
        +saveCheckpoint(): Promise~void~
        +loadCheckpoint(): Promise~Checkpoint~
        +deleteCheckpoint(): Promise~void~
        +listCheckpoints(): Promise~Checkpoint[]~
    }

    class BaseTool {
        <<abstract>>
        +name: string
        +description: string
        +parameters: JSONSchema
        +execute(args): Promise~unknown~
    }

    class ToolRegistry {
        +register(tool): void
        +unregister(name): boolean
        +get(name): BaseTool
        +list(): BaseTool[]
    }

    class BaseSession {
        <<abstract>>
        +id: string
        +status: SessionStatus
        +messages: Message[]
        +usage: Usage
        +send(input): void
        +receive(): AsyncGenerator~AgentEvent~
        +save(): Promise~void~
    }

    class Middleware {
        <<function>>
        (state, next) => Promise~AgentLoopState~
    }

    Agent --> ModelClient : uses
    Agent --> ToolRegistry : uses
    Agent --> StateStore : uses
    Agent --> BaseSession : creates
    Agent ..> Middleware : applies
    ToolRegistry --> BaseTool : contains

🧰 Additional Tools

CLI

DimCode includes a terminal UI (TUI) for interactive agent sessions:

# Install globally
npm install -g dimcode@latest

# Run
dim

Run the local API server + Web GUI:

dim server --open

Features:

  • Interactive chat interface
  • Session management
  • Tool approval system
  • Settings configuration

See docs/cli.md and docs/server.md for details.

ACP Server

Expose DimCode as an Agent Client Protocol server for editor integrations:

bun run acp-server

Configuration for Zed (settings.json):

{
  "agent_servers": {
    "dimcode": {
      "command": "pnpm",
      "args": ["--dir", "/path/to/DimCode", "acp-server"]
    }
  }
}

See docs/acp-server.md for details.

📚 Documentation

FAQs

Package last updated on 01 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts