@compilr-dev/agents 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { LLMProvider, Message, ChatOptions, StreamChunk } from './providers/types.js';
5
5
  import type { Tool, ToolDefinition, ToolRegistry, ToolExecutionResult } from './tools/types.js';
6
- import type { ContextStats, VerbosityLevel } from './context/types.js';
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';
9
9
  import type { Guardrail, GuardrailInput, GuardrailResult, GuardrailManagerOptions } from './guardrails/types.js';
@@ -13,6 +13,7 @@ import type { UsageTrackerOptions, UsageStats, BudgetStatus, TokenUsage } from '
13
13
  import type { HooksConfig } from './hooks/types.js';
14
14
  import { PermissionManager } from './permissions/manager.js';
15
15
  import { ContextManager } from './context/manager.js';
16
+ import { FileAccessTracker } from './context/file-tracker.js';
16
17
  import { AnchorManager } from './anchors/manager.js';
17
18
  import { GuardrailManager } from './guardrails/manager.js';
18
19
  /**
@@ -125,6 +126,18 @@ export type AgentEvent = {
125
126
  } | {
126
127
  type: 'usage_budget_exceeded';
127
128
  status: BudgetStatus;
129
+ } | {
130
+ type: 'suggest';
131
+ action: string;
132
+ reason?: string;
133
+ } | {
134
+ type: 'iteration_limit_reached';
135
+ iteration: number;
136
+ maxIterations: number;
137
+ } | {
138
+ type: 'iteration_limit_extended';
139
+ newMaxIterations: number;
140
+ addedIterations: number;
128
141
  };
129
142
  /**
130
143
  * Event handler function type
@@ -189,8 +202,36 @@ export interface AgentConfig {
189
202
  * - 'error': Throw MaxIterationsError immediately
190
203
  * - 'summarize': Generate a final summary response before throwing
191
204
  * - 'continue': Return partial result without throwing (response will be empty)
205
+ *
206
+ * Note: If onIterationLimitReached callback is provided, it takes precedence.
192
207
  */
193
208
  iterationLimitBehavior?: 'error' | 'summarize' | 'continue';
209
+ /**
210
+ * Callback invoked when the agent reaches its iteration limit.
211
+ * Allows the caller to decide whether to continue or stop.
212
+ *
213
+ * @param context - Information about the current state
214
+ * @returns Promise resolving to:
215
+ * - `false` to stop the agent gracefully
216
+ * - A positive number to continue with that many additional iterations
217
+ *
218
+ * If this callback is provided and returns a number, the agent will continue
219
+ * running with the extended iteration limit. This takes precedence over
220
+ * iterationLimitBehavior.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * onIterationLimitReached: async ({ iteration, toolCallCount }) => {
225
+ * const answer = await askUser(`Agent used ${iteration} iterations. Continue?`);
226
+ * return answer === 'yes' ? 50 : false; // Add 50 more or stop
227
+ * }
228
+ * ```
229
+ */
230
+ onIterationLimitReached?: (context: {
231
+ iteration: number;
232
+ maxIterations: number;
233
+ toolCallCount: number;
234
+ }) => Promise<number | false>;
194
235
  /**
195
236
  * Chat options (model, temperature, etc.)
196
237
  */
@@ -199,6 +240,12 @@ export interface AgentConfig {
199
240
  * Custom tool registry (optional, creates new one if not provided)
200
241
  */
201
242
  toolRegistry?: ToolRegistry;
243
+ /**
244
+ * Default timeout for tool execution in milliseconds (default: 30000 = 30s).
245
+ * Set to 0 to disable timeout. Only used when toolRegistry is not provided.
246
+ * Sub-agents inherit this timeout from the parent agent.
247
+ */
248
+ toolTimeoutMs?: number;
202
249
  /**
203
250
  * Context manager for tracking and managing context window usage.
204
251
  * If not provided, context management is disabled.
@@ -428,6 +475,31 @@ export interface AgentConfig {
428
475
  * ```
429
476
  */
430
477
  hooks?: HooksConfig;
478
+ /**
479
+ * Enable file access tracking for context restoration hints.
480
+ *
481
+ * When enabled, the agent tracks which files were read, referenced (grep/glob),
482
+ * and modified during execution. After context compaction, these hints are
483
+ * injected to help the LLM understand what files it previously accessed.
484
+ *
485
+ * Requires contextManager to be set (hints are injected after compaction).
486
+ *
487
+ * @default false
488
+ *
489
+ * @example
490
+ * ```typescript
491
+ * const agent = new Agent({
492
+ * provider,
493
+ * contextManager: new ContextManager({ provider }),
494
+ * enableFileTracking: true, // Automatically track file accesses
495
+ * });
496
+ *
497
+ * // After compaction, the agent will inject hints like:
498
+ * // [Context compacted. Previously accessed files:]
499
+ * // Read (3 files): file1.ts (100 lines), file2.ts (50 lines)...
500
+ * ```
501
+ */
502
+ enableFileTracking?: boolean;
431
503
  }
432
504
  /**
433
505
  * Options for a single run
@@ -631,6 +703,7 @@ export declare class Agent {
631
703
  private readonly contextManager?;
632
704
  private readonly autoContextManagement;
633
705
  private readonly onEvent?;
706
+ private readonly onIterationLimitReached?;
634
707
  private readonly checkpointer?;
635
708
  private readonly _sessionId;
636
709
  private readonly autoCheckpoint;
@@ -670,6 +743,10 @@ export declare class Agent {
670
743
  * Hooks manager for lifecycle hooks
671
744
  */
672
745
  private readonly hooksManager?;
746
+ /**
747
+ * File access tracker for context restoration hints
748
+ */
749
+ private readonly fileTracker?;
673
750
  constructor(config: AgentConfig);
674
751
  /**
675
752
  * Create an agent with project memory loaded from files.
@@ -1065,6 +1142,96 @@ export declare class Agent {
1065
1142
  * Get current verbosity level based on context pressure
1066
1143
  */
1067
1144
  getVerbosityLevel(): VerbosityLevel;
1145
+ /**
1146
+ * Get the file access tracker (if file tracking is enabled)
1147
+ */
1148
+ getFileTracker(): FileAccessTracker | undefined;
1149
+ /**
1150
+ * Format context restoration hints based on tracked file accesses.
1151
+ * Returns empty string if no files have been accessed or file tracking is disabled.
1152
+ */
1153
+ formatRestorationHints(): string;
1154
+ /**
1155
+ * Inject context restoration hints into messages after compaction/summarization.
1156
+ * Modifies messages array in place if hints are available.
1157
+ *
1158
+ * @internal
1159
+ */
1160
+ private injectRestorationHints;
1161
+ /**
1162
+ * Compact the conversation context to reduce token usage.
1163
+ *
1164
+ * This is the recommended way to trigger context compaction externally.
1165
+ * It handles:
1166
+ * 1. Summarizing older messages
1167
+ * 2. Repairing tool use/result pairing (prevents API errors)
1168
+ * 3. Injecting context restoration hints (if file tracking is enabled)
1169
+ * 4. Updating the conversation history
1170
+ *
1171
+ * @param options - Compaction options
1172
+ * @returns Compaction result with statistics
1173
+ *
1174
+ * @example
1175
+ * ```typescript
1176
+ * // Basic compaction
1177
+ * const result = await agent.compact();
1178
+ * console.log(`Reduced from ${result.originalTokens} to ${result.summaryTokens} tokens`);
1179
+ *
1180
+ * // Compaction without restoration hints
1181
+ * await agent.compact({ injectRestorationHints: false });
1182
+ *
1183
+ * // Emergency compaction (more aggressive)
1184
+ * await agent.compact({ emergency: true });
1185
+ * ```
1186
+ */
1187
+ compact(options?: {
1188
+ /**
1189
+ * Inject file restoration hints after compaction.
1190
+ * Only applies if file tracking is enabled.
1191
+ * Default: true (if file tracking is enabled)
1192
+ */
1193
+ injectRestorationHints?: boolean;
1194
+ /**
1195
+ * Use emergency mode (more aggressive summarization).
1196
+ * Default: auto-detect based on context utilization
1197
+ */
1198
+ emergency?: boolean;
1199
+ /**
1200
+ * Target utilization after compaction (0-1).
1201
+ * Default: from context manager config (typically 0.5)
1202
+ */
1203
+ targetUtilization?: number;
1204
+ /**
1205
+ * Use smart category-aware compaction instead of simple summarization.
1206
+ * Smart compaction:
1207
+ * - Preserves system and recent messages completely
1208
+ * - Saves large tool results to files
1209
+ * - Summarizes history with LLM
1210
+ * Default: true
1211
+ */
1212
+ useSmartCompaction?: boolean;
1213
+ }): Promise<{
1214
+ /** Whether compaction was successful */
1215
+ success: boolean;
1216
+ /** Original token count */
1217
+ originalTokens: number;
1218
+ /** Token count after compaction */
1219
+ summaryTokens: number;
1220
+ /** Number of summarization rounds performed */
1221
+ rounds: number;
1222
+ /** Number of messages preserved (not summarized) */
1223
+ messagesPreserved: number;
1224
+ /** Whether restoration hints were injected */
1225
+ restorationHintsInjected: boolean;
1226
+ /** Number of orphaned tool_results removed */
1227
+ toolResultsRepaired: number;
1228
+ /** The generated summary (for debugging/display) */
1229
+ summary: string;
1230
+ /** Files created during smart compaction (tool results saved to files) */
1231
+ filesCreated?: string[];
1232
+ /** Category-specific statistics (only for smart compaction) */
1233
+ categoryStats?: SmartCompactionResult['categoryStats'];
1234
+ }>;
1068
1235
  /**
1069
1236
  * Serialize the current agent state to an AgentState object.
1070
1237
  * This can be used for manual persistence or transferring state.
package/dist/agent.js CHANGED
@@ -7,11 +7,14 @@ import { ProjectMemoryLoader } from './memory/loader.js';
7
7
  import { UsageTracker } from './costs/tracker.js';
8
8
  import { DefaultToolRegistry } from './tools/registry.js';
9
9
  import { ContextManager } from './context/manager.js';
10
+ import { FileAccessTracker } from './context/file-tracker.js';
11
+ import { createFileTrackingHook } from './context/file-tracking-hook.js';
10
12
  import { AnchorManager } from './anchors/manager.js';
11
13
  import { GuardrailManager } from './guardrails/manager.js';
12
14
  import { MaxIterationsError, ToolLoopError } from './errors.js';
13
15
  import { generateSessionId, createAgentState, deserializeTodos } from './state/agent-state.js';
14
16
  import { getDefaultTodoStore, createIsolatedTodoStore } from './tools/builtin/todo.js';
17
+ import { repairToolPairing } from './messages/index.js';
15
18
  /**
16
19
  * Agent class - orchestrates LLM interactions with tool use
17
20
  *
@@ -40,6 +43,7 @@ export class Agent {
40
43
  contextManager;
41
44
  autoContextManagement;
42
45
  onEvent;
46
+ onIterationLimitReached;
43
47
  // State management
44
48
  checkpointer;
45
49
  _sessionId;
@@ -80,6 +84,10 @@ export class Agent {
80
84
  * Hooks manager for lifecycle hooks
81
85
  */
82
86
  hooksManager;
87
+ /**
88
+ * File access tracker for context restoration hints
89
+ */
90
+ fileTracker;
83
91
  constructor(config) {
84
92
  this.provider = config.provider;
85
93
  this.systemPrompt = config.systemPrompt ?? '';
@@ -87,11 +95,16 @@ export class Agent {
87
95
  this.maxConsecutiveToolCalls = config.maxConsecutiveToolCalls ?? 3;
88
96
  this.iterationLimitBehavior = config.iterationLimitBehavior ?? 'error';
89
97
  this.chatOptions = config.chatOptions ?? {};
90
- this.toolRegistry = config.toolRegistry ?? new DefaultToolRegistry();
98
+ this.toolRegistry =
99
+ config.toolRegistry ??
100
+ new DefaultToolRegistry({
101
+ defaultTimeoutMs: config.toolTimeoutMs,
102
+ });
91
103
  this.contextManager = config.contextManager;
92
104
  this.autoContextManagement =
93
105
  config.autoContextManagement ?? config.contextManager !== undefined;
94
106
  this.onEvent = config.onEvent;
107
+ this.onIterationLimitReached = config.onIterationLimitReached;
95
108
  // State management
96
109
  this.checkpointer = config.checkpointer;
97
110
  this._sessionId = config.sessionId ?? generateSessionId();
@@ -156,8 +169,18 @@ export class Agent {
156
169
  }
157
170
  });
158
171
  }
159
- // Hooks manager
160
- if (config.hooks !== undefined) {
172
+ // File tracking for context restoration hints
173
+ if (config.enableFileTracking && config.contextManager) {
174
+ this.fileTracker = new FileAccessTracker();
175
+ const trackingHook = createFileTrackingHook(this.fileTracker);
176
+ // Merge with existing hooks or create new hooks config
177
+ const hooksConfig = config.hooks ?? {};
178
+ hooksConfig.afterTool = hooksConfig.afterTool ?? [];
179
+ hooksConfig.afterTool.push(trackingHook);
180
+ this.hooksManager = new HooksManager({ hooks: hooksConfig });
181
+ }
182
+ else if (config.hooks !== undefined) {
183
+ // Hooks manager without file tracking
161
184
  this.hooksManager = new HooksManager({ hooks: config.hooks });
162
185
  }
163
186
  }
@@ -713,6 +736,173 @@ export class Agent {
713
736
  getVerbosityLevel() {
714
737
  return this.contextManager?.getVerbosityLevel() ?? 'full';
715
738
  }
739
+ /**
740
+ * Get the file access tracker (if file tracking is enabled)
741
+ */
742
+ getFileTracker() {
743
+ return this.fileTracker;
744
+ }
745
+ /**
746
+ * Format context restoration hints based on tracked file accesses.
747
+ * Returns empty string if no files have been accessed or file tracking is disabled.
748
+ */
749
+ formatRestorationHints() {
750
+ return (this.fileTracker?.formatRestorationHints({
751
+ verbosityLevel: this.getVerbosityLevel(),
752
+ }) ?? '');
753
+ }
754
+ /**
755
+ * Inject context restoration hints into messages after compaction/summarization.
756
+ * Modifies messages array in place if hints are available.
757
+ *
758
+ * @internal
759
+ */
760
+ injectRestorationHints(messages) {
761
+ if (!this.fileTracker || this.fileTracker.size === 0) {
762
+ return;
763
+ }
764
+ const hints = this.formatRestorationHints();
765
+ if (!hints) {
766
+ return;
767
+ }
768
+ // Inject as a user message after the last system message
769
+ const systemIndex = messages.findIndex((m) => m.role === 'system');
770
+ const insertIndex = systemIndex >= 0 ? systemIndex + 1 : 0;
771
+ messages.splice(insertIndex, 0, {
772
+ role: 'user',
773
+ content: hints,
774
+ });
775
+ }
776
+ // ==========================================================================
777
+ // Context Compaction
778
+ // ==========================================================================
779
+ /**
780
+ * Compact the conversation context to reduce token usage.
781
+ *
782
+ * This is the recommended way to trigger context compaction externally.
783
+ * It handles:
784
+ * 1. Summarizing older messages
785
+ * 2. Repairing tool use/result pairing (prevents API errors)
786
+ * 3. Injecting context restoration hints (if file tracking is enabled)
787
+ * 4. Updating the conversation history
788
+ *
789
+ * @param options - Compaction options
790
+ * @returns Compaction result with statistics
791
+ *
792
+ * @example
793
+ * ```typescript
794
+ * // Basic compaction
795
+ * const result = await agent.compact();
796
+ * console.log(`Reduced from ${result.originalTokens} to ${result.summaryTokens} tokens`);
797
+ *
798
+ * // Compaction without restoration hints
799
+ * await agent.compact({ injectRestorationHints: false });
800
+ *
801
+ * // Emergency compaction (more aggressive)
802
+ * await agent.compact({ emergency: true });
803
+ * ```
804
+ */
805
+ async compact(options) {
806
+ // Check if context manager is available
807
+ if (!this.contextManager) {
808
+ return {
809
+ success: false,
810
+ originalTokens: 0,
811
+ summaryTokens: 0,
812
+ rounds: 0,
813
+ messagesPreserved: this.conversationHistory.length,
814
+ restorationHintsInjected: false,
815
+ toolResultsRepaired: 0,
816
+ summary: '',
817
+ };
818
+ }
819
+ const shouldInjectHints = options?.injectRestorationHints !== false && this.fileTracker;
820
+ const useSmartCompaction = options?.useSmartCompaction !== false; // Default to true
821
+ // Build full message list including system prompt
822
+ const messages = this.systemPrompt
823
+ ? [{ role: 'system', content: this.systemPrompt }, ...this.conversationHistory]
824
+ : [...this.conversationHistory];
825
+ let summarized;
826
+ let originalTokens;
827
+ let summaryTokens;
828
+ let rounds;
829
+ let messagesPreserved;
830
+ let summary;
831
+ let filesCreated;
832
+ let categoryStats;
833
+ if (useSmartCompaction) {
834
+ // Use smart category-aware compaction
835
+ const { messages: compactedMessages, result: smartResult } = await this.contextManager.smartCompact(messages, {
836
+ generateSummary: (msgs) => this.generateSummary(msgs),
837
+ saveToFile: async (content, index) => {
838
+ // Save to a temp file and return the path
839
+ const path = `/tmp/compacted-tool-result-${String(Date.now())}-${String(index)}.txt`;
840
+ const { writeFile } = await import('node:fs/promises');
841
+ await writeFile(path, content, 'utf-8');
842
+ return path;
843
+ },
844
+ emergency: options?.emergency,
845
+ targetUtilization: options?.targetUtilization,
846
+ });
847
+ summarized = compactedMessages;
848
+ originalTokens = smartResult.tokensBefore;
849
+ summaryTokens = smartResult.tokensAfter;
850
+ rounds = smartResult.summarizationRounds;
851
+ summary = smartResult.summary || '';
852
+ filesCreated = smartResult.filesCreated;
853
+ categoryStats = smartResult.categoryStats;
854
+ // Count preserved messages (system + recent)
855
+ messagesPreserved = compactedMessages.filter((m) => m.role === 'system' || categoryStats?.recentMessages.action === 'preserved').length;
856
+ }
857
+ else {
858
+ // Use simple summarization (legacy behavior)
859
+ const { messages: summarizedMessages, result } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs), {
860
+ emergency: options.emergency,
861
+ targetUtilization: options.targetUtilization,
862
+ });
863
+ summarized = summarizedMessages;
864
+ originalTokens = result.originalTokens;
865
+ summaryTokens = result.summaryTokens;
866
+ rounds = result.rounds;
867
+ messagesPreserved = result.messagesPreserved;
868
+ summary = result.summary;
869
+ }
870
+ // Repair tool pairing issues (fixes Gemini API errors)
871
+ const repaired = repairToolPairing(summarized);
872
+ const toolResultsRepaired = summarized.length - repaired.length;
873
+ // Inject restoration hints if enabled
874
+ let restorationHintsInjected = false;
875
+ if (shouldInjectHints) {
876
+ const sizeBefore = repaired.length;
877
+ this.injectRestorationHints(repaired);
878
+ restorationHintsInjected = repaired.length > sizeBefore;
879
+ }
880
+ // Extract new history (skip system message)
881
+ const newHistory = repaired.filter((m) => m.role !== 'system');
882
+ // Update conversation history
883
+ this.conversationHistory = newHistory;
884
+ // Update context manager's token count
885
+ await this.contextManager.updateTokenCount(repaired);
886
+ // Emit event
887
+ this.onEvent?.({
888
+ type: 'context_summarized',
889
+ tokensBefore: originalTokens,
890
+ tokensAfter: summaryTokens,
891
+ rounds,
892
+ });
893
+ return {
894
+ success: true,
895
+ originalTokens,
896
+ summaryTokens,
897
+ rounds,
898
+ messagesPreserved,
899
+ restorationHintsInjected,
900
+ toolResultsRepaired,
901
+ summary,
902
+ filesCreated,
903
+ categoryStats,
904
+ };
905
+ }
716
906
  // ==========================================================================
717
907
  // State Management
718
908
  // ==========================================================================
@@ -913,8 +1103,9 @@ export class Agent {
913
1103
  maxContextTokens: subAgentMaxTokens,
914
1104
  },
915
1105
  });
916
- // Create tool registry for sub-agent
917
- const subAgentToolRegistry = new DefaultToolRegistry();
1106
+ // Create tool registry for sub-agent, inheriting timeout settings from parent
1107
+ const parentRegistryOptions = this.toolRegistry.getOptions();
1108
+ const subAgentToolRegistry = new DefaultToolRegistry(parentRegistryOptions);
918
1109
  // If tools specified, use those; otherwise inherit from parent
919
1110
  const toolsToRegister = config.tools ??
920
1111
  this.toolRegistry
@@ -1188,7 +1379,7 @@ export class Agent {
1188
1379
  * Run the agent with a user message
1189
1380
  */
1190
1381
  async run(userMessage, options) {
1191
- const maxIterations = options?.maxIterations ?? this.maxIterations;
1382
+ let maxIterations = options?.maxIterations ?? this.maxIterations;
1192
1383
  const chatOptions = { ...this.chatOptions, ...options?.chatOptions };
1193
1384
  const signal = options?.signal;
1194
1385
  // Combined event emitter
@@ -1243,6 +1434,8 @@ export class Agent {
1243
1434
  // Perform emergency summarization
1244
1435
  const { messages: summarized, result } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs), { emergency: true });
1245
1436
  messages = summarized;
1437
+ // Inject restoration hints after summarization
1438
+ this.injectRestorationHints(messages);
1246
1439
  emit({
1247
1440
  type: 'context_summarized',
1248
1441
  tokensBefore: result.originalTokens,
@@ -1390,12 +1583,15 @@ export class Agent {
1390
1583
  // If no tool uses, we're done
1391
1584
  if (toolUses.length === 0) {
1392
1585
  finalResponse = text;
1393
- // Add final assistant response to history
1394
- const finalAssistantMsg = {
1395
- role: 'assistant',
1396
- content: text,
1397
- };
1398
- newMessages.push(finalAssistantMsg);
1586
+ // Add final assistant response to history (only if non-empty)
1587
+ // Empty responses can occur after silent tools like 'suggest'
1588
+ if (text) {
1589
+ const finalAssistantMsg = {
1590
+ role: 'assistant',
1591
+ content: text,
1592
+ };
1593
+ newMessages.push(finalAssistantMsg);
1594
+ }
1399
1595
  // Run afterIteration hooks
1400
1596
  if (this.hooksManager) {
1401
1597
  await this.hooksManager.runAfterIteration({
@@ -1450,11 +1646,26 @@ export class Agent {
1450
1646
  success: false,
1451
1647
  error: `Permission denied: ${permResult.reason ?? 'Tool execution not allowed'}`,
1452
1648
  };
1453
- // Skip to tool_end and continue to next tool
1649
+ // Emit tool_end and record the tool call
1454
1650
  emit({ type: 'tool_end', name: toolUse.name, result });
1455
1651
  const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
1456
1652
  toolCalls.push(toolCallEntry);
1457
1653
  iterationToolCalls.push(toolCallEntry);
1654
+ // CRITICAL: Add tool_result message to messages array
1655
+ // Claude API requires every tool_use to have a corresponding tool_result
1656
+ const toolResultMsg = {
1657
+ role: 'user',
1658
+ content: [
1659
+ {
1660
+ type: 'tool_result',
1661
+ toolUseId: toolUse.id,
1662
+ content: `Error: ${result.error ?? 'Permission denied'}`,
1663
+ isError: true,
1664
+ },
1665
+ ],
1666
+ };
1667
+ messages.push(toolResultMsg);
1668
+ newMessages.push(toolResultMsg);
1458
1669
  continue;
1459
1670
  }
1460
1671
  emit({ type: 'permission_granted', toolName: toolUse.name, level: permResult.level });
@@ -1472,11 +1683,26 @@ export class Agent {
1472
1683
  success: false,
1473
1684
  error: `Guardrail blocked: ${message}`,
1474
1685
  };
1475
- // Skip to tool_end and continue to next tool
1686
+ // Emit tool_end and record the tool call
1476
1687
  emit({ type: 'tool_end', name: toolUse.name, result });
1477
1688
  const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
1478
1689
  toolCalls.push(toolCallEntry);
1479
1690
  iterationToolCalls.push(toolCallEntry);
1691
+ // CRITICAL: Add tool_result message to messages array
1692
+ // Claude API requires every tool_use to have a corresponding tool_result
1693
+ const toolResultMsg = {
1694
+ role: 'user',
1695
+ content: [
1696
+ {
1697
+ type: 'tool_result',
1698
+ toolUseId: toolUse.id,
1699
+ content: `Error: ${result.error ?? 'Blocked by guardrail'}`,
1700
+ isError: true,
1701
+ },
1702
+ ],
1703
+ };
1704
+ messages.push(toolResultMsg);
1705
+ newMessages.push(toolResultMsg);
1480
1706
  continue;
1481
1707
  }
1482
1708
  else if (guardrailResult.action === 'warn') {
@@ -1602,6 +1828,8 @@ export class Agent {
1602
1828
  const tokensBefore = this.contextManager.getTokenCount();
1603
1829
  const compactResult = await this.contextManager.compactCategory(messages, 'toolResults', (content, index) => Promise.resolve(`[compacted_tool_result_${String(index)}]`));
1604
1830
  messages = compactResult.messages;
1831
+ // Inject restoration hints after compaction
1832
+ this.injectRestorationHints(messages);
1605
1833
  emit({
1606
1834
  type: 'context_compacted',
1607
1835
  tokensBefore,
@@ -1612,6 +1840,8 @@ export class Agent {
1612
1840
  // Approaching context limit - summarize
1613
1841
  const { messages: summarized, result: sumResult } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs));
1614
1842
  messages = summarized;
1843
+ // Inject restoration hints after summarization
1844
+ this.injectRestorationHints(messages);
1615
1845
  emit({
1616
1846
  type: 'context_summarized',
1617
1847
  tokensBefore: sumResult.originalTokens,
@@ -1652,6 +1882,30 @@ export class Agent {
1652
1882
  });
1653
1883
  }
1654
1884
  emit({ type: 'iteration_end', iteration: iterations });
1885
+ // Check if we're about to hit the iteration limit
1886
+ // If callback is defined, ask if we should continue
1887
+ if (iterations >= maxIterations && this.onIterationLimitReached) {
1888
+ emit({
1889
+ type: 'iteration_limit_reached',
1890
+ iteration: iterations,
1891
+ maxIterations,
1892
+ });
1893
+ const result = await this.onIterationLimitReached({
1894
+ iteration: iterations,
1895
+ maxIterations,
1896
+ toolCallCount: toolCalls.length,
1897
+ });
1898
+ if (typeof result === 'number' && result > 0) {
1899
+ // Extend the limit and continue
1900
+ maxIterations += result;
1901
+ emit({
1902
+ type: 'iteration_limit_extended',
1903
+ newMaxIterations: maxIterations,
1904
+ addedIterations: result,
1905
+ });
1906
+ }
1907
+ // If false or 0, loop will exit naturally on next condition check
1908
+ }
1655
1909
  }
1656
1910
  // Check if we hit max iterations without completing
1657
1911
  if (!aborted && iterations >= maxIterations && finalResponse === '') {