
Research
/Security News
Chrome and Firefox Extensions Posing as Free VPNs Add Clipboard Stealers via Malicious Updates
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.
A TypeScript SDK for building AI agents with streaming support, tool calling, and middleware pattern.
preToolUse vs permissionRequestpreToolUse is now only for mutating tool calls (modifiedToolCall).
Permission decisions (allow / deny) must be handled in permissionRequest.
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 callpermissionRequest to allow or block executionpnpm add goatchain
# or
npm install goatchain
# or
bun add goatchain
GoatChain SDK is built around three core components:
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 is the core component for managing conversations. It handles message history, state persistence, and event streaming.
// 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,
},
})
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
}
}
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:
session.setCwd()// 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')
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
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
}
}
| Event Type | Description | Key Fields |
|---|---|---|
iteration_start | Agent loop iteration begins | iteration |
text_delta | Partial text response | delta |
thinking_start | Reasoning phase begins | - |
thinking_delta | Reasoning content | delta |
thinking_end | Reasoning phase ends | - |
tool_call_start | Tool invocation begins | name, id |
tool_call_delta | Tool arguments stream | delta |
tool_call_end | Tool call complete | name, args |
tool_result | Tool execution result | result, error |
iteration_end | Iteration complete | usage, iteration |
done | Stream finished | stopReason, usage |
error | Error occurred | error |
// 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
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
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)
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
}
// Add message manually
session.addMessage({
role: 'user',
content: 'Custom message',
})
// Save session state manually
await session.save()
// Clear session history
session.messages = []
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
})
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
}
// 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)
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')
GoatChain SDK exports the following built-in tools:
| Tool Class | Runtime Name | Category | Purpose |
|---|---|---|---|
ReadTool | Read | File | Read file content (text, binary metadata, and selected converted formats) |
WriteTool | Write | File | Create or overwrite files |
EditTool | Edit | File | In-place text replacement edits |
GlobTool | Glob | File/Search | Find files by glob pattern |
GrepTool | Grep | File/Search | Search file contents by pattern |
BashTool | Bash | Command | Execute shell commands |
WebSearchTool | WebSearch | Web | Search the web (e.g. via Serper API) |
WebFetchTool | WebFetch | Web | Fetch and extract content from a specific URL |
TodoWriteTool | TodoWrite | Planning | Manage structured todo lists |
TodoPlanTool | TodoPlan | Planning | Create/update planning todos for plan flows |
AskUserTool | AskUserQuestion | Interaction | Ask the user structured follow-up questions |
EnterPlanModeTool | EnterPlanMode | Mode | Enter plan mode |
ExitPlanModeTool | ExitPlanMode | Mode | Exit 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,
})
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.
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:
| Tool | What it does | How cwd is applied | Per-call override | Extra sandbox options |
|---|---|---|---|---|
ReadTool | Reads files (and some converted formats) | Relative file_path resolves from cwd | file_path can be absolute | allowedDirectory, fileBlacklist, disableBlacklist |
WriteTool | Writes/overwrites files | Relative file_path resolves from cwd | file_path can be absolute | allowedDirectory, fileBlacklist, disableBlacklist |
EditTool | Replaces old_string with new_string in a file | Relative file_path resolves from cwd | file_path can be absolute | fileBlacklist, disableBlacklist |
GlobTool | Finds files by pattern | Search root defaults to cwd | path argument can change search root | fileBlacklist, disableBlacklist |
GrepTool | Searches text content in files | Search runs under cwd | path argument narrows search scope | fileBlacklist, disableBlacklist |
BashTool | Runs shell commands | Commands execute in cwd | workdir argument overrides per call | None |
Directory & Protection Options:
| Option | Description | Example |
|---|---|---|
cwd | Working directory for resolving relative paths | { cwd: '/app/output' } |
allowedDirectory | Restrict file access to this directory only (blocks path traversal) | { allowedDirectory: '/app/output' } |
When to use allowedDirectory:
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,
})
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()
Sessions support lifecycle hooks that let you intercept user input, tool calls, and session/subagent lifecycle events.
GoatChain has three relevant mechanisms for tool execution control:
preToolUse Hook - For tool-call mutation before permission/approval
modifiedToolCallpermissionRequestpermissionRequest Hook - For programmatic auto-approval/blocking
preToolUse, so it sees the modified tool callallow: true → skips approval flow (execution still goes through normal middleware/disabled checks)allow: false → tool is blockedApproval System (via toolContext.approval) - For interactive user approval
requires_action eventThese are independent: If permissionRequest returns allow: true, the approval system is bypassed.
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[]
}
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)toolContext.approval) if still requiredpostToolUse or postToolUseFailurestop (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)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)
},
},
})
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 }
},
},
})
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:
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
},
},
})
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...
}
}
See examples/tool-approval-session.ts for a full example demonstrating:
# Run the example
bun run examples/tool-approval-session.ts
Key features shown:
requires_action eventsThe 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,
}
}
},
},
})
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' },
},
})
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
// 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
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
}),
)
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.
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')
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')
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')
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')
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
}
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
}),
})
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)
}
// 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',
}),
})
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[]
}
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,
})
For testing or temporary state:
import { InMemoryStateStore } from 'goatchain'
const stateStore = new InMemoryStateStore({
deleteOnComplete: false,
})
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
}
// 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')
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)
}
}
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)}...`)
}
}
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)
}
}
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)
}
}
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}`)
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:
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}`)
}
}
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 layermiddleware?: Middleware[] - Middleware list to register at startupmcpServers?: MCPServerConfig[] - MCP server configuration listenableLogging?: boolean - Enable internal logscreateSession(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' })
id: string - Agent IDname: string - Agent namesystemPrompt: string - System promptmodel: ModelClient - Current model clienttools?: ToolRegistry - Tool registrystateStore?: StateStore - State storesessionManager?: BaseSessionManager - Session managermiddlewareNames: string[] - List of middleware namessend(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
id: string - Session IDstatus: SessionStatus - Session status ('idle' | 'running' | 'completed' | 'error')messages: Message[] - Message historyusage: Usage - Token usage statisticscreatedAt: number - Creation timestampupdatedAt: number - Last update timestampinterface Message {
role: 'system' | 'user' | 'assistant' | 'tool'
content: string | ToolCall[] | ToolResult[]
name?: string // For tool messages
toolCallId?: string // For tool messages
}
interface Usage {
promptTokens: number
completionTokens: number
totalTokens: number
}
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
}
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
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:
See docs/cli.md and docs/server.md for details.
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.
dim server and use the browser UIFAQs
Unknown package
The npm package goatchain receives a total of 51 weekly downloads. As such, goatchain popularity was classified as not popular.
We found that goatchain demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

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.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.

Security News
Rolldown paused Rust React Compiler integration after a 5MB binary size increase raised concerns about shipping React-specific code to all Vite users.