@caupulican/pi-agent-core 0.86.14 → 0.90.0
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-loop.d.ts +2 -13
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +279 -146
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent.d.ts +5 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +4 -0
- package/dist/agent.js.map +1 -1
- package/dist/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/compaction/branch-summarization.js +11 -15
- package/dist/compaction/branch-summarization.js.map +1 -1
- package/dist/compaction/compaction.d.ts +3 -0
- package/dist/compaction/compaction.d.ts.map +1 -1
- package/dist/compaction/compaction.js +37 -46
- package/dist/compaction/compaction.js.map +1 -1
- package/dist/compaction/loop.d.ts +4 -0
- package/dist/compaction/loop.d.ts.map +1 -1
- package/dist/compaction/loop.js +7 -2
- package/dist/compaction/loop.js.map +1 -1
- package/dist/compaction/utils.d.ts +1 -1
- package/dist/compaction/utils.d.ts.map +1 -1
- package/dist/compaction/utils.js +13 -41
- package/dist/compaction/utils.js.map +1 -1
- package/dist/compaction/verification.js +2 -2
- package/dist/compaction/verification.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/provider-request-estimator.d.ts +6 -0
- package/dist/provider-request-estimator.d.ts.map +1 -0
- package/dist/provider-request-estimator.js +105 -0
- package/dist/provider-request-estimator.js.map +1 -0
- package/dist/provider-request-planner.d.ts +15 -0
- package/dist/provider-request-planner.d.ts.map +1 -0
- package/dist/provider-request-planner.js +168 -0
- package/dist/provider-request-planner.js.map +1 -0
- package/dist/provider-tool-projection.d.ts +7 -0
- package/dist/provider-tool-projection.d.ts.map +1 -0
- package/dist/provider-tool-projection.js +140 -0
- package/dist/provider-tool-projection.js.map +1 -0
- package/dist/tool-failure-memory.d.ts +8 -0
- package/dist/tool-failure-memory.d.ts.map +1 -1
- package/dist/tool-failure-memory.js +163 -70
- package/dist/tool-failure-memory.js.map +1 -1
- package/dist/tool-failure-recovery-gate.d.ts +60 -0
- package/dist/tool-failure-recovery-gate.d.ts.map +1 -0
- package/dist/tool-failure-recovery-gate.js +386 -0
- package/dist/tool-failure-recovery-gate.js.map +1 -0
- package/dist/tool-failure-recovery-protocol.d.ts +15 -0
- package/dist/tool-failure-recovery-protocol.d.ts.map +1 -0
- package/dist/tool-failure-recovery-protocol.js +43 -0
- package/dist/tool-failure-recovery-protocol.js.map +1 -0
- package/dist/types.d.ts +115 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/agent-loop.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
5
|
import { EventStream } from "@caupulican/pi-ai/event-stream";
|
|
6
|
-
import { streamSimple } from "@caupulican/pi-ai/stream";
|
|
7
6
|
import { formatToolRepairStandingRule, REPEATED_SUCCESSFUL_TOOL_CALL_FAILURE, } from "@caupulican/pi-ai/tool-repair-registry";
|
|
8
7
|
import { ToolArgumentValidationError, validateToolArguments, } from "@caupulican/pi-ai/validation";
|
|
9
|
-
import {
|
|
8
|
+
import { startPlannedAgentProviderRequest } from "./provider-request-planner.js";
|
|
9
|
+
import { assessToolFailure, clearToolFailure, createRepeatedToolFailureResult, createToolFailureMemoryTracker, createToolFailureRecoveryExhaustedResult, createToolFailureResult, getUnresolvedToolFailure, normalizeToolSignature, rememberToolFailure, toolFailureCorrection, } from "./tool-failure-memory.js";
|
|
10
|
+
import { TOOL_FAILURE_RECOVERY_ACCOUNTING_WAVE_SIZE, ToolFailureRecoveryGate, } from "./tool-failure-recovery-gate.js";
|
|
11
|
+
import { appendMandatoryToolFailureDeliveryPrompt } from "./tool-failure-recovery-protocol.js";
|
|
10
12
|
import { DEFAULT_MAX_STALL_TURNS } from "./types.js";
|
|
11
13
|
import { createEmptyUsage } from "./usage.js";
|
|
14
|
+
export { resolveRequestPreflightMaxTokens } from "./provider-request-planner.js";
|
|
12
15
|
/**
|
|
13
16
|
* Start an agent loop with a new prompt message.
|
|
14
17
|
* The prompt is added to the context and events are emitted for it.
|
|
@@ -88,6 +91,24 @@ function createLoopFailureMessage(error, config, aborted) {
|
|
|
88
91
|
timestamp: Date.now(),
|
|
89
92
|
};
|
|
90
93
|
}
|
|
94
|
+
function createMandatoryRecoveryDeliveryFallback(halt, config) {
|
|
95
|
+
const diagnostic = halt.record.diagnostic ?? halt.diagnostic;
|
|
96
|
+
return {
|
|
97
|
+
role: "assistant",
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `Tool recovery stopped for ${halt.record.tool}: ${diagnostic} Required recovery: ${halt.record.correction}`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
api: config.model.api,
|
|
105
|
+
provider: config.model.provider,
|
|
106
|
+
model: config.model.id,
|
|
107
|
+
usage: createEmptyUsage(),
|
|
108
|
+
stopReason: "stop",
|
|
109
|
+
timestamp: Date.now(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
91
112
|
function createAgentStream() {
|
|
92
113
|
return new EventStream((event) => event.type === "agent_end", (event) => (event.type === "agent_end" ? event.messages : []));
|
|
93
114
|
}
|
|
@@ -143,31 +164,6 @@ function repeatsSuccessfulTextProtocolBatch(incomingSignature, toolCalls, previo
|
|
|
143
164
|
return phase === "edited" && toolCall.name === "edit" && previousCall.name === "edit";
|
|
144
165
|
});
|
|
145
166
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Apply one request-local preflight without mutating persistent loop configuration.
|
|
148
|
-
* Shared with isolated tool-free provider calls so every transport boundary has identical
|
|
149
|
-
* validation and non-widening semantics.
|
|
150
|
-
*/
|
|
151
|
-
export async function resolveRequestPreflightMaxTokens(options) {
|
|
152
|
-
if (!options.requestPreflight)
|
|
153
|
-
return options.maxTokens;
|
|
154
|
-
if (options.maxTokens !== undefined && (!Number.isSafeInteger(options.maxTokens) || options.maxTokens <= 0)) {
|
|
155
|
-
throw new TypeError("request maxTokens must be a positive safe integer");
|
|
156
|
-
}
|
|
157
|
-
const preflight = await options.requestPreflight({ model: options.model, context: options.context, maxTokens: options.maxTokens }, options.signal);
|
|
158
|
-
if (preflight?.maxTokens === undefined)
|
|
159
|
-
return options.maxTokens;
|
|
160
|
-
if (!Number.isSafeInteger(preflight.maxTokens) || preflight.maxTokens <= 0) {
|
|
161
|
-
throw new TypeError("requestPreflight.maxTokens must be a positive safe integer");
|
|
162
|
-
}
|
|
163
|
-
const ceilings = [preflight.maxTokens];
|
|
164
|
-
if (options.maxTokens !== undefined)
|
|
165
|
-
ceilings.push(options.maxTokens);
|
|
166
|
-
if (Number.isSafeInteger(options.model.maxTokens) && options.model.maxTokens > 0) {
|
|
167
|
-
ceilings.push(options.model.maxTokens);
|
|
168
|
-
}
|
|
169
|
-
return Math.min(...ceilings);
|
|
170
|
-
}
|
|
171
167
|
/**
|
|
172
168
|
* Main loop logic shared by agentLoop and agentLoopContinue.
|
|
173
169
|
*/
|
|
@@ -189,6 +185,8 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
189
185
|
const validationFailureTracker = { repeats: 0 };
|
|
190
186
|
const repairTeachTracker = new Map();
|
|
191
187
|
let toolFailureMemory = createToolFailureMemoryTracker(currentContext.messages);
|
|
188
|
+
const toolFailureRecoveryGate = new ToolFailureRecoveryGate();
|
|
189
|
+
let mandatoryRecoveryDeliveryPending = false;
|
|
192
190
|
let lastSuccessfulTextProtocolBatch;
|
|
193
191
|
// Check for steering messages at start (user may have typed while waiting)
|
|
194
192
|
let pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
@@ -214,8 +212,26 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
214
212
|
}
|
|
215
213
|
pendingMessages = [];
|
|
216
214
|
}
|
|
217
|
-
//
|
|
218
|
-
|
|
215
|
+
// Recovery exhaustion receives exactly one provider turn without tools so the mandatory
|
|
216
|
+
// diagnostic can reach the user instead of terminating on an undelivered tool result.
|
|
217
|
+
const recoveryDeliveryTurn = mandatoryRecoveryDeliveryPending;
|
|
218
|
+
const recoveryDeliveryHalt = recoveryDeliveryTurn ? toolFailureRecoveryGate.getHalt() : undefined;
|
|
219
|
+
if (recoveryDeliveryTurn && !recoveryDeliveryHalt) {
|
|
220
|
+
throw new Error("Mandatory recovery delivery started without a recovery halt");
|
|
221
|
+
}
|
|
222
|
+
const responseContext = recoveryDeliveryHalt
|
|
223
|
+
? {
|
|
224
|
+
...currentContext,
|
|
225
|
+
systemPrompt: appendMandatoryToolFailureDeliveryPrompt(currentContext.systemPrompt, {
|
|
226
|
+
tool: recoveryDeliveryHalt.record.tool,
|
|
227
|
+
failureCode: recoveryDeliveryHalt.record.failureCode,
|
|
228
|
+
diagnostic: recoveryDeliveryHalt.record.diagnostic ?? recoveryDeliveryHalt.diagnostic,
|
|
229
|
+
requiredAction: recoveryDeliveryHalt.record.correction,
|
|
230
|
+
}),
|
|
231
|
+
tools: [],
|
|
232
|
+
}
|
|
233
|
+
: currentContext;
|
|
234
|
+
const message = await streamAssistantResponse(responseContext, config, signal, emit, streamFn);
|
|
219
235
|
newMessages.push(message);
|
|
220
236
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
221
237
|
await emit({ type: "turn_end", message, toolResults: [] });
|
|
@@ -225,6 +241,30 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
225
241
|
// Check for tool calls
|
|
226
242
|
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
227
243
|
const toolResults = [];
|
|
244
|
+
if (recoveryDeliveryTurn) {
|
|
245
|
+
if (!recoveryDeliveryHalt) {
|
|
246
|
+
throw new Error("Mandatory recovery delivery continued without a recovery halt");
|
|
247
|
+
}
|
|
248
|
+
if (toolCalls.length > 0) {
|
|
249
|
+
toolResults.push(...(await rejectRecoveryDeliveryToolCalls(toolCalls, recoveryDeliveryHalt, emit)));
|
|
250
|
+
for (const result of toolResults) {
|
|
251
|
+
currentContext.messages.push(result);
|
|
252
|
+
newMessages.push(result);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
await emit({ type: "turn_end", message, toolResults });
|
|
256
|
+
if (toolCalls.length > 0) {
|
|
257
|
+
const fallback = createMandatoryRecoveryDeliveryFallback(recoveryDeliveryHalt, config);
|
|
258
|
+
currentContext.messages.push(fallback);
|
|
259
|
+
newMessages.push(fallback);
|
|
260
|
+
await emit({ type: "turn_start" });
|
|
261
|
+
await emit({ type: "message_start", message: fallback });
|
|
262
|
+
await emit({ type: "message_end", message: fallback });
|
|
263
|
+
await emit({ type: "turn_end", message: fallback, toolResults: [] });
|
|
264
|
+
}
|
|
265
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
228
268
|
hasMoreToolCalls = false;
|
|
229
269
|
if (toolCalls.length > 0) {
|
|
230
270
|
const textProtocolBatch = toolCalls.every((toolCall) => toolCall.source === "text-protocol");
|
|
@@ -233,9 +273,15 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
233
273
|
repeatsSuccessfulTextProtocolBatch(incomingBatchSignature, toolCalls, lastSuccessfulTextProtocolBatch)
|
|
234
274
|
? lastSuccessfulTextProtocolBatch?.messages
|
|
235
275
|
: undefined;
|
|
236
|
-
const executedToolBatch = await executeToolCalls(currentContext, message, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
|
|
276
|
+
const executedToolBatch = await executeToolCalls(currentContext, message, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, previousSuccessfulTextProtocolResults, signal, emit);
|
|
237
277
|
toolResults.push(...executedToolBatch.messages);
|
|
238
|
-
|
|
278
|
+
if (toolFailureRecoveryGate.isHalted()) {
|
|
279
|
+
mandatoryRecoveryDeliveryPending = true;
|
|
280
|
+
hasMoreToolCalls = true;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
hasMoreToolCalls = !executedToolBatch.terminate;
|
|
284
|
+
}
|
|
239
285
|
for (const result of toolResults) {
|
|
240
286
|
currentContext.messages.push(result);
|
|
241
287
|
newMessages.push(result);
|
|
@@ -258,7 +304,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
258
304
|
}
|
|
259
305
|
await emit({ type: "turn_end", message, toolResults });
|
|
260
306
|
// Runaway-loop backstop (cost guard): detect a model stuck repeating one action.
|
|
261
|
-
if (stallLimit > 0 && toolCalls.length > 0) {
|
|
307
|
+
if (!mandatoryRecoveryDeliveryPending && stallLimit > 0 && toolCalls.length > 0) {
|
|
262
308
|
const signature = normalizeToolSignature(toolCalls.map((c) => [c.name, c.arguments ?? null]));
|
|
263
309
|
stallWindow.push(signature);
|
|
264
310
|
if (stallWindow.length > stallLimit * STALL_WINDOW_PERIODS)
|
|
@@ -288,12 +334,13 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
288
334
|
reasoning: nextTurnSnapshot.thinkingLevel ?? config.reasoning,
|
|
289
335
|
};
|
|
290
336
|
}
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
337
|
+
if (!mandatoryRecoveryDeliveryPending &&
|
|
338
|
+
(await config.shouldStopAfterTurn?.({
|
|
339
|
+
message,
|
|
340
|
+
toolResults,
|
|
341
|
+
context: currentContext,
|
|
342
|
+
newMessages,
|
|
343
|
+
}))) {
|
|
297
344
|
await emit({ type: "agent_end", messages: newMessages });
|
|
298
345
|
return;
|
|
299
346
|
}
|
|
@@ -319,46 +366,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
319
366
|
* and request preflight immediately before transport.
|
|
320
367
|
*/
|
|
321
368
|
export async function startAgentProviderRequest(context, config, signal, streamFn) {
|
|
322
|
-
|
|
323
|
-
// unresolved state is carried separately in the system prompt until the same operation succeeds.
|
|
324
|
-
const sanitized = sanitizeToolFailureContext(context.messages, context.systemPrompt);
|
|
325
|
-
let messages = sanitized.messages;
|
|
326
|
-
if (config.transformContext) {
|
|
327
|
-
messages = await config.transformContext(messages, signal);
|
|
328
|
-
}
|
|
329
|
-
// Convert to LLM-compatible messages (AgentMessage[] → Message[])
|
|
330
|
-
const llmMessages = await config.convertToLlm(messages);
|
|
331
|
-
// Build LLM context
|
|
332
|
-
const llmContext = {
|
|
333
|
-
systemPrompt: sanitized.systemPrompt,
|
|
334
|
-
messages: llmMessages,
|
|
335
|
-
tools: context.tools,
|
|
336
|
-
};
|
|
337
|
-
const streamFunction = streamFn || streamSimple;
|
|
338
|
-
const requestMaxTokens = await resolveRequestPreflightMaxTokens({
|
|
339
|
-
requestPreflight: config.requestPreflight,
|
|
340
|
-
model: config.model,
|
|
341
|
-
context: llmContext,
|
|
342
|
-
maxTokens: config.maxTokens,
|
|
343
|
-
signal,
|
|
344
|
-
});
|
|
345
|
-
// Resolve credentials only after the request-local authority/budget gate accepts the request.
|
|
346
|
-
// This prevents an already-exhausted background lane from refreshing OAuth/SSO credentials.
|
|
347
|
-
const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
|
|
348
|
-
const requestReasoning = config.resolveRequestReasoning
|
|
349
|
-
? config.resolveRequestReasoning(config.reasoning, {
|
|
350
|
-
model: config.model,
|
|
351
|
-
context: llmContext,
|
|
352
|
-
maxTokens: requestMaxTokens,
|
|
353
|
-
})
|
|
354
|
-
: config.reasoning;
|
|
355
|
-
return await streamFunction(config.model, llmContext, {
|
|
356
|
-
...config,
|
|
357
|
-
apiKey: resolvedApiKey,
|
|
358
|
-
maxTokens: requestMaxTokens,
|
|
359
|
-
reasoning: requestReasoning,
|
|
360
|
-
signal,
|
|
361
|
-
});
|
|
369
|
+
return startPlannedAgentProviderRequest(context, config, signal, streamFn);
|
|
362
370
|
}
|
|
363
371
|
/**
|
|
364
372
|
* Stream an assistant response from the LLM.
|
|
@@ -426,82 +434,131 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
|
|
|
426
434
|
/**
|
|
427
435
|
* Execute tool calls from an assistant message.
|
|
428
436
|
*/
|
|
429
|
-
async function executeToolCalls(currentContext, assistantMessage, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
|
|
437
|
+
async function executeToolCalls(currentContext, assistantMessage, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, previousSuccessfulTextProtocolResults, signal, emit) {
|
|
430
438
|
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
431
439
|
const hasSequentialToolCall = toolCalls.some((tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential");
|
|
440
|
+
const execCtx = {
|
|
441
|
+
context: currentContext,
|
|
442
|
+
assistantMessage,
|
|
443
|
+
config,
|
|
444
|
+
validationFailureTracker,
|
|
445
|
+
repairTeachTracker,
|
|
446
|
+
toolFailureMemory,
|
|
447
|
+
toolFailureRecoveryGate,
|
|
448
|
+
previousSuccessfulResults: previousSuccessfulTextProtocolResults,
|
|
449
|
+
signal,
|
|
450
|
+
emit,
|
|
451
|
+
};
|
|
432
452
|
if (config.toolExecution === "sequential" || hasSequentialToolCall) {
|
|
433
|
-
return executeToolCallsSequential(
|
|
453
|
+
return executeToolCallsSequential(execCtx, toolCalls);
|
|
434
454
|
}
|
|
435
|
-
return executeToolCallsParallel(
|
|
455
|
+
return executeToolCallsParallel(execCtx, toolCalls);
|
|
436
456
|
}
|
|
437
|
-
async function prepareAndStartToolCall(
|
|
438
|
-
const preparation =
|
|
439
|
-
? createRepeatedSuccessfulToolCallOutcome(
|
|
440
|
-
: await prepareToolCall(
|
|
441
|
-
await emitToolExecutionStart(toolCall, emit);
|
|
457
|
+
async function prepareAndStartToolCall(execCtx, toolCall, index) {
|
|
458
|
+
const preparation = execCtx.previousSuccessfulResults
|
|
459
|
+
? createRepeatedSuccessfulToolCallOutcome(execCtx.previousSuccessfulResults[index])
|
|
460
|
+
: await prepareToolCall(execCtx.context, execCtx.assistantMessage, toolCall, execCtx.config, execCtx.validationFailureTracker, execCtx.toolFailureMemory, execCtx.toolFailureRecoveryGate, execCtx.signal);
|
|
461
|
+
await emitToolExecutionStart(toolCall, execCtx.emit);
|
|
442
462
|
if (preparation.kind === "immediate") {
|
|
443
|
-
emitToolArgumentValidationTelemetry(config, preparation.validationEvent, "not_run", "none");
|
|
444
|
-
return {
|
|
463
|
+
emitToolArgumentValidationTelemetry(execCtx.config, preparation.validationEvent, "not_run", "none");
|
|
464
|
+
return {
|
|
465
|
+
kind: "finalized",
|
|
466
|
+
finalized: finalizeRejectedToolCall(toolCall, preparation, execCtx.toolFailureMemory, execCtx.toolFailureRecoveryGate),
|
|
467
|
+
};
|
|
445
468
|
}
|
|
446
469
|
return { kind: "prepared", preparation };
|
|
447
470
|
}
|
|
448
|
-
async function
|
|
471
|
+
async function finalizeStartedToolCall(execCtx, started) {
|
|
472
|
+
if (started.kind === "finalized")
|
|
473
|
+
return started.finalized;
|
|
474
|
+
return executeAndFinalizePreparedToolCall(execCtx.context, execCtx.assistantMessage, started.preparation, execCtx.config, execCtx.repairTeachTracker, execCtx.toolFailureMemory, execCtx.toolFailureRecoveryGate, execCtx.signal, execCtx.emit);
|
|
475
|
+
}
|
|
476
|
+
async function executeToolCallsSequential(execCtx, toolCalls) {
|
|
449
477
|
const finalizedCalls = [];
|
|
450
478
|
const messages = [];
|
|
451
479
|
for (const [index, toolCall] of toolCalls.entries()) {
|
|
452
|
-
const started = await prepareAndStartToolCall(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
finalized = started.finalized;
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
finalized = await executeAndFinalizePreparedToolCall(currentContext, assistantMessage, started.preparation, config, repairTeachTracker, toolFailureMemory, signal, emit);
|
|
459
|
-
}
|
|
460
|
-
await emitToolExecutionEnd(finalized, emit);
|
|
480
|
+
const started = await prepareAndStartToolCall(execCtx, toolCall, index);
|
|
481
|
+
const finalized = applyToolFailureRecoveryEffect(execCtx.toolFailureRecoveryGate, await finalizeStartedToolCall(execCtx, started));
|
|
482
|
+
await emitToolExecutionEnd(finalized, execCtx.emit);
|
|
461
483
|
const toolResultMessage = createToolResultMessage(finalized);
|
|
462
|
-
await emitToolResultMessage(toolResultMessage, emit);
|
|
484
|
+
await emitToolResultMessage(toolResultMessage, execCtx.emit);
|
|
463
485
|
finalizedCalls.push(finalized);
|
|
464
486
|
messages.push(toolResultMessage);
|
|
465
|
-
if (signal?.aborted) {
|
|
487
|
+
if (execCtx.signal?.aborted) {
|
|
466
488
|
break;
|
|
467
489
|
}
|
|
468
490
|
}
|
|
469
491
|
return {
|
|
470
492
|
messages,
|
|
471
|
-
terminate: shouldTerminateToolBatch(finalizedCalls),
|
|
493
|
+
terminate: execCtx.toolFailureRecoveryGate.isHalted() || shouldTerminateToolBatch(finalizedCalls),
|
|
472
494
|
};
|
|
473
495
|
}
|
|
474
|
-
async function executeToolCallsParallel(
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
496
|
+
async function executeToolCallsParallel(execCtx, toolCalls) {
|
|
497
|
+
const orderedFinalizedCalls = [];
|
|
498
|
+
let nextIndex = 0;
|
|
499
|
+
// Account each bounded concurrent wave before launching more calls. Otherwise a single
|
|
500
|
+
// assistant batch could start an unbounded number of failures before the circuit observes one.
|
|
501
|
+
while (nextIndex < toolCalls.length && !execCtx.signal?.aborted) {
|
|
502
|
+
const waveEnd = Math.min(nextIndex + TOOL_FAILURE_RECOVERY_ACCOUNTING_WAVE_SIZE, toolCalls.length);
|
|
503
|
+
const wave = [];
|
|
504
|
+
const completionOrder = [];
|
|
505
|
+
for (; nextIndex < waveEnd; nextIndex++) {
|
|
506
|
+
const toolCall = toolCalls[nextIndex];
|
|
507
|
+
const started = await prepareAndStartToolCall(execCtx, toolCall, nextIndex);
|
|
508
|
+
if (started.kind === "finalized") {
|
|
509
|
+
await emitToolExecutionEnd(started.finalized, execCtx.emit);
|
|
510
|
+
wave.push(started.finalized);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
wave.push(async () => {
|
|
514
|
+
const finalized = await finalizeStartedToolCall(execCtx, started);
|
|
515
|
+
completionOrder.push(finalized);
|
|
516
|
+
return finalized;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (execCtx.signal?.aborted) {
|
|
520
|
+
nextIndex++;
|
|
482
521
|
break;
|
|
483
522
|
}
|
|
484
|
-
continue;
|
|
485
523
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
524
|
+
const unappliedWave = await Promise.all(wave.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))));
|
|
525
|
+
const appliedByOriginal = new Map();
|
|
526
|
+
const finalizedWave = unappliedWave.map((finalized) => {
|
|
527
|
+
const applied = applyToolFailureRecoveryEffect(execCtx.toolFailureRecoveryGate, finalized);
|
|
528
|
+
appliedByOriginal.set(finalized, applied);
|
|
529
|
+
return applied;
|
|
490
530
|
});
|
|
491
|
-
|
|
492
|
-
|
|
531
|
+
for (const finalized of completionOrder) {
|
|
532
|
+
await emitToolExecutionEnd(appliedByOriginal.get(finalized) ?? finalized, execCtx.emit);
|
|
493
533
|
}
|
|
534
|
+
orderedFinalizedCalls.push(...finalizedWave);
|
|
494
535
|
}
|
|
495
|
-
const orderedFinalizedCalls = await Promise.all(finalizedCalls.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))));
|
|
496
536
|
const messages = [];
|
|
497
537
|
for (const finalized of orderedFinalizedCalls) {
|
|
498
538
|
const toolResultMessage = createToolResultMessage(finalized);
|
|
499
|
-
await emitToolResultMessage(toolResultMessage, emit);
|
|
539
|
+
await emitToolResultMessage(toolResultMessage, execCtx.emit);
|
|
500
540
|
messages.push(toolResultMessage);
|
|
501
541
|
}
|
|
502
542
|
return {
|
|
503
543
|
messages,
|
|
504
|
-
terminate: shouldTerminateToolBatch(orderedFinalizedCalls),
|
|
544
|
+
terminate: execCtx.toolFailureRecoveryGate.isHalted() || shouldTerminateToolBatch(orderedFinalizedCalls),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function applyToolFailureRecoveryEffect(gate, finalized) {
|
|
548
|
+
const halt = gate.apply(finalized.executionGateEffect);
|
|
549
|
+
if (!halt)
|
|
550
|
+
return finalized;
|
|
551
|
+
return createRecoveryExhaustedToolCallOutcome(finalized, halt);
|
|
552
|
+
}
|
|
553
|
+
function createRecoveryExhaustedToolCallOutcome(finalized, halt) {
|
|
554
|
+
const exhaustedResult = createToolFailureRecoveryExhaustedResult(halt.record, halt.diagnostic);
|
|
555
|
+
return {
|
|
556
|
+
...finalized,
|
|
557
|
+
result: {
|
|
558
|
+
...exhaustedResult,
|
|
559
|
+
...(finalized.result.usage ? { usage: finalized.result.usage } : {}),
|
|
560
|
+
},
|
|
561
|
+
isError: true,
|
|
505
562
|
};
|
|
506
563
|
}
|
|
507
564
|
const DEFAULT_TOOL_VALIDATION_ESCALATION_THRESHOLD = 3;
|
|
@@ -527,6 +584,18 @@ function createRepeatedSuccessfulToolCallOutcome(previousResult) {
|
|
|
527
584
|
...(previousResult ? { repeatedSuccessfulCall: { previousToolCallId: previousResult.toolCallId } } : {}),
|
|
528
585
|
};
|
|
529
586
|
}
|
|
587
|
+
function createAbortedToolCallOutcome(validationEvent, executionGateReservation) {
|
|
588
|
+
return {
|
|
589
|
+
kind: "immediate",
|
|
590
|
+
result: createErrorToolResult("Operation aborted"),
|
|
591
|
+
isError: true,
|
|
592
|
+
phase: "cancelled",
|
|
593
|
+
failureCode: "aborted",
|
|
594
|
+
correction: "Retry only if the operation is still required.",
|
|
595
|
+
...(executionGateReservation ? { executionGateReservation } : {}),
|
|
596
|
+
validationEvent,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
530
599
|
function shouldTerminateToolBatch(finalizedCalls) {
|
|
531
600
|
return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
|
|
532
601
|
}
|
|
@@ -621,7 +690,7 @@ function handleValidationFailure(error, config, tracker) {
|
|
|
621
690
|
});
|
|
622
691
|
return `${error.message}\n\nRepeated validation failure (${tracker.repeats} identical attempts). Use this full schema and example before retrying:\n${error.enrichment}`;
|
|
623
692
|
}
|
|
624
|
-
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, signal) {
|
|
693
|
+
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, toolFailureMemory, toolFailureRecoveryGate, signal) {
|
|
625
694
|
const tool = currentContext.tools?.find((candidate) => candidate.name === toolCall.name);
|
|
626
695
|
if (!tool) {
|
|
627
696
|
return {
|
|
@@ -646,6 +715,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
646
715
|
};
|
|
647
716
|
}
|
|
648
717
|
let validationEvent;
|
|
718
|
+
let executionGateReservation;
|
|
649
719
|
try {
|
|
650
720
|
const preparedToolCall = prepareToolCallArguments(tool, toolCall);
|
|
651
721
|
const validatedArgs = validateToolArguments(tool, preparedToolCall, {
|
|
@@ -664,6 +734,28 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
664
734
|
toolCall.rawArguments ??= toolCall.arguments;
|
|
665
735
|
toolCall.arguments = validatedArgs;
|
|
666
736
|
}
|
|
737
|
+
const unresolvedRecord = getUnresolvedToolFailure(toolFailureMemory, toolCall.name, validatedArgs);
|
|
738
|
+
const admission = toolFailureRecoveryGate.admit(tool, validatedArgs, unresolvedRecord);
|
|
739
|
+
if (admission.kind === "blocked") {
|
|
740
|
+
const failureCode = admission.exhausted ? "recovery_exhausted" : "repeated_failed_operation";
|
|
741
|
+
const result = admission.exhausted
|
|
742
|
+
? createToolFailureRecoveryExhaustedResult(admission.record, admission.diagnostic ?? "Tool failure recovery budget exhausted.")
|
|
743
|
+
: createRepeatedToolFailureResult(admission.record);
|
|
744
|
+
const memoryRecord = result.details.piToolFailureMemory;
|
|
745
|
+
toolFailureMemory.set(admission.record.failureKey, memoryRecord);
|
|
746
|
+
return {
|
|
747
|
+
kind: "immediate",
|
|
748
|
+
result,
|
|
749
|
+
isError: true,
|
|
750
|
+
phase: admission.record.phase,
|
|
751
|
+
failureCode,
|
|
752
|
+
correction: memoryRecord.correction,
|
|
753
|
+
diagnostic: memoryRecord.diagnostic,
|
|
754
|
+
repeatedToolFailure: true,
|
|
755
|
+
validationEvent: createValidationBounceTelemetry(config, toolCall, failureCode),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
executionGateReservation = admission.reservation;
|
|
667
759
|
if (config.beforeToolCall) {
|
|
668
760
|
const beforeResult = await config.beforeToolCall({
|
|
669
761
|
assistantMessage,
|
|
@@ -672,15 +764,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
672
764
|
context: currentContext,
|
|
673
765
|
}, signal);
|
|
674
766
|
if (signal?.aborted) {
|
|
675
|
-
return
|
|
676
|
-
kind: "immediate",
|
|
677
|
-
result: createErrorToolResult("Operation aborted"),
|
|
678
|
-
isError: true,
|
|
679
|
-
phase: "cancelled",
|
|
680
|
-
failureCode: "aborted",
|
|
681
|
-
correction: "Retry only if the operation is still required.",
|
|
682
|
-
validationEvent,
|
|
683
|
-
};
|
|
767
|
+
return createAbortedToolCallOutcome(validationEvent, executionGateReservation);
|
|
684
768
|
}
|
|
685
769
|
if (beforeResult?.block) {
|
|
686
770
|
const reason = beforeResult.reason || "Tool execution was blocked";
|
|
@@ -692,26 +776,20 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
692
776
|
failureCode: "blocked",
|
|
693
777
|
correction: "Choose an allowed approach or request the required authority before retrying.",
|
|
694
778
|
diagnostic: reason,
|
|
779
|
+
...(executionGateReservation ? { executionGateReservation } : {}),
|
|
695
780
|
validationEvent,
|
|
696
781
|
};
|
|
697
782
|
}
|
|
698
783
|
}
|
|
699
784
|
if (signal?.aborted) {
|
|
700
|
-
return
|
|
701
|
-
kind: "immediate",
|
|
702
|
-
result: createErrorToolResult("Operation aborted"),
|
|
703
|
-
isError: true,
|
|
704
|
-
phase: "cancelled",
|
|
705
|
-
failureCode: "aborted",
|
|
706
|
-
correction: "Retry only if the operation is still required.",
|
|
707
|
-
validationEvent,
|
|
708
|
-
};
|
|
785
|
+
return createAbortedToolCallOutcome(validationEvent, executionGateReservation);
|
|
709
786
|
}
|
|
710
787
|
return {
|
|
711
788
|
kind: "prepared",
|
|
712
789
|
toolCall,
|
|
713
790
|
tool,
|
|
714
791
|
args: validatedArgs,
|
|
792
|
+
...(executionGateReservation ? { executionGateReservation } : {}),
|
|
715
793
|
validationEvent,
|
|
716
794
|
};
|
|
717
795
|
}
|
|
@@ -731,13 +809,29 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
731
809
|
? validationFailureCorrection(validationEvent, toolCall.name)
|
|
732
810
|
: toolFailureCorrection(message, "rejected", "preflight"),
|
|
733
811
|
diagnostic: isToolArgumentValidationError(error) ? undefined : message,
|
|
812
|
+
...(executionGateReservation ? { executionGateReservation } : {}),
|
|
734
813
|
validationEvent,
|
|
735
814
|
};
|
|
736
815
|
}
|
|
737
816
|
}
|
|
738
|
-
function finalizeRejectedToolCall(toolCall, outcome, tracker) {
|
|
817
|
+
function finalizeRejectedToolCall(toolCall, outcome, tracker, toolFailureRecoveryGate) {
|
|
818
|
+
if (outcome.repeatedToolFailure) {
|
|
819
|
+
return {
|
|
820
|
+
toolCall,
|
|
821
|
+
result: outcome.result,
|
|
822
|
+
isError: true,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
739
825
|
const record = rememberToolFailure(tracker, toolCall.name, toolCall.arguments, "rejected", outcome.failureCode, outcome.correction, outcome.diagnostic, outcome.phase);
|
|
740
|
-
const
|
|
826
|
+
const halt = toolFailureRecoveryGate.apply({
|
|
827
|
+
kind: "failure",
|
|
828
|
+
record,
|
|
829
|
+
args: toolCall.arguments,
|
|
830
|
+
...(outcome.executionGateReservation ? { reservation: outcome.executionGateReservation } : {}),
|
|
831
|
+
});
|
|
832
|
+
const failureResult = halt
|
|
833
|
+
? createToolFailureRecoveryExhaustedResult(halt.record, halt.diagnostic)
|
|
834
|
+
: createToolFailureResult(record, outcome.result.terminate);
|
|
741
835
|
return {
|
|
742
836
|
toolCall,
|
|
743
837
|
result: outcome.repeatedSuccessfulCall
|
|
@@ -780,12 +874,12 @@ function getBackgroundToolCallDelay(config) {
|
|
|
780
874
|
return undefined;
|
|
781
875
|
return delay;
|
|
782
876
|
}
|
|
783
|
-
async function executeAndFinalizePreparedToolCall(currentContext, assistantMessage, prepared, config, repairTeachTracker, toolFailureMemory, foregroundSignal, emit) {
|
|
877
|
+
async function executeAndFinalizePreparedToolCall(currentContext, assistantMessage, prepared, config, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, foregroundSignal, emit) {
|
|
784
878
|
const backgroundDelay = getBackgroundToolCallDelay(config);
|
|
785
879
|
if (!config.handoffToolCall ||
|
|
786
880
|
(backgroundDelay === undefined && config.subscribeToolCallHandoffRequest === undefined)) {
|
|
787
881
|
const executed = await executePreparedToolCall(prepared, foregroundSignal, emit);
|
|
788
|
-
return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, foregroundSignal);
|
|
882
|
+
return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, foregroundSignal);
|
|
789
883
|
}
|
|
790
884
|
const executionAbort = createLinkedToolAbort(foregroundSignal);
|
|
791
885
|
const startedAt = Date.now();
|
|
@@ -795,7 +889,8 @@ async function executeAndFinalizePreparedToolCall(currentContext, assistantMessa
|
|
|
795
889
|
if (emitForegroundUpdates)
|
|
796
890
|
return emit(event);
|
|
797
891
|
});
|
|
798
|
-
|
|
892
|
+
const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, executionAbort.signal);
|
|
893
|
+
return finalized;
|
|
799
894
|
})();
|
|
800
895
|
completion.then(executionAbort.detachForeground, executionAbort.detachForeground);
|
|
801
896
|
let deadlineTimer;
|
|
@@ -831,6 +926,9 @@ async function executeAndFinalizePreparedToolCall(currentContext, assistantMessa
|
|
|
831
926
|
return outcome.value;
|
|
832
927
|
if (executionAbort.signal.aborted)
|
|
833
928
|
return completion;
|
|
929
|
+
let handoffAccepted = false;
|
|
930
|
+
const handedOffCompletion = completion.then((finalized) => handoffAccepted ? applyToolFailureRecoveryEffect(toolFailureRecoveryGate, finalized) : finalized);
|
|
931
|
+
void handedOffCompletion.catch(() => undefined);
|
|
834
932
|
let handoff;
|
|
835
933
|
try {
|
|
836
934
|
handoff = config.handoffToolCall({
|
|
@@ -839,13 +937,14 @@ async function executeAndFinalizePreparedToolCall(currentContext, assistantMessa
|
|
|
839
937
|
args: prepared.args,
|
|
840
938
|
context: currentContext,
|
|
841
939
|
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
842
|
-
completion,
|
|
940
|
+
completion: handedOffCompletion,
|
|
843
941
|
cancel: executionAbort.cancel,
|
|
844
942
|
});
|
|
845
943
|
}
|
|
846
944
|
catch {
|
|
847
945
|
return completion;
|
|
848
946
|
}
|
|
947
|
+
handoffAccepted = handoff !== undefined;
|
|
849
948
|
if (!handoff)
|
|
850
949
|
return completion;
|
|
851
950
|
emitForegroundUpdates = false;
|
|
@@ -913,11 +1012,12 @@ function appendRepairTeachNotes(result, toolCall, tracker, config) {
|
|
|
913
1012
|
taught: true,
|
|
914
1013
|
};
|
|
915
1014
|
}
|
|
916
|
-
async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, signal) {
|
|
1015
|
+
async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, signal) {
|
|
917
1016
|
let result = executed.result;
|
|
918
1017
|
let isError = executed.isError;
|
|
919
1018
|
let failureMessage = executed.failureMessage ?? "";
|
|
920
1019
|
let errorClass = executed.errorClass;
|
|
1020
|
+
let executionGateEffect;
|
|
921
1021
|
if (config.afterToolCall) {
|
|
922
1022
|
try {
|
|
923
1023
|
const afterResult = await config.afterToolCall({
|
|
@@ -949,11 +1049,27 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
|
|
|
949
1049
|
const usage = result.usage;
|
|
950
1050
|
const effectiveFailureMessage = failureMessage || result.content.find((block) => block.type === "text")?.text || "Tool execution failed";
|
|
951
1051
|
const assessment = assessToolFailure(effectiveFailureMessage, "failed", errorClass);
|
|
952
|
-
const
|
|
1052
|
+
const recoveryPlan = toolFailureRecoveryGate.planFailure(prepared.tool, prepared.args, assessment.failureCode, currentContext.tools ?? [], prepared.executionGateReservation);
|
|
1053
|
+
const record = rememberToolFailure(toolFailureMemory, prepared.toolCall.name, prepared.args, "failed", assessment.failureCode, recoveryPlan.guidance, assessment.diagnostic, assessment.phase);
|
|
1054
|
+
executionGateEffect = {
|
|
1055
|
+
kind: "failure",
|
|
1056
|
+
record,
|
|
1057
|
+
args: prepared.args,
|
|
1058
|
+
targets: recoveryPlan.targets,
|
|
1059
|
+
...(prepared.executionGateReservation ? { reservation: prepared.executionGateReservation } : {}),
|
|
1060
|
+
};
|
|
953
1061
|
result = { ...createToolFailureResult(record, result.terminate), usage };
|
|
954
1062
|
}
|
|
955
1063
|
else {
|
|
956
1064
|
clearToolFailure(toolFailureMemory, prepared.toolCall.name, prepared.args);
|
|
1065
|
+
if (!executed.isError) {
|
|
1066
|
+
executionGateEffect = {
|
|
1067
|
+
kind: "success",
|
|
1068
|
+
tool: prepared.tool,
|
|
1069
|
+
args: prepared.args,
|
|
1070
|
+
evidenceResult: executed.result,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
957
1073
|
}
|
|
958
1074
|
const repaired = isError
|
|
959
1075
|
? { result, taught: false }
|
|
@@ -963,6 +1079,7 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
|
|
|
963
1079
|
toolCall: prepared.toolCall,
|
|
964
1080
|
result: repaired.result,
|
|
965
1081
|
isError,
|
|
1082
|
+
executionGateEffect,
|
|
966
1083
|
};
|
|
967
1084
|
}
|
|
968
1085
|
function createErrorToolResult(message) {
|
|
@@ -1015,4 +1132,20 @@ async function emitToolResultMessage(toolResultMessage, emit) {
|
|
|
1015
1132
|
await emit({ type: "message_start", message: toolResultMessage });
|
|
1016
1133
|
await emit({ type: "message_end", message: toolResultMessage });
|
|
1017
1134
|
}
|
|
1135
|
+
async function rejectRecoveryDeliveryToolCalls(toolCalls, halt, emit) {
|
|
1136
|
+
const messages = [];
|
|
1137
|
+
for (const toolCall of toolCalls) {
|
|
1138
|
+
await emitToolExecutionStart(toolCall, emit);
|
|
1139
|
+
const finalized = {
|
|
1140
|
+
toolCall,
|
|
1141
|
+
result: createToolFailureRecoveryExhaustedResult(halt.record, "Tool execution is disabled during the mandatory recovery delivery turn."),
|
|
1142
|
+
isError: true,
|
|
1143
|
+
};
|
|
1144
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
1145
|
+
const message = createToolResultMessage(finalized);
|
|
1146
|
+
await emitToolResultMessage(message, emit);
|
|
1147
|
+
messages.push(message);
|
|
1148
|
+
}
|
|
1149
|
+
return messages;
|
|
1150
|
+
}
|
|
1018
1151
|
//# sourceMappingURL=agent-loop.js.map
|