goatchain 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +535 -28
- package/dist/agent/agent.d.ts +9 -1
- package/dist/agent/hooks/types.d.ts +4 -0
- package/dist/agent/types.d.ts +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +255 -252
- package/dist/mcp-client/client.d.ts +44 -0
- package/dist/mcp-client/http-client.d.ts +38 -0
- package/dist/mcp-client/index.d.ts +8 -0
- package/dist/mcp-client/manager.d.ts +22 -0
- package/dist/mcp-client/proxy-tool.d.ts +18 -0
- package/dist/mcp-client/stdio-client.d.ts +64 -0
- package/dist/mcp-client/types.d.ts +54 -0
- package/dist/session/session.d.ts +19 -0
- package/dist/tool/builtin/askUser.d.ts +1 -1
- package/dist/tool/builtin/astGrepReplace.d.ts +1 -1
- package/dist/tool/builtin/astGrepSearch.d.ts +1 -1
- package/dist/tool/builtin/edit.d.ts +1 -1
- package/dist/tool/builtin/enterPlanMode.d.ts +1 -1
- package/dist/tool/builtin/exitPlanMode.d.ts +1 -1
- package/dist/tool/builtin/glob.d.ts +1 -1
- package/dist/tool/builtin/grep.d.ts +1 -1
- package/dist/tool/builtin/interactive-bash/tools.d.ts +1 -1
- package/dist/tool/builtin/read.d.ts +1 -1
- package/dist/tool/builtin/skill.d.ts +1 -1
- package/dist/tool/builtin/todoPlan.d.ts +1 -1
- package/dist/tool/builtin/todoWrite.d.ts +1 -1
- package/dist/tool/builtin/webFetch.d.ts +1 -1
- package/dist/tool/builtin/webSearch.d.ts +1 -1
- package/dist/tool/builtin/write.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Session is the core component for managing conversations. It handles message his
|
|
|
70
70
|
const session = await agent.createSession()
|
|
71
71
|
|
|
72
72
|
// Create session with custom ID
|
|
73
|
-
const session = await agent.createSession({
|
|
73
|
+
const session = await agent.createSession({ sessionId: 'my-session-id' })
|
|
74
74
|
|
|
75
75
|
// Create session with configuration overrides
|
|
76
76
|
const session = await agent.createSession({
|
|
@@ -86,8 +86,10 @@ const session = await agent.createSession({
|
|
|
86
86
|
|
|
87
87
|
```typescript
|
|
88
88
|
interface CreateSessionOptions {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
sessionId?: string // Custom session ID
|
|
90
|
+
model?: ModelRef // Optional model override
|
|
91
|
+
maxIterations?: number // Max agent loop iterations (default: 1000)
|
|
92
|
+
cwd?: string // Working directory for file operations
|
|
91
93
|
requestParams?: {
|
|
92
94
|
temperature?: number // Model temperature
|
|
93
95
|
maxTokens?: number // Max output tokens
|
|
@@ -96,6 +98,33 @@ interface CreateSessionOptions {
|
|
|
96
98
|
}
|
|
97
99
|
```
|
|
98
100
|
|
|
101
|
+
### Working Directory (CWD) Configuration
|
|
102
|
+
|
|
103
|
+
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):
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Set CWD when creating a session
|
|
107
|
+
const session = await agent.createSession({
|
|
108
|
+
cwd: '/path/to/project',
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Get the current working directory
|
|
112
|
+
const cwd = session.getCwd()
|
|
113
|
+
console.log('Current directory:', cwd)
|
|
114
|
+
|
|
115
|
+
// Change the working directory at runtime
|
|
116
|
+
session.setCwd('/path/to/another/project')
|
|
117
|
+
|
|
118
|
+
// All file operations now use the new directory
|
|
119
|
+
session.send('Read the README.md file')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Benefits:**
|
|
123
|
+
- Automatically sets the working directory for all tools that support it
|
|
124
|
+
- Persisted across session saves and restores
|
|
125
|
+
- Can be changed at runtime with `session.setCwd()`
|
|
126
|
+
- Simplifies file path management in multi-project environments
|
|
127
|
+
|
|
99
128
|
### Sending Messages
|
|
100
129
|
|
|
101
130
|
```typescript
|
|
@@ -108,6 +137,40 @@ session.send('First question')
|
|
|
108
137
|
session.send('Follow-up question')
|
|
109
138
|
```
|
|
110
139
|
|
|
140
|
+
#### Send Options
|
|
141
|
+
|
|
142
|
+
You can pass options to `send()` to control tool execution, approval, and more:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Auto-approve all tools for this request
|
|
146
|
+
session.send('Create files and search the web', {
|
|
147
|
+
toolContext: {
|
|
148
|
+
approval: {
|
|
149
|
+
autoApprove: true, // Skip approval for all tools
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Require approval only for high-risk tools
|
|
155
|
+
session.send('Analyze the codebase', {
|
|
156
|
+
toolContext: {
|
|
157
|
+
approval: {
|
|
158
|
+
strategy: 'high_risk', // Only prompt for high/critical risk tools
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Combine with session-level CWD
|
|
164
|
+
session.setCwd('/path/to/project')
|
|
165
|
+
session.send('Read and analyze all TypeScript files', {
|
|
166
|
+
toolContext: {
|
|
167
|
+
approval: { autoApprove: true },
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
// Tools will use /path/to/project as working directory
|
|
171
|
+
// and execute without requiring approval
|
|
172
|
+
```
|
|
173
|
+
|
|
111
174
|
### Receiving Events
|
|
112
175
|
|
|
113
176
|
The `receive()` method returns an async generator that streams events:
|
|
@@ -187,7 +250,7 @@ console.log(session.updatedAt) // Last update timestamp
|
|
|
187
250
|
|
|
188
251
|
### Session Persistence
|
|
189
252
|
|
|
190
|
-
Sessions can be saved and restored:
|
|
253
|
+
Sessions can be saved and restored. All session state including message history, configuration, and working directory (cwd) are preserved:
|
|
191
254
|
|
|
192
255
|
```typescript
|
|
193
256
|
// Save session to snapshot
|
|
@@ -200,6 +263,12 @@ session.restoreFromSnapshot(snapshot)
|
|
|
200
263
|
// Or create a new session from snapshot
|
|
201
264
|
const restored = await agent.createSession()
|
|
202
265
|
restored.restoreFromSnapshot(snapshot)
|
|
266
|
+
|
|
267
|
+
// The restored session maintains all state including:
|
|
268
|
+
// - Message history
|
|
269
|
+
// - Configuration overrides
|
|
270
|
+
// - Working directory (cwd)
|
|
271
|
+
// - Usage statistics
|
|
203
272
|
```
|
|
204
273
|
|
|
205
274
|
### Multi-turn Conversations
|
|
@@ -248,7 +317,7 @@ const agent = new Agent({
|
|
|
248
317
|
})
|
|
249
318
|
|
|
250
319
|
// Start session (automatically checkpointed)
|
|
251
|
-
const session = await agent.createSession({
|
|
320
|
+
const session = await agent.createSession({ sessionId: 'session-123' })
|
|
252
321
|
session.send('Start a long task')
|
|
253
322
|
for await (const event of session.receive()) {
|
|
254
323
|
// Process events...
|
|
@@ -376,6 +445,36 @@ const agent = new Agent({
|
|
|
376
445
|
})
|
|
377
446
|
```
|
|
378
447
|
|
|
448
|
+
### MCP Servers (HTTP + stdio)
|
|
449
|
+
|
|
450
|
+
GoatChain can connect MCP servers and register their remote tools automatically:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
const agent = new Agent({
|
|
454
|
+
name: 'MCP Assistant',
|
|
455
|
+
systemPrompt: 'You are helpful.',
|
|
456
|
+
model,
|
|
457
|
+
mcpServers: [
|
|
458
|
+
{
|
|
459
|
+
id: 'weather',
|
|
460
|
+
name: 'Weather API',
|
|
461
|
+
transport: 'http',
|
|
462
|
+
url: 'https://example.com/mcp',
|
|
463
|
+
auth: { type: 'bearer', token: process.env.WEATHER_API_KEY! },
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
id: 'local-tools',
|
|
467
|
+
name: 'Local Tools',
|
|
468
|
+
transport: 'stdio',
|
|
469
|
+
command: 'node',
|
|
470
|
+
args: ['./mcp-servers/tools.js'],
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
})
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
See `docs/mcp.md` for details.
|
|
477
|
+
|
|
379
478
|
### Configuring File Tool Working Directory
|
|
380
479
|
|
|
381
480
|
File-related tools (`ReadTool`, `WriteTool`, `EditTool`, `GlobTool`, `GrepTool`, `BashTool`) support configuring the working directory:
|
|
@@ -397,24 +496,29 @@ tools.register(new BashTool({ cwd: OUTPUT_DIR }))
|
|
|
397
496
|
// Option 2: Restrict to specific directory (security sandbox)
|
|
398
497
|
// This prevents access to files outside the allowed directory
|
|
399
498
|
const tools = new ToolRegistry()
|
|
400
|
-
tools.register(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
499
|
+
tools.register(
|
|
500
|
+
new ReadTool({
|
|
501
|
+
cwd: OUTPUT_DIR,
|
|
502
|
+
allowedDirectory: OUTPUT_DIR, // Only allow reads within OUTPUT_DIR
|
|
503
|
+
}),
|
|
504
|
+
)
|
|
505
|
+
tools.register(
|
|
506
|
+
new WriteTool({
|
|
507
|
+
cwd: OUTPUT_DIR,
|
|
508
|
+
allowedDirectory: OUTPUT_DIR, // Only allow writes within OUTPUT_DIR
|
|
509
|
+
}),
|
|
510
|
+
)
|
|
408
511
|
```
|
|
409
512
|
|
|
410
513
|
**Directory Configuration Options:**
|
|
411
514
|
|
|
412
|
-
| Option
|
|
413
|
-
|
|
414
|
-
| `cwd`
|
|
515
|
+
| Option | Description | Example |
|
|
516
|
+
| ------------------ | ------------------------------------------------------------------- | ------------------------------------- |
|
|
517
|
+
| `cwd` | Working directory for resolving relative paths | `{ cwd: '/app/output' }` |
|
|
415
518
|
| `allowedDirectory` | Restrict file access to this directory only (blocks path traversal) | `{ allowedDirectory: '/app/output' }` |
|
|
416
519
|
|
|
417
520
|
**When to use `allowedDirectory`:**
|
|
521
|
+
|
|
418
522
|
- When you want to sandbox the agent to a specific directory
|
|
419
523
|
- To prevent accidental access to sensitive files
|
|
420
524
|
- For production environments with security requirements
|
|
@@ -480,6 +584,324 @@ const allTools = registry.list()
|
|
|
480
584
|
const openaiTools = registry.toOpenAIFormat()
|
|
481
585
|
```
|
|
482
586
|
|
|
587
|
+
## 🎣 Tool Approval & Hooks
|
|
588
|
+
|
|
589
|
+
Sessions support lifecycle hooks that let you intercept tool calls for approval, logging, or custom logic.
|
|
590
|
+
|
|
591
|
+
### Key Concepts
|
|
592
|
+
|
|
593
|
+
GoatChain has two separate mechanisms for controlling tool execution:
|
|
594
|
+
|
|
595
|
+
1. **preToolUse Hook** - For programmatic auto-approval/blocking
|
|
596
|
+
- `allow: true` → Tool executes immediately, **skips approval flow**
|
|
597
|
+
- `allow: false` → Tool is blocked
|
|
598
|
+
- Use this for automated scenarios where you want to programmatically approve/deny tools
|
|
599
|
+
|
|
600
|
+
2. **Approval System** (via `toolContext.approval`) - For interactive user approval
|
|
601
|
+
- Pauses execution on high-risk tools
|
|
602
|
+
- Shows `requires_action` event
|
|
603
|
+
- Resumes with user's approval decisions
|
|
604
|
+
- Use this for interactive UIs where users manually approve tools
|
|
605
|
+
|
|
606
|
+
**These are independent**: If you use `preToolUse` with `allow: true`, the approval system is bypassed entirely.
|
|
607
|
+
|
|
608
|
+
### Hook Types
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
interface ToolHooks {
|
|
612
|
+
// Called before tool execution
|
|
613
|
+
// - If allow: true, tool executes immediately and skips approval flow
|
|
614
|
+
// - If allow: false, tool is blocked
|
|
615
|
+
// - Can modify tool call with modifiedToolCall
|
|
616
|
+
preToolUse?: (ctx: HookContext) => Promise<PreToolUseResult>
|
|
617
|
+
|
|
618
|
+
// Called after successful tool execution
|
|
619
|
+
postToolUse?: (ctx: HookContext, result: unknown) => Promise<void>
|
|
620
|
+
|
|
621
|
+
// Called after tool execution failure
|
|
622
|
+
postToolUseFailure?: (ctx: HookContext, error: Error) => Promise<void>
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
interface HookContext {
|
|
626
|
+
sessionId: string
|
|
627
|
+
toolCall: {
|
|
628
|
+
id: string
|
|
629
|
+
type: 'function'
|
|
630
|
+
function: {
|
|
631
|
+
name: string
|
|
632
|
+
arguments: string
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
toolContext: ToolExecutionContext
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
interface PreToolUseResult {
|
|
639
|
+
// If true, allows tool to execute and skips approval flow
|
|
640
|
+
// If false, blocks tool execution immediately
|
|
641
|
+
allow: boolean
|
|
642
|
+
// Optional: Modify the tool call before execution
|
|
643
|
+
modifiedToolCall?: ToolCall
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Basic Hook Usage
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
import { Agent, ToolRegistry, ReadTool, WriteTool } from 'goatchain'
|
|
651
|
+
|
|
652
|
+
const agent = new Agent({
|
|
653
|
+
name: 'MyAgent',
|
|
654
|
+
systemPrompt: 'You are helpful.',
|
|
655
|
+
model,
|
|
656
|
+
tools: new ToolRegistry()
|
|
657
|
+
.register(new ReadTool())
|
|
658
|
+
.register(new WriteTool()),
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
// Create session with hooks
|
|
662
|
+
const session = await agent.createSession({
|
|
663
|
+
hooks: {
|
|
664
|
+
preToolUse: async (ctx) => {
|
|
665
|
+
const toolName = ctx.toolCall.function.name
|
|
666
|
+
console.log(`Tool requested: ${toolName}`)
|
|
667
|
+
|
|
668
|
+
// Returning { allow: true } skips approval flow and executes tool immediately
|
|
669
|
+
// Returning { allow: false } blocks tool execution
|
|
670
|
+
return { allow: true }
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
postToolUse: async (ctx, result) => {
|
|
674
|
+
console.log(`Tool ${ctx.toolCall.function.name} completed successfully`)
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
postToolUseFailure: async (ctx, error) => {
|
|
678
|
+
console.error(`Tool ${ctx.toolCall.function.name} failed:`, error)
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
})
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Auto-Approval with preToolUse Hook
|
|
685
|
+
|
|
686
|
+
The `preToolUse` hook is powerful because `allow: true` **bypasses the approval flow entirely**, even for high-risk tools. This is useful for automated scenarios:
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
import type { RiskLevel } from 'goatchain'
|
|
690
|
+
|
|
691
|
+
// Define auto-approval policy
|
|
692
|
+
function shouldAutoApprove(toolName: string, riskLevel: RiskLevel): boolean {
|
|
693
|
+
// Auto-approve safe and low risk tools
|
|
694
|
+
if (riskLevel === 'safe' || riskLevel === 'low') {
|
|
695
|
+
return true
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Block critical tools
|
|
699
|
+
if (riskLevel === 'critical') {
|
|
700
|
+
return false
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// For medium/high risk, you can implement custom logic
|
|
704
|
+
// e.g., check environment, user permissions, etc.
|
|
705
|
+
return process.env.AUTO_APPROVE_ALL === 'true'
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const session = await agent.createSession({
|
|
709
|
+
hooks: {
|
|
710
|
+
preToolUse: async (ctx) => {
|
|
711
|
+
const tool = agent.tools.get(ctx.toolCall.function.name)
|
|
712
|
+
const riskLevel = tool?.riskLevel ?? 'safe'
|
|
713
|
+
const allow = shouldAutoApprove(ctx.toolCall.function.name, riskLevel)
|
|
714
|
+
|
|
715
|
+
if (allow) {
|
|
716
|
+
console.log(`✓ Auto-approved: ${ctx.toolCall.function.name}`)
|
|
717
|
+
} else {
|
|
718
|
+
console.log(`✗ Blocked: ${ctx.toolCall.function.name}`)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return { allow }
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
})
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Auto-Approve All Tools with toolContext
|
|
728
|
+
|
|
729
|
+
The simplest way to bypass approval for a specific request is using `toolContext.approval.autoApprove`:
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
// Auto-approve all tools for this specific send() call
|
|
733
|
+
session.send('Create a file and search the web', {
|
|
734
|
+
toolContext: {
|
|
735
|
+
approval: {
|
|
736
|
+
autoApprove: true, // Bypasses approval for ALL tools in this request
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
for await (const event of session.receive()) {
|
|
742
|
+
// All tools execute without requiring approval
|
|
743
|
+
if (event.type === 'text_delta') {
|
|
744
|
+
process.stdout.write(event.delta)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**Use cases:**
|
|
750
|
+
- **Automated workflows**: Scripts that run without user interaction
|
|
751
|
+
- **Testing**: E2E tests that need tools to execute automatically
|
|
752
|
+
- **Trusted environments**: When you trust the agent's tool usage completely
|
|
753
|
+
|
|
754
|
+
**Approval strategies:**
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
session.send('Your task', {
|
|
758
|
+
toolContext: {
|
|
759
|
+
approval: {
|
|
760
|
+
autoApprove: true, // Approve all tools automatically
|
|
761
|
+
// OR
|
|
762
|
+
strategy: 'high_risk', // Only require approval for high/critical risk tools
|
|
763
|
+
// OR
|
|
764
|
+
strategy: 'all', // Require approval for every tool call
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
})
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### Interactive Approval with Pause/Resume
|
|
771
|
+
|
|
772
|
+
**Important**: The `preToolUse` hook with `allow: true` **skips approval**. For interactive approval flows where you want to pause and ask the user, use `toolContext.approval` instead:
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
import type { AgentLoopCheckpoint } from 'goatchain'
|
|
776
|
+
|
|
777
|
+
// Step 1: Start session WITHOUT preToolUse hook (to use approval system)
|
|
778
|
+
const session = await agent.createSession()
|
|
779
|
+
|
|
780
|
+
let checkpoint: AgentLoopCheckpoint | undefined
|
|
781
|
+
|
|
782
|
+
session.send('Create a file and delete it', {
|
|
783
|
+
toolContext: {
|
|
784
|
+
approval: { strategy: 'high_risk' }, // Pause on high-risk tools for user approval
|
|
785
|
+
},
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
// Collect checkpoint when requires_action event is emitted
|
|
789
|
+
for await (const event of session.receive()) {
|
|
790
|
+
if (event.type === 'requires_action') {
|
|
791
|
+
// Save checkpoint
|
|
792
|
+
checkpoint = event.checkpoint ||
|
|
793
|
+
await agent.stateStore?.loadCheckpoint(event.checkpointRef?.sessionId)
|
|
794
|
+
break // Pause here
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Step 2: Resume with approval decisions
|
|
799
|
+
if (checkpoint) {
|
|
800
|
+
// Build approval decisions for pending tools
|
|
801
|
+
const decisions = Object.fromEntries(
|
|
802
|
+
checkpoint.pendingToolCalls.map((pending) => {
|
|
803
|
+
const toolName = pending.toolCall.function.name
|
|
804
|
+
const approved = confirm(`Approve ${toolName}?`)
|
|
805
|
+
|
|
806
|
+
return [
|
|
807
|
+
pending.toolCall.id,
|
|
808
|
+
{ approved, reason: approved ? undefined : 'User denied' },
|
|
809
|
+
]
|
|
810
|
+
})
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
// Resume session with decisions
|
|
814
|
+
for await (const event of session.receive({
|
|
815
|
+
toolContext: {
|
|
816
|
+
approval: { decisions },
|
|
817
|
+
},
|
|
818
|
+
})) {
|
|
819
|
+
// Process events...
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### Complete Example: Interactive Approval System
|
|
825
|
+
|
|
826
|
+
See `examples/tool-approval-session.ts` for a full example demonstrating:
|
|
827
|
+
|
|
828
|
+
- **Sync approval** - Real-time approval during tool execution
|
|
829
|
+
- **Async approval** - Pause/resume pattern for user interaction
|
|
830
|
+
- **Blocked tools** - Denying high-risk tools automatically
|
|
831
|
+
- **Risk-based policies** - Different approval rules per risk level
|
|
832
|
+
|
|
833
|
+
```bash
|
|
834
|
+
# Run the example
|
|
835
|
+
bun run examples/tool-approval-session.ts
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**Key features shown:**
|
|
839
|
+
- Creating custom tools with risk levels
|
|
840
|
+
- Implementing approval hooks with async delays
|
|
841
|
+
- Pausing execution on `requires_action` events
|
|
842
|
+
- Resuming sessions with approval decisions
|
|
843
|
+
- Pretty-printed logging with colors and symbols
|
|
844
|
+
|
|
845
|
+
### Modifying Tool Calls with preToolUse
|
|
846
|
+
|
|
847
|
+
The `preToolUse` hook can also modify tool calls before execution:
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
const session = await agent.createSession({
|
|
851
|
+
hooks: {
|
|
852
|
+
preToolUse: async (ctx) => {
|
|
853
|
+
const toolName = ctx.toolCall.function.name
|
|
854
|
+
|
|
855
|
+
// Example: Add safety constraints to file writes
|
|
856
|
+
if (toolName === 'Write') {
|
|
857
|
+
const args = JSON.parse(ctx.toolCall.function.arguments)
|
|
858
|
+
|
|
859
|
+
// Modify the tool call to add restrictions
|
|
860
|
+
const modifiedToolCall = {
|
|
861
|
+
...ctx.toolCall,
|
|
862
|
+
function: {
|
|
863
|
+
...ctx.toolCall.function,
|
|
864
|
+
arguments: JSON.stringify({
|
|
865
|
+
...args,
|
|
866
|
+
// Force files to be written in a safe directory
|
|
867
|
+
file_path: `/safe-dir/${args.file_path}`,
|
|
868
|
+
}),
|
|
869
|
+
},
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return {
|
|
873
|
+
allow: true,
|
|
874
|
+
modifiedToolCall,
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return { allow: true }
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
})
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
### Tool Context
|
|
885
|
+
|
|
886
|
+
The `toolContext` parameter in `send()` and `receive()` allows passing additional context:
|
|
887
|
+
|
|
888
|
+
```typescript
|
|
889
|
+
session.send('Do something risky', {
|
|
890
|
+
toolContext: {
|
|
891
|
+
approval: {
|
|
892
|
+
strategy: 'high_risk', // Pause on high-risk tools
|
|
893
|
+
// or
|
|
894
|
+
decisions: {
|
|
895
|
+
'tool_call_id_123': { approved: true },
|
|
896
|
+
'tool_call_id_456': { approved: false, reason: 'Too dangerous' },
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
// Your custom context
|
|
900
|
+
custom: { userId: '123', environment: 'production' },
|
|
901
|
+
},
|
|
902
|
+
})
|
|
903
|
+
```
|
|
904
|
+
|
|
483
905
|
## 🧅 Middleware System
|
|
484
906
|
|
|
485
907
|
GoatChain uses a Koa-style onion model for middleware. Each middleware wraps around the core execution:
|
|
@@ -922,7 +1344,7 @@ try {
|
|
|
922
1344
|
session = await agent.resumeSession(sessionId)
|
|
923
1345
|
console.log('Resumed existing session')
|
|
924
1346
|
} catch {
|
|
925
|
-
session = await agent.createSession({
|
|
1347
|
+
session = await agent.createSession({ sessionId })
|
|
926
1348
|
console.log('Created new session')
|
|
927
1349
|
}
|
|
928
1350
|
|
|
@@ -1024,7 +1446,74 @@ console.log(`Total messages: ${session.messages.length}`)
|
|
|
1024
1446
|
console.log(`Total tokens: ${session.usage.totalTokens}`)
|
|
1025
1447
|
```
|
|
1026
1448
|
|
|
1027
|
-
### Example 6:
|
|
1449
|
+
### Example 6: Working Directory with Auto-Approval
|
|
1450
|
+
|
|
1451
|
+
Combine session-level working directory with auto-approval for automated file operations:
|
|
1452
|
+
|
|
1453
|
+
```typescript
|
|
1454
|
+
import { Agent, createModel, createOpenAIAdapter, createBuiltinTools } from 'goatchain'
|
|
1455
|
+
|
|
1456
|
+
const model = createModel({
|
|
1457
|
+
adapter: createOpenAIAdapter({
|
|
1458
|
+
defaultModelId: 'gpt-4o',
|
|
1459
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
1460
|
+
}),
|
|
1461
|
+
})
|
|
1462
|
+
|
|
1463
|
+
const agent = new Agent({
|
|
1464
|
+
name: 'File Agent',
|
|
1465
|
+
systemPrompt: 'You are a file management assistant.',
|
|
1466
|
+
model,
|
|
1467
|
+
tools: createBuiltinTools(), // All file tools included
|
|
1468
|
+
})
|
|
1469
|
+
|
|
1470
|
+
// Set working directory at session creation
|
|
1471
|
+
const session = await agent.createSession({
|
|
1472
|
+
cwd: '/path/to/project',
|
|
1473
|
+
})
|
|
1474
|
+
|
|
1475
|
+
// Auto-approve all file operations for automated workflow
|
|
1476
|
+
session.send('List all TypeScript files, then create a summary.md file', {
|
|
1477
|
+
toolContext: {
|
|
1478
|
+
approval: {
|
|
1479
|
+
autoApprove: true, // No approval prompts - fully automated
|
|
1480
|
+
},
|
|
1481
|
+
},
|
|
1482
|
+
})
|
|
1483
|
+
|
|
1484
|
+
for await (const event of session.receive()) {
|
|
1485
|
+
if (event.type === 'text_delta') {
|
|
1486
|
+
process.stdout.write(event.delta)
|
|
1487
|
+
}
|
|
1488
|
+
else if (event.type === 'tool_call_start') {
|
|
1489
|
+
console.log(`\nExecuting: ${event.toolName}`)
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Change directory and continue with auto-approval
|
|
1494
|
+
session.setCwd('/path/to/another/project')
|
|
1495
|
+
session.send('Analyze the project structure and create a report', {
|
|
1496
|
+
toolContext: {
|
|
1497
|
+
approval: { autoApprove: true },
|
|
1498
|
+
},
|
|
1499
|
+
})
|
|
1500
|
+
|
|
1501
|
+
for await (const event of session.receive()) {
|
|
1502
|
+
if (event.type === 'text_delta') {
|
|
1503
|
+
process.stdout.write(event.delta)
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
**Perfect for:**
|
|
1509
|
+
- CI/CD pipelines that need file analysis
|
|
1510
|
+
- Automated code review bots
|
|
1511
|
+
- Project documentation generators
|
|
1512
|
+
- Bulk file operations without manual intervention
|
|
1513
|
+
|
|
1514
|
+
### Example 7: Tool-level Working Directory (Advanced)
|
|
1515
|
+
|
|
1516
|
+
For more control, you can configure individual tools with specific directories and restrictions:
|
|
1028
1517
|
|
|
1029
1518
|
```typescript
|
|
1030
1519
|
import path from 'node:path'
|
|
@@ -1051,14 +1540,18 @@ const OUTPUT_DIR = path.resolve(process.cwd(), 'output')
|
|
|
1051
1540
|
|
|
1052
1541
|
// Configure tools with working directory and restrictions
|
|
1053
1542
|
const tools = new ToolRegistry()
|
|
1054
|
-
tools.register(
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1543
|
+
tools.register(
|
|
1544
|
+
new ReadTool({
|
|
1545
|
+
cwd: OUTPUT_DIR,
|
|
1546
|
+
allowedDirectory: OUTPUT_DIR, // Sandbox: only allow reads in OUTPUT_DIR
|
|
1547
|
+
}),
|
|
1548
|
+
)
|
|
1549
|
+
tools.register(
|
|
1550
|
+
new WriteTool({
|
|
1551
|
+
cwd: OUTPUT_DIR,
|
|
1552
|
+
allowedDirectory: OUTPUT_DIR, // Sandbox: only allow writes in OUTPUT_DIR
|
|
1553
|
+
}),
|
|
1554
|
+
)
|
|
1062
1555
|
tools.register(new GlobTool({ cwd: OUTPUT_DIR }))
|
|
1063
1556
|
tools.register(new GrepTool({ cwd: OUTPUT_DIR }))
|
|
1064
1557
|
|
|
@@ -1072,7 +1565,7 @@ You can create, read, and modify files within this sandbox.`,
|
|
|
1072
1565
|
})
|
|
1073
1566
|
|
|
1074
1567
|
const session = await agent.createSession()
|
|
1075
|
-
session.send(
|
|
1568
|
+
session.send("Create a report.md file with a summary of today's tasks")
|
|
1076
1569
|
|
|
1077
1570
|
for await (const event of session.receive()) {
|
|
1078
1571
|
if (event.type === 'text_delta') {
|
|
@@ -1224,8 +1717,22 @@ const snapshot = session.toSnapshot()
|
|
|
1224
1717
|
|
|
1225
1718
|
Restore session from snapshot.
|
|
1226
1719
|
|
|
1720
|
+
**`getCwd(): string | undefined`**
|
|
1721
|
+
|
|
1722
|
+
Get the current working directory for this session.
|
|
1723
|
+
|
|
1227
1724
|
```typescript
|
|
1228
|
-
session.
|
|
1725
|
+
const cwd = session.getCwd()
|
|
1726
|
+
console.log('Working directory:', cwd)
|
|
1727
|
+
```
|
|
1728
|
+
|
|
1729
|
+
**`setCwd(cwd: string): void`**
|
|
1730
|
+
|
|
1731
|
+
Set the current working directory for this session. This automatically syncs the new directory to all tools that support it.
|
|
1732
|
+
|
|
1733
|
+
```typescript
|
|
1734
|
+
session.setCwd('/path/to/project')
|
|
1735
|
+
// All file operation tools now use this directory
|
|
1229
1736
|
```
|
|
1230
1737
|
|
|
1231
1738
|
#### Properties
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ModelClient, ModelRef } from '../model';
|
|
2
2
|
import type { BaseSessionManager } from '../session';
|
|
3
3
|
import type { StateStore } from '../state';
|
|
4
|
-
import type { ToolRegistry } from '../tool';
|
|
5
4
|
import type { Middleware, NamedMiddleware } from './middleware';
|
|
6
5
|
import type { AgentLoopState, AgentOptions, CreateSessionOptions, SessionHandleOptions } from './types';
|
|
6
|
+
import { McpServerManager } from '../mcp-client';
|
|
7
7
|
import { Session } from '../session';
|
|
8
|
+
import { ToolRegistry } from '../tool';
|
|
8
9
|
/**
|
|
9
10
|
* Agent class - the main orchestrator.
|
|
10
11
|
*
|
|
@@ -29,12 +30,15 @@ export declare class Agent {
|
|
|
29
30
|
private _enableLogging;
|
|
30
31
|
private _initializationPromise;
|
|
31
32
|
private _initialized;
|
|
33
|
+
private _mcpManager?;
|
|
32
34
|
constructor(options: AgentOptions);
|
|
33
35
|
/**
|
|
34
36
|
* Initialize middleware asynchronously (internal helper for constructor)
|
|
35
37
|
* @private
|
|
36
38
|
*/
|
|
37
39
|
private _initializeMiddleware;
|
|
40
|
+
private _initializeMcpServers;
|
|
41
|
+
private _initializeAll;
|
|
38
42
|
/**
|
|
39
43
|
* Ensure agent is initialized (all middleware registered)
|
|
40
44
|
* @private
|
|
@@ -64,6 +68,10 @@ export declare class Agent {
|
|
|
64
68
|
* Get the tool registry
|
|
65
69
|
*/
|
|
66
70
|
get tools(): ToolRegistry | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Get MCP server manager, if configured.
|
|
73
|
+
*/
|
|
74
|
+
get mcpManager(): McpServerManager | undefined;
|
|
67
75
|
/**
|
|
68
76
|
* Get the state store
|
|
69
77
|
*/
|