@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.
Files changed (58) hide show
  1. package/dist/agent-loop.d.ts +2 -13
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +279 -146
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/agent.d.ts +5 -1
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +4 -0
  8. package/dist/agent.js.map +1 -1
  9. package/dist/compaction/branch-summarization.d.ts.map +1 -1
  10. package/dist/compaction/branch-summarization.js +11 -15
  11. package/dist/compaction/branch-summarization.js.map +1 -1
  12. package/dist/compaction/compaction.d.ts +3 -0
  13. package/dist/compaction/compaction.d.ts.map +1 -1
  14. package/dist/compaction/compaction.js +37 -46
  15. package/dist/compaction/compaction.js.map +1 -1
  16. package/dist/compaction/loop.d.ts +4 -0
  17. package/dist/compaction/loop.d.ts.map +1 -1
  18. package/dist/compaction/loop.js +7 -2
  19. package/dist/compaction/loop.js.map +1 -1
  20. package/dist/compaction/utils.d.ts +1 -1
  21. package/dist/compaction/utils.d.ts.map +1 -1
  22. package/dist/compaction/utils.js +13 -41
  23. package/dist/compaction/utils.js.map +1 -1
  24. package/dist/compaction/verification.js +2 -2
  25. package/dist/compaction/verification.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/provider-request-estimator.d.ts +6 -0
  31. package/dist/provider-request-estimator.d.ts.map +1 -0
  32. package/dist/provider-request-estimator.js +105 -0
  33. package/dist/provider-request-estimator.js.map +1 -0
  34. package/dist/provider-request-planner.d.ts +15 -0
  35. package/dist/provider-request-planner.d.ts.map +1 -0
  36. package/dist/provider-request-planner.js +168 -0
  37. package/dist/provider-request-planner.js.map +1 -0
  38. package/dist/provider-tool-projection.d.ts +7 -0
  39. package/dist/provider-tool-projection.d.ts.map +1 -0
  40. package/dist/provider-tool-projection.js +140 -0
  41. package/dist/provider-tool-projection.js.map +1 -0
  42. package/dist/tool-failure-memory.d.ts +8 -0
  43. package/dist/tool-failure-memory.d.ts.map +1 -1
  44. package/dist/tool-failure-memory.js +163 -70
  45. package/dist/tool-failure-memory.js.map +1 -1
  46. package/dist/tool-failure-recovery-gate.d.ts +60 -0
  47. package/dist/tool-failure-recovery-gate.d.ts.map +1 -0
  48. package/dist/tool-failure-recovery-gate.js +386 -0
  49. package/dist/tool-failure-recovery-gate.js.map +1 -0
  50. package/dist/tool-failure-recovery-protocol.d.ts +15 -0
  51. package/dist/tool-failure-recovery-protocol.d.ts.map +1 -0
  52. package/dist/tool-failure-recovery-protocol.js +43 -0
  53. package/dist/tool-failure-recovery-protocol.js.map +1 -0
  54. package/dist/types.d.ts +115 -8
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/types.js +12 -0
  57. package/dist/types.js.map +1 -1
  58. package/package.json +2 -2
@@ -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 { assessToolFailure, clearToolFailure, createToolFailureMemoryTracker, createToolFailureResult, normalizeToolSignature, rememberToolFailure, sanitizeToolFailureContext, toolFailureCorrection, } from "./tool-failure-memory.js";
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
- // Stream assistant response
218
- const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
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
- hasMoreToolCalls = !executedToolBatch.terminate;
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 (await config.shouldStopAfterTurn?.({
292
- message,
293
- toolResults,
294
- context: currentContext,
295
- newMessages,
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
- // Failed protocol turns never reach host transforms or provider conversion. Their bounded,
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(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
453
+ return executeToolCallsSequential(execCtx, toolCalls);
434
454
  }
435
- return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
455
+ return executeToolCallsParallel(execCtx, toolCalls);
436
456
  }
437
- async function prepareAndStartToolCall(currentContext, assistantMessage, toolCall, index, config, validationFailureTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
438
- const preparation = previousSuccessfulTextProtocolResults
439
- ? createRepeatedSuccessfulToolCallOutcome(previousSuccessfulTextProtocolResults[index])
440
- : await prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, signal);
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 { kind: "finalized", finalized: finalizeRejectedToolCall(toolCall, preparation, toolFailureMemory) };
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 executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
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(currentContext, assistantMessage, toolCall, index, config, validationFailureTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
453
- let finalized;
454
- if (started.kind === "finalized") {
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(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
475
- const finalizedCalls = [];
476
- for (const [index, toolCall] of toolCalls.entries()) {
477
- const started = await prepareAndStartToolCall(currentContext, assistantMessage, toolCall, index, config, validationFailureTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
478
- if (started.kind === "finalized") {
479
- await emitToolExecutionEnd(started.finalized, emit);
480
- finalizedCalls.push(started.finalized);
481
- if (signal?.aborted) {
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
- finalizedCalls.push(async () => {
487
- const finalized = await executeAndFinalizePreparedToolCall(currentContext, assistantMessage, started.preparation, config, repairTeachTracker, toolFailureMemory, signal, emit);
488
- await emitToolExecutionEnd(finalized, emit);
489
- return finalized;
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
- if (signal?.aborted) {
492
- break;
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 failureResult = createToolFailureResult(record, outcome.result.terminate);
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
- return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, executionAbort.signal);
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 record = rememberToolFailure(toolFailureMemory, prepared.toolCall.name, prepared.args, "failed", assessment.failureCode, assessment.guidance, assessment.diagnostic, assessment.phase);
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