@compilr-dev/agents 0.3.26 → 0.3.27

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
@@ -256,6 +256,27 @@ export interface AgentConfig {
256
256
  maxIterations: number;
257
257
  toolCallCount: number;
258
258
  }) => Promise<number | false>;
259
+ /**
260
+ * Callback when tool loop is detected (same tool called N times with identical input).
261
+ *
262
+ * When provided, the agent asks the user instead of throwing ToolLoopError.
263
+ * Return `true` to continue (reset the counter), `false` to stop.
264
+ *
265
+ * When not provided, ToolLoopError is thrown (backwards compatible).
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * onToolLoopDetected: async ({ toolName, consecutiveCalls }) => {
270
+ * const answer = await askUser(`${toolName} called ${consecutiveCalls} times. Continue?`);
271
+ * return answer === 'yes';
272
+ * }
273
+ * ```
274
+ */
275
+ onToolLoopDetected?: (context: {
276
+ toolName: string;
277
+ consecutiveCalls: number;
278
+ input: Record<string, unknown>;
279
+ }) => Promise<boolean>;
259
280
  /**
260
281
  * Chat options (model, temperature, etc.)
261
282
  */
@@ -850,6 +871,7 @@ export declare class Agent {
850
871
  private readonly autoContextManagement;
851
872
  private readonly onEvent?;
852
873
  private readonly onIterationLimitReached?;
874
+ private readonly onToolLoopDetected?;
853
875
  private readonly retryConfig;
854
876
  private readonly checkpointer?;
855
877
  private readonly _sessionId;
package/dist/agent.js CHANGED
@@ -50,6 +50,7 @@ export class Agent {
50
50
  autoContextManagement;
51
51
  onEvent;
52
52
  onIterationLimitReached;
53
+ onToolLoopDetected;
53
54
  // Retry configuration
54
55
  retryConfig;
55
56
  // State management
@@ -139,6 +140,7 @@ export class Agent {
139
140
  }
140
141
  this.onEvent = config.onEvent;
141
142
  this.onIterationLimitReached = config.onIterationLimitReached;
143
+ this.onToolLoopDetected = config.onToolLoopDetected;
142
144
  // State management
143
145
  this.checkpointer = config.checkpointer;
144
146
  this._sessionId = config.sessionId ?? generateSessionId();
@@ -2091,13 +2093,36 @@ export class Agent {
2091
2093
  aborted = true;
2092
2094
  break;
2093
2095
  }
2096
+ const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
2097
+ toolCalls.push(toolCallEntry);
2098
+ iterationToolCalls.push(toolCallEntry);
2099
+ // Always push the tool_result BEFORE loop detection
2100
+ // so the conversation history stays valid if we throw
2101
+ messages.push(toolResultMsg);
2102
+ newMessages.push(toolResultMsg);
2094
2103
  // Tool loop detection (still applies per-tool)
2095
2104
  if (this.maxConsecutiveToolCalls > 0) {
2096
2105
  const currentHash = hashToolCall(toolUse.name, toolUse.input);
2097
2106
  if (currentHash === lastToolCallHash) {
2098
2107
  consecutiveIdenticalCalls++;
2099
2108
  if (consecutiveIdenticalCalls >= this.maxConsecutiveToolCalls) {
2100
- throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
2109
+ if (this.onToolLoopDetected) {
2110
+ // Ask user: continue or stop?
2111
+ const shouldContinue = await this.onToolLoopDetected({
2112
+ toolName: toolUse.name,
2113
+ consecutiveCalls: consecutiveIdenticalCalls,
2114
+ input: toolUse.input,
2115
+ });
2116
+ if (shouldContinue) {
2117
+ consecutiveIdenticalCalls = 0; // Reset counter
2118
+ }
2119
+ else {
2120
+ throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
2121
+ }
2122
+ }
2123
+ else {
2124
+ throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
2125
+ }
2101
2126
  }
2102
2127
  emit({
2103
2128
  type: 'tool_loop_warning',
@@ -2110,11 +2135,6 @@ export class Agent {
2110
2135
  consecutiveIdenticalCalls = 1;
2111
2136
  }
2112
2137
  }
2113
- const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
2114
- toolCalls.push(toolCallEntry);
2115
- iterationToolCalls.push(toolCallEntry);
2116
- messages.push(toolResultMsg);
2117
- newMessages.push(toolResultMsg);
2118
2138
  // Stamp for observation masking
2119
2139
  if (this.observationMasker) {
2120
2140
  const block = toolResultMsg.content[0];
@@ -2134,13 +2154,35 @@ export class Agent {
2134
2154
  aborted = true;
2135
2155
  break;
2136
2156
  }
2157
+ const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
2158
+ toolCalls.push(toolCallEntry);
2159
+ iterationToolCalls.push(toolCallEntry);
2160
+ // Always push the tool_result BEFORE loop detection
2161
+ // so the conversation history stays valid if we throw
2162
+ messages.push(toolResultMsg);
2163
+ newMessages.push(toolResultMsg);
2137
2164
  // Tool loop detection
2138
2165
  if (this.maxConsecutiveToolCalls > 0) {
2139
2166
  const currentHash = hashToolCall(toolUse.name, toolUse.input);
2140
2167
  if (currentHash === lastToolCallHash) {
2141
2168
  consecutiveIdenticalCalls++;
2142
2169
  if (consecutiveIdenticalCalls >= this.maxConsecutiveToolCalls) {
2143
- throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
2170
+ if (this.onToolLoopDetected) {
2171
+ const shouldContinue = await this.onToolLoopDetected({
2172
+ toolName: toolUse.name,
2173
+ consecutiveCalls: consecutiveIdenticalCalls,
2174
+ input: toolUse.input,
2175
+ });
2176
+ if (shouldContinue) {
2177
+ consecutiveIdenticalCalls = 0;
2178
+ }
2179
+ else {
2180
+ throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
2181
+ }
2182
+ }
2183
+ else {
2184
+ throw new ToolLoopError(toolUse.name, consecutiveIdenticalCalls, toolUse.input);
2185
+ }
2144
2186
  }
2145
2187
  emit({
2146
2188
  type: 'tool_loop_warning',
@@ -2153,11 +2195,6 @@ export class Agent {
2153
2195
  consecutiveIdenticalCalls = 1;
2154
2196
  }
2155
2197
  }
2156
- const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
2157
- toolCalls.push(toolCallEntry);
2158
- iterationToolCalls.push(toolCallEntry);
2159
- messages.push(toolResultMsg);
2160
- newMessages.push(toolResultMsg);
2161
2198
  // Stamp for observation masking
2162
2199
  if (this.observationMasker) {
2163
2200
  const block = toolResultMsg.content[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.3.26",
3
+ "version": "0.3.27",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",