🚀 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.32
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
  variants?: ModelRef[] // Optional fallback model refs
  maxIterations?: number // Max agent loop iterations (default: 1000)
  cwd?: string // Working directory for file operations
  messageQueueConfig?: {
    autoProcessQueue?: boolean // Auto-process queued messages (default: true)
    maxQueueSize?: number // Optional queue length limit
  }
  requestParams?: {
    temperature?: number // Model temperature
    maxTokens?: number // Max output tokens
    stop?: string[] // Optional stop sequences
    [key: string]: unknown // Provider-specific request params
  }
}

Working Directory (CWD) Configuration

You can set a working directory for the session, which will be automatically applied to tools that support setCwd() (for example Read, Write, Edit, Glob, Grep, and Bash):

// 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')
session.send('Follow-up question')

Send Options

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

// Higher priority messages are processed first (smaller number = higher priority)
session.send('Low priority', { priority: 10 })
session.send('High priority', { priority: 1 })

send() returns a message ID that can be used for queue management:

const messageId = session.send('Can be cancelled later')
session.cancelQueuedMessage(messageId)

Message Queue

Session now supports queue-based messaging by default. You can enqueue multiple messages safely even while a previous receive() is running.

const session = await agent.createSession()

session.send('First message')
session.send('Second message')

// Batch enqueue with priority
session.sendBatch([
  { input: 'Task A', priority: 2 },
  { input: 'Task B', priority: 1 },
])

// Inspect queue state
const queue = session.getQueueStatus()
console.log(queue.length, queue.isProcessing)

// Remove queued messages
session.clearQueue()

Manual queue mode:

const session = await agent.createSession({
  messageQueueConfig: { autoProcessQueue: false },
})

session.send('Message 1')
session.send('Message 2')

for await (const event of session.receive()) {
  // only Message 1
}

for await (const event of session.receive()) {
  // Message 2
}

Tool context and approval options still work as before:

// 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.toolName ?? event.callId}`)
      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
  }
}

Session Event Types

Event TypeDescriptionKey Fields
session_createdNew session stream startssessionId
text_start / text_delta / text_endAssistant text streamdelta, content
thinking_start / thinking_delta / thinking_endModel reasoning streamdelta, content
tool_call_start / tool_call_delta / tool_call_endTool call lifecyclecallId, toolName, toolCall
tool_output_start / tool_output_deltaLive tool stdout/stderr streamtool_call_id, delta, isStderr
tool_resultTool execution resulttool_call_id, result, isError
tool_approval_requestedHigh-risk tool needs approvaltool_call_id, toolName, riskLevel, args
requires_actionExecution paused for approval or AskUser answerskind, checkpoint, checkpointRef, questions
tool_skippedTool execution skippedtool_call_id, toolName, reason
iteration_start / iteration_endAgent loop iteration lifecycleiteration, usage
subagent_eventForwarded subagent statussubagentId, subagentType, phase
compression_start / compression_endContext compression lifecycletokensBefore, tokensAfter, strategy
hook_evaluationPrompt-hook evaluation lifecyclehookName, phase, status
doneStream finishedstopReason, modelStopReason, error, usage

Session State Management

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

// Check session status
console.log(session.status) // 'active' | 'paused' | 'completed' | 'error' | 'archived'

// 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 the full conversation history plus the system prompt
console.log(session.messages.length) // 5 (system + 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 Utilities

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

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

// Advanced: direct message mutation is allowed, but prefer snapshots/checkpoints
// when you need reproducible restore flows.
session.messages.push({ role: 'assistant', content: 'Synthetic entry' })

🤖 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
TaskToolTaskSubagentRun a registered subagent task (for example Explore)
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>)
    | PromptHookEntry
    | Array<
        | ((ctx: UserPromptSubmitContext) => Promise<UserPromptSubmitResult>)
        | PromptHookEntry
      >

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

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

// Backward-compatible alias
type ToolHooks = AgentHooks

interface PromptHookEntry {
  type: 'prompt'
  prompt: string // use $ARGUMENTS to inject serialized input
  model?: { provider: string; modelId: string }
  timeoutMs?: number // default: 30000
}

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[]
}

Prompt Hook Evaluation

Prompt hooks are evaluation-only in current SDK behavior:

  • Prompt evaluation does not change execution decisions or mutate input/tool calls
  • Supported prompt hooks: userPromptSubmit, preToolUse, permissionRequest, postToolUse, postToolUseFailure, subagentStop
  • permissionRequest prompt evaluation only runs when the approval path is entered
  • Each evaluation is persisted in session.metadata._hookEvaluations

Prompt evaluation emits hook_evaluation stream events with phase:

  • start
  • stream (text delta)
  • end (status/result/error)

hook_evaluation event shape:

interface HookEvaluationEvent extends BaseEvent {
  type: 'hook_evaluation'
  evaluationId: string
  hookName:
    | 'permissionRequest'
    | 'preToolUse'
    | 'postToolUse'
    | 'postToolUseFailure'
    | 'subagentStop'
    | 'userPromptSubmit'
  phase: 'start' | 'stream' | 'end'
  prompt?: string
  input?: unknown
  delta?: string
  rawResponse?: string
  result?: unknown
  usage?: Usage
  durationMs?: number
  status?: 'success' | 'error' | 'timeout'
  error?: { code?: string; message: string }
  toolCallId?: string
}

Metadata persistence shape:

session.metadata._hookEvaluations = {
  preToolUse: [
    {
      evaluationId: '...',
      timestamp: 1730000000000,
      hookName: 'preToolUse',
      prompt: '...',
      input: { ... },
      status: 'success',
      durationMs: 120,
      rawResponse: '{"ok":true}',
      result: { ok: true },
      usage: { promptTokens: 100, completionTokens: 20, totalTokens: 120 },
      toolCallId: 'call_123',
    },
  ],
}

Complete prompt hook example (function hook + prompt hook + event handling):

import { Agent } from 'goatchain'
import type { HookEvaluationEvent } from 'goatchain'

const session = await agent.createSession({
  hooks: {
    preToolUse: [
      async (ctx) => {
        // normal behavior hook
        return undefined
      },
      {
        type: 'prompt',
        prompt: 'Analyze this tool call: $ARGUMENTS',
      },
    ],
    permissionRequest: {
      type: 'prompt',
      prompt: 'Review approval context: $ARGUMENTS',
    },
  },
})

session.send('Do the task', {
  toolContext: {
    approval: { strategy: 'high_risk' },
  },
})

for await (const event of session.receive()) {
  if (event.type === 'hook_evaluation') {
    const ev = event as HookEvaluationEvent
    if (ev.phase === 'end') {
      console.log('hook evaluation done:', ev.hookName, ev.status, ev.result)
    }
  }
}

console.log(session.metadata?._hookEvaluations)

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: (() => {
    const tools = new ToolRegistry()
    tools.register(new ReadTool())
    tools.register(new WriteTool())
    return tools
  })(),
})

// 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() is used for approval state and AskUser resume data:

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' },
      },
    },
    askUser: {
      answers: {
        tool_call_id_789: {
          framework: 'React',
          styling: 'Tailwind CSS',
        },
      },
    },
  },
})

🧅 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)
await 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 = await 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'
await agent.use(createPlanModeMiddleware())

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

Context Compression Middleware

Automatically compresses context from the full raw transcript when the prompt approaches the model context window. The middleware now:

  • reuses any persisted rolling summary first
  • removes old tool messages before touching the current round
  • preserves the last user round
  • performs at most one AI summary pass per overflow event
  • guarantees the final prompt stays within contextLength or fails locally before the model call
  • can emit per-stage snapshots in the large E2E example for inspection
import { createContextCompressionMiddleware } from 'goatchain'

// Automatically named 'context_compression'
await agent.use(
  createContextCompressionMiddleware({
    contextLength: 128000,
  }),
)

The large E2E example writes round-N/stage1.json through stage4.json under examples/output/context-compression-large-e2e/ by default, so you can inspect each compression step directly.

See src/spec/middleware.md for the full middleware and compression spec.

Custom Middleware Examples

Logging Middleware

await 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

await 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 })

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

Custom Retry Middleware

await 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
  currentThinking?: string // Current reasoning content, if the model emits it
  shouldContinue: boolean // Whether to continue loop
  stopReason?: string // Reason for stopping
  usage: Usage // Cumulative token usage
  error?: Error // Error if any
  metadata: Record<string, unknown> // Middleware/hook shared runtime data
}

🔌 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

  save<T>(sessionId: string, key: string, data: T): Promise<void>
  load<T>(sessionId: string, key: string): Promise<T | undefined>
  delete(sessionId: string, key: string): Promise<void>
  listKeys(sessionId: string): Promise<string[]>
  listSessions(): Promise<string[]>

  // Checkpoint helpers
  saveCheckpoint(checkpoint: AgentLoopCheckpoint): Promise<void>
  loadCheckpoint(sessionId: string): Promise<AgentLoopCheckpoint | undefined>
  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

// Prefer SDK-managed checkpoints during session.receive().
// You can still inspect or clean them up manually:
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.toolName}`)
  } 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
await 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
await 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,
  ToolRegistry,
  ReadTool,
  WriteTool,
  EditTool,
  GlobTool,
  GrepTool,
} 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 EditTool())
tools.register(new GlobTool())
tools.register(new GrepTool())

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

// 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.toolName}`)
  }
}

📖 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, options?): string

Enqueue a message and return its queue message ID.

const id = session.send('Hello!')

sendBatch(messages): string[]

Batch enqueue messages and return queue message IDs.

const ids = session.sendBatch([
  { input: 'task-1', priority: 1 },
  { input: 'task-2', priority: 2 },
])

cancelQueuedMessage(messageId): boolean

Cancel a queued message by ID.

session.cancelQueuedMessage(id)

getQueueStatus(): MessageQueueStatus

Query queue length, preview list, processing status, and config.

console.log(session.getQueueStatus())

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 ('active' | 'paused' | 'completed' | 'error' | 'archived')
  • 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: MessageContent
  reasoning_content?: string // Assistant messages
  tool_calls?: ToolCall[] // Assistant messages
  tool_call_id?: string // Tool messages
  name?: string
  isError?: boolean // Tool messages
}

Usage Type

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

AgentEvent Types

type AgentEvent =
  | TextStartEvent
  | TextDeltaEvent
  | TextEndEvent
  | ToolCallStartEvent
  | ToolCallDeltaEvent
  | ToolCallEndEvent
  | ToolOutputStartEvent
  | ToolOutputDeltaEvent
  | ToolApprovalRequestedEvent
  | RequiresActionEvent
  | ToolSkippedEvent
  | ToolResultEvent
  | ThinkingStartEvent
  | ThinkingDeltaEvent
  | ThinkingEndEvent
  | IterationStartEvent
  | IterationEndEvent
  | DoneEvent

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

interface ToolCallStartEvent {
  type: 'tool_call_start'
  callId: string
  toolName?: string
}

interface RequiresActionEvent {
  type: 'requires_action'
  kind: 'tool_approval' | 'ask_user'
  checkpoint?: AgentLoopCheckpoint
  checkpointRef?: { sessionId: string }
}

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

interface DoneEvent {
  type: 'done'
  finalResponse?: string
  stopReason:
    | 'max_iterations'
    | 'final_response'
    | 'error'
    | 'cancelled'
    | 'approval_required'
    | 'max_follow_ups'
  modelStopReason?: 'tool_call' | 'final' | 'length' | 'error' | 'cancelled'
  error?: {
    code?: string
    message: string
    status?: number
    retryable?: boolean
  }
  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, options?): string
        +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:

dim acp

For source checkouts, use a cwd-independent command:

node /absolute/path/to/GoatChain/scripts/acpx-agent.mjs

Configuration for Zed (settings.json):

{
  "agent_servers": {
    "dimcode": {
      "command": "/absolute/path/to/dim",
      "args": ["acp"]
    }
  }
}

For OpenClaw acpx, use either /absolute/path/to/dim acp or node /absolute/path/to/GoatChain/scripts/acpx-agent.mjs. Do not use bun run acp-server there; it depends on the launcher cwd being the GoatChain repo root.

See docs/acp-server.md for details.

📚 Documentation

FAQs

Package last updated on 10 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