@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.
Files changed (160) hide show
  1. package/README.md +1277 -0
  2. package/dist/agent.d.ts +1272 -0
  3. package/dist/agent.js +1912 -0
  4. package/dist/anchors/builtin.d.ts +24 -0
  5. package/dist/anchors/builtin.js +61 -0
  6. package/dist/anchors/index.d.ts +6 -0
  7. package/dist/anchors/index.js +5 -0
  8. package/dist/anchors/manager.d.ts +115 -0
  9. package/dist/anchors/manager.js +412 -0
  10. package/dist/anchors/types.d.ts +168 -0
  11. package/dist/anchors/types.js +10 -0
  12. package/dist/context/index.d.ts +12 -0
  13. package/dist/context/index.js +10 -0
  14. package/dist/context/manager.d.ts +224 -0
  15. package/dist/context/manager.js +770 -0
  16. package/dist/context/types.d.ts +377 -0
  17. package/dist/context/types.js +7 -0
  18. package/dist/costs/index.d.ts +8 -0
  19. package/dist/costs/index.js +7 -0
  20. package/dist/costs/tracker.d.ts +121 -0
  21. package/dist/costs/tracker.js +295 -0
  22. package/dist/costs/types.d.ts +157 -0
  23. package/dist/costs/types.js +8 -0
  24. package/dist/errors.d.ts +178 -0
  25. package/dist/errors.js +249 -0
  26. package/dist/guardrails/builtin.d.ts +27 -0
  27. package/dist/guardrails/builtin.js +223 -0
  28. package/dist/guardrails/index.d.ts +6 -0
  29. package/dist/guardrails/index.js +5 -0
  30. package/dist/guardrails/manager.d.ts +117 -0
  31. package/dist/guardrails/manager.js +288 -0
  32. package/dist/guardrails/types.d.ts +159 -0
  33. package/dist/guardrails/types.js +7 -0
  34. package/dist/hooks/index.d.ts +31 -0
  35. package/dist/hooks/index.js +29 -0
  36. package/dist/hooks/manager.d.ts +147 -0
  37. package/dist/hooks/manager.js +600 -0
  38. package/dist/hooks/types.d.ts +368 -0
  39. package/dist/hooks/types.js +12 -0
  40. package/dist/index.d.ts +45 -0
  41. package/dist/index.js +73 -0
  42. package/dist/mcp/client.d.ts +93 -0
  43. package/dist/mcp/client.js +287 -0
  44. package/dist/mcp/errors.d.ts +60 -0
  45. package/dist/mcp/errors.js +78 -0
  46. package/dist/mcp/index.d.ts +43 -0
  47. package/dist/mcp/index.js +45 -0
  48. package/dist/mcp/manager.d.ts +120 -0
  49. package/dist/mcp/manager.js +276 -0
  50. package/dist/mcp/tools.d.ts +54 -0
  51. package/dist/mcp/tools.js +99 -0
  52. package/dist/mcp/types.d.ts +150 -0
  53. package/dist/mcp/types.js +40 -0
  54. package/dist/memory/index.d.ts +8 -0
  55. package/dist/memory/index.js +7 -0
  56. package/dist/memory/loader.d.ts +114 -0
  57. package/dist/memory/loader.js +463 -0
  58. package/dist/memory/types.d.ts +182 -0
  59. package/dist/memory/types.js +8 -0
  60. package/dist/messages/index.d.ts +82 -0
  61. package/dist/messages/index.js +155 -0
  62. package/dist/permissions/index.d.ts +5 -0
  63. package/dist/permissions/index.js +4 -0
  64. package/dist/permissions/manager.d.ts +125 -0
  65. package/dist/permissions/manager.js +379 -0
  66. package/dist/permissions/types.d.ts +162 -0
  67. package/dist/permissions/types.js +7 -0
  68. package/dist/providers/claude.d.ts +90 -0
  69. package/dist/providers/claude.js +348 -0
  70. package/dist/providers/index.d.ts +8 -0
  71. package/dist/providers/index.js +11 -0
  72. package/dist/providers/mock.d.ts +133 -0
  73. package/dist/providers/mock.js +204 -0
  74. package/dist/providers/types.d.ts +168 -0
  75. package/dist/providers/types.js +4 -0
  76. package/dist/rate-limit/index.d.ts +45 -0
  77. package/dist/rate-limit/index.js +47 -0
  78. package/dist/rate-limit/limiter.d.ts +104 -0
  79. package/dist/rate-limit/limiter.js +326 -0
  80. package/dist/rate-limit/provider-wrapper.d.ts +112 -0
  81. package/dist/rate-limit/provider-wrapper.js +201 -0
  82. package/dist/rate-limit/retry.d.ts +108 -0
  83. package/dist/rate-limit/retry.js +287 -0
  84. package/dist/rate-limit/types.d.ts +181 -0
  85. package/dist/rate-limit/types.js +22 -0
  86. package/dist/rehearsal/file-analyzer.d.ts +22 -0
  87. package/dist/rehearsal/file-analyzer.js +351 -0
  88. package/dist/rehearsal/git-analyzer.d.ts +22 -0
  89. package/dist/rehearsal/git-analyzer.js +472 -0
  90. package/dist/rehearsal/index.d.ts +35 -0
  91. package/dist/rehearsal/index.js +36 -0
  92. package/dist/rehearsal/manager.d.ts +100 -0
  93. package/dist/rehearsal/manager.js +290 -0
  94. package/dist/rehearsal/types.d.ts +235 -0
  95. package/dist/rehearsal/types.js +8 -0
  96. package/dist/skills/index.d.ts +160 -0
  97. package/dist/skills/index.js +282 -0
  98. package/dist/state/agent-state.d.ts +41 -0
  99. package/dist/state/agent-state.js +88 -0
  100. package/dist/state/checkpointer.d.ts +110 -0
  101. package/dist/state/checkpointer.js +362 -0
  102. package/dist/state/errors.d.ts +66 -0
  103. package/dist/state/errors.js +88 -0
  104. package/dist/state/index.d.ts +35 -0
  105. package/dist/state/index.js +37 -0
  106. package/dist/state/serializer.d.ts +55 -0
  107. package/dist/state/serializer.js +172 -0
  108. package/dist/state/types.d.ts +312 -0
  109. package/dist/state/types.js +14 -0
  110. package/dist/tools/builtin/bash-output.d.ts +61 -0
  111. package/dist/tools/builtin/bash-output.js +90 -0
  112. package/dist/tools/builtin/bash.d.ts +150 -0
  113. package/dist/tools/builtin/bash.js +354 -0
  114. package/dist/tools/builtin/edit.d.ts +50 -0
  115. package/dist/tools/builtin/edit.js +215 -0
  116. package/dist/tools/builtin/glob.d.ts +62 -0
  117. package/dist/tools/builtin/glob.js +244 -0
  118. package/dist/tools/builtin/grep.d.ts +74 -0
  119. package/dist/tools/builtin/grep.js +363 -0
  120. package/dist/tools/builtin/index.d.ts +44 -0
  121. package/dist/tools/builtin/index.js +69 -0
  122. package/dist/tools/builtin/kill-shell.d.ts +44 -0
  123. package/dist/tools/builtin/kill-shell.js +80 -0
  124. package/dist/tools/builtin/read-file.d.ts +57 -0
  125. package/dist/tools/builtin/read-file.js +184 -0
  126. package/dist/tools/builtin/shell-manager.d.ts +176 -0
  127. package/dist/tools/builtin/shell-manager.js +337 -0
  128. package/dist/tools/builtin/task.d.ts +202 -0
  129. package/dist/tools/builtin/task.js +350 -0
  130. package/dist/tools/builtin/todo.d.ts +207 -0
  131. package/dist/tools/builtin/todo.js +453 -0
  132. package/dist/tools/builtin/utils.d.ts +27 -0
  133. package/dist/tools/builtin/utils.js +70 -0
  134. package/dist/tools/builtin/web-fetch.d.ts +96 -0
  135. package/dist/tools/builtin/web-fetch.js +290 -0
  136. package/dist/tools/builtin/write-file.d.ts +54 -0
  137. package/dist/tools/builtin/write-file.js +147 -0
  138. package/dist/tools/define.d.ts +60 -0
  139. package/dist/tools/define.js +65 -0
  140. package/dist/tools/index.d.ts +10 -0
  141. package/dist/tools/index.js +37 -0
  142. package/dist/tools/registry.d.ts +79 -0
  143. package/dist/tools/registry.js +151 -0
  144. package/dist/tools/types.d.ts +59 -0
  145. package/dist/tools/types.js +4 -0
  146. package/dist/tracing/hooks.d.ts +58 -0
  147. package/dist/tracing/hooks.js +377 -0
  148. package/dist/tracing/index.d.ts +51 -0
  149. package/dist/tracing/index.js +55 -0
  150. package/dist/tracing/logging.d.ts +78 -0
  151. package/dist/tracing/logging.js +310 -0
  152. package/dist/tracing/manager.d.ts +160 -0
  153. package/dist/tracing/manager.js +468 -0
  154. package/dist/tracing/otel.d.ts +102 -0
  155. package/dist/tracing/otel.js +246 -0
  156. package/dist/tracing/types.d.ts +346 -0
  157. package/dist/tracing/types.js +38 -0
  158. package/dist/utils/index.d.ts +23 -0
  159. package/dist/utils/index.js +44 -0
  160. 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
+ }