@compilr-dev/agents 0.3.26 → 0.3.28
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 +22 -0
- package/dist/agent.js +49 -12
- package/dist/providers/claude.d.ts +12 -0
- package/dist/providers/claude.js +64 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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];
|
|
@@ -107,6 +107,18 @@ export declare class ClaudeProvider implements LLMProvider {
|
|
|
107
107
|
* reducing token costs by up to 90% on subsequent requests.
|
|
108
108
|
*/
|
|
109
109
|
private wrapSystemPromptWithCache;
|
|
110
|
+
/**
|
|
111
|
+
* Add cache_control breakpoint to conversation messages.
|
|
112
|
+
*
|
|
113
|
+
* Caches the conversation history prefix (all messages except the most recent turn).
|
|
114
|
+
* This avoids re-processing the entire history on each API call.
|
|
115
|
+
* Only applies when there are enough messages to benefit (>= 4 messages = 2+ turns).
|
|
116
|
+
*
|
|
117
|
+
* Strategy: place cache_control on the last content block of the second-to-last
|
|
118
|
+
* user message. This caches system + tools + all messages up to that point.
|
|
119
|
+
* Only the most recent user message is uncached (and processed at full cost).
|
|
120
|
+
*/
|
|
121
|
+
private addCacheControlToMessages;
|
|
110
122
|
/**
|
|
111
123
|
* Add cache_control to the last tool definition.
|
|
112
124
|
*
|
package/dist/providers/claude.js
CHANGED
|
@@ -66,7 +66,9 @@ export class ClaudeProvider {
|
|
|
66
66
|
model: options?.model ?? this.defaultModel,
|
|
67
67
|
max_tokens: options?.maxTokens ?? this.defaultMaxTokens,
|
|
68
68
|
system: shouldCache && systemPrompt ? this.wrapSystemPromptWithCache(systemPrompt) : systemPrompt,
|
|
69
|
-
messages:
|
|
69
|
+
messages: shouldCache
|
|
70
|
+
? this.addCacheControlToMessages(anthropicMessages)
|
|
71
|
+
: anthropicMessages,
|
|
70
72
|
tools: tools.length > 0
|
|
71
73
|
? shouldCache
|
|
72
74
|
? this.addCacheControlToLastTool(tools)
|
|
@@ -255,6 +257,67 @@ export class ClaudeProvider {
|
|
|
255
257
|
},
|
|
256
258
|
];
|
|
257
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Add cache_control breakpoint to conversation messages.
|
|
262
|
+
*
|
|
263
|
+
* Caches the conversation history prefix (all messages except the most recent turn).
|
|
264
|
+
* This avoids re-processing the entire history on each API call.
|
|
265
|
+
* Only applies when there are enough messages to benefit (>= 4 messages = 2+ turns).
|
|
266
|
+
*
|
|
267
|
+
* Strategy: place cache_control on the last content block of the second-to-last
|
|
268
|
+
* user message. This caches system + tools + all messages up to that point.
|
|
269
|
+
* Only the most recent user message is uncached (and processed at full cost).
|
|
270
|
+
*/
|
|
271
|
+
addCacheControlToMessages(messages) {
|
|
272
|
+
// Need at least 4 messages (2 turns) to benefit from caching
|
|
273
|
+
if (messages.length < 4)
|
|
274
|
+
return messages;
|
|
275
|
+
// Find the second-to-last user message
|
|
276
|
+
let targetIndex = -1;
|
|
277
|
+
let userCount = 0;
|
|
278
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
279
|
+
if (messages[i].role === 'user') {
|
|
280
|
+
userCount++;
|
|
281
|
+
if (userCount === 2) {
|
|
282
|
+
targetIndex = i;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (targetIndex < 0)
|
|
288
|
+
return messages;
|
|
289
|
+
// Clone messages to avoid mutating originals
|
|
290
|
+
const result = messages.map((msg, idx) => {
|
|
291
|
+
if (idx !== targetIndex)
|
|
292
|
+
return msg;
|
|
293
|
+
// Add cache_control to the last content block of this message
|
|
294
|
+
const content = msg.content;
|
|
295
|
+
if (typeof content === 'string') {
|
|
296
|
+
return {
|
|
297
|
+
...msg,
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: 'text',
|
|
301
|
+
text: content,
|
|
302
|
+
cache_control: { type: 'ephemeral' },
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
308
|
+
const lastBlock = content[content.length - 1];
|
|
309
|
+
return {
|
|
310
|
+
...msg,
|
|
311
|
+
content: [
|
|
312
|
+
...content.slice(0, -1),
|
|
313
|
+
{ ...lastBlock, cache_control: { type: 'ephemeral' } },
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return msg;
|
|
318
|
+
});
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
258
321
|
/**
|
|
259
322
|
* Add cache_control to the last tool definition.
|
|
260
323
|
*
|