@caupulican/pi-agent-core 0.86.11 → 0.86.17

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.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAyB7D,OAAO,KAAK,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EAEZ,aAAa,EAGb,uBAAuB,EACvB,sBAAsB,EACtB,QAAQ,EACR,kBAAkB,EAClB,MAAM,YAAY,CAAC;AAIpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAEzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAIzC;AAuBD,wBAAsB,YAAY,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAWzB;AAyFD;;;;GAIG;AACH,wBAAsB,gCAAgC,CAAC,OAAO,EAAE;IAC/D,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,uBAAuB,EAChC,MAAM,CAAC,EAAE,WAAW,KAChB,sBAAsB,GAAG,SAAS,GAAG,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAC;IACtF,KAAK,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,uBAAuB,CAAC,SAAS,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmB9B;AAmLD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CA8CxC;AA47BD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,kBAAkB,GAAG,SAAS,CAO7F"}
1
+ {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAmC7D,OAAO,KAAK,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EAEZ,aAAa,EAEb,uBAAuB,EACvB,sBAAsB,EACtB,QAAQ,EACR,kBAAkB,EAClB,MAAM,YAAY,CAAC;AAIpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAEzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAIzC;AAuBD,wBAAsB,YAAY,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAWzB;AAyFD;;;;GAIG;AACH,wBAAsB,gCAAgC,CAAC,OAAO,EAAE;IAC/D,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,uBAAuB,EAChC,MAAM,CAAC,EAAE,WAAW,KAChB,sBAAsB,GAAG,SAAS,GAAG,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAC;IACtF,KAAK,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,uBAAuB,CAAC,SAAS,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmB9B;AAqLD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CA8CxC;AA+hCD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,kBAAkB,GAAG,SAAS,CAO7F"}
@@ -6,7 +6,8 @@ import { EventStream } from "@caupulican/pi-ai/event-stream";
6
6
  import { streamSimple } from "@caupulican/pi-ai/stream";
7
7
  import { formatToolRepairStandingRule, REPEATED_SUCCESSFUL_TOOL_CALL_FAILURE, } from "@caupulican/pi-ai/tool-repair-registry";
8
8
  import { ToolArgumentValidationError, validateToolArguments, } from "@caupulican/pi-ai/validation";
9
- import { assessToolFailure, clearToolFailure, createToolFailureMemoryTracker, createToolFailureResult, normalizeToolSignature, rememberToolFailure, sanitizeToolFailureContext, toolFailureCorrection, } from "./tool-failure-memory.js";
9
+ import { assessToolFailure, clearToolFailure, createRepeatedToolFailureResult, createToolFailureMemoryTracker, createToolFailureRecoveryExhaustedResult, createToolFailureResult, getUnresolvedToolFailure, normalizeToolSignature, rememberToolFailure, sanitizeToolFailureContext, toolFailureCorrection, } from "./tool-failure-memory.js";
10
+ import { TOOL_FAILURE_RECOVERY_ACCOUNTING_WAVE_SIZE, ToolFailureRecoveryGate, } from "./tool-failure-recovery-gate.js";
10
11
  import { DEFAULT_MAX_STALL_TURNS } from "./types.js";
11
12
  import { createEmptyUsage } from "./usage.js";
12
13
  /**
@@ -189,6 +190,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
189
190
  const validationFailureTracker = { repeats: 0 };
190
191
  const repairTeachTracker = new Map();
191
192
  let toolFailureMemory = createToolFailureMemoryTracker(currentContext.messages);
193
+ const toolFailureRecoveryGate = new ToolFailureRecoveryGate();
192
194
  let lastSuccessfulTextProtocolBatch;
193
195
  // Check for steering messages at start (user may have typed while waiting)
194
196
  let pendingMessages = (await config.getSteeringMessages?.()) || [];
@@ -233,7 +235,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
233
235
  repeatsSuccessfulTextProtocolBatch(incomingBatchSignature, toolCalls, lastSuccessfulTextProtocolBatch)
234
236
  ? lastSuccessfulTextProtocolBatch?.messages
235
237
  : undefined;
236
- const executedToolBatch = await executeToolCalls(currentContext, message, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
238
+ const executedToolBatch = await executeToolCalls(currentContext, message, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, previousSuccessfulTextProtocolResults, signal, emit);
237
239
  toolResults.push(...executedToolBatch.messages);
238
240
  hasMoreToolCalls = !executedToolBatch.terminate;
239
241
  for (const result of toolResults) {
@@ -426,82 +428,131 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
426
428
  /**
427
429
  * Execute tool calls from an assistant message.
428
430
  */
429
- async function executeToolCalls(currentContext, assistantMessage, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
431
+ async function executeToolCalls(currentContext, assistantMessage, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, previousSuccessfulTextProtocolResults, signal, emit) {
430
432
  const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
431
433
  const hasSequentialToolCall = toolCalls.some((tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential");
434
+ const execCtx = {
435
+ context: currentContext,
436
+ assistantMessage,
437
+ config,
438
+ validationFailureTracker,
439
+ repairTeachTracker,
440
+ toolFailureMemory,
441
+ toolFailureRecoveryGate,
442
+ previousSuccessfulResults: previousSuccessfulTextProtocolResults,
443
+ signal,
444
+ emit,
445
+ };
432
446
  if (config.toolExecution === "sequential" || hasSequentialToolCall) {
433
- return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
447
+ return executeToolCallsSequential(execCtx, toolCalls);
434
448
  }
435
- return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
449
+ return executeToolCallsParallel(execCtx, toolCalls);
436
450
  }
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);
451
+ async function prepareAndStartToolCall(execCtx, toolCall, index) {
452
+ const preparation = execCtx.previousSuccessfulResults
453
+ ? createRepeatedSuccessfulToolCallOutcome(execCtx.previousSuccessfulResults[index])
454
+ : await prepareToolCall(execCtx.context, execCtx.assistantMessage, toolCall, execCtx.config, execCtx.validationFailureTracker, execCtx.toolFailureMemory, execCtx.toolFailureRecoveryGate, execCtx.signal);
455
+ await emitToolExecutionStart(toolCall, execCtx.emit);
442
456
  if (preparation.kind === "immediate") {
443
- emitToolArgumentValidationTelemetry(config, preparation.validationEvent, "not_run", "none");
444
- return { kind: "finalized", finalized: finalizeRejectedToolCall(toolCall, preparation, toolFailureMemory) };
457
+ emitToolArgumentValidationTelemetry(execCtx.config, preparation.validationEvent, "not_run", "none");
458
+ return {
459
+ kind: "finalized",
460
+ finalized: finalizeRejectedToolCall(toolCall, preparation, execCtx.toolFailureMemory, execCtx.toolFailureRecoveryGate),
461
+ };
445
462
  }
446
463
  return { kind: "prepared", preparation };
447
464
  }
448
- async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
465
+ async function finalizeStartedToolCall(execCtx, started) {
466
+ if (started.kind === "finalized")
467
+ return started.finalized;
468
+ return executeAndFinalizePreparedToolCall(execCtx.context, execCtx.assistantMessage, started.preparation, execCtx.config, execCtx.repairTeachTracker, execCtx.toolFailureMemory, execCtx.toolFailureRecoveryGate, execCtx.signal, execCtx.emit);
469
+ }
470
+ async function executeToolCallsSequential(execCtx, toolCalls) {
449
471
  const finalizedCalls = [];
450
472
  const messages = [];
451
473
  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);
474
+ const started = await prepareAndStartToolCall(execCtx, toolCall, index);
475
+ const finalized = applyToolFailureRecoveryEffect(execCtx.toolFailureRecoveryGate, await finalizeStartedToolCall(execCtx, started));
476
+ await emitToolExecutionEnd(finalized, execCtx.emit);
461
477
  const toolResultMessage = createToolResultMessage(finalized);
462
- await emitToolResultMessage(toolResultMessage, emit);
478
+ await emitToolResultMessage(toolResultMessage, execCtx.emit);
463
479
  finalizedCalls.push(finalized);
464
480
  messages.push(toolResultMessage);
465
- if (signal?.aborted) {
481
+ if (execCtx.signal?.aborted) {
466
482
  break;
467
483
  }
468
484
  }
469
485
  return {
470
486
  messages,
471
- terminate: shouldTerminateToolBatch(finalizedCalls),
487
+ terminate: execCtx.toolFailureRecoveryGate.isHalted() || shouldTerminateToolBatch(finalizedCalls),
472
488
  };
473
489
  }
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) {
490
+ async function executeToolCallsParallel(execCtx, toolCalls) {
491
+ const orderedFinalizedCalls = [];
492
+ let nextIndex = 0;
493
+ // Account each bounded concurrent wave before launching more calls. Otherwise a single
494
+ // assistant batch could start an unbounded number of failures before the circuit observes one.
495
+ while (nextIndex < toolCalls.length && !execCtx.signal?.aborted) {
496
+ const waveEnd = Math.min(nextIndex + TOOL_FAILURE_RECOVERY_ACCOUNTING_WAVE_SIZE, toolCalls.length);
497
+ const wave = [];
498
+ const completionOrder = [];
499
+ for (; nextIndex < waveEnd; nextIndex++) {
500
+ const toolCall = toolCalls[nextIndex];
501
+ const started = await prepareAndStartToolCall(execCtx, toolCall, nextIndex);
502
+ if (started.kind === "finalized") {
503
+ await emitToolExecutionEnd(started.finalized, execCtx.emit);
504
+ wave.push(started.finalized);
505
+ }
506
+ else {
507
+ wave.push(async () => {
508
+ const finalized = await finalizeStartedToolCall(execCtx, started);
509
+ completionOrder.push(finalized);
510
+ return finalized;
511
+ });
512
+ }
513
+ if (execCtx.signal?.aborted) {
514
+ nextIndex++;
482
515
  break;
483
516
  }
484
- continue;
485
517
  }
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;
518
+ const unappliedWave = await Promise.all(wave.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))));
519
+ const appliedByOriginal = new Map();
520
+ const finalizedWave = unappliedWave.map((finalized) => {
521
+ const applied = applyToolFailureRecoveryEffect(execCtx.toolFailureRecoveryGate, finalized);
522
+ appliedByOriginal.set(finalized, applied);
523
+ return applied;
490
524
  });
491
- if (signal?.aborted) {
492
- break;
525
+ for (const finalized of completionOrder) {
526
+ await emitToolExecutionEnd(appliedByOriginal.get(finalized) ?? finalized, execCtx.emit);
493
527
  }
528
+ orderedFinalizedCalls.push(...finalizedWave);
494
529
  }
495
- const orderedFinalizedCalls = await Promise.all(finalizedCalls.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))));
496
530
  const messages = [];
497
531
  for (const finalized of orderedFinalizedCalls) {
498
532
  const toolResultMessage = createToolResultMessage(finalized);
499
- await emitToolResultMessage(toolResultMessage, emit);
533
+ await emitToolResultMessage(toolResultMessage, execCtx.emit);
500
534
  messages.push(toolResultMessage);
501
535
  }
502
536
  return {
503
537
  messages,
504
- terminate: shouldTerminateToolBatch(orderedFinalizedCalls),
538
+ terminate: execCtx.toolFailureRecoveryGate.isHalted() || shouldTerminateToolBatch(orderedFinalizedCalls),
539
+ };
540
+ }
541
+ function applyToolFailureRecoveryEffect(gate, finalized) {
542
+ const halt = gate.apply(finalized.executionGateEffect);
543
+ if (!halt)
544
+ return finalized;
545
+ return createRecoveryExhaustedToolCallOutcome(finalized, halt);
546
+ }
547
+ function createRecoveryExhaustedToolCallOutcome(finalized, halt) {
548
+ const exhaustedResult = createToolFailureRecoveryExhaustedResult(halt.record, halt.diagnostic);
549
+ return {
550
+ ...finalized,
551
+ result: {
552
+ ...exhaustedResult,
553
+ ...(finalized.result.usage ? { usage: finalized.result.usage } : {}),
554
+ },
555
+ isError: true,
505
556
  };
506
557
  }
507
558
  const DEFAULT_TOOL_VALIDATION_ESCALATION_THRESHOLD = 3;
@@ -527,6 +578,18 @@ function createRepeatedSuccessfulToolCallOutcome(previousResult) {
527
578
  ...(previousResult ? { repeatedSuccessfulCall: { previousToolCallId: previousResult.toolCallId } } : {}),
528
579
  };
529
580
  }
581
+ function createAbortedToolCallOutcome(validationEvent, executionGateReservation) {
582
+ return {
583
+ kind: "immediate",
584
+ result: createErrorToolResult("Operation aborted"),
585
+ isError: true,
586
+ phase: "cancelled",
587
+ failureCode: "aborted",
588
+ correction: "Retry only if the operation is still required.",
589
+ ...(executionGateReservation ? { executionGateReservation } : {}),
590
+ validationEvent,
591
+ };
592
+ }
530
593
  function shouldTerminateToolBatch(finalizedCalls) {
531
594
  return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
532
595
  }
@@ -621,7 +684,7 @@ function handleValidationFailure(error, config, tracker) {
621
684
  });
622
685
  return `${error.message}\n\nRepeated validation failure (${tracker.repeats} identical attempts). Use this full schema and example before retrying:\n${error.enrichment}`;
623
686
  }
624
- async function prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, signal) {
687
+ async function prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, toolFailureMemory, toolFailureRecoveryGate, signal) {
625
688
  const tool = currentContext.tools?.find((candidate) => candidate.name === toolCall.name);
626
689
  if (!tool) {
627
690
  return {
@@ -646,6 +709,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
646
709
  };
647
710
  }
648
711
  let validationEvent;
712
+ let executionGateReservation;
649
713
  try {
650
714
  const preparedToolCall = prepareToolCallArguments(tool, toolCall);
651
715
  const validatedArgs = validateToolArguments(tool, preparedToolCall, {
@@ -664,6 +728,28 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
664
728
  toolCall.rawArguments ??= toolCall.arguments;
665
729
  toolCall.arguments = validatedArgs;
666
730
  }
731
+ const unresolvedRecord = getUnresolvedToolFailure(toolFailureMemory, toolCall.name, validatedArgs);
732
+ const admission = toolFailureRecoveryGate.admit(tool, validatedArgs, unresolvedRecord);
733
+ if (admission.kind === "blocked") {
734
+ const failureCode = admission.exhausted ? "recovery_exhausted" : "repeated_failed_operation";
735
+ const result = admission.exhausted
736
+ ? createToolFailureRecoveryExhaustedResult(admission.record, admission.diagnostic ?? "Tool failure recovery budget exhausted.")
737
+ : createRepeatedToolFailureResult(admission.record);
738
+ const memoryRecord = result.details.piToolFailureMemory;
739
+ toolFailureMemory.set(admission.record.failureKey, memoryRecord);
740
+ return {
741
+ kind: "immediate",
742
+ result,
743
+ isError: true,
744
+ phase: admission.record.phase,
745
+ failureCode,
746
+ correction: memoryRecord.correction,
747
+ diagnostic: memoryRecord.diagnostic,
748
+ repeatedToolFailure: true,
749
+ validationEvent: createValidationBounceTelemetry(config, toolCall, failureCode),
750
+ };
751
+ }
752
+ executionGateReservation = admission.reservation;
667
753
  if (config.beforeToolCall) {
668
754
  const beforeResult = await config.beforeToolCall({
669
755
  assistantMessage,
@@ -672,15 +758,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
672
758
  context: currentContext,
673
759
  }, signal);
674
760
  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
- };
761
+ return createAbortedToolCallOutcome(validationEvent, executionGateReservation);
684
762
  }
685
763
  if (beforeResult?.block) {
686
764
  const reason = beforeResult.reason || "Tool execution was blocked";
@@ -692,26 +770,20 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
692
770
  failureCode: "blocked",
693
771
  correction: "Choose an allowed approach or request the required authority before retrying.",
694
772
  diagnostic: reason,
773
+ ...(executionGateReservation ? { executionGateReservation } : {}),
695
774
  validationEvent,
696
775
  };
697
776
  }
698
777
  }
699
778
  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
- };
779
+ return createAbortedToolCallOutcome(validationEvent, executionGateReservation);
709
780
  }
710
781
  return {
711
782
  kind: "prepared",
712
783
  toolCall,
713
784
  tool,
714
785
  args: validatedArgs,
786
+ ...(executionGateReservation ? { executionGateReservation } : {}),
715
787
  validationEvent,
716
788
  };
717
789
  }
@@ -731,13 +803,29 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
731
803
  ? validationFailureCorrection(validationEvent, toolCall.name)
732
804
  : toolFailureCorrection(message, "rejected", "preflight"),
733
805
  diagnostic: isToolArgumentValidationError(error) ? undefined : message,
806
+ ...(executionGateReservation ? { executionGateReservation } : {}),
734
807
  validationEvent,
735
808
  };
736
809
  }
737
810
  }
738
- function finalizeRejectedToolCall(toolCall, outcome, tracker) {
811
+ function finalizeRejectedToolCall(toolCall, outcome, tracker, toolFailureRecoveryGate) {
812
+ if (outcome.repeatedToolFailure) {
813
+ return {
814
+ toolCall,
815
+ result: outcome.result,
816
+ isError: true,
817
+ };
818
+ }
739
819
  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);
820
+ const halt = toolFailureRecoveryGate.apply({
821
+ kind: "failure",
822
+ record,
823
+ args: toolCall.arguments,
824
+ ...(outcome.executionGateReservation ? { reservation: outcome.executionGateReservation } : {}),
825
+ });
826
+ const failureResult = halt
827
+ ? createToolFailureRecoveryExhaustedResult(halt.record, halt.diagnostic)
828
+ : createToolFailureResult(record, outcome.result.terminate);
741
829
  return {
742
830
  toolCall,
743
831
  result: outcome.repeatedSuccessfulCall
@@ -780,12 +868,12 @@ function getBackgroundToolCallDelay(config) {
780
868
  return undefined;
781
869
  return delay;
782
870
  }
783
- async function executeAndFinalizePreparedToolCall(currentContext, assistantMessage, prepared, config, repairTeachTracker, toolFailureMemory, foregroundSignal, emit) {
871
+ async function executeAndFinalizePreparedToolCall(currentContext, assistantMessage, prepared, config, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, foregroundSignal, emit) {
784
872
  const backgroundDelay = getBackgroundToolCallDelay(config);
785
873
  if (!config.handoffToolCall ||
786
874
  (backgroundDelay === undefined && config.subscribeToolCallHandoffRequest === undefined)) {
787
875
  const executed = await executePreparedToolCall(prepared, foregroundSignal, emit);
788
- return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, foregroundSignal);
876
+ return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, foregroundSignal);
789
877
  }
790
878
  const executionAbort = createLinkedToolAbort(foregroundSignal);
791
879
  const startedAt = Date.now();
@@ -795,7 +883,8 @@ async function executeAndFinalizePreparedToolCall(currentContext, assistantMessa
795
883
  if (emitForegroundUpdates)
796
884
  return emit(event);
797
885
  });
798
- return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, executionAbort.signal);
886
+ const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, executionAbort.signal);
887
+ return finalized;
799
888
  })();
800
889
  completion.then(executionAbort.detachForeground, executionAbort.detachForeground);
801
890
  let deadlineTimer;
@@ -831,6 +920,9 @@ async function executeAndFinalizePreparedToolCall(currentContext, assistantMessa
831
920
  return outcome.value;
832
921
  if (executionAbort.signal.aborted)
833
922
  return completion;
923
+ let handoffAccepted = false;
924
+ const handedOffCompletion = completion.then((finalized) => handoffAccepted ? applyToolFailureRecoveryEffect(toolFailureRecoveryGate, finalized) : finalized);
925
+ void handedOffCompletion.catch(() => undefined);
834
926
  let handoff;
835
927
  try {
836
928
  handoff = config.handoffToolCall({
@@ -839,13 +931,14 @@ async function executeAndFinalizePreparedToolCall(currentContext, assistantMessa
839
931
  args: prepared.args,
840
932
  context: currentContext,
841
933
  elapsedMs: Math.max(0, Date.now() - startedAt),
842
- completion,
934
+ completion: handedOffCompletion,
843
935
  cancel: executionAbort.cancel,
844
936
  });
845
937
  }
846
938
  catch {
847
939
  return completion;
848
940
  }
941
+ handoffAccepted = handoff !== undefined;
849
942
  if (!handoff)
850
943
  return completion;
851
944
  emitForegroundUpdates = false;
@@ -913,11 +1006,12 @@ function appendRepairTeachNotes(result, toolCall, tracker, config) {
913
1006
  taught: true,
914
1007
  };
915
1008
  }
916
- async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, signal) {
1009
+ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, toolFailureRecoveryGate, signal) {
917
1010
  let result = executed.result;
918
1011
  let isError = executed.isError;
919
1012
  let failureMessage = executed.failureMessage ?? "";
920
1013
  let errorClass = executed.errorClass;
1014
+ let executionGateEffect;
921
1015
  if (config.afterToolCall) {
922
1016
  try {
923
1017
  const afterResult = await config.afterToolCall({
@@ -949,11 +1043,27 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
949
1043
  const usage = result.usage;
950
1044
  const effectiveFailureMessage = failureMessage || result.content.find((block) => block.type === "text")?.text || "Tool execution failed";
951
1045
  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);
1046
+ const recoveryPlan = toolFailureRecoveryGate.planFailure(prepared.tool, prepared.args, assessment.failureCode, currentContext.tools ?? [], prepared.executionGateReservation);
1047
+ const record = rememberToolFailure(toolFailureMemory, prepared.toolCall.name, prepared.args, "failed", assessment.failureCode, recoveryPlan.guidance, assessment.diagnostic, assessment.phase);
1048
+ executionGateEffect = {
1049
+ kind: "failure",
1050
+ record,
1051
+ args: prepared.args,
1052
+ targets: recoveryPlan.targets,
1053
+ ...(prepared.executionGateReservation ? { reservation: prepared.executionGateReservation } : {}),
1054
+ };
953
1055
  result = { ...createToolFailureResult(record, result.terminate), usage };
954
1056
  }
955
1057
  else {
956
1058
  clearToolFailure(toolFailureMemory, prepared.toolCall.name, prepared.args);
1059
+ if (!executed.isError) {
1060
+ executionGateEffect = {
1061
+ kind: "success",
1062
+ tool: prepared.tool,
1063
+ args: prepared.args,
1064
+ evidenceResult: executed.result,
1065
+ };
1066
+ }
957
1067
  }
958
1068
  const repaired = isError
959
1069
  ? { result, taught: false }
@@ -963,6 +1073,7 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
963
1073
  toolCall: prepared.toolCall,
964
1074
  result: repaired.result,
965
1075
  isError,
1076
+ executionGateEffect,
966
1077
  };
967
1078
  }
968
1079
  function createErrorToolResult(message) {