@compilr-dev/agents 0.3.1 → 0.3.4

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 (44) hide show
  1. package/dist/agent.d.ts +107 -2
  2. package/dist/agent.js +151 -22
  3. package/dist/context/manager.d.ts +8 -0
  4. package/dist/context/manager.js +25 -2
  5. package/dist/errors.d.ts +20 -1
  6. package/dist/errors.js +44 -2
  7. package/dist/index.d.ts +16 -1
  8. package/dist/index.js +13 -1
  9. package/dist/messages/index.d.ts +12 -5
  10. package/dist/messages/index.js +53 -15
  11. package/dist/providers/claude.js +7 -0
  12. package/dist/providers/fireworks.d.ts +86 -0
  13. package/dist/providers/fireworks.js +123 -0
  14. package/dist/providers/gemini-native.d.ts +86 -0
  15. package/dist/providers/gemini-native.js +374 -0
  16. package/dist/providers/groq.d.ts +86 -0
  17. package/dist/providers/groq.js +123 -0
  18. package/dist/providers/index.d.ts +17 -2
  19. package/dist/providers/index.js +13 -2
  20. package/dist/providers/openai-compatible.js +12 -1
  21. package/dist/providers/openrouter.d.ts +95 -0
  22. package/dist/providers/openrouter.js +138 -0
  23. package/dist/providers/perplexity.d.ts +86 -0
  24. package/dist/providers/perplexity.js +123 -0
  25. package/dist/providers/together.d.ts +86 -0
  26. package/dist/providers/together.js +123 -0
  27. package/dist/providers/types.d.ts +19 -0
  28. package/dist/state/agent-state.d.ts +1 -0
  29. package/dist/state/agent-state.js +2 -0
  30. package/dist/state/serializer.js +20 -2
  31. package/dist/state/types.d.ts +5 -0
  32. package/dist/tools/builtin/ask-user-simple.js +1 -0
  33. package/dist/tools/builtin/ask-user.js +1 -0
  34. package/dist/tools/builtin/bash.js +123 -2
  35. package/dist/tools/builtin/shell-manager.d.ts +15 -0
  36. package/dist/tools/builtin/shell-manager.js +51 -0
  37. package/dist/tools/builtin/suggest.js +1 -0
  38. package/dist/tools/builtin/todo.js +2 -0
  39. package/dist/tools/define.d.ts +6 -0
  40. package/dist/tools/define.js +1 -0
  41. package/dist/tools/types.d.ts +19 -0
  42. package/dist/utils/index.d.ts +119 -4
  43. package/dist/utils/index.js +164 -13
  44. package/package.json +7 -1
package/dist/agent.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Agent - The main class for running AI agents with tool use
3
3
  */
4
4
  import type { LLMProvider, Message, ChatOptions, StreamChunk } from './providers/types.js';
5
- import type { Tool, ToolDefinition, ToolRegistry, ToolExecutionResult } from './tools/types.js';
5
+ import type { Tool, ToolDefinition, ToolRegistry, ToolExecutionResult, ToolExecutionContext } from './tools/types.js';
6
6
  import type { ContextStats, VerbosityLevel, SmartCompactionResult } from './context/types.js';
7
7
  import type { AgentState, Checkpointer, SessionMetadata } from './state/types.js';
8
8
  import type { Anchor, AnchorInput, AnchorQueryOptions, AnchorClearOptions, AnchorManagerOptions } from './anchors/types.js';
@@ -16,6 +16,7 @@ import { ContextManager } from './context/manager.js';
16
16
  import { FileAccessTracker } from './context/file-tracker.js';
17
17
  import { AnchorManager } from './anchors/manager.js';
18
18
  import { GuardrailManager } from './guardrails/manager.js';
19
+ import { type RetryConfig } from './utils/index.js';
19
20
  /**
20
21
  * Event types emitted during agent execution
21
22
  */
@@ -146,6 +147,18 @@ export type AgentEvent = {
146
147
  type: 'iteration_limit_extended';
147
148
  newMaxIterations: number;
148
149
  addedIterations: number;
150
+ } | {
151
+ type: 'llm_retry';
152
+ attempt: number;
153
+ maxAttempts: number;
154
+ error: string;
155
+ delayMs: number;
156
+ provider: string;
157
+ } | {
158
+ type: 'llm_retry_exhausted';
159
+ attempts: number;
160
+ error: string;
161
+ provider: string;
149
162
  };
150
163
  /**
151
164
  * Event handler function type
@@ -268,6 +281,29 @@ export interface AgentConfig {
268
281
  * Event handler for monitoring agent execution
269
282
  */
270
283
  onEvent?: AgentEventHandler;
284
+ /**
285
+ * Configuration for automatic retry on transient LLM errors.
286
+ * Enabled by default with sensible defaults (10 attempts, exponential backoff).
287
+ *
288
+ * Retryable errors include:
289
+ * - Rate limit (429)
290
+ * - Server errors (5xx)
291
+ * - Connection/network errors
292
+ * - Anthropic overloaded (529)
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * const agent = new Agent({
297
+ * provider,
298
+ * retry: {
299
+ * maxAttempts: 5, // Max 5 retries
300
+ * baseDelayMs: 2000, // Start with 2s delay
301
+ * maxDelayMs: 60000, // Cap at 60s
302
+ * }
303
+ * });
304
+ * ```
305
+ */
306
+ retry?: RetryConfig;
271
307
  /**
272
308
  * Checkpointer for persisting agent state.
273
309
  * If provided, enables checkpoint() and resume() functionality.
@@ -377,6 +413,15 @@ export interface AgentConfig {
377
413
  * ```
378
414
  */
379
415
  permissions?: PermissionManagerOptions;
416
+ /**
417
+ * Pre-existing PermissionManager instance.
418
+ *
419
+ * Use this to share a PermissionManager between agents (e.g., parent and sub-agents).
420
+ * When provided, takes precedence over `permissions` options.
421
+ *
422
+ * This is primarily used internally for sub-agent permission inheritance.
423
+ */
424
+ permissionManager?: PermissionManager;
380
425
  /**
381
426
  * Pre-loaded project memory to prepend to system prompt.
382
427
  *
@@ -544,6 +589,29 @@ export interface RunOptions {
544
589
  * ```
545
590
  */
546
591
  toolFilter?: string[];
592
+ /**
593
+ * Callback to provide additional tool execution context.
594
+ * Called before each tool execution, allowing the caller to inject
595
+ * abort signals or other context-specific options.
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * // Provide abort signal for bash commands (for Ctrl+B backgrounding)
600
+ * const bashAbortController = new AbortController();
601
+ * await agent.stream(message, {
602
+ * getToolContext: (toolName, toolUseId) => {
603
+ * if (toolName === 'bash') {
604
+ * return {
605
+ * abortSignal: bashAbortController.signal,
606
+ * onBackground: (shellId, output) => { ... },
607
+ * };
608
+ * }
609
+ * return {};
610
+ * },
611
+ * });
612
+ * ```
613
+ */
614
+ getToolContext?: (toolName: string, toolUseId: string) => Partial<Omit<ToolExecutionContext, 'toolUseId' | 'onOutput'>>;
547
615
  }
548
616
  /**
549
617
  * Agent run result
@@ -649,6 +717,20 @@ export interface SubAgentConfig {
649
717
  * Default: false (uses shared store)
650
718
  */
651
719
  stateIsolation?: boolean;
720
+ /**
721
+ * Inherit parent's permission manager.
722
+ *
723
+ * When true (default), the sub-agent uses the parent's PermissionManager,
724
+ * sharing session grants and permission rules. This ensures:
725
+ * - Sub-agents follow the same permission rules as parent
726
+ * - Session grants from parent are available to sub-agents
727
+ * - User sees permission prompts for sub-agent tool usage
728
+ *
729
+ * Set to false to allow sub-agents to bypass permissions (use with caution).
730
+ *
731
+ * Default: true
732
+ */
733
+ inheritPermissions?: boolean;
652
734
  }
653
735
  /**
654
736
  * Result from a sub-agent execution
@@ -712,6 +794,7 @@ export declare class Agent {
712
794
  private readonly autoContextManagement;
713
795
  private readonly onEvent?;
714
796
  private readonly onIterationLimitReached?;
797
+ private readonly retryConfig;
715
798
  private readonly checkpointer?;
716
799
  private readonly _sessionId;
717
800
  private readonly autoCheckpoint;
@@ -1136,8 +1219,14 @@ export declare class Agent {
1136
1219
  /**
1137
1220
  * Set the conversation history (for manual compaction/restoration)
1138
1221
  * Also updates the context manager's token count if configured.
1222
+ *
1223
+ * @param messages - The message history to restore
1224
+ * @param options - Optional restore options
1225
+ * @param options.turnCount - The turn count to restore (important for compaction)
1139
1226
  */
1140
- setHistory(messages: Message[]): Promise<this>;
1227
+ setHistory(messages: Message[], options?: {
1228
+ turnCount?: number;
1229
+ }): Promise<this>;
1141
1230
  /**
1142
1231
  * Get the context manager (if configured)
1143
1232
  */
@@ -1439,6 +1528,10 @@ export declare class Agent {
1439
1528
  * Get all registered tool definitions
1440
1529
  */
1441
1530
  getToolDefinitions(): ToolDefinition[];
1531
+ /**
1532
+ * Check if a tool is marked as silent (no spinner or result output)
1533
+ */
1534
+ isToolSilent(name: string): boolean;
1442
1535
  /**
1443
1536
  * Run the agent with a user message
1444
1537
  */
@@ -1454,6 +1547,18 @@ export declare class Agent {
1454
1547
  * Process stream chunks into text, tool uses, and usage data
1455
1548
  */
1456
1549
  private processChunks;
1550
+ /**
1551
+ * Wrap provider.chat() with automatic retry on transient errors.
1552
+ *
1553
+ * Retries the entire stream on failure with exponential backoff.
1554
+ * Emits llm_retry events before each retry attempt.
1555
+ *
1556
+ * @param messages - Messages to send
1557
+ * @param options - Chat options
1558
+ * @param emit - Event emitter function
1559
+ * @param signal - Optional abort signal
1560
+ */
1561
+ private chatWithRetry;
1457
1562
  /**
1458
1563
  * Generate a summary of messages using the LLM provider.
1459
1564
  * Used for context summarization when approaching limits.
package/dist/agent.js CHANGED
@@ -11,10 +11,11 @@ import { FileAccessTracker } from './context/file-tracker.js';
11
11
  import { createFileTrackingHook } from './context/file-tracking-hook.js';
12
12
  import { AnchorManager } from './anchors/manager.js';
13
13
  import { GuardrailManager } from './guardrails/manager.js';
14
- import { MaxIterationsError, ToolLoopError } from './errors.js';
14
+ import { MaxIterationsError, ToolLoopError, ProviderError } from './errors.js';
15
15
  import { generateSessionId, createAgentState, deserializeTodos } from './state/agent-state.js';
16
16
  import { getDefaultTodoStore, createIsolatedTodoStore } from './tools/builtin/todo.js';
17
17
  import { repairToolPairing } from './messages/index.js';
18
+ import { DEFAULT_RETRY_CONFIG, withRetryGenerator } from './utils/index.js';
18
19
  /**
19
20
  * Agent class - orchestrates LLM interactions with tool use
20
21
  *
@@ -44,6 +45,8 @@ export class Agent {
44
45
  autoContextManagement;
45
46
  onEvent;
46
47
  onIterationLimitReached;
48
+ // Retry configuration
49
+ retryConfig;
47
50
  // State management
48
51
  checkpointer;
49
52
  _sessionId;
@@ -120,7 +123,12 @@ export class Agent {
120
123
  this.guardrailManager = new GuardrailManager(config.guardrails);
121
124
  }
122
125
  // Permission management
123
- if (config.permissions !== undefined) {
126
+ // Use pre-existing permissionManager if provided (for sub-agent inheritance)
127
+ // Otherwise create one from options
128
+ if (config.permissionManager !== undefined) {
129
+ this.permissionManager = config.permissionManager;
130
+ }
131
+ else if (config.permissions !== undefined) {
124
132
  this.permissionManager = new PermissionManager(config.permissions);
125
133
  }
126
134
  // Project memory - store and prepend to system prompt
@@ -183,6 +191,13 @@ export class Agent {
183
191
  // Hooks manager without file tracking
184
192
  this.hooksManager = new HooksManager({ hooks: config.hooks });
185
193
  }
194
+ // Retry configuration with defaults
195
+ this.retryConfig = {
196
+ enabled: config.retry?.enabled ?? DEFAULT_RETRY_CONFIG.enabled,
197
+ maxAttempts: config.retry?.maxAttempts ?? DEFAULT_RETRY_CONFIG.maxAttempts,
198
+ baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs,
199
+ maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs,
200
+ };
186
201
  }
187
202
  // ==========================================================================
188
203
  // Static Factory Methods
@@ -710,11 +725,28 @@ export class Agent {
710
725
  /**
711
726
  * Set the conversation history (for manual compaction/restoration)
712
727
  * Also updates the context manager's token count if configured.
728
+ *
729
+ * @param messages - The message history to restore
730
+ * @param options - Optional restore options
731
+ * @param options.turnCount - The turn count to restore (important for compaction)
713
732
  */
714
- async setHistory(messages) {
715
- this.conversationHistory = [...messages];
733
+ async setHistory(messages, options) {
734
+ // Repair tool_use/tool_result pairing to fix any corrupted messages
735
+ // This is especially important after resuming from a saved session
736
+ const repairedMessages = repairToolPairing(messages);
737
+ this.conversationHistory = [...repairedMessages];
716
738
  if (this.contextManager) {
717
- await this.contextManager.updateTokenCount(messages);
739
+ await this.contextManager.updateTokenCount(repairedMessages);
740
+ // Restore turn count if provided
741
+ if (options?.turnCount !== undefined) {
742
+ this.contextManager.setTurnCount(options.turnCount);
743
+ }
744
+ else {
745
+ // Infer turn count from messages as a fallback
746
+ // Count user messages as a proxy for turns
747
+ const inferredTurnCount = repairedMessages.filter((m) => m.role === 'user').length;
748
+ this.contextManager.setTurnCount(inferredTurnCount);
749
+ }
718
750
  }
719
751
  return this;
720
752
  }
@@ -926,6 +958,7 @@ export class Agent {
926
958
  messages: this.conversationHistory,
927
959
  todos: todoStore.getAll(),
928
960
  currentIteration: this._currentIteration,
961
+ turnCount: this.contextManager?.getTurnCount() ?? 0,
929
962
  totalTokensUsed: this._totalTokensUsed,
930
963
  createdAt: this._createdAt,
931
964
  });
@@ -1054,6 +1087,11 @@ export class Agent {
1054
1087
  agent._createdAt = state.createdAt;
1055
1088
  agent._totalTokensUsed = state.totalTokensUsed;
1056
1089
  agent._currentIteration = state.currentIteration;
1090
+ // Restore turn count to context manager if configured
1091
+ // Note: turnCount is guaranteed to exist - deserializer ensures backward compatibility
1092
+ if (agent.contextManager) {
1093
+ agent.contextManager.setTurnCount(state.turnCount);
1094
+ }
1057
1095
  // Restore todos
1058
1096
  if (state.todos.length > 0) {
1059
1097
  const todoStore = getDefaultTodoStore();
@@ -1118,6 +1156,9 @@ export class Agent {
1118
1156
  for (const tool of toolsToRegister) {
1119
1157
  subAgentToolRegistry.register(tool);
1120
1158
  }
1159
+ // Determine if sub-agent should inherit parent's permissions
1160
+ // Default: true (sub-agents follow same permission rules as parent)
1161
+ const inheritPermissions = config.inheritPermissions ?? true;
1121
1162
  // Create the sub-agent
1122
1163
  const subAgent = new Agent({
1123
1164
  provider: this.provider,
@@ -1128,6 +1169,12 @@ export class Agent {
1128
1169
  contextManager: subAgentContextManager,
1129
1170
  autoContextManagement: true,
1130
1171
  onEvent: this.onEvent, // Forward events to parent
1172
+ // Sub-agents should summarize their findings when hitting iteration limit
1173
+ // rather than throwing an error with no response
1174
+ iterationLimitBehavior: 'summarize',
1175
+ // Inherit parent's permission manager if enabled
1176
+ // This shares session grants and permission rules with sub-agents
1177
+ permissionManager: inheritPermissions ? this.permissionManager : undefined,
1131
1178
  });
1132
1179
  // Store the sub-agent
1133
1180
  this.subAgents.set(config.name, { config, agent: subAgent });
@@ -1375,6 +1422,13 @@ export class Agent {
1375
1422
  getToolDefinitions() {
1376
1423
  return this.toolRegistry.getDefinitions();
1377
1424
  }
1425
+ /**
1426
+ * Check if a tool is marked as silent (no spinner or result output)
1427
+ */
1428
+ isToolSilent(name) {
1429
+ const tool = this.toolRegistry.get(name);
1430
+ return tool?.silent === true;
1431
+ }
1378
1432
  /**
1379
1433
  * Run the agent with a user message
1380
1434
  */
@@ -1382,6 +1436,7 @@ export class Agent {
1382
1436
  let maxIterations = options?.maxIterations ?? this.maxIterations;
1383
1437
  const chatOptions = { ...this.chatOptions, ...options?.chatOptions };
1384
1438
  const signal = options?.signal;
1439
+ const getToolContext = options?.getToolContext;
1385
1440
  // Combined event emitter
1386
1441
  const emit = (event) => {
1387
1442
  this.onEvent?.(event);
@@ -1390,22 +1445,34 @@ export class Agent {
1390
1445
  // Build messages: system prompt + history + new user message
1391
1446
  let messages = [];
1392
1447
  // Add system message if present
1393
- if (this.systemPrompt) {
1394
- messages.push({
1395
- role: 'system',
1396
- content: this.systemPrompt,
1397
- });
1398
- }
1399
- // Inject anchors (critical information that survives context compaction)
1448
+ // NOTE: Anchors are now appended to the system prompt (not a separate message)
1449
+ // to avoid multiple system messages which can confuse Claude's role identity
1450
+ let systemContent = this.systemPrompt;
1451
+ // Inject anchors into the system prompt (not as separate message)
1400
1452
  if (this.anchorManager && this.anchorManager.size > 0) {
1401
1453
  const anchorsContent = this.anchorManager.format();
1402
1454
  if (anchorsContent) {
1403
- messages.push({
1404
- role: 'system',
1405
- content: `## Active Anchors (Critical Information)\n\n${anchorsContent}`,
1406
- });
1455
+ // Insert anchors BEFORE the role ending (if present) so role reinforcement stays at the end
1456
+ const roleEndingMarker = '## YOUR ASSIGNED ROLE:';
1457
+ const roleEndingIndex = systemContent.indexOf(roleEndingMarker);
1458
+ if (roleEndingIndex > 0) {
1459
+ // Insert anchors before the role ending
1460
+ const beforeRole = systemContent.substring(0, roleEndingIndex);
1461
+ const roleEnding = systemContent.substring(roleEndingIndex);
1462
+ systemContent = `${beforeRole}\n\n---\n\n## Active Anchors (Critical Information)\n\n${anchorsContent}\n\n---\n\n${roleEnding}`;
1463
+ }
1464
+ else {
1465
+ // No role ending, just append anchors
1466
+ systemContent = `${systemContent}\n\n---\n\n## Active Anchors (Critical Information)\n\n${anchorsContent}`;
1467
+ }
1407
1468
  }
1408
1469
  }
1470
+ if (systemContent) {
1471
+ messages.push({
1472
+ role: 'system',
1473
+ content: systemContent,
1474
+ });
1475
+ }
1409
1476
  // Add conversation history
1410
1477
  messages.push(...this.conversationHistory);
1411
1478
  // Add new user message
@@ -1514,10 +1581,10 @@ export class Agent {
1514
1581
  const llmStartTime = Date.now();
1515
1582
  const chunks = [];
1516
1583
  try {
1517
- for await (const chunk of this.provider.chat(messages, {
1584
+ for await (const chunk of this.chatWithRetry(messages, {
1518
1585
  ...chatOptions,
1519
1586
  tools: tools.length > 0 ? tools : undefined,
1520
- })) {
1587
+ }, emit, signal)) {
1521
1588
  // Check for abort during streaming
1522
1589
  if (signal?.aborted) {
1523
1590
  aborted = true;
@@ -1615,6 +1682,7 @@ export class Agent {
1615
1682
  id: tu.id,
1616
1683
  name: tu.name,
1617
1684
  input: tu.input,
1685
+ signature: tu.signature, // Gemini 3 thought signature (required for multi-turn)
1618
1686
  })),
1619
1687
  ],
1620
1688
  };
@@ -1757,6 +1825,8 @@ export class Agent {
1757
1825
  }
1758
1826
  const toolStartTime = Date.now();
1759
1827
  try {
1828
+ // Get additional context from caller (e.g., for bash backgrounding)
1829
+ const additionalContext = getToolContext?.(toolUse.name, toolUse.id) ?? {};
1760
1830
  const toolContext = {
1761
1831
  toolUseId: toolUse.id,
1762
1832
  onOutput: (output, stream) => {
@@ -1768,6 +1838,8 @@ export class Agent {
1768
1838
  stream,
1769
1839
  });
1770
1840
  },
1841
+ // Merge in additional context (abortSignal, onBackground, etc.)
1842
+ ...additionalContext,
1771
1843
  };
1772
1844
  result = await this.toolRegistry.execute(toolUse.name, toolInput, toolContext);
1773
1845
  }
@@ -2111,6 +2183,7 @@ export class Agent {
2111
2183
  id: chunk.toolUse.id,
2112
2184
  name: chunk.toolUse.name,
2113
2185
  inputJson: '',
2186
+ signature: chunk.toolUse.signature, // Capture Gemini 3 thought signature
2114
2187
  };
2115
2188
  }
2116
2189
  break;
@@ -2129,6 +2202,7 @@ export class Agent {
2129
2202
  id: currentToolUse.id,
2130
2203
  name: currentToolUse.name,
2131
2204
  input,
2205
+ signature: currentToolUse.signature, // Pass through the signature
2132
2206
  });
2133
2207
  }
2134
2208
  catch {
@@ -2146,6 +2220,53 @@ export class Agent {
2146
2220
  }
2147
2221
  return { text, toolUses, usage, model };
2148
2222
  }
2223
+ /**
2224
+ * Wrap provider.chat() with automatic retry on transient errors.
2225
+ *
2226
+ * Retries the entire stream on failure with exponential backoff.
2227
+ * Emits llm_retry events before each retry attempt.
2228
+ *
2229
+ * @param messages - Messages to send
2230
+ * @param options - Chat options
2231
+ * @param emit - Event emitter function
2232
+ * @param signal - Optional abort signal
2233
+ */
2234
+ chatWithRetry(messages, options, emit, signal) {
2235
+ // If retry is disabled, return the raw provider stream
2236
+ if (!this.retryConfig.enabled) {
2237
+ return this.provider.chat(messages, options);
2238
+ }
2239
+ const providerName = this.provider.name;
2240
+ const { maxAttempts, baseDelayMs, maxDelayMs } = this.retryConfig;
2241
+ return withRetryGenerator(() => this.provider.chat(messages, options), {
2242
+ maxAttempts,
2243
+ baseDelayMs,
2244
+ maxDelayMs,
2245
+ isRetryable: (error) => {
2246
+ // Use the ProviderError's built-in retryable check
2247
+ return error instanceof ProviderError && error.isRetryable();
2248
+ },
2249
+ onRetry: (attempt, max, error, delayMs) => {
2250
+ emit({
2251
+ type: 'llm_retry',
2252
+ attempt,
2253
+ maxAttempts: max,
2254
+ error: error.message,
2255
+ delayMs,
2256
+ provider: providerName,
2257
+ });
2258
+ },
2259
+ onExhausted: (attempts, error) => {
2260
+ emit({
2261
+ type: 'llm_retry_exhausted',
2262
+ attempts,
2263
+ error: error.message,
2264
+ provider: providerName,
2265
+ });
2266
+ },
2267
+ signal,
2268
+ });
2269
+ }
2149
2270
  /**
2150
2271
  * Generate a summary of messages using the LLM provider.
2151
2272
  * Used for context summarization when approaching limits.
@@ -2184,11 +2305,15 @@ Keep the summary under 500 words.
2184
2305
  Conversation to summarize:
2185
2306
  ${messages.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n\n')}`;
2186
2307
  const summaryMessages = [{ role: 'user', content: summaryPrompt }];
2308
+ // Create emit function for retry events (uses main event handler)
2309
+ const emit = (event) => {
2310
+ this.onEvent?.(event);
2311
+ };
2187
2312
  let summary = '';
2188
- for await (const chunk of this.provider.chat(summaryMessages, {
2313
+ for await (const chunk of this.chatWithRetry(summaryMessages, {
2189
2314
  ...this.chatOptions,
2190
2315
  maxTokens: this.contextManager?.getConfig().summarization.summaryMaxTokens ?? 2000,
2191
- })) {
2316
+ }, emit)) {
2192
2317
  if (chunk.type === 'text' && chunk.text) {
2193
2318
  summary += chunk.text;
2194
2319
  }
@@ -2227,11 +2352,15 @@ ${messages
2227
2352
  .map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content.slice(0, 500) : '[complex content]'}`)
2228
2353
  .join('\n\n')}`;
2229
2354
  const summaryMessages = [{ role: 'user', content: summaryPrompt }];
2355
+ // Create emit function for retry events (uses main event handler)
2356
+ const emit = (event) => {
2357
+ this.onEvent?.(event);
2358
+ };
2230
2359
  let summary = '';
2231
- for await (const chunk of this.provider.chat(summaryMessages, {
2360
+ for await (const chunk of this.chatWithRetry(summaryMessages, {
2232
2361
  ...this.chatOptions,
2233
2362
  maxTokens: 1000, // Keep summary concise
2234
- })) {
2363
+ }, emit)) {
2235
2364
  if (chunk.type === 'text' && chunk.text) {
2236
2365
  summary += chunk.text;
2237
2366
  }
@@ -98,6 +98,14 @@ export declare class ContextManager {
98
98
  * Increment turn count (call after each assistant response)
99
99
  */
100
100
  incrementTurn(): void;
101
+ /**
102
+ * Get the current turn count
103
+ */
104
+ getTurnCount(): number;
105
+ /**
106
+ * Set the turn count (for restoring from saved state)
107
+ */
108
+ setTurnCount(count: number): void;
101
109
  /**
102
110
  * Check if compaction is needed
103
111
  */
@@ -17,6 +17,7 @@
17
17
  * }
18
18
  * ```
19
19
  */
20
+ import { repairToolPairing } from '../messages/index.js';
20
21
  /**
21
22
  * Default budget allocation
22
23
  */
@@ -176,6 +177,23 @@ export class ContextManager {
176
177
  incrementTurn() {
177
178
  this.turnCount++;
178
179
  }
180
+ /**
181
+ * Get the current turn count
182
+ */
183
+ getTurnCount() {
184
+ return this.turnCount;
185
+ }
186
+ /**
187
+ * Set the turn count (for restoring from saved state)
188
+ */
189
+ setTurnCount(count) {
190
+ this.turnCount = count;
191
+ // If we're restoring state, assume last compaction was at turn 0
192
+ // to prevent immediate compaction after restore
193
+ if (this.lastCompactionTurn === 0 && count > 0) {
194
+ this.lastCompactionTurn = count;
195
+ }
196
+ }
179
197
  /**
180
198
  * Check if compaction is needed
181
199
  */
@@ -588,12 +606,15 @@ export class ContextManager {
588
606
  }
589
607
  // Reconstruct messages in original order
590
608
  // System messages come first, then summary (if any), then tool results, then recent
591
- const finalMessages = [
609
+ let finalMessages = [
592
610
  ...categorized.system,
593
611
  ...compactedHistory,
594
612
  ...compactedToolResults,
595
613
  ...categorized.recentMessages,
596
614
  ];
615
+ // Repair tool_use/tool_result pairing after compaction
616
+ // This fixes orphaned blocks that cause API errors
617
+ finalMessages = repairToolPairing(finalMessages);
597
618
  // If history was summarized, add assistant acknowledgment after summary
598
619
  if (categoryStats.history.action === 'summarized' && compactedHistory.length > 0) {
599
620
  // Insert assistant acknowledgment after the summary message
@@ -894,7 +915,9 @@ export class ContextManager {
894
915
  compactedOld.push({ ...msg, content: compactedBlocks });
895
916
  }
896
917
  }
897
- const compactedMessages = [...compactedOld, ...recentMessages];
918
+ // Repair tool_use/tool_result pairing after compaction
919
+ // This fixes orphaned blocks that cause API errors
920
+ const compactedMessages = repairToolPairing([...compactedOld, ...recentMessages]);
898
921
  const tokensAfter = await this.countTokens(compactedMessages);
899
922
  this.compactionCount++;
900
923
  this.lastCompactionTurn = this.turnCount;
package/dist/errors.d.ts CHANGED
@@ -43,7 +43,26 @@ export declare class ProviderError extends AgentError {
43
43
  */
44
44
  isServerError(): boolean;
45
45
  /**
46
- * Check if this error is retryable
46
+ * Check if this is a connection/network error.
47
+ * These errors typically have no status code but are retryable.
48
+ */
49
+ isConnectionError(): boolean;
50
+ /**
51
+ * Check if this is an Anthropic overloaded error (529)
52
+ */
53
+ isOverloadedError(): boolean;
54
+ /**
55
+ * Check if this error is retryable.
56
+ * Retryable errors include:
57
+ * - Rate limit (429)
58
+ * - Server errors (5xx)
59
+ * - Connection/network errors
60
+ * - Anthropic overloaded (529)
61
+ *
62
+ * Non-retryable errors include:
63
+ * - Authentication errors (401, 403)
64
+ * - Bad request (400)
65
+ * - Not found (404)
47
66
  */
48
67
  isRetryable(): boolean;
49
68
  }
package/dist/errors.js CHANGED
@@ -62,10 +62,52 @@ export class ProviderError extends AgentError {
62
62
  return this.statusCode !== undefined && this.statusCode >= 500 && this.statusCode < 600;
63
63
  }
64
64
  /**
65
- * Check if this error is retryable
65
+ * Check if this is a connection/network error.
66
+ * These errors typically have no status code but are retryable.
67
+ */
68
+ isConnectionError() {
69
+ if (this.statusCode !== undefined) {
70
+ return false;
71
+ }
72
+ const msg = this.message.toLowerCase();
73
+ return (msg.includes('connection') ||
74
+ msg.includes('econnrefused') ||
75
+ msg.includes('econnreset') ||
76
+ msg.includes('etimedout') ||
77
+ msg.includes('enotfound') ||
78
+ msg.includes('network') ||
79
+ msg.includes('socket') ||
80
+ msg.includes('dns'));
81
+ }
82
+ /**
83
+ * Check if this is an Anthropic overloaded error (529)
84
+ */
85
+ isOverloadedError() {
86
+ return this.statusCode === 529;
87
+ }
88
+ /**
89
+ * Check if this error is retryable.
90
+ * Retryable errors include:
91
+ * - Rate limit (429)
92
+ * - Server errors (5xx)
93
+ * - Connection/network errors
94
+ * - Anthropic overloaded (529)
95
+ *
96
+ * Non-retryable errors include:
97
+ * - Authentication errors (401, 403)
98
+ * - Bad request (400)
99
+ * - Not found (404)
66
100
  */
67
101
  isRetryable() {
68
- return this.isRateLimitError() || this.isServerError();
102
+ // Never retry auth errors
103
+ if (this.isAuthError()) {
104
+ return false;
105
+ }
106
+ // Retry transient errors
107
+ return (this.isRateLimitError() ||
108
+ this.isServerError() ||
109
+ this.isConnectionError() ||
110
+ this.isOverloadedError());
69
111
  }
70
112
  }
71
113
  /**