@compilr-dev/agents 0.0.1
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 +1277 -0
- package/dist/agent.d.ts +1272 -0
- package/dist/agent.js +1912 -0
- package/dist/anchors/builtin.d.ts +24 -0
- package/dist/anchors/builtin.js +61 -0
- package/dist/anchors/index.d.ts +6 -0
- package/dist/anchors/index.js +5 -0
- package/dist/anchors/manager.d.ts +115 -0
- package/dist/anchors/manager.js +412 -0
- package/dist/anchors/types.d.ts +168 -0
- package/dist/anchors/types.js +10 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +10 -0
- package/dist/context/manager.d.ts +224 -0
- package/dist/context/manager.js +770 -0
- package/dist/context/types.d.ts +377 -0
- package/dist/context/types.js +7 -0
- package/dist/costs/index.d.ts +8 -0
- package/dist/costs/index.js +7 -0
- package/dist/costs/tracker.d.ts +121 -0
- package/dist/costs/tracker.js +295 -0
- package/dist/costs/types.d.ts +157 -0
- package/dist/costs/types.js +8 -0
- package/dist/errors.d.ts +178 -0
- package/dist/errors.js +249 -0
- package/dist/guardrails/builtin.d.ts +27 -0
- package/dist/guardrails/builtin.js +223 -0
- package/dist/guardrails/index.d.ts +6 -0
- package/dist/guardrails/index.js +5 -0
- package/dist/guardrails/manager.d.ts +117 -0
- package/dist/guardrails/manager.js +288 -0
- package/dist/guardrails/types.d.ts +159 -0
- package/dist/guardrails/types.js +7 -0
- package/dist/hooks/index.d.ts +31 -0
- package/dist/hooks/index.js +29 -0
- package/dist/hooks/manager.d.ts +147 -0
- package/dist/hooks/manager.js +600 -0
- package/dist/hooks/types.d.ts +368 -0
- package/dist/hooks/types.js +12 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +73 -0
- package/dist/mcp/client.d.ts +93 -0
- package/dist/mcp/client.js +287 -0
- package/dist/mcp/errors.d.ts +60 -0
- package/dist/mcp/errors.js +78 -0
- package/dist/mcp/index.d.ts +43 -0
- package/dist/mcp/index.js +45 -0
- package/dist/mcp/manager.d.ts +120 -0
- package/dist/mcp/manager.js +276 -0
- package/dist/mcp/tools.d.ts +54 -0
- package/dist/mcp/tools.js +99 -0
- package/dist/mcp/types.d.ts +150 -0
- package/dist/mcp/types.js +40 -0
- package/dist/memory/index.d.ts +8 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/loader.d.ts +114 -0
- package/dist/memory/loader.js +463 -0
- package/dist/memory/types.d.ts +182 -0
- package/dist/memory/types.js +8 -0
- package/dist/messages/index.d.ts +82 -0
- package/dist/messages/index.js +155 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.js +4 -0
- package/dist/permissions/manager.d.ts +125 -0
- package/dist/permissions/manager.js +379 -0
- package/dist/permissions/types.d.ts +162 -0
- package/dist/permissions/types.js +7 -0
- package/dist/providers/claude.d.ts +90 -0
- package/dist/providers/claude.js +348 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +11 -0
- package/dist/providers/mock.d.ts +133 -0
- package/dist/providers/mock.js +204 -0
- package/dist/providers/types.d.ts +168 -0
- package/dist/providers/types.js +4 -0
- package/dist/rate-limit/index.d.ts +45 -0
- package/dist/rate-limit/index.js +47 -0
- package/dist/rate-limit/limiter.d.ts +104 -0
- package/dist/rate-limit/limiter.js +326 -0
- package/dist/rate-limit/provider-wrapper.d.ts +112 -0
- package/dist/rate-limit/provider-wrapper.js +201 -0
- package/dist/rate-limit/retry.d.ts +108 -0
- package/dist/rate-limit/retry.js +287 -0
- package/dist/rate-limit/types.d.ts +181 -0
- package/dist/rate-limit/types.js +22 -0
- package/dist/rehearsal/file-analyzer.d.ts +22 -0
- package/dist/rehearsal/file-analyzer.js +351 -0
- package/dist/rehearsal/git-analyzer.d.ts +22 -0
- package/dist/rehearsal/git-analyzer.js +472 -0
- package/dist/rehearsal/index.d.ts +35 -0
- package/dist/rehearsal/index.js +36 -0
- package/dist/rehearsal/manager.d.ts +100 -0
- package/dist/rehearsal/manager.js +290 -0
- package/dist/rehearsal/types.d.ts +235 -0
- package/dist/rehearsal/types.js +8 -0
- package/dist/skills/index.d.ts +160 -0
- package/dist/skills/index.js +282 -0
- package/dist/state/agent-state.d.ts +41 -0
- package/dist/state/agent-state.js +88 -0
- package/dist/state/checkpointer.d.ts +110 -0
- package/dist/state/checkpointer.js +362 -0
- package/dist/state/errors.d.ts +66 -0
- package/dist/state/errors.js +88 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.js +37 -0
- package/dist/state/serializer.d.ts +55 -0
- package/dist/state/serializer.js +172 -0
- package/dist/state/types.d.ts +312 -0
- package/dist/state/types.js +14 -0
- package/dist/tools/builtin/bash-output.d.ts +61 -0
- package/dist/tools/builtin/bash-output.js +90 -0
- package/dist/tools/builtin/bash.d.ts +150 -0
- package/dist/tools/builtin/bash.js +354 -0
- package/dist/tools/builtin/edit.d.ts +50 -0
- package/dist/tools/builtin/edit.js +215 -0
- package/dist/tools/builtin/glob.d.ts +62 -0
- package/dist/tools/builtin/glob.js +244 -0
- package/dist/tools/builtin/grep.d.ts +74 -0
- package/dist/tools/builtin/grep.js +363 -0
- package/dist/tools/builtin/index.d.ts +44 -0
- package/dist/tools/builtin/index.js +69 -0
- package/dist/tools/builtin/kill-shell.d.ts +44 -0
- package/dist/tools/builtin/kill-shell.js +80 -0
- package/dist/tools/builtin/read-file.d.ts +57 -0
- package/dist/tools/builtin/read-file.js +184 -0
- package/dist/tools/builtin/shell-manager.d.ts +176 -0
- package/dist/tools/builtin/shell-manager.js +337 -0
- package/dist/tools/builtin/task.d.ts +202 -0
- package/dist/tools/builtin/task.js +350 -0
- package/dist/tools/builtin/todo.d.ts +207 -0
- package/dist/tools/builtin/todo.js +453 -0
- package/dist/tools/builtin/utils.d.ts +27 -0
- package/dist/tools/builtin/utils.js +70 -0
- package/dist/tools/builtin/web-fetch.d.ts +96 -0
- package/dist/tools/builtin/web-fetch.js +290 -0
- package/dist/tools/builtin/write-file.d.ts +54 -0
- package/dist/tools/builtin/write-file.js +147 -0
- package/dist/tools/define.d.ts +60 -0
- package/dist/tools/define.js +65 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/registry.d.ts +79 -0
- package/dist/tools/registry.js +151 -0
- package/dist/tools/types.d.ts +59 -0
- package/dist/tools/types.js +4 -0
- package/dist/tracing/hooks.d.ts +58 -0
- package/dist/tracing/hooks.js +377 -0
- package/dist/tracing/index.d.ts +51 -0
- package/dist/tracing/index.js +55 -0
- package/dist/tracing/logging.d.ts +78 -0
- package/dist/tracing/logging.js +310 -0
- package/dist/tracing/manager.d.ts +160 -0
- package/dist/tracing/manager.js +468 -0
- package/dist/tracing/otel.d.ts +102 -0
- package/dist/tracing/otel.js +246 -0
- package/dist/tracing/types.d.ts +346 -0
- package/dist/tracing/types.js +38 -0
- package/dist/utils/index.d.ts +23 -0
- package/dist/utils/index.js +44 -0
- package/package.json +79 -0
package/dist/agent.js
ADDED
|
@@ -0,0 +1,1912 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent - The main class for running AI agents with tool use
|
|
3
|
+
*/
|
|
4
|
+
import { PermissionManager } from './permissions/manager.js';
|
|
5
|
+
import { HooksManager } from './hooks/manager.js';
|
|
6
|
+
import { ProjectMemoryLoader } from './memory/loader.js';
|
|
7
|
+
import { UsageTracker } from './costs/tracker.js';
|
|
8
|
+
import { DefaultToolRegistry } from './tools/registry.js';
|
|
9
|
+
import { ContextManager } from './context/manager.js';
|
|
10
|
+
import { AnchorManager } from './anchors/manager.js';
|
|
11
|
+
import { GuardrailManager } from './guardrails/manager.js';
|
|
12
|
+
import { MaxIterationsError, ToolLoopError } from './errors.js';
|
|
13
|
+
import { generateSessionId, createAgentState, deserializeTodos } from './state/agent-state.js';
|
|
14
|
+
import { getDefaultTodoStore, createIsolatedTodoStore } from './tools/builtin/todo.js';
|
|
15
|
+
/**
|
|
16
|
+
* Agent class - orchestrates LLM interactions with tool use
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const agent = new Agent({
|
|
21
|
+
* provider: new ClaudeProvider({ apiKey: 'sk-...' }),
|
|
22
|
+
* systemPrompt: 'You are a helpful assistant.',
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* agent.registerTool(readFileTool);
|
|
26
|
+
* agent.registerTool(writeFileTool);
|
|
27
|
+
*
|
|
28
|
+
* const result = await agent.run('Read the contents of package.json');
|
|
29
|
+
* console.log(result.response);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class Agent {
|
|
33
|
+
provider;
|
|
34
|
+
systemPrompt;
|
|
35
|
+
maxIterations;
|
|
36
|
+
maxConsecutiveToolCalls;
|
|
37
|
+
iterationLimitBehavior;
|
|
38
|
+
chatOptions;
|
|
39
|
+
toolRegistry;
|
|
40
|
+
contextManager;
|
|
41
|
+
autoContextManagement;
|
|
42
|
+
onEvent;
|
|
43
|
+
// State management
|
|
44
|
+
checkpointer;
|
|
45
|
+
_sessionId;
|
|
46
|
+
autoCheckpoint;
|
|
47
|
+
checkpointOnAbort;
|
|
48
|
+
_createdAt;
|
|
49
|
+
_totalTokensUsed = 0;
|
|
50
|
+
_currentIteration = 0;
|
|
51
|
+
/**
|
|
52
|
+
* Conversation history - persists across run() calls
|
|
53
|
+
*/
|
|
54
|
+
conversationHistory = [];
|
|
55
|
+
/**
|
|
56
|
+
* Registered sub-agents
|
|
57
|
+
*/
|
|
58
|
+
subAgents = new Map();
|
|
59
|
+
/**
|
|
60
|
+
* Anchor manager for critical information that survives context compaction
|
|
61
|
+
*/
|
|
62
|
+
anchorManager;
|
|
63
|
+
/**
|
|
64
|
+
* Guardrail manager for pattern-based safety checks
|
|
65
|
+
*/
|
|
66
|
+
guardrailManager;
|
|
67
|
+
/**
|
|
68
|
+
* Permission manager for tool-level access control
|
|
69
|
+
*/
|
|
70
|
+
permissionManager;
|
|
71
|
+
/**
|
|
72
|
+
* Loaded project memory (instructions from CLAUDE.md, etc.)
|
|
73
|
+
*/
|
|
74
|
+
projectMemory;
|
|
75
|
+
/**
|
|
76
|
+
* Usage tracker for token usage monitoring
|
|
77
|
+
*/
|
|
78
|
+
usageTracker;
|
|
79
|
+
/**
|
|
80
|
+
* Hooks manager for lifecycle hooks
|
|
81
|
+
*/
|
|
82
|
+
hooksManager;
|
|
83
|
+
constructor(config) {
|
|
84
|
+
this.provider = config.provider;
|
|
85
|
+
this.systemPrompt = config.systemPrompt ?? '';
|
|
86
|
+
this.maxIterations = config.maxIterations ?? 10;
|
|
87
|
+
this.maxConsecutiveToolCalls = config.maxConsecutiveToolCalls ?? 3;
|
|
88
|
+
this.iterationLimitBehavior = config.iterationLimitBehavior ?? 'error';
|
|
89
|
+
this.chatOptions = config.chatOptions ?? {};
|
|
90
|
+
this.toolRegistry = config.toolRegistry ?? new DefaultToolRegistry();
|
|
91
|
+
this.contextManager = config.contextManager;
|
|
92
|
+
this.autoContextManagement =
|
|
93
|
+
config.autoContextManagement ?? config.contextManager !== undefined;
|
|
94
|
+
this.onEvent = config.onEvent;
|
|
95
|
+
// State management
|
|
96
|
+
this.checkpointer = config.checkpointer;
|
|
97
|
+
this._sessionId = config.sessionId ?? generateSessionId();
|
|
98
|
+
this.autoCheckpoint = config.autoCheckpoint ?? false;
|
|
99
|
+
this.checkpointOnAbort = config.checkpointOnAbort ?? false;
|
|
100
|
+
this._createdAt = new Date().toISOString();
|
|
101
|
+
// Anchor management
|
|
102
|
+
if (config.anchors !== undefined) {
|
|
103
|
+
this.anchorManager = new AnchorManager(config.anchors);
|
|
104
|
+
}
|
|
105
|
+
// Guardrail management
|
|
106
|
+
if (config.guardrails !== undefined) {
|
|
107
|
+
this.guardrailManager = new GuardrailManager(config.guardrails);
|
|
108
|
+
}
|
|
109
|
+
// Permission management
|
|
110
|
+
if (config.permissions !== undefined) {
|
|
111
|
+
this.permissionManager = new PermissionManager(config.permissions);
|
|
112
|
+
}
|
|
113
|
+
// Project memory - store and prepend to system prompt
|
|
114
|
+
if (config.projectMemory !== undefined) {
|
|
115
|
+
this.projectMemory = config.projectMemory;
|
|
116
|
+
// Prepend memory content to system prompt
|
|
117
|
+
if (config.projectMemory.content) {
|
|
118
|
+
const memorySection = [
|
|
119
|
+
'# Project Instructions',
|
|
120
|
+
'',
|
|
121
|
+
config.projectMemory.content,
|
|
122
|
+
'',
|
|
123
|
+
'---',
|
|
124
|
+
'',
|
|
125
|
+
].join('\n');
|
|
126
|
+
this.systemPrompt = memorySection + this.systemPrompt;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Usage tracking - initialize with session ID
|
|
130
|
+
if (config.usage !== undefined) {
|
|
131
|
+
this.usageTracker = new UsageTracker({
|
|
132
|
+
...config.usage,
|
|
133
|
+
sessionId: config.usage.sessionId ?? this._sessionId,
|
|
134
|
+
});
|
|
135
|
+
// Forward usage events to agent event handler
|
|
136
|
+
this.usageTracker.onEvent((event) => {
|
|
137
|
+
if (event.type === 'usage:recorded') {
|
|
138
|
+
this.onEvent?.({
|
|
139
|
+
type: 'usage_recorded',
|
|
140
|
+
tokens: event.record.tokens,
|
|
141
|
+
model: event.record.model,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else if (event.type === 'usage:budget_warning') {
|
|
145
|
+
this.onEvent?.({
|
|
146
|
+
type: 'usage_budget_warning',
|
|
147
|
+
status: event.status,
|
|
148
|
+
threshold: event.threshold,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
else if (event.type === 'usage:budget_exceeded') {
|
|
152
|
+
this.onEvent?.({
|
|
153
|
+
type: 'usage_budget_exceeded',
|
|
154
|
+
status: event.status,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Hooks manager
|
|
160
|
+
if (config.hooks !== undefined) {
|
|
161
|
+
this.hooksManager = new HooksManager({ hooks: config.hooks });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ==========================================================================
|
|
165
|
+
// Static Factory Methods
|
|
166
|
+
// ==========================================================================
|
|
167
|
+
/**
|
|
168
|
+
* Create an agent with project memory loaded from files.
|
|
169
|
+
*
|
|
170
|
+
* This factory method automatically discovers and loads project-specific
|
|
171
|
+
* instructions from files like CLAUDE.md, GEMINI.md, PROJECT.md, etc.
|
|
172
|
+
*
|
|
173
|
+
* @param config - Agent configuration
|
|
174
|
+
* @param memoryOptions - Project memory loading options
|
|
175
|
+
* @param memoryDir - Directory to search for memory files (defaults to cwd)
|
|
176
|
+
* @returns Agent instance with loaded project memory
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* // Load Claude-specific instructions
|
|
181
|
+
* const agent = await Agent.createWithMemory(
|
|
182
|
+
* {
|
|
183
|
+
* provider,
|
|
184
|
+
* systemPrompt: 'You are a helpful assistant.',
|
|
185
|
+
* },
|
|
186
|
+
* { providers: 'claude' },
|
|
187
|
+
* '/path/to/project'
|
|
188
|
+
* );
|
|
189
|
+
*
|
|
190
|
+
* // Load instructions for multiple providers
|
|
191
|
+
* const agent = await Agent.createWithMemory(
|
|
192
|
+
* { provider },
|
|
193
|
+
* { providers: ['claude', 'gemini'], includeGeneric: true }
|
|
194
|
+
* );
|
|
195
|
+
*
|
|
196
|
+
* // Access loaded memory
|
|
197
|
+
* const memory = agent.getProjectMemory();
|
|
198
|
+
* console.log(`Loaded ${memory?.files.length} memory files`);
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
static async createWithMemory(config, memoryOptions, memoryDir) {
|
|
202
|
+
const loader = new ProjectMemoryLoader(memoryOptions);
|
|
203
|
+
const memory = await loader.load(memoryDir ?? process.cwd());
|
|
204
|
+
return new Agent({
|
|
205
|
+
...config,
|
|
206
|
+
projectMemory: memory,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// ==========================================================================
|
|
210
|
+
// Session ID getter
|
|
211
|
+
// ==========================================================================
|
|
212
|
+
/**
|
|
213
|
+
* Get the session ID for this agent instance
|
|
214
|
+
*/
|
|
215
|
+
get sessionId() {
|
|
216
|
+
return this._sessionId;
|
|
217
|
+
}
|
|
218
|
+
// ==========================================================================
|
|
219
|
+
// Project Memory - Access loaded project instructions
|
|
220
|
+
// ==========================================================================
|
|
221
|
+
/**
|
|
222
|
+
* Get the loaded project memory (if any).
|
|
223
|
+
*
|
|
224
|
+
* Project memory contains instructions loaded from files like
|
|
225
|
+
* CLAUDE.md, GEMINI.md, PROJECT.md, etc.
|
|
226
|
+
*
|
|
227
|
+
* @returns The loaded project memory, or undefined if none was loaded
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```typescript
|
|
231
|
+
* const memory = agent.getProjectMemory();
|
|
232
|
+
* if (memory) {
|
|
233
|
+
* console.log(`Loaded ${memory.files.length} instruction files`);
|
|
234
|
+
* console.log(`Total tokens: ~${memory.estimatedTokens}`);
|
|
235
|
+
* for (const file of memory.files) {
|
|
236
|
+
* console.log(` - ${file.relativePath}`);
|
|
237
|
+
* }
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
getProjectMemory() {
|
|
242
|
+
return this.projectMemory;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Check if project memory was loaded
|
|
246
|
+
*/
|
|
247
|
+
hasProjectMemory() {
|
|
248
|
+
return this.projectMemory !== undefined && this.projectMemory.files.length > 0;
|
|
249
|
+
}
|
|
250
|
+
// ==========================================================================
|
|
251
|
+
// Usage Tracking - Token usage monitoring
|
|
252
|
+
// ==========================================================================
|
|
253
|
+
/**
|
|
254
|
+
* Get usage tracking statistics.
|
|
255
|
+
*
|
|
256
|
+
* @returns Usage statistics or undefined if usage tracking is not enabled
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* const stats = agent.getUsageStats();
|
|
261
|
+
* if (stats) {
|
|
262
|
+
* console.log(`Total calls: ${stats.totalCalls}`);
|
|
263
|
+
* console.log(`Total tokens: ${stats.totalTokens}`);
|
|
264
|
+
* console.log(`Input tokens: ${stats.totalInputTokens}`);
|
|
265
|
+
* console.log(`Output tokens: ${stats.totalOutputTokens}`);
|
|
266
|
+
* }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
getUsageStats() {
|
|
270
|
+
return this.usageTracker?.getStats();
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get total tokens used across all LLM calls.
|
|
274
|
+
*/
|
|
275
|
+
getTotalTokens() {
|
|
276
|
+
return this.usageTracker?.getTotalTokens() ?? 0;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get total input tokens used across all LLM calls.
|
|
280
|
+
*/
|
|
281
|
+
getTotalInputTokens() {
|
|
282
|
+
return this.usageTracker?.getTotalInputTokens() ?? 0;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get total output tokens used across all LLM calls.
|
|
286
|
+
*/
|
|
287
|
+
getTotalOutputTokens() {
|
|
288
|
+
return this.usageTracker?.getTotalOutputTokens() ?? 0;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get budget status.
|
|
292
|
+
*/
|
|
293
|
+
getBudgetStatus() {
|
|
294
|
+
return this.usageTracker?.getBudgetStatus();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if budget is exceeded.
|
|
298
|
+
*/
|
|
299
|
+
isBudgetExceeded() {
|
|
300
|
+
return this.usageTracker?.isBudgetExceeded() ?? false;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Get a human-readable usage summary.
|
|
304
|
+
*/
|
|
305
|
+
getUsageSummary() {
|
|
306
|
+
return this.usageTracker?.getSummary();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Reset usage tracking data.
|
|
310
|
+
*/
|
|
311
|
+
resetUsageTracking() {
|
|
312
|
+
this.usageTracker?.reset();
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Check if usage tracking is enabled.
|
|
316
|
+
*/
|
|
317
|
+
hasUsageTracking() {
|
|
318
|
+
return this.usageTracker !== undefined && this.usageTracker.isEnabled;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Record usage manually (for custom provider integrations).
|
|
322
|
+
*
|
|
323
|
+
* @internal
|
|
324
|
+
*/
|
|
325
|
+
recordUsage(model, provider, tokens) {
|
|
326
|
+
this.usageTracker?.record({ model, provider, tokens });
|
|
327
|
+
}
|
|
328
|
+
// ==========================================================================
|
|
329
|
+
// Anchor Management - Critical information that survives context compaction
|
|
330
|
+
// ==========================================================================
|
|
331
|
+
/**
|
|
332
|
+
* Add an anchor (critical information that survives context compaction).
|
|
333
|
+
*
|
|
334
|
+
* Anchors are injected into every LLM call and never get compacted.
|
|
335
|
+
* Use them for information that must not be forgotten.
|
|
336
|
+
*
|
|
337
|
+
* @param input - Anchor input
|
|
338
|
+
* @returns The created anchor, or undefined if anchors are not enabled
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```typescript
|
|
342
|
+
* // Session anchor - lives for this session only
|
|
343
|
+
* agent.addAnchor({
|
|
344
|
+
* content: 'This session we implemented: edit tool, grep tool',
|
|
345
|
+
* priority: 'critical',
|
|
346
|
+
* scope: 'session',
|
|
347
|
+
* });
|
|
348
|
+
*
|
|
349
|
+
* // Safety anchor - persisted across sessions
|
|
350
|
+
* agent.addAnchor({
|
|
351
|
+
* content: 'Never delete files in /important without confirmation',
|
|
352
|
+
* priority: 'safety',
|
|
353
|
+
* scope: 'persistent',
|
|
354
|
+
* });
|
|
355
|
+
*
|
|
356
|
+
* // Temporary anchor - expires after 1 hour
|
|
357
|
+
* agent.addAnchor({
|
|
358
|
+
* content: 'Currently working on feature X',
|
|
359
|
+
* priority: 'info',
|
|
360
|
+
* scope: 'temporary',
|
|
361
|
+
* expiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
|
362
|
+
* });
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
addAnchor(input) {
|
|
366
|
+
if (!this.anchorManager)
|
|
367
|
+
return undefined;
|
|
368
|
+
const anchor = this.anchorManager.add(input);
|
|
369
|
+
this.onEvent?.({ type: 'anchor_added', anchor });
|
|
370
|
+
return anchor;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get an anchor by ID
|
|
374
|
+
*/
|
|
375
|
+
getAnchor(id) {
|
|
376
|
+
return this.anchorManager?.get(id);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get all anchors, optionally filtered
|
|
380
|
+
*
|
|
381
|
+
* @param options - Query options for filtering
|
|
382
|
+
* @returns Array of anchors
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```typescript
|
|
386
|
+
* // Get all anchors
|
|
387
|
+
* const all = agent.getAnchors();
|
|
388
|
+
*
|
|
389
|
+
* // Get only safety anchors
|
|
390
|
+
* const safety = agent.getAnchors({ priority: 'safety' });
|
|
391
|
+
*
|
|
392
|
+
* // Get session anchors with specific tags
|
|
393
|
+
* const tagged = agent.getAnchors({ scope: 'session', tags: ['files'] });
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
396
|
+
getAnchors(options) {
|
|
397
|
+
return this.anchorManager?.getAll(options) ?? [];
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Check if an anchor exists
|
|
401
|
+
*/
|
|
402
|
+
hasAnchor(id) {
|
|
403
|
+
return this.anchorManager?.has(id) ?? false;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Remove an anchor by ID
|
|
407
|
+
*
|
|
408
|
+
* @returns true if anchor was removed, false if not found
|
|
409
|
+
*/
|
|
410
|
+
removeAnchor(id) {
|
|
411
|
+
if (!this.anchorManager)
|
|
412
|
+
return false;
|
|
413
|
+
const removed = this.anchorManager.remove(id);
|
|
414
|
+
if (removed) {
|
|
415
|
+
this.onEvent?.({ type: 'anchor_removed', anchorId: id });
|
|
416
|
+
}
|
|
417
|
+
return removed;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Clear anchors based on criteria
|
|
421
|
+
*
|
|
422
|
+
* @param options - Clear options for filtering which anchors to remove
|
|
423
|
+
* @returns Number of anchors removed
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* // Clear all session anchors
|
|
428
|
+
* agent.clearAnchors({ scope: 'session' });
|
|
429
|
+
*
|
|
430
|
+
* // Clear expired temporary anchors
|
|
431
|
+
* agent.clearAnchors({ expiredOnly: true });
|
|
432
|
+
*
|
|
433
|
+
* // Clear anchors with specific tags
|
|
434
|
+
* agent.clearAnchors({ tags: ['auto'] });
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
clearAnchors(options) {
|
|
438
|
+
return this.anchorManager?.clear(options) ?? 0;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get the anchor manager (if configured)
|
|
442
|
+
*/
|
|
443
|
+
getAnchorManager() {
|
|
444
|
+
return this.anchorManager;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Check if anchors are enabled
|
|
448
|
+
*/
|
|
449
|
+
hasAnchors() {
|
|
450
|
+
return this.anchorManager !== undefined;
|
|
451
|
+
}
|
|
452
|
+
// ==========================================================================
|
|
453
|
+
// Guardrail Management - Pattern-based safety checks
|
|
454
|
+
// ==========================================================================
|
|
455
|
+
/**
|
|
456
|
+
* Add a custom guardrail
|
|
457
|
+
*
|
|
458
|
+
* @param input - Guardrail definition
|
|
459
|
+
* @returns The created guardrail, or undefined if guardrails are not enabled
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```typescript
|
|
463
|
+
* agent.addGuardrail({
|
|
464
|
+
* id: 'no-delete-important',
|
|
465
|
+
* name: 'Important Files Protection',
|
|
466
|
+
* description: 'Prevent deletion of important files',
|
|
467
|
+
* patterns: [/rm.*important/i, /delete.*important/i],
|
|
468
|
+
* action: 'block',
|
|
469
|
+
* message: 'Cannot delete files marked as important',
|
|
470
|
+
* scope: ['bash'],
|
|
471
|
+
* });
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
addGuardrail(input) {
|
|
475
|
+
return this.guardrailManager?.add(input);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Get a guardrail by ID
|
|
479
|
+
*/
|
|
480
|
+
getGuardrail(id) {
|
|
481
|
+
return this.guardrailManager?.get(id);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get all guardrails
|
|
485
|
+
*/
|
|
486
|
+
getGuardrails() {
|
|
487
|
+
return this.guardrailManager?.getAll() ?? [];
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Check if a guardrail exists
|
|
491
|
+
*/
|
|
492
|
+
hasGuardrail(id) {
|
|
493
|
+
return this.guardrailManager?.has(id) ?? false;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Remove a guardrail by ID
|
|
497
|
+
*/
|
|
498
|
+
removeGuardrail(id) {
|
|
499
|
+
return this.guardrailManager?.remove(id) ?? false;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Enable a guardrail by ID
|
|
503
|
+
*/
|
|
504
|
+
enableGuardrail(id) {
|
|
505
|
+
return this.guardrailManager?.enable(id) ?? false;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Disable a guardrail by ID
|
|
509
|
+
*/
|
|
510
|
+
disableGuardrail(id) {
|
|
511
|
+
return this.guardrailManager?.disable(id) ?? false;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Get the guardrail manager (if configured)
|
|
515
|
+
*/
|
|
516
|
+
getGuardrailManager() {
|
|
517
|
+
return this.guardrailManager;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Check if guardrails are enabled
|
|
521
|
+
*/
|
|
522
|
+
hasGuardrails() {
|
|
523
|
+
return this.guardrailManager !== undefined;
|
|
524
|
+
}
|
|
525
|
+
// ==========================================================================
|
|
526
|
+
// Permission Management - Tool-level access control
|
|
527
|
+
// ==========================================================================
|
|
528
|
+
/**
|
|
529
|
+
* Add a permission rule for a tool
|
|
530
|
+
*
|
|
531
|
+
* @param rule - Permission rule to add
|
|
532
|
+
* @returns this for chaining
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```typescript
|
|
536
|
+
* agent.addPermission({
|
|
537
|
+
* toolName: 'bash',
|
|
538
|
+
* level: 'once',
|
|
539
|
+
* description: 'Execute shell commands',
|
|
540
|
+
* });
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
addPermission(rule) {
|
|
544
|
+
this.permissionManager?.addRule(rule);
|
|
545
|
+
return this;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Remove a permission rule by tool name
|
|
549
|
+
*/
|
|
550
|
+
removePermission(toolName) {
|
|
551
|
+
return this.permissionManager?.removeRule(toolName) ?? false;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Get a permission rule by tool name
|
|
555
|
+
*/
|
|
556
|
+
getPermission(toolName) {
|
|
557
|
+
return this.permissionManager?.getRule(toolName);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get all permission rules
|
|
561
|
+
*/
|
|
562
|
+
getPermissions() {
|
|
563
|
+
return this.permissionManager?.getAllRules() ?? [];
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Set the permission level for a tool
|
|
567
|
+
*
|
|
568
|
+
* @param toolName - Tool name or pattern
|
|
569
|
+
* @param level - Permission level
|
|
570
|
+
* @param description - Optional description
|
|
571
|
+
*/
|
|
572
|
+
setPermissionLevel(toolName, level, description) {
|
|
573
|
+
this.permissionManager?.setLevel(toolName, level, description);
|
|
574
|
+
return this;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Get the effective permission level for a tool
|
|
578
|
+
*/
|
|
579
|
+
getPermissionLevel(toolName) {
|
|
580
|
+
return this.permissionManager?.getLevel(toolName) ?? 'always';
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Grant session-level permission for a tool
|
|
584
|
+
*/
|
|
585
|
+
grantSessionPermission(toolName) {
|
|
586
|
+
this.permissionManager?.grantSession(toolName);
|
|
587
|
+
return this;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Revoke session-level permission for a tool
|
|
591
|
+
*/
|
|
592
|
+
revokeSessionPermission(toolName) {
|
|
593
|
+
return this.permissionManager?.revokeSession(toolName) ?? false;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Clear all session-level permissions
|
|
597
|
+
*/
|
|
598
|
+
clearSessionPermissions() {
|
|
599
|
+
this.permissionManager?.clearSessionGrants();
|
|
600
|
+
return this;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Get all tools with session-level permission
|
|
604
|
+
*/
|
|
605
|
+
getSessionPermissions() {
|
|
606
|
+
return this.permissionManager?.getSessionGrants() ?? [];
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Get the permission manager (if configured)
|
|
610
|
+
*/
|
|
611
|
+
getPermissionManager() {
|
|
612
|
+
return this.permissionManager;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Check if permissions are enabled
|
|
616
|
+
*/
|
|
617
|
+
hasPermissions() {
|
|
618
|
+
return this.permissionManager !== undefined;
|
|
619
|
+
}
|
|
620
|
+
// ==========================================================================
|
|
621
|
+
// Custom Event Streaming
|
|
622
|
+
// ==========================================================================
|
|
623
|
+
/**
|
|
624
|
+
* Emit a custom event that will be streamed to event handlers.
|
|
625
|
+
*
|
|
626
|
+
* This allows tools, middleware, and user code to emit custom events
|
|
627
|
+
* that are streamed alongside built-in agent events.
|
|
628
|
+
*
|
|
629
|
+
* Inspired by LangGraph's get_stream_writer() pattern.
|
|
630
|
+
* Addresses issues like LangGraph #6330 (preserve event metadata).
|
|
631
|
+
*
|
|
632
|
+
* @param config - Custom event configuration
|
|
633
|
+
*
|
|
634
|
+
* @example
|
|
635
|
+
* ```typescript
|
|
636
|
+
* agent.emitCustomEvent({
|
|
637
|
+
* name: 'progress',
|
|
638
|
+
* data: { step: 1, total: 5, message: 'Processing...' },
|
|
639
|
+
* metadata: { toolName: 'myTool' },
|
|
640
|
+
* });
|
|
641
|
+
* ```
|
|
642
|
+
*/
|
|
643
|
+
emitCustomEvent(config) {
|
|
644
|
+
this.onEvent?.({
|
|
645
|
+
type: 'custom',
|
|
646
|
+
name: config.name,
|
|
647
|
+
data: config.data,
|
|
648
|
+
metadata: config.metadata,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Get a stream writer function for emitting custom events.
|
|
653
|
+
*
|
|
654
|
+
* This returns a simple function that can be passed to tools or
|
|
655
|
+
* middleware for streaming progress updates.
|
|
656
|
+
*
|
|
657
|
+
* @param eventName - Name for all events emitted by this writer
|
|
658
|
+
* @returns A stream writer function
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```typescript
|
|
662
|
+
* const writer = agent.getStreamWriter('myTool');
|
|
663
|
+
* writer('Starting...', { phase: 'init' });
|
|
664
|
+
* writer('Processing...', { phase: 'work', progress: 50 });
|
|
665
|
+
* writer('Done!', { phase: 'complete' });
|
|
666
|
+
* ```
|
|
667
|
+
*/
|
|
668
|
+
getStreamWriter(eventName = 'stream') {
|
|
669
|
+
return (data, metadata) => {
|
|
670
|
+
this.emitCustomEvent({ name: eventName, data, metadata });
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Clear conversation history to start fresh
|
|
675
|
+
*/
|
|
676
|
+
clearHistory() {
|
|
677
|
+
this.conversationHistory = [];
|
|
678
|
+
this.contextManager?.reset();
|
|
679
|
+
return this;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Get the current conversation history
|
|
683
|
+
*/
|
|
684
|
+
getHistory() {
|
|
685
|
+
return [...this.conversationHistory];
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Get the context manager (if configured)
|
|
689
|
+
*/
|
|
690
|
+
getContextManager() {
|
|
691
|
+
return this.contextManager;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Get context statistics
|
|
695
|
+
*/
|
|
696
|
+
getContextStats() {
|
|
697
|
+
return this.contextManager?.getStats(this.conversationHistory.length);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Get current verbosity level based on context pressure
|
|
701
|
+
*/
|
|
702
|
+
getVerbosityLevel() {
|
|
703
|
+
return this.contextManager?.getVerbosityLevel() ?? 'full';
|
|
704
|
+
}
|
|
705
|
+
// ==========================================================================
|
|
706
|
+
// State Management
|
|
707
|
+
// ==========================================================================
|
|
708
|
+
/**
|
|
709
|
+
* Serialize the current agent state to an AgentState object.
|
|
710
|
+
* This can be used for manual persistence or transferring state.
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* ```typescript
|
|
714
|
+
* const state = agent.serialize();
|
|
715
|
+
* const json = JSON.stringify(state);
|
|
716
|
+
* // Store json somewhere...
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
719
|
+
serialize() {
|
|
720
|
+
const todoStore = getDefaultTodoStore();
|
|
721
|
+
return createAgentState({
|
|
722
|
+
sessionId: this._sessionId,
|
|
723
|
+
systemPrompt: this.systemPrompt,
|
|
724
|
+
model: this.chatOptions.model,
|
|
725
|
+
messages: this.conversationHistory,
|
|
726
|
+
todos: todoStore.getAll(),
|
|
727
|
+
currentIteration: this._currentIteration,
|
|
728
|
+
totalTokensUsed: this._totalTokensUsed,
|
|
729
|
+
createdAt: this._createdAt,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Save the current state using the configured checkpointer.
|
|
734
|
+
* Throws if no checkpointer is configured.
|
|
735
|
+
*
|
|
736
|
+
* @param metadata - Optional metadata overrides
|
|
737
|
+
* @returns The session ID
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```typescript
|
|
741
|
+
* const agent = new Agent({
|
|
742
|
+
* provider,
|
|
743
|
+
* checkpointer: new FileCheckpointer('~/.myapp/sessions/'),
|
|
744
|
+
* });
|
|
745
|
+
*
|
|
746
|
+
* await agent.run('Hello!');
|
|
747
|
+
* const sessionId = await agent.checkpoint();
|
|
748
|
+
* console.log(`Saved as: ${sessionId}`);
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
async checkpoint(metadata) {
|
|
752
|
+
if (!this.checkpointer) {
|
|
753
|
+
throw new Error('No checkpointer configured. Provide a checkpointer in AgentConfig.');
|
|
754
|
+
}
|
|
755
|
+
const state = this.serialize();
|
|
756
|
+
await this.checkpointer.save(this._sessionId, state, metadata);
|
|
757
|
+
return this._sessionId;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Check if a checkpointer is configured
|
|
761
|
+
*/
|
|
762
|
+
hasCheckpointer() {
|
|
763
|
+
return this.checkpointer !== undefined;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Save a partial checkpoint on abort or error.
|
|
767
|
+
* This is called internally when checkpointOnAbort is enabled.
|
|
768
|
+
*
|
|
769
|
+
* @param reason - Why the checkpoint was saved ('aborted' or 'error')
|
|
770
|
+
* @param emit - Event emitter function
|
|
771
|
+
* @returns Promise that resolves when checkpoint is saved
|
|
772
|
+
*/
|
|
773
|
+
async saveAbortCheckpoint(reason, emit) {
|
|
774
|
+
if (!this.checkpointOnAbort || !this.checkpointer) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
const sessionId = await this.checkpoint({
|
|
779
|
+
tags: ['partial', reason],
|
|
780
|
+
});
|
|
781
|
+
emit({ type: 'abort_checkpoint_saved', sessionId, reason });
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
emit({
|
|
785
|
+
type: 'abort_checkpoint_failed',
|
|
786
|
+
error: error instanceof Error ? error.message : String(error),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Resume an agent from a saved session.
|
|
792
|
+
*
|
|
793
|
+
* @param sessionId - Session ID to resume
|
|
794
|
+
* @param options - Resume options (provider and checkpointer required)
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```typescript
|
|
798
|
+
* const checkpointer = new FileCheckpointer('~/.myapp/sessions/');
|
|
799
|
+
*
|
|
800
|
+
* // Resume a previous session
|
|
801
|
+
* const agent = await Agent.resume('session_abc123', {
|
|
802
|
+
* provider: new ClaudeProvider({ apiKey: '...' }),
|
|
803
|
+
* checkpointer,
|
|
804
|
+
* });
|
|
805
|
+
*
|
|
806
|
+
* // Continue the conversation
|
|
807
|
+
* await agent.run('Continue where we left off...');
|
|
808
|
+
* ```
|
|
809
|
+
*/
|
|
810
|
+
static async resume(sessionId, options) {
|
|
811
|
+
const state = await options.checkpointer.load(sessionId);
|
|
812
|
+
if (!state) {
|
|
813
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
814
|
+
}
|
|
815
|
+
return Agent.fromState(state, {
|
|
816
|
+
provider: options.provider,
|
|
817
|
+
checkpointer: options.checkpointer,
|
|
818
|
+
tools: options.tools,
|
|
819
|
+
onEvent: options.onEvent,
|
|
820
|
+
systemPrompt: options.systemPrompt,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Create an agent from a serialized AgentState object.
|
|
825
|
+
*
|
|
826
|
+
* @param state - The serialized agent state
|
|
827
|
+
* @param options - Options for the new agent
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* ```typescript
|
|
831
|
+
* // Load state from somewhere
|
|
832
|
+
* const json = await fs.readFile('session.json', 'utf-8');
|
|
833
|
+
* const state = JSON.parse(json);
|
|
834
|
+
*
|
|
835
|
+
* // Create agent from state
|
|
836
|
+
* const agent = Agent.fromState(state, { provider });
|
|
837
|
+
* await agent.run('Continue...');
|
|
838
|
+
* ```
|
|
839
|
+
*/
|
|
840
|
+
static fromState(state, options) {
|
|
841
|
+
// Create the agent with restored state
|
|
842
|
+
const agent = new Agent({
|
|
843
|
+
provider: options.provider,
|
|
844
|
+
systemPrompt: options.systemPrompt ?? state.systemPrompt,
|
|
845
|
+
checkpointer: options.checkpointer,
|
|
846
|
+
sessionId: state.sessionId,
|
|
847
|
+
onEvent: options.onEvent,
|
|
848
|
+
chatOptions: state.model ? { model: state.model } : undefined,
|
|
849
|
+
});
|
|
850
|
+
// Restore conversation history
|
|
851
|
+
agent.conversationHistory = [...state.messages];
|
|
852
|
+
// Restore internal state
|
|
853
|
+
agent._createdAt = state.createdAt;
|
|
854
|
+
agent._totalTokensUsed = state.totalTokensUsed;
|
|
855
|
+
agent._currentIteration = state.currentIteration;
|
|
856
|
+
// Restore todos
|
|
857
|
+
if (state.todos.length > 0) {
|
|
858
|
+
const todoStore = getDefaultTodoStore();
|
|
859
|
+
const deserializedTodos = deserializeTodos(state.todos);
|
|
860
|
+
for (const todo of deserializedTodos) {
|
|
861
|
+
todoStore.add(todo);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
// Register tools if provided
|
|
865
|
+
if (options.tools) {
|
|
866
|
+
for (const tool of options.tools) {
|
|
867
|
+
agent.registerTool(tool);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return agent;
|
|
871
|
+
}
|
|
872
|
+
// ==========================================================================
|
|
873
|
+
// Sub-Agent Management
|
|
874
|
+
// ==========================================================================
|
|
875
|
+
/**
|
|
876
|
+
* Create and register a sub-agent with isolated context.
|
|
877
|
+
*
|
|
878
|
+
* Sub-agents are specialized agents that handle discrete tasks independently.
|
|
879
|
+
* They have their own context window and can have different tools/permissions.
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* ```typescript
|
|
883
|
+
* agent.createSubAgent({
|
|
884
|
+
* name: 'code-reviewer',
|
|
885
|
+
* description: 'Reviews code for security and quality issues',
|
|
886
|
+
* systemPrompt: 'You are a code review specialist...',
|
|
887
|
+
* tools: [readFileTool], // Restricted tools
|
|
888
|
+
* contextMode: 'isolated',
|
|
889
|
+
* });
|
|
890
|
+
*
|
|
891
|
+
* const result = await agent.runSubAgent('code-reviewer', 'Review src/auth.ts');
|
|
892
|
+
* ```
|
|
893
|
+
*/
|
|
894
|
+
createSubAgent(config) {
|
|
895
|
+
// Calculate context budget for sub-agent
|
|
896
|
+
const parentMaxTokens = this.contextManager?.getMaxTokens() ?? 200000;
|
|
897
|
+
const subAgentMaxTokens = config.contextBudget ?? Math.floor(parentMaxTokens * 0.25);
|
|
898
|
+
// Create context manager for sub-agent (always isolated)
|
|
899
|
+
const subAgentContextManager = new ContextManager({
|
|
900
|
+
provider: this.provider,
|
|
901
|
+
config: {
|
|
902
|
+
maxContextTokens: subAgentMaxTokens,
|
|
903
|
+
},
|
|
904
|
+
});
|
|
905
|
+
// Create tool registry for sub-agent
|
|
906
|
+
const subAgentToolRegistry = new DefaultToolRegistry();
|
|
907
|
+
// If tools specified, use those; otherwise inherit from parent
|
|
908
|
+
const toolsToRegister = config.tools ??
|
|
909
|
+
this.toolRegistry
|
|
910
|
+
.getDefinitions()
|
|
911
|
+
.map((def) => {
|
|
912
|
+
const parentTool = this.toolRegistry.get(def.name);
|
|
913
|
+
return parentTool;
|
|
914
|
+
})
|
|
915
|
+
.filter((t) => t !== undefined);
|
|
916
|
+
for (const tool of toolsToRegister) {
|
|
917
|
+
subAgentToolRegistry.register(tool);
|
|
918
|
+
}
|
|
919
|
+
// Create the sub-agent
|
|
920
|
+
const subAgent = new Agent({
|
|
921
|
+
provider: this.provider,
|
|
922
|
+
systemPrompt: config.systemPrompt ?? this.systemPrompt,
|
|
923
|
+
maxIterations: config.maxIterations ?? 5,
|
|
924
|
+
chatOptions: config.chatOptions ?? this.chatOptions,
|
|
925
|
+
toolRegistry: subAgentToolRegistry,
|
|
926
|
+
contextManager: subAgentContextManager,
|
|
927
|
+
autoContextManagement: true,
|
|
928
|
+
onEvent: this.onEvent, // Forward events to parent
|
|
929
|
+
});
|
|
930
|
+
// Store the sub-agent
|
|
931
|
+
this.subAgents.set(config.name, { config, agent: subAgent });
|
|
932
|
+
return this;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Run a sub-agent with a specific task.
|
|
936
|
+
*
|
|
937
|
+
* The sub-agent executes independently with its own context and returns
|
|
938
|
+
* the result to the parent agent.
|
|
939
|
+
*
|
|
940
|
+
* @param name - Name of the registered sub-agent
|
|
941
|
+
* @param task - Task description for the sub-agent
|
|
942
|
+
* @param options - Optional run options
|
|
943
|
+
*/
|
|
944
|
+
async runSubAgent(name, task, options) {
|
|
945
|
+
const entry = this.subAgents.get(name);
|
|
946
|
+
if (!entry) {
|
|
947
|
+
return {
|
|
948
|
+
name,
|
|
949
|
+
response: '',
|
|
950
|
+
success: false,
|
|
951
|
+
error: `Sub-agent '${name}' not found. Register it first with createSubAgent().`,
|
|
952
|
+
iterations: 0,
|
|
953
|
+
toolCalls: [],
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const { config, agent } = entry;
|
|
957
|
+
// Emit sub-agent start event
|
|
958
|
+
this.onEvent?.({ type: 'subagent_start', name, task });
|
|
959
|
+
try {
|
|
960
|
+
// Prepare context based on mode
|
|
961
|
+
let contextPrefix = '';
|
|
962
|
+
if (config.contextMode === 'inherited') {
|
|
963
|
+
// Generate a summary of parent context for the sub-agent
|
|
964
|
+
const parentHistory = this.getHistory();
|
|
965
|
+
if (parentHistory.length > 0) {
|
|
966
|
+
contextPrefix = this.generateContextSummary(parentHistory);
|
|
967
|
+
contextPrefix = `[Parent Context Summary]\n${contextPrefix}\n\n[Your Task]\n`;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else if (config.contextMode === 'shared') {
|
|
971
|
+
// Copy parent history to sub-agent (not recommended for large contexts)
|
|
972
|
+
const parentHistory = this.getHistory();
|
|
973
|
+
for (const msg of parentHistory) {
|
|
974
|
+
agent.conversationHistory.push({ ...msg });
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// 'isolated' mode: fresh context (default), no prefix needed
|
|
978
|
+
// Run the sub-agent
|
|
979
|
+
const result = await agent.run(contextPrefix + task, options);
|
|
980
|
+
// Truncate tool call results to prevent memory bloat
|
|
981
|
+
const maxToolResultSize = config.maxToolResultSize ?? 50 * 1024; // 50KB default
|
|
982
|
+
const truncatedToolCalls = result.toolCalls.map((tc) => ({
|
|
983
|
+
name: tc.name,
|
|
984
|
+
input: tc.input,
|
|
985
|
+
result: truncateToolResult(tc.result, maxToolResultSize),
|
|
986
|
+
}));
|
|
987
|
+
const subAgentResult = {
|
|
988
|
+
name,
|
|
989
|
+
response: result.response,
|
|
990
|
+
success: true,
|
|
991
|
+
iterations: result.iterations,
|
|
992
|
+
toolCalls: truncatedToolCalls,
|
|
993
|
+
contextStats: result.contextStats,
|
|
994
|
+
};
|
|
995
|
+
// Emit sub-agent end event
|
|
996
|
+
this.onEvent?.({ type: 'subagent_end', name, result: subAgentResult });
|
|
997
|
+
// Clear sub-agent history for next use (isolated context)
|
|
998
|
+
if (config.contextMode !== 'shared') {
|
|
999
|
+
agent.clearHistory();
|
|
1000
|
+
}
|
|
1001
|
+
// Auto-dispose if configured
|
|
1002
|
+
if (config.autoDispose) {
|
|
1003
|
+
this.disposeSubAgent(name);
|
|
1004
|
+
}
|
|
1005
|
+
return subAgentResult;
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
const subAgentResult = {
|
|
1009
|
+
name,
|
|
1010
|
+
response: '',
|
|
1011
|
+
success: false,
|
|
1012
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1013
|
+
iterations: 0,
|
|
1014
|
+
toolCalls: [],
|
|
1015
|
+
};
|
|
1016
|
+
this.onEvent?.({ type: 'subagent_end', name, result: subAgentResult });
|
|
1017
|
+
return subAgentResult;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Run multiple sub-agents in parallel with proper state isolation.
|
|
1022
|
+
*
|
|
1023
|
+
* This method ensures that parallel sub-agents don't share mutable state,
|
|
1024
|
+
* preventing race conditions and state leakage between concurrent runs.
|
|
1025
|
+
*
|
|
1026
|
+
* Inspired by LangGraph issue #6446: Parallel subgraphs with shared
|
|
1027
|
+
* state keys cause InvalidUpdateError.
|
|
1028
|
+
*
|
|
1029
|
+
* @param tasks - Array of {name, task} objects to run in parallel
|
|
1030
|
+
* @param options - Optional run options applied to all sub-agents
|
|
1031
|
+
* @returns Array of results in the same order as tasks
|
|
1032
|
+
*
|
|
1033
|
+
* @example
|
|
1034
|
+
* ```typescript
|
|
1035
|
+
* // Run code review and security scan in parallel
|
|
1036
|
+
* const results = await agent.runParallelSubAgents([
|
|
1037
|
+
* { name: 'code-reviewer', task: 'Review src/auth.ts' },
|
|
1038
|
+
* { name: 'security-scanner', task: 'Scan src/auth.ts for vulnerabilities' },
|
|
1039
|
+
* ]);
|
|
1040
|
+
* ```
|
|
1041
|
+
*/
|
|
1042
|
+
async runParallelSubAgents(tasks, options) {
|
|
1043
|
+
// Create isolated stores for each sub-agent to prevent state leakage
|
|
1044
|
+
const isolatedStores = tasks.map(() => createIsolatedTodoStore());
|
|
1045
|
+
// Run all sub-agents in parallel
|
|
1046
|
+
const promises = tasks.map(async ({ name, task }, index) => {
|
|
1047
|
+
const entry = this.subAgents.get(name);
|
|
1048
|
+
if (!entry) {
|
|
1049
|
+
return {
|
|
1050
|
+
name,
|
|
1051
|
+
response: '',
|
|
1052
|
+
success: false,
|
|
1053
|
+
error: `Sub-agent '${name}' not found. Register it first with createSubAgent().`,
|
|
1054
|
+
iterations: 0,
|
|
1055
|
+
toolCalls: [],
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
// Store the isolated store for this sub-agent run
|
|
1059
|
+
// Note: The actual store isolation would require tool-level changes
|
|
1060
|
+
// For now, we emit a note about isolation in the result
|
|
1061
|
+
const _isolatedStore = isolatedStores[index];
|
|
1062
|
+
// Run the sub-agent (state isolation is noted in the result)
|
|
1063
|
+
const result = await this.runSubAgent(name, task, options);
|
|
1064
|
+
return {
|
|
1065
|
+
...result,
|
|
1066
|
+
// Mark that this was run with isolation
|
|
1067
|
+
};
|
|
1068
|
+
});
|
|
1069
|
+
return Promise.all(promises);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Get a registered sub-agent by name
|
|
1073
|
+
*/
|
|
1074
|
+
getSubAgent(name) {
|
|
1075
|
+
return this.subAgents.get(name)?.agent;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Get all registered sub-agent names
|
|
1079
|
+
*/
|
|
1080
|
+
getSubAgentNames() {
|
|
1081
|
+
return Array.from(this.subAgents.keys());
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Remove a registered sub-agent (alias for disposeSubAgent)
|
|
1085
|
+
*/
|
|
1086
|
+
removeSubAgent(name) {
|
|
1087
|
+
return this.disposeSubAgent(name);
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Dispose a sub-agent and release its resources.
|
|
1091
|
+
*
|
|
1092
|
+
* This clears the sub-agent's:
|
|
1093
|
+
* - Conversation history
|
|
1094
|
+
* - Context manager state
|
|
1095
|
+
* - Tool registry
|
|
1096
|
+
*
|
|
1097
|
+
* After disposal, the sub-agent must be re-created to use again.
|
|
1098
|
+
*/
|
|
1099
|
+
disposeSubAgent(name) {
|
|
1100
|
+
const entry = this.subAgents.get(name);
|
|
1101
|
+
if (!entry) {
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
const { agent } = entry;
|
|
1105
|
+
// Clear conversation history
|
|
1106
|
+
agent.clearHistory();
|
|
1107
|
+
// Clear tool registry
|
|
1108
|
+
const registry = agent.toolRegistry;
|
|
1109
|
+
registry.clear();
|
|
1110
|
+
// Remove from map
|
|
1111
|
+
return this.subAgents.delete(name);
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Dispose all sub-agents and release their resources.
|
|
1115
|
+
*
|
|
1116
|
+
* Useful for cleanup when the parent agent is done or
|
|
1117
|
+
* to free memory during long-running sessions.
|
|
1118
|
+
*/
|
|
1119
|
+
disposeAllSubAgents() {
|
|
1120
|
+
for (const name of this.subAgents.keys()) {
|
|
1121
|
+
this.disposeSubAgent(name);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Generate a concise summary of context for inherited mode
|
|
1126
|
+
*/
|
|
1127
|
+
generateContextSummary(messages) {
|
|
1128
|
+
// Simple summary: extract key points from recent messages
|
|
1129
|
+
const recentMessages = messages.slice(-10);
|
|
1130
|
+
const summaryParts = [];
|
|
1131
|
+
for (const msg of recentMessages) {
|
|
1132
|
+
if (msg.role === 'system')
|
|
1133
|
+
continue;
|
|
1134
|
+
const msgContent = typeof msg.content === 'string'
|
|
1135
|
+
? msg.content
|
|
1136
|
+
: msg.content
|
|
1137
|
+
.map((b) => {
|
|
1138
|
+
switch (b.type) {
|
|
1139
|
+
case 'text':
|
|
1140
|
+
return b.text;
|
|
1141
|
+
case 'tool_use':
|
|
1142
|
+
return `[Tool: ${b.name}]`;
|
|
1143
|
+
case 'tool_result':
|
|
1144
|
+
return `[Tool Result]`;
|
|
1145
|
+
}
|
|
1146
|
+
})
|
|
1147
|
+
.join(' ');
|
|
1148
|
+
// Truncate long content
|
|
1149
|
+
const truncated = msgContent.length > 200 ? msgContent.slice(0, 200) + '...' : msgContent;
|
|
1150
|
+
summaryParts.push(`${msg.role}: ${truncated}`);
|
|
1151
|
+
}
|
|
1152
|
+
return summaryParts.join('\n');
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Register a tool for the agent to use
|
|
1156
|
+
*/
|
|
1157
|
+
registerTool(tool) {
|
|
1158
|
+
this.toolRegistry.register(tool);
|
|
1159
|
+
return this;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Register multiple tools at once
|
|
1163
|
+
*/
|
|
1164
|
+
registerTools(tools) {
|
|
1165
|
+
for (const tool of tools) {
|
|
1166
|
+
this.toolRegistry.register(tool);
|
|
1167
|
+
}
|
|
1168
|
+
return this;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Get all registered tool definitions
|
|
1172
|
+
*/
|
|
1173
|
+
getToolDefinitions() {
|
|
1174
|
+
return this.toolRegistry.getDefinitions();
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Run the agent with a user message
|
|
1178
|
+
*/
|
|
1179
|
+
async run(userMessage, options) {
|
|
1180
|
+
const maxIterations = options?.maxIterations ?? this.maxIterations;
|
|
1181
|
+
const chatOptions = { ...this.chatOptions, ...options?.chatOptions };
|
|
1182
|
+
const signal = options?.signal;
|
|
1183
|
+
// Combined event emitter
|
|
1184
|
+
const emit = (event) => {
|
|
1185
|
+
this.onEvent?.(event);
|
|
1186
|
+
options?.onEvent?.(event);
|
|
1187
|
+
};
|
|
1188
|
+
// Build messages: system prompt + history + new user message
|
|
1189
|
+
let messages = [];
|
|
1190
|
+
// Add system message if present
|
|
1191
|
+
if (this.systemPrompt) {
|
|
1192
|
+
messages.push({
|
|
1193
|
+
role: 'system',
|
|
1194
|
+
content: this.systemPrompt,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
// Inject anchors (critical information that survives context compaction)
|
|
1198
|
+
if (this.anchorManager && this.anchorManager.size > 0) {
|
|
1199
|
+
const anchorsContent = this.anchorManager.format();
|
|
1200
|
+
if (anchorsContent) {
|
|
1201
|
+
messages.push({
|
|
1202
|
+
role: 'system',
|
|
1203
|
+
content: `## Active Anchors (Critical Information)\n\n${anchorsContent}`,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
// Add conversation history
|
|
1208
|
+
messages.push(...this.conversationHistory);
|
|
1209
|
+
// Add new user message
|
|
1210
|
+
const userMsg = {
|
|
1211
|
+
role: 'user',
|
|
1212
|
+
content: userMessage,
|
|
1213
|
+
};
|
|
1214
|
+
messages.push(userMsg);
|
|
1215
|
+
// Track new messages added during this run (for appending to history)
|
|
1216
|
+
const newMessages = [userMsg];
|
|
1217
|
+
// Context management: update token count and check for proactive management
|
|
1218
|
+
if (this.contextManager && this.autoContextManagement) {
|
|
1219
|
+
await this.contextManager.updateTokenCount(messages);
|
|
1220
|
+
// Track system prompt tokens
|
|
1221
|
+
if (this.systemPrompt) {
|
|
1222
|
+
const systemTokens = this.contextManager.estimateTokens(this.systemPrompt);
|
|
1223
|
+
this.contextManager.updateCategoryUsage('system', systemTokens);
|
|
1224
|
+
}
|
|
1225
|
+
// Check if we need to manage context before starting
|
|
1226
|
+
if (this.contextManager.needsEmergencySummarization()) {
|
|
1227
|
+
emit({
|
|
1228
|
+
type: 'context_warning',
|
|
1229
|
+
utilization: this.contextManager.getUtilization(),
|
|
1230
|
+
threshold: 0.95,
|
|
1231
|
+
});
|
|
1232
|
+
// Perform emergency summarization
|
|
1233
|
+
const { messages: summarized, result } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs), { emergency: true });
|
|
1234
|
+
messages = summarized;
|
|
1235
|
+
emit({
|
|
1236
|
+
type: 'context_summarized',
|
|
1237
|
+
tokensBefore: result.originalTokens,
|
|
1238
|
+
tokensAfter: result.summaryTokens,
|
|
1239
|
+
rounds: result.rounds,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
else if (this.contextManager.needsSummarization()) {
|
|
1243
|
+
emit({
|
|
1244
|
+
type: 'context_warning',
|
|
1245
|
+
utilization: this.contextManager.getUtilization(),
|
|
1246
|
+
threshold: 0.9,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const toolCalls = [];
|
|
1251
|
+
let iterations = 0;
|
|
1252
|
+
let finalResponse = '';
|
|
1253
|
+
let aborted = false;
|
|
1254
|
+
// Tool loop detection: track consecutive identical calls
|
|
1255
|
+
let lastToolCallHash = '';
|
|
1256
|
+
let consecutiveIdenticalCalls = 0;
|
|
1257
|
+
// Hash function for tool call comparison
|
|
1258
|
+
const hashToolCall = (name, input) => {
|
|
1259
|
+
return `${name}:${JSON.stringify(input, Object.keys(input).sort())}`;
|
|
1260
|
+
};
|
|
1261
|
+
// Agentic loop
|
|
1262
|
+
while (iterations < maxIterations) {
|
|
1263
|
+
// Check for abort
|
|
1264
|
+
if (signal?.aborted) {
|
|
1265
|
+
aborted = true;
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
iterations++;
|
|
1269
|
+
emit({ type: 'iteration_start', iteration: iterations });
|
|
1270
|
+
// Hook context for this iteration
|
|
1271
|
+
const hookContext = {
|
|
1272
|
+
sessionId: this._sessionId,
|
|
1273
|
+
iteration: iterations,
|
|
1274
|
+
signal,
|
|
1275
|
+
metadata: {},
|
|
1276
|
+
};
|
|
1277
|
+
// Track tool calls for this iteration (for afterIteration hook)
|
|
1278
|
+
const iterationToolCalls = [];
|
|
1279
|
+
// Run beforeIteration hooks
|
|
1280
|
+
if (this.hooksManager) {
|
|
1281
|
+
const shouldContinue = await this.hooksManager.runBeforeIteration({
|
|
1282
|
+
...hookContext,
|
|
1283
|
+
maxIterations,
|
|
1284
|
+
messages,
|
|
1285
|
+
});
|
|
1286
|
+
if (!shouldContinue) {
|
|
1287
|
+
emit({ type: 'iteration_end', iteration: iterations });
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
// Get tool definitions
|
|
1292
|
+
let tools = this.toolRegistry.getDefinitions();
|
|
1293
|
+
// Run beforeLLM hooks (can modify messages and tools)
|
|
1294
|
+
if (this.hooksManager) {
|
|
1295
|
+
const llmHookResult = await this.hooksManager.runBeforeLLM({
|
|
1296
|
+
...hookContext,
|
|
1297
|
+
messages,
|
|
1298
|
+
tools,
|
|
1299
|
+
});
|
|
1300
|
+
messages = llmHookResult.messages;
|
|
1301
|
+
tools = llmHookResult.tools;
|
|
1302
|
+
}
|
|
1303
|
+
// Call LLM
|
|
1304
|
+
emit({ type: 'llm_start' });
|
|
1305
|
+
const llmStartTime = Date.now();
|
|
1306
|
+
const chunks = [];
|
|
1307
|
+
try {
|
|
1308
|
+
for await (const chunk of this.provider.chat(messages, {
|
|
1309
|
+
...chatOptions,
|
|
1310
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
1311
|
+
})) {
|
|
1312
|
+
// Check for abort during streaming
|
|
1313
|
+
if (signal?.aborted) {
|
|
1314
|
+
aborted = true;
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
chunks.push(chunk);
|
|
1318
|
+
emit({ type: 'llm_chunk', chunk });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
if (signal?.aborted) {
|
|
1323
|
+
aborted = true;
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
// Run onError hooks
|
|
1327
|
+
if (this.hooksManager && error instanceof Error) {
|
|
1328
|
+
const errorResult = await this.hooksManager.runOnError({
|
|
1329
|
+
...hookContext,
|
|
1330
|
+
error,
|
|
1331
|
+
phase: 'llm',
|
|
1332
|
+
});
|
|
1333
|
+
if (errorResult.handled) {
|
|
1334
|
+
// Error was handled by hook, continue with next iteration
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
if (errorResult.error) {
|
|
1338
|
+
throw errorResult.error;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
if (aborted) {
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
// Process response
|
|
1347
|
+
const { text, toolUses, usage, model } = this.processChunks(chunks);
|
|
1348
|
+
emit({ type: 'llm_end', text, hasToolUses: toolUses.length > 0 });
|
|
1349
|
+
// Record usage if available
|
|
1350
|
+
if (usage && model) {
|
|
1351
|
+
this.recordUsage(model, this.provider.name, {
|
|
1352
|
+
inputTokens: usage.inputTokens,
|
|
1353
|
+
outputTokens: usage.outputTokens,
|
|
1354
|
+
totalTokens: usage.inputTokens + usage.outputTokens,
|
|
1355
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
1356
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
// Run afterLLM hooks
|
|
1360
|
+
if (this.hooksManager) {
|
|
1361
|
+
await this.hooksManager.runAfterLLM({
|
|
1362
|
+
...hookContext,
|
|
1363
|
+
messages,
|
|
1364
|
+
tools,
|
|
1365
|
+
text,
|
|
1366
|
+
toolUses,
|
|
1367
|
+
usage: usage
|
|
1368
|
+
? { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens }
|
|
1369
|
+
: undefined,
|
|
1370
|
+
model,
|
|
1371
|
+
durationMs: Date.now() - llmStartTime,
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
// If no tool uses, we're done
|
|
1375
|
+
if (toolUses.length === 0) {
|
|
1376
|
+
finalResponse = text;
|
|
1377
|
+
// Add final assistant response to history
|
|
1378
|
+
const finalAssistantMsg = {
|
|
1379
|
+
role: 'assistant',
|
|
1380
|
+
content: text,
|
|
1381
|
+
};
|
|
1382
|
+
newMessages.push(finalAssistantMsg);
|
|
1383
|
+
// Run afterIteration hooks
|
|
1384
|
+
if (this.hooksManager) {
|
|
1385
|
+
await this.hooksManager.runAfterIteration({
|
|
1386
|
+
...hookContext,
|
|
1387
|
+
maxIterations,
|
|
1388
|
+
messages,
|
|
1389
|
+
toolCalls: iterationToolCalls,
|
|
1390
|
+
completedWithText: true,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
emit({ type: 'iteration_end', iteration: iterations });
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
// Add assistant message with tool uses
|
|
1397
|
+
const assistantMsg = {
|
|
1398
|
+
role: 'assistant',
|
|
1399
|
+
content: [
|
|
1400
|
+
...(text ? [{ type: 'text', text }] : []),
|
|
1401
|
+
...toolUses.map((tu) => ({
|
|
1402
|
+
type: 'tool_use',
|
|
1403
|
+
id: tu.id,
|
|
1404
|
+
name: tu.name,
|
|
1405
|
+
input: tu.input,
|
|
1406
|
+
})),
|
|
1407
|
+
],
|
|
1408
|
+
};
|
|
1409
|
+
messages.push(assistantMsg);
|
|
1410
|
+
newMessages.push(assistantMsg);
|
|
1411
|
+
// Execute tools and add results
|
|
1412
|
+
for (const toolUse of toolUses) {
|
|
1413
|
+
// Check for abort before each tool
|
|
1414
|
+
if (signal?.aborted) {
|
|
1415
|
+
aborted = true;
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
emit({ type: 'tool_start', name: toolUse.name, input: toolUse.input });
|
|
1419
|
+
let result;
|
|
1420
|
+
// Check permissions before execution
|
|
1421
|
+
if (this.permissionManager) {
|
|
1422
|
+
const permResult = await this.permissionManager.check(toolUse.name, toolUse.input);
|
|
1423
|
+
if (permResult.askedUser) {
|
|
1424
|
+
emit({ type: 'permission_asked', toolName: toolUse.name, level: permResult.level });
|
|
1425
|
+
}
|
|
1426
|
+
if (!permResult.allowed) {
|
|
1427
|
+
emit({
|
|
1428
|
+
type: 'permission_denied',
|
|
1429
|
+
toolName: toolUse.name,
|
|
1430
|
+
level: permResult.level,
|
|
1431
|
+
reason: permResult.reason,
|
|
1432
|
+
});
|
|
1433
|
+
result = {
|
|
1434
|
+
success: false,
|
|
1435
|
+
error: `Permission denied: ${permResult.reason ?? 'Tool execution not allowed'}`,
|
|
1436
|
+
};
|
|
1437
|
+
// Skip to tool_end and continue to next tool
|
|
1438
|
+
emit({ type: 'tool_end', name: toolUse.name, result });
|
|
1439
|
+
const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
|
|
1440
|
+
toolCalls.push(toolCallEntry);
|
|
1441
|
+
iterationToolCalls.push(toolCallEntry);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
emit({ type: 'permission_granted', toolName: toolUse.name, level: permResult.level });
|
|
1445
|
+
}
|
|
1446
|
+
// Check guardrails before execution
|
|
1447
|
+
if (this.guardrailManager) {
|
|
1448
|
+
const { proceed, result: guardrailResult } = await this.guardrailManager.checkAndHandle(toolUse.name, toolUse.input);
|
|
1449
|
+
if (guardrailResult.triggered) {
|
|
1450
|
+
emit({ type: 'guardrail_triggered', result: guardrailResult });
|
|
1451
|
+
if (!proceed) {
|
|
1452
|
+
// Guardrail blocked the execution
|
|
1453
|
+
const message = guardrailResult.guardrail?.message ?? 'Operation blocked by guardrail';
|
|
1454
|
+
emit({ type: 'guardrail_blocked', result: guardrailResult, message });
|
|
1455
|
+
result = {
|
|
1456
|
+
success: false,
|
|
1457
|
+
error: `Guardrail blocked: ${message}`,
|
|
1458
|
+
};
|
|
1459
|
+
// Skip to tool_end and continue to next tool
|
|
1460
|
+
emit({ type: 'tool_end', name: toolUse.name, result });
|
|
1461
|
+
const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
|
|
1462
|
+
toolCalls.push(toolCallEntry);
|
|
1463
|
+
iterationToolCalls.push(toolCallEntry);
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
else if (guardrailResult.action === 'warn') {
|
|
1467
|
+
// Log warning and continue
|
|
1468
|
+
const message = guardrailResult.guardrail?.message ?? 'Warning from guardrail';
|
|
1469
|
+
emit({ type: 'guardrail_warning', result: guardrailResult, message });
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
// Run beforeTool hooks (can skip or modify input)
|
|
1474
|
+
let toolInput = toolUse.input;
|
|
1475
|
+
if (this.hooksManager) {
|
|
1476
|
+
const beforeToolResult = await this.hooksManager.runBeforeTool({
|
|
1477
|
+
...hookContext,
|
|
1478
|
+
toolName: toolUse.name,
|
|
1479
|
+
input: toolInput,
|
|
1480
|
+
});
|
|
1481
|
+
if (!beforeToolResult.proceed) {
|
|
1482
|
+
result = beforeToolResult.skipResult ?? { success: false, error: 'Skipped by hook' };
|
|
1483
|
+
emit({ type: 'tool_end', name: toolUse.name, result });
|
|
1484
|
+
const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
|
|
1485
|
+
toolCalls.push(toolCallEntry);
|
|
1486
|
+
iterationToolCalls.push(toolCallEntry);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
toolInput = beforeToolResult.input;
|
|
1490
|
+
}
|
|
1491
|
+
const toolStartTime = Date.now();
|
|
1492
|
+
try {
|
|
1493
|
+
result = await this.toolRegistry.execute(toolUse.name, toolInput);
|
|
1494
|
+
}
|
|
1495
|
+
catch (error) {
|
|
1496
|
+
// Run onError hooks for tool errors (thrown exceptions)
|
|
1497
|
+
if (this.hooksManager && error instanceof Error) {
|
|
1498
|
+
const errorResult = await this.hooksManager.runOnError({
|
|
1499
|
+
...hookContext,
|
|
1500
|
+
error,
|
|
1501
|
+
phase: 'tool',
|
|
1502
|
+
toolName: toolUse.name,
|
|
1503
|
+
});
|
|
1504
|
+
if (errorResult.recovery) {
|
|
1505
|
+
result = errorResult.recovery;
|
|
1506
|
+
}
|
|
1507
|
+
else {
|
|
1508
|
+
result = {
|
|
1509
|
+
success: false,
|
|
1510
|
+
error: errorResult.error?.message ?? error.message,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
result = {
|
|
1516
|
+
success: false,
|
|
1517
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// Run onError hooks for failed tool results (not thrown, but returned errors)
|
|
1522
|
+
// The tool registry catches errors internally and returns { success: false }
|
|
1523
|
+
if (this.hooksManager && !result.success) {
|
|
1524
|
+
const syntheticError = new Error(result.error);
|
|
1525
|
+
const errorResult = await this.hooksManager.runOnError({
|
|
1526
|
+
...hookContext,
|
|
1527
|
+
error: syntheticError,
|
|
1528
|
+
phase: 'tool',
|
|
1529
|
+
toolName: toolUse.name,
|
|
1530
|
+
});
|
|
1531
|
+
if (errorResult.recovery) {
|
|
1532
|
+
result = errorResult.recovery;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
// Run afterTool hooks (can modify result)
|
|
1536
|
+
if (this.hooksManager) {
|
|
1537
|
+
result = await this.hooksManager.runAfterTool({
|
|
1538
|
+
...hookContext,
|
|
1539
|
+
toolName: toolUse.name,
|
|
1540
|
+
input: toolInput,
|
|
1541
|
+
result,
|
|
1542
|
+
durationMs: Date.now() - toolStartTime,
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
emit({ type: 'tool_end', name: toolUse.name, result });
|
|
1546
|
+
// Tool loop detection: check for consecutive identical calls
|
|
1547
|
+
if (this.maxConsecutiveToolCalls > 0) {
|
|
1548
|
+
const currentHash = hashToolCall(toolUse.name, toolUse.input);
|
|
1549
|
+
if (currentHash === lastToolCallHash) {
|
|
1550
|
+
consecutiveIdenticalCalls++;
|
|
1551
|
+
if (consecutiveIdenticalCalls >= this.maxConsecutiveToolCalls) {
|
|
1552
|
+
throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
|
|
1553
|
+
}
|
|
1554
|
+
// Emit warning before throwing
|
|
1555
|
+
emit({
|
|
1556
|
+
type: 'tool_loop_warning',
|
|
1557
|
+
toolName: toolUse.name,
|
|
1558
|
+
consecutiveCalls: consecutiveIdenticalCalls,
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
else {
|
|
1562
|
+
// Different tool call, reset counter
|
|
1563
|
+
lastToolCallHash = currentHash;
|
|
1564
|
+
consecutiveIdenticalCalls = 1;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
|
|
1568
|
+
toolCalls.push(toolCallEntry);
|
|
1569
|
+
iterationToolCalls.push(toolCallEntry);
|
|
1570
|
+
// Build tool result content
|
|
1571
|
+
let toolResultContent = result.success
|
|
1572
|
+
? JSON.stringify(result.result)
|
|
1573
|
+
: `Error: ${result.error ?? 'Unknown error'}`;
|
|
1574
|
+
// Context management: pre-flight check and filtering for tool results
|
|
1575
|
+
if (this.contextManager && this.autoContextManagement) {
|
|
1576
|
+
const estimatedTokens = this.contextManager.estimateTokens(toolResultContent);
|
|
1577
|
+
const preflight = this.contextManager.canAddContent(estimatedTokens, 'toolResults');
|
|
1578
|
+
if (!preflight.allowed) {
|
|
1579
|
+
if (preflight.action === 'reject') {
|
|
1580
|
+
// Content too large - filter it
|
|
1581
|
+
const filtered = this.contextManager.filterContent(toolResultContent, 'tool_result');
|
|
1582
|
+
toolResultContent = filtered.content;
|
|
1583
|
+
}
|
|
1584
|
+
else if (preflight.action === 'compact') {
|
|
1585
|
+
// Category budget exceeded - compact the category first
|
|
1586
|
+
const tokensBefore = this.contextManager.getTokenCount();
|
|
1587
|
+
const compactResult = await this.contextManager.compactCategory(messages, 'toolResults', (content, index) => Promise.resolve(`[compacted_tool_result_${String(index)}]`));
|
|
1588
|
+
messages = compactResult.messages;
|
|
1589
|
+
emit({
|
|
1590
|
+
type: 'context_compacted',
|
|
1591
|
+
tokensBefore,
|
|
1592
|
+
tokensAfter: this.contextManager.getTokenCount(),
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
else if (preflight.action === 'summarize') {
|
|
1596
|
+
// Approaching context limit - summarize
|
|
1597
|
+
const { messages: summarized, result: sumResult } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs));
|
|
1598
|
+
messages = summarized;
|
|
1599
|
+
emit({
|
|
1600
|
+
type: 'context_summarized',
|
|
1601
|
+
tokensBefore: sumResult.originalTokens,
|
|
1602
|
+
tokensAfter: sumResult.summaryTokens,
|
|
1603
|
+
rounds: sumResult.rounds,
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
// Track tool result tokens
|
|
1608
|
+
const finalTokens = this.contextManager.estimateTokens(toolResultContent);
|
|
1609
|
+
this.contextManager.addToCategory('toolResults', finalTokens);
|
|
1610
|
+
}
|
|
1611
|
+
const toolResultMsg = {
|
|
1612
|
+
role: 'user',
|
|
1613
|
+
content: [
|
|
1614
|
+
{
|
|
1615
|
+
type: 'tool_result',
|
|
1616
|
+
toolUseId: toolUse.id,
|
|
1617
|
+
content: toolResultContent,
|
|
1618
|
+
isError: !result.success,
|
|
1619
|
+
},
|
|
1620
|
+
],
|
|
1621
|
+
};
|
|
1622
|
+
messages.push(toolResultMsg);
|
|
1623
|
+
newMessages.push(toolResultMsg);
|
|
1624
|
+
}
|
|
1625
|
+
if (aborted) {
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
// Run afterIteration hooks
|
|
1629
|
+
if (this.hooksManager) {
|
|
1630
|
+
await this.hooksManager.runAfterIteration({
|
|
1631
|
+
...hookContext,
|
|
1632
|
+
maxIterations,
|
|
1633
|
+
messages,
|
|
1634
|
+
toolCalls: iterationToolCalls,
|
|
1635
|
+
completedWithText: false,
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
emit({ type: 'iteration_end', iteration: iterations });
|
|
1639
|
+
}
|
|
1640
|
+
// Check if we hit max iterations without completing
|
|
1641
|
+
if (!aborted && iterations >= maxIterations && finalResponse === '') {
|
|
1642
|
+
if (this.iterationLimitBehavior === 'summarize') {
|
|
1643
|
+
// Generate a summary response before throwing
|
|
1644
|
+
try {
|
|
1645
|
+
finalResponse = await this.generateIterationLimitSummary(messages, maxIterations, toolCalls);
|
|
1646
|
+
emit({ type: 'done', response: finalResponse });
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
// If summary generation fails, still throw the error
|
|
1650
|
+
throw new MaxIterationsError(maxIterations);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
else if (this.iterationLimitBehavior === 'continue') {
|
|
1654
|
+
// Return partial result without throwing
|
|
1655
|
+
finalResponse =
|
|
1656
|
+
`[Agent reached maximum iterations (${String(maxIterations)}) without completing. ` +
|
|
1657
|
+
`${String(toolCalls.length)} tool calls were made.]`;
|
|
1658
|
+
}
|
|
1659
|
+
else {
|
|
1660
|
+
// Default: throw error
|
|
1661
|
+
throw new MaxIterationsError(maxIterations);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (!aborted && !(iterations >= maxIterations && this.iterationLimitBehavior === 'summarize')) {
|
|
1665
|
+
emit({ type: 'done', response: finalResponse });
|
|
1666
|
+
}
|
|
1667
|
+
// Context management: increment turn count and update token count
|
|
1668
|
+
if (this.contextManager) {
|
|
1669
|
+
this.contextManager.incrementTurn();
|
|
1670
|
+
await this.contextManager.updateTokenCount(messages);
|
|
1671
|
+
}
|
|
1672
|
+
// Append new messages to conversation history (persists for next run)
|
|
1673
|
+
this.conversationHistory.push(...newMessages);
|
|
1674
|
+
// Update internal state tracking
|
|
1675
|
+
this._currentIteration = iterations;
|
|
1676
|
+
// Note: token tracking would require provider-specific token counting
|
|
1677
|
+
// Build result with optional context stats
|
|
1678
|
+
const result = {
|
|
1679
|
+
response: finalResponse,
|
|
1680
|
+
messages,
|
|
1681
|
+
iterations,
|
|
1682
|
+
toolCalls,
|
|
1683
|
+
aborted,
|
|
1684
|
+
};
|
|
1685
|
+
if (this.contextManager) {
|
|
1686
|
+
result.contextStats = this.contextManager.getStats(messages.length);
|
|
1687
|
+
}
|
|
1688
|
+
// Save partial checkpoint on abort (LangGraph issue #5672)
|
|
1689
|
+
if (aborted) {
|
|
1690
|
+
await this.saveAbortCheckpoint('aborted', emit);
|
|
1691
|
+
}
|
|
1692
|
+
// Auto-checkpoint if configured (only on successful completion)
|
|
1693
|
+
if (!aborted && this.autoCheckpoint && this.checkpointer) {
|
|
1694
|
+
await this.checkpoint();
|
|
1695
|
+
}
|
|
1696
|
+
return result;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Stream the agent's response with full tool use support
|
|
1700
|
+
*
|
|
1701
|
+
* Yields AgentEvent objects as the agent executes, allowing
|
|
1702
|
+
* real-time monitoring of the agentic loop.
|
|
1703
|
+
*/
|
|
1704
|
+
async *stream(userMessage, options) {
|
|
1705
|
+
// Use a simple queue-based approach
|
|
1706
|
+
const eventQueue = [];
|
|
1707
|
+
let finished = false;
|
|
1708
|
+
let error = null;
|
|
1709
|
+
// Promise that resolves when a new event is available
|
|
1710
|
+
let notifyNewEvent = null;
|
|
1711
|
+
const waitForEvent = () => new Promise((resolve) => {
|
|
1712
|
+
notifyNewEvent = resolve;
|
|
1713
|
+
});
|
|
1714
|
+
// Run agent in background, collecting events
|
|
1715
|
+
const runPromise = this.run(userMessage, {
|
|
1716
|
+
...options,
|
|
1717
|
+
onEvent: (event) => {
|
|
1718
|
+
options?.onEvent?.(event);
|
|
1719
|
+
eventQueue.push(event);
|
|
1720
|
+
notifyNewEvent?.();
|
|
1721
|
+
notifyNewEvent = null;
|
|
1722
|
+
},
|
|
1723
|
+
});
|
|
1724
|
+
// Handle completion
|
|
1725
|
+
runPromise
|
|
1726
|
+
.then(() => {
|
|
1727
|
+
finished = true;
|
|
1728
|
+
notifyNewEvent?.();
|
|
1729
|
+
})
|
|
1730
|
+
.catch((err) => {
|
|
1731
|
+
error = err;
|
|
1732
|
+
finished = true;
|
|
1733
|
+
notifyNewEvent?.();
|
|
1734
|
+
});
|
|
1735
|
+
// Yield events as they arrive
|
|
1736
|
+
// Note: ESLint can't track async mutations to `finished`
|
|
1737
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1738
|
+
while (eventQueue.length > 0 || !finished) {
|
|
1739
|
+
if (eventQueue.length > 0) {
|
|
1740
|
+
const event = eventQueue.shift();
|
|
1741
|
+
if (event) {
|
|
1742
|
+
yield event;
|
|
1743
|
+
}
|
|
1744
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1745
|
+
}
|
|
1746
|
+
else if (!finished) {
|
|
1747
|
+
await waitForEvent();
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
// Re-throw any error from the run
|
|
1751
|
+
if (error instanceof Error) {
|
|
1752
|
+
throw error;
|
|
1753
|
+
}
|
|
1754
|
+
else if (error !== null && error !== undefined) {
|
|
1755
|
+
throw new Error(typeof error === 'string' ? error : 'Unknown error in agent stream');
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Process stream chunks into text, tool uses, and usage data
|
|
1760
|
+
*/
|
|
1761
|
+
processChunks(chunks) {
|
|
1762
|
+
let text = '';
|
|
1763
|
+
const toolUses = [];
|
|
1764
|
+
let currentToolUse = null;
|
|
1765
|
+
let usage;
|
|
1766
|
+
let model;
|
|
1767
|
+
for (const chunk of chunks) {
|
|
1768
|
+
switch (chunk.type) {
|
|
1769
|
+
case 'text':
|
|
1770
|
+
text += chunk.text ?? '';
|
|
1771
|
+
break;
|
|
1772
|
+
case 'tool_use_start':
|
|
1773
|
+
if (chunk.toolUse) {
|
|
1774
|
+
currentToolUse = {
|
|
1775
|
+
id: chunk.toolUse.id,
|
|
1776
|
+
name: chunk.toolUse.name,
|
|
1777
|
+
inputJson: '',
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
break;
|
|
1781
|
+
case 'tool_use_delta':
|
|
1782
|
+
if (currentToolUse && chunk.text) {
|
|
1783
|
+
currentToolUse.inputJson += chunk.text;
|
|
1784
|
+
}
|
|
1785
|
+
break;
|
|
1786
|
+
case 'tool_use_end':
|
|
1787
|
+
if (currentToolUse) {
|
|
1788
|
+
try {
|
|
1789
|
+
const input = currentToolUse.inputJson
|
|
1790
|
+
? JSON.parse(currentToolUse.inputJson)
|
|
1791
|
+
: {};
|
|
1792
|
+
toolUses.push({
|
|
1793
|
+
id: currentToolUse.id,
|
|
1794
|
+
name: currentToolUse.name,
|
|
1795
|
+
input,
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
catch {
|
|
1799
|
+
// Invalid JSON, skip this tool use
|
|
1800
|
+
}
|
|
1801
|
+
currentToolUse = null;
|
|
1802
|
+
}
|
|
1803
|
+
break;
|
|
1804
|
+
case 'done':
|
|
1805
|
+
// Capture usage data from done chunk
|
|
1806
|
+
usage = chunk.usage;
|
|
1807
|
+
model = chunk.model;
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return { text, toolUses, usage, model };
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Generate a summary of messages using the LLM provider.
|
|
1815
|
+
* Used for context summarization when approaching limits.
|
|
1816
|
+
*/
|
|
1817
|
+
async generateSummary(messages) {
|
|
1818
|
+
const summaryPrompt = `Please provide a concise summary of the following conversation. Focus on:
|
|
1819
|
+
1. Key decisions made
|
|
1820
|
+
2. Important context established
|
|
1821
|
+
3. Tasks completed or in progress
|
|
1822
|
+
4. Any relevant file paths or code snippets mentioned
|
|
1823
|
+
|
|
1824
|
+
Keep the summary under 500 words.
|
|
1825
|
+
|
|
1826
|
+
Conversation to summarize:
|
|
1827
|
+
${messages.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n\n')}`;
|
|
1828
|
+
const summaryMessages = [{ role: 'user', content: summaryPrompt }];
|
|
1829
|
+
let summary = '';
|
|
1830
|
+
for await (const chunk of this.provider.chat(summaryMessages, {
|
|
1831
|
+
...this.chatOptions,
|
|
1832
|
+
maxTokens: this.contextManager?.getConfig().summarization.summaryMaxTokens ?? 2000,
|
|
1833
|
+
})) {
|
|
1834
|
+
if (chunk.type === 'text' && chunk.text) {
|
|
1835
|
+
summary += chunk.text;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return summary || 'No summary generated.';
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Generate a summary when the agent hits max iterations.
|
|
1842
|
+
* This provides a graceful fallback instead of just throwing an error.
|
|
1843
|
+
*/
|
|
1844
|
+
async generateIterationLimitSummary(messages, maxIterations, toolCalls) {
|
|
1845
|
+
// Build a summary of tool calls made
|
|
1846
|
+
const toolSummary = toolCalls.length > 0
|
|
1847
|
+
? toolCalls
|
|
1848
|
+
.map((tc) => {
|
|
1849
|
+
const status = tc.result.success ? 'succeeded' : 'failed';
|
|
1850
|
+
return `- ${tc.name}: ${status}`;
|
|
1851
|
+
})
|
|
1852
|
+
.join('\n')
|
|
1853
|
+
: 'No tools were called.';
|
|
1854
|
+
const summaryPrompt = `The agent has reached its maximum iteration limit (${String(maxIterations)}) before completing the task.
|
|
1855
|
+
|
|
1856
|
+
Here is a summary of the tool calls made:
|
|
1857
|
+
${toolSummary}
|
|
1858
|
+
|
|
1859
|
+
Based on the conversation history below, please provide:
|
|
1860
|
+
1. A brief summary of what was accomplished
|
|
1861
|
+
2. What the agent was attempting to do when it stopped
|
|
1862
|
+
3. Suggested next steps for the user
|
|
1863
|
+
|
|
1864
|
+
Keep the response concise and helpful.
|
|
1865
|
+
|
|
1866
|
+
Recent conversation:
|
|
1867
|
+
${messages
|
|
1868
|
+
.slice(-10)
|
|
1869
|
+
.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content.slice(0, 500) : '[complex content]'}`)
|
|
1870
|
+
.join('\n\n')}`;
|
|
1871
|
+
const summaryMessages = [{ role: 'user', content: summaryPrompt }];
|
|
1872
|
+
let summary = '';
|
|
1873
|
+
for await (const chunk of this.provider.chat(summaryMessages, {
|
|
1874
|
+
...this.chatOptions,
|
|
1875
|
+
maxTokens: 1000, // Keep summary concise
|
|
1876
|
+
})) {
|
|
1877
|
+
if (chunk.type === 'text' && chunk.text) {
|
|
1878
|
+
summary += chunk.text;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (!summary) {
|
|
1882
|
+
// Fallback if LLM fails to generate summary
|
|
1883
|
+
return (`[Agent reached maximum iterations (${String(maxIterations)}). ` +
|
|
1884
|
+
`${String(toolCalls.length)} tool calls were made. ` +
|
|
1885
|
+
`Consider increasing maxIterations or breaking the task into smaller steps.]`);
|
|
1886
|
+
}
|
|
1887
|
+
return `[Iteration limit reached]\n\n${summary}`;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Truncate tool result data to prevent memory bloat.
|
|
1892
|
+
* Keeps important metadata but truncates large result values.
|
|
1893
|
+
*/
|
|
1894
|
+
function truncateToolResult(result, maxSize) {
|
|
1895
|
+
if (!result.success || result.result === undefined) {
|
|
1896
|
+
return result;
|
|
1897
|
+
}
|
|
1898
|
+
const resultStr = JSON.stringify(result.result);
|
|
1899
|
+
if (resultStr.length <= maxSize) {
|
|
1900
|
+
return result;
|
|
1901
|
+
}
|
|
1902
|
+
// Truncate the result
|
|
1903
|
+
const headSize = Math.floor(maxSize * 0.7);
|
|
1904
|
+
const tailSize = Math.floor(maxSize * 0.2);
|
|
1905
|
+
const head = resultStr.slice(0, headSize);
|
|
1906
|
+
const tail = resultStr.slice(-tailSize);
|
|
1907
|
+
const omitted = resultStr.length - headSize - tailSize;
|
|
1908
|
+
return {
|
|
1909
|
+
success: result.success,
|
|
1910
|
+
result: `[Truncated: ${String(omitted)} chars omitted]\n${head}...\n...\n${tail}`,
|
|
1911
|
+
};
|
|
1912
|
+
}
|