@caupulican/pi-agent-core 0.81.40 → 0.81.42

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 (50) hide show
  1. package/README.md +10 -5
  2. package/dist/agent-loop.d.ts +21 -1
  3. package/dist/agent-loop.d.ts.map +1 -1
  4. package/dist/agent-loop.js +134 -93
  5. package/dist/agent-loop.js.map +1 -1
  6. package/dist/compaction/branch-summarization.d.ts +4 -2
  7. package/dist/compaction/branch-summarization.d.ts.map +1 -1
  8. package/dist/compaction/branch-summarization.js +4 -0
  9. package/dist/compaction/branch-summarization.js.map +1 -1
  10. package/dist/compaction/compaction.d.ts +24 -3
  11. package/dist/compaction/compaction.d.ts.map +1 -1
  12. package/dist/compaction/compaction.js +70 -32
  13. package/dist/compaction/compaction.js.map +1 -1
  14. package/dist/compaction/loop.d.ts.map +1 -1
  15. package/dist/compaction/loop.js +10 -5
  16. package/dist/compaction/loop.js.map +1 -1
  17. package/dist/compaction/utils.d.ts.map +1 -1
  18. package/dist/compaction/utils.js +3 -1
  19. package/dist/compaction/utils.js.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/proxy.d.ts +1 -1
  25. package/dist/proxy.d.ts.map +1 -1
  26. package/dist/proxy.js +1 -0
  27. package/dist/proxy.js.map +1 -1
  28. package/dist/reliability/classifier.d.ts.map +1 -1
  29. package/dist/reliability/classifier.js +3 -3
  30. package/dist/reliability/classifier.js.map +1 -1
  31. package/dist/session/session-manager.d.ts +7 -3
  32. package/dist/session/session-manager.d.ts.map +1 -1
  33. package/dist/session/session-manager.js +4 -2
  34. package/dist/session/session-manager.js.map +1 -1
  35. package/dist/tool-failure-memory.d.ts +41 -0
  36. package/dist/tool-failure-memory.d.ts.map +1 -0
  37. package/dist/tool-failure-memory.js +348 -0
  38. package/dist/tool-failure-memory.js.map +1 -0
  39. package/dist/types.d.ts +37 -2
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js.map +1 -1
  42. package/dist/usage.d.ts +5 -0
  43. package/dist/usage.d.ts.map +1 -1
  44. package/dist/usage.js +32 -0
  45. package/dist/usage.js.map +1 -1
  46. package/dist/uuid.d.ts +1 -1
  47. package/dist/uuid.d.ts.map +1 -1
  48. package/dist/uuid.js +1 -49
  49. package/dist/uuid.js.map +1 -1
  50. package/package.json +2 -2
@@ -2,7 +2,8 @@
2
2
  * Agent loop that works with AgentMessage throughout.
3
3
  * Transforms to Message[] only at the LLM call boundary.
4
4
  */
5
- import { EventStream, getToolExecutionErrorGuidance, streamSimple, ToolArgumentValidationError, validateToolArguments, } from "@caupulican/pi-ai";
5
+ import { EventStream, formatToolRepairStandingRule, streamSimple, ToolArgumentValidationError, validateToolArguments, } from "@caupulican/pi-ai";
6
+ import { assessToolFailure, clearToolFailure, createToolFailureMemoryTracker, createToolFailureResult, normalizeToolSignature, rememberToolFailure, sanitizeToolFailureContext, toolFailureCorrection, } from "./tool-failure-memory.js";
6
7
  import { DEFAULT_MAX_STALL_TURNS } from "./types.js";
7
8
  import { createEmptyUsage } from "./usage.js";
8
9
  /**
@@ -106,18 +107,29 @@ function createAgentStream() {
106
107
  */
107
108
  const STALL_WINDOW_PERIODS = 4;
108
109
  /**
109
- * Normalize a tool-call batch into a stable signature for runaway-loop detection. Volatile argument
110
- * tokens — epoch/timestamps, UUIDs, long hashes/nonces — are masked so a model retrying the SAME call
111
- * with a fresh timestamp/id each turn still collapses to one signature and is detected (bug #28). Only
112
- * clearly-volatile patterns are masked: short numbers (`file2.ts`, `line 42`, `count: 3`) are kept so
113
- * genuinely-distinct calls (reading numbered files, different line ranges) are NOT falsely merged.
110
+ * Apply one request-local preflight without mutating persistent loop configuration.
111
+ * Shared with isolated tool-free provider calls so every transport boundary has identical
112
+ * validation and non-widening semantics.
114
113
  */
115
- function normalizeToolSignature(pairs) {
116
- return JSON.stringify(pairs)
117
- .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<uuid>")
118
- .replace(/\d{4}-\d{2}-\d{2}[tT][0-9:.]+(?:z|[+-]\d{2}:?\d{2})?/gi, "<ts>")
119
- .replace(/\b[0-9a-f]{16,}\b/gi, "<hex>")
120
- .replace(/\d{10,}/g, "<num>");
114
+ export async function resolveRequestPreflightMaxTokens(options) {
115
+ if (!options.requestPreflight)
116
+ return options.maxTokens;
117
+ if (options.maxTokens !== undefined && (!Number.isSafeInteger(options.maxTokens) || options.maxTokens <= 0)) {
118
+ throw new TypeError("request maxTokens must be a positive safe integer");
119
+ }
120
+ const preflight = await options.requestPreflight({ model: options.model, context: options.context, maxTokens: options.maxTokens }, options.signal);
121
+ if (preflight?.maxTokens === undefined)
122
+ return options.maxTokens;
123
+ if (!Number.isSafeInteger(preflight.maxTokens) || preflight.maxTokens <= 0) {
124
+ throw new TypeError("requestPreflight.maxTokens must be a positive safe integer");
125
+ }
126
+ const ceilings = [preflight.maxTokens];
127
+ if (options.maxTokens !== undefined)
128
+ ceilings.push(options.maxTokens);
129
+ if (Number.isSafeInteger(options.model.maxTokens) && options.model.maxTokens > 0) {
130
+ ceilings.push(options.model.maxTokens);
131
+ }
132
+ return Math.min(...ceilings);
121
133
  }
122
134
  /**
123
135
  * Main loop logic shared by agentLoop and agentLoopContinue.
@@ -139,7 +151,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
139
151
  const stallWindow = [];
140
152
  const validationFailureTracker = { repeats: 0 };
141
153
  const repairTeachTracker = new Map();
142
- const executionFailureTracker = { repeats: 0 };
154
+ let toolFailureMemory = createToolFailureMemoryTracker(currentContext.messages);
143
155
  // Check for steering messages at start (user may have typed while waiting)
144
156
  let pendingMessages = (await config.getSteeringMessages?.()) || [];
145
157
  // Outer loop: continues when queued follow-up messages arrive after agent would stop
@@ -176,7 +188,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
176
188
  const toolResults = [];
177
189
  hasMoreToolCalls = false;
178
190
  if (toolCalls.length > 0) {
179
- const executedToolBatch = await executeToolCalls(currentContext, message, config, validationFailureTracker, repairTeachTracker, executionFailureTracker, signal, emit);
191
+ const executedToolBatch = await executeToolCalls(currentContext, message, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit);
180
192
  toolResults.push(...executedToolBatch.messages);
181
193
  hasMoreToolCalls = !executedToolBatch.terminate;
182
194
  for (const result of toolResults) {
@@ -207,6 +219,9 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
207
219
  const nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);
208
220
  if (nextTurnSnapshot) {
209
221
  currentContext = nextTurnSnapshot.context ?? currentContext;
222
+ if (nextTurnSnapshot.context) {
223
+ toolFailureMemory = createToolFailureMemoryTracker(currentContext.messages);
224
+ }
210
225
  config = {
211
226
  ...config,
212
227
  model: nextTurnSnapshot.model ?? config.model,
@@ -237,12 +252,17 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
237
252
  await emit({ type: "agent_end", messages: newMessages });
238
253
  }
239
254
  /**
240
- * Stream an assistant response from the LLM.
241
- * This is where AgentMessage[] gets transformed to Message[] for the LLM.
255
+ * Start one provider request through the canonical agent-loop boundary.
256
+ *
257
+ * All callers, including host-owned tool-free finalization, receive the same failure-context
258
+ * sanitization, context transformation/conversion, dynamic authentication, request-local reasoning,
259
+ * and request preflight immediately before transport.
242
260
  */
243
- async function streamAssistantResponse(context, config, signal, emit, streamFn) {
244
- // Apply context transform if configured (AgentMessage[] → AgentMessage[])
245
- let messages = context.messages;
261
+ export async function startAgentProviderRequest(context, config, signal, streamFn) {
262
+ // Failed protocol turns never reach host transforms or provider conversion. Their bounded,
263
+ // unresolved state is carried separately in the system prompt until the same operation succeeds.
264
+ const sanitized = sanitizeToolFailureContext(context.messages, context.systemPrompt);
265
+ let messages = sanitized.messages;
246
266
  if (config.transformContext) {
247
267
  messages = await config.transformContext(messages, signal);
248
268
  }
@@ -250,26 +270,42 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
250
270
  const llmMessages = await config.convertToLlm(messages);
251
271
  // Build LLM context
252
272
  const llmContext = {
253
- systemPrompt: context.systemPrompt,
273
+ systemPrompt: sanitized.systemPrompt,
254
274
  messages: llmMessages,
255
275
  tools: context.tools,
256
276
  };
257
277
  const streamFunction = streamFn || streamSimple;
258
- // Resolve API key (important for expiring tokens)
278
+ const requestMaxTokens = await resolveRequestPreflightMaxTokens({
279
+ requestPreflight: config.requestPreflight,
280
+ model: config.model,
281
+ context: llmContext,
282
+ maxTokens: config.maxTokens,
283
+ signal,
284
+ });
285
+ // Resolve credentials only after the request-local authority/budget gate accepts the request.
286
+ // This prevents an already-exhausted background lane from refreshing OAuth/SSO credentials.
259
287
  const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
260
288
  const requestReasoning = config.resolveRequestReasoning
261
289
  ? config.resolveRequestReasoning(config.reasoning, {
262
290
  model: config.model,
263
291
  context: llmContext,
264
- maxTokens: config.maxTokens,
292
+ maxTokens: requestMaxTokens,
265
293
  })
266
294
  : config.reasoning;
267
- const response = await streamFunction(config.model, llmContext, {
295
+ return await streamFunction(config.model, llmContext, {
268
296
  ...config,
269
297
  apiKey: resolvedApiKey,
298
+ maxTokens: requestMaxTokens,
270
299
  reasoning: requestReasoning,
271
300
  signal,
272
301
  });
302
+ }
303
+ /**
304
+ * Stream an assistant response from the LLM.
305
+ * This is where AgentMessage[] gets transformed to Message[] for the LLM.
306
+ */
307
+ async function streamAssistantResponse(context, config, signal, emit, streamFn) {
308
+ const response = await startAgentProviderRequest(context, config, signal, streamFn);
273
309
  let partialMessage = null;
274
310
  let addedPartial = false;
275
311
  for await (const event of response) {
@@ -330,15 +366,15 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
330
366
  /**
331
367
  * Execute tool calls from an assistant message.
332
368
  */
333
- async function executeToolCalls(currentContext, assistantMessage, config, validationFailureTracker, repairTeachTracker, executionFailureTracker, signal, emit) {
369
+ async function executeToolCalls(currentContext, assistantMessage, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit) {
334
370
  const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
335
371
  const hasSequentialToolCall = toolCalls.some((tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential");
336
372
  if (config.toolExecution === "sequential" || hasSequentialToolCall) {
337
- return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, executionFailureTracker, signal, emit);
373
+ return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit);
338
374
  }
339
- return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, executionFailureTracker, signal, emit);
375
+ return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit);
340
376
  }
341
- async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, executionFailureTracker, signal, emit) {
377
+ async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit) {
342
378
  const finalizedCalls = [];
343
379
  const messages = [];
344
380
  for (const toolCall of toolCalls) {
@@ -346,17 +382,12 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
346
382
  await emitToolExecutionStart(toolCall, emit);
347
383
  let finalized;
348
384
  if (preparation.kind === "immediate") {
349
- resetExecutionFailureTracker(executionFailureTracker);
350
385
  emitToolArgumentValidationTelemetry(config, preparation.validationEvent, "not_run", "none");
351
- finalized = {
352
- toolCall,
353
- result: preparation.result,
354
- isError: preparation.isError,
355
- };
386
+ finalized = finalizeRejectedToolCall(toolCall, preparation, toolFailureMemory);
356
387
  }
357
388
  else {
358
389
  const executed = await executePreparedToolCall(preparation, signal, emit);
359
- finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, repairTeachTracker, executionFailureTracker, signal);
390
+ finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, repairTeachTracker, toolFailureMemory, signal);
360
391
  }
361
392
  await emitToolExecutionEnd(finalized, emit);
362
393
  const toolResultMessage = createToolResultMessage(finalized);
@@ -372,19 +403,14 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
372
403
  terminate: shouldTerminateToolBatch(finalizedCalls),
373
404
  };
374
405
  }
375
- async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, executionFailureTracker, signal, emit) {
406
+ async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit) {
376
407
  const finalizedCalls = [];
377
408
  for (const toolCall of toolCalls) {
378
409
  const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, signal);
379
410
  await emitToolExecutionStart(toolCall, emit);
380
411
  if (preparation.kind === "immediate") {
381
- resetExecutionFailureTracker(executionFailureTracker);
382
412
  emitToolArgumentValidationTelemetry(config, preparation.validationEvent, "not_run", "none");
383
- const finalized = {
384
- toolCall,
385
- result: preparation.result,
386
- isError: preparation.isError,
387
- };
413
+ const finalized = finalizeRejectedToolCall(toolCall, preparation, toolFailureMemory);
388
414
  await emitToolExecutionEnd(finalized, emit);
389
415
  finalizedCalls.push(finalized);
390
416
  if (signal?.aborted) {
@@ -394,7 +420,7 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
394
420
  }
395
421
  finalizedCalls.push(async () => {
396
422
  const executed = await executePreparedToolCall(preparation, signal, emit);
397
- const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, repairTeachTracker, executionFailureTracker, signal);
423
+ const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, repairTeachTracker, toolFailureMemory, signal);
398
424
  await emitToolExecutionEnd(finalized, emit);
399
425
  return finalized;
400
426
  });
@@ -416,7 +442,6 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
416
442
  }
417
443
  const DEFAULT_TOOL_VALIDATION_ESCALATION_THRESHOLD = 3;
418
444
  const TOOL_REPAIR_TEACH_EVERY = 5;
419
- const TOOL_EXECUTION_FAILURE_TEACH_AT = 2;
420
445
  function shouldTerminateToolBatch(finalizedCalls) {
421
446
  return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
422
447
  }
@@ -447,6 +472,20 @@ function createValidationBounceTelemetry(config, toolCall, errorKeyword) {
447
472
  executionOutcome: "not_run",
448
473
  };
449
474
  }
475
+ function validationFailureCorrection(event, toolName) {
476
+ const shape = event?.failureShape
477
+ ?.slice(0, 3)
478
+ .map((entry) => `${entry.path}: expected ${entry.expectedType}, received ${entry.receivedType}`)
479
+ .join("; ");
480
+ const rules = [
481
+ ...new Set((event?.failureModes ?? [])
482
+ .filter((mode) => mode !== "other")
483
+ .map((mode) => formatToolRepairStandingRule(mode))),
484
+ ];
485
+ return [`Match ${toolName} arguments to its current schema.`, shape ? `Fix ${shape}.` : undefined, ...rules]
486
+ .filter((part) => part !== undefined)
487
+ .join(" ");
488
+ }
450
489
  function resetValidationFailureTracker(tracker) {
451
490
  tracker.signature = undefined;
452
491
  tracker.repeats = 0;
@@ -503,6 +542,8 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
503
542
  kind: "immediate",
504
543
  result: createErrorToolResult(toolCall.errorMessage),
505
544
  isError: true,
545
+ failureCode: "malformed_call",
546
+ correction: "Resend one complete JSON argument object matching the current tool schema.",
506
547
  validationEvent: createValidationBounceTelemetry(config, toolCall, "unknown_tool"),
507
548
  };
508
549
  }
@@ -512,6 +553,8 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
512
553
  kind: "immediate",
513
554
  result: createErrorToolResult(`Tool ${toolCall.name} not found`),
514
555
  isError: true,
556
+ failureCode: "unknown_tool",
557
+ correction: "Choose a tool from the currently available tool list.",
515
558
  validationEvent: createValidationBounceTelemetry(config, toolCall, "unknown_tool"),
516
559
  };
517
560
  }
@@ -546,14 +589,20 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
546
589
  kind: "immediate",
547
590
  result: createErrorToolResult("Operation aborted"),
548
591
  isError: true,
592
+ failureCode: "aborted",
593
+ correction: "Retry only if the operation is still required.",
549
594
  validationEvent,
550
595
  };
551
596
  }
552
597
  if (beforeResult?.block) {
598
+ const reason = beforeResult.reason || "Tool execution was blocked";
553
599
  return {
554
600
  kind: "immediate",
555
- result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
601
+ result: createErrorToolResult(reason),
556
602
  isError: true,
603
+ failureCode: "blocked",
604
+ correction: "Choose an allowed approach or request the required authority before retrying.",
605
+ diagnostic: reason,
557
606
  validationEvent,
558
607
  };
559
608
  }
@@ -563,6 +612,8 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
563
612
  kind: "immediate",
564
613
  result: createErrorToolResult("Operation aborted"),
565
614
  isError: true,
615
+ failureCode: "aborted",
616
+ correction: "Retry only if the operation is still required.",
566
617
  validationEvent,
567
618
  };
568
619
  }
@@ -584,10 +635,22 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
584
635
  kind: "immediate",
585
636
  result: createErrorToolResult(message),
586
637
  isError: true,
638
+ failureCode: isToolArgumentValidationError(error) ? "invalid_arguments" : "preflight_error",
639
+ correction: isToolArgumentValidationError(error)
640
+ ? validationFailureCorrection(validationEvent, toolCall.name)
641
+ : toolFailureCorrection(message, "rejected"),
587
642
  validationEvent,
588
643
  };
589
644
  }
590
645
  }
646
+ function finalizeRejectedToolCall(toolCall, outcome, tracker) {
647
+ const record = rememberToolFailure(tracker, toolCall.name, toolCall.arguments, "rejected", outcome.failureCode, outcome.correction, outcome.diagnostic);
648
+ return {
649
+ toolCall,
650
+ result: createToolFailureResult(record, outcome.result.terminate),
651
+ isError: true,
652
+ };
653
+ }
591
654
  async function executePreparedToolCall(prepared, signal, emit) {
592
655
  const updateEvents = [];
593
656
  try {
@@ -601,15 +664,23 @@ async function executePreparedToolCall(prepared, signal, emit) {
601
664
  })));
602
665
  });
603
666
  await Promise.all(updateEvents);
604
- return { result, isError: false };
667
+ return {
668
+ result,
669
+ // Tool definitions can report an expected operation failure without
670
+ // throwing. Keep the returned result intact through afterToolCall so
671
+ // policy hooks can inspect its bounded diagnostics and metadata.
672
+ isError: result.isError === true,
673
+ ...(result.isError === true ? { errorClass: "tool_result_error" } : {}),
674
+ };
605
675
  }
606
676
  catch (error) {
607
677
  await Promise.all(updateEvents);
608
678
  const message = error instanceof Error ? error.message : String(error);
609
679
  return {
610
- result: createErrorToolResultWithGuidance(message),
680
+ result: createErrorToolResult(message),
611
681
  isError: true,
612
682
  errorClass: error instanceof Error ? error.name : typeof error,
683
+ failureMessage: message,
613
684
  };
614
685
  }
615
686
  }
@@ -637,39 +708,11 @@ function appendRepairTeachNotes(result, toolCall, tracker, config) {
637
708
  taught: true,
638
709
  };
639
710
  }
640
- function resetExecutionFailureTracker(tracker) {
641
- tracker.signature = undefined;
642
- tracker.repeats = 0;
643
- }
644
- function executionFailureSignature(prepared, errorClass) {
645
- return `${normalizeToolSignature([[prepared.toolCall.name, prepared.args]])}\0${errorClass}`;
646
- }
647
- function appendExecutionFailureTeachNote(result, prepared, errorClass, tracker) {
648
- const signature = executionFailureSignature(prepared, errorClass ?? "tool-error");
649
- if (tracker.signature === signature) {
650
- tracker.repeats++;
651
- }
652
- else {
653
- tracker.signature = signature;
654
- tracker.repeats = 1;
655
- }
656
- if (tracker.repeats !== TOOL_EXECUTION_FAILURE_TEACH_AT || tracker.taughtSignature === signature)
657
- return result;
658
- tracker.taughtSignature = signature;
659
- return {
660
- ...result,
661
- content: [
662
- {
663
- type: "text",
664
- text: `[harness] This exact ${prepared.toolCall.name} call failed twice with the same ${errorClass ?? "tool"} error. Change the arguments or approach; do not resend the identical call.`,
665
- },
666
- ...result.content,
667
- ],
668
- };
669
- }
670
- async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, executionFailureTracker, signal) {
711
+ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, repairTeachTracker, toolFailureMemory, signal) {
671
712
  let result = executed.result;
672
713
  let isError = executed.isError;
714
+ let failureMessage = executed.failureMessage ?? "";
715
+ let errorClass = executed.errorClass;
673
716
  if (config.afterToolCall) {
674
717
  try {
675
718
  const afterResult = await config.afterToolCall({
@@ -684,23 +727,32 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
684
727
  result = {
685
728
  content: afterResult.content ?? result.content,
686
729
  details: afterResult.details ?? result.details,
730
+ usage: afterResult.usage ?? result.usage,
687
731
  terminate: afterResult.terminate ?? result.terminate,
688
732
  };
689
733
  isError = afterResult.isError ?? isError;
690
734
  }
691
735
  }
692
736
  catch (error) {
693
- result = createErrorToolResult(error instanceof Error ? error.message : String(error));
737
+ failureMessage = error instanceof Error ? error.message : String(error);
738
+ errorClass = error instanceof Error ? error.name : typeof error;
739
+ result = { ...createErrorToolResult(failureMessage), usage: result.usage };
694
740
  isError = true;
695
741
  }
696
742
  }
697
743
  if (isError) {
698
- result = appendExecutionFailureTeachNote(result, prepared, executed.errorClass, executionFailureTracker);
744
+ const usage = result.usage;
745
+ const effectiveFailureMessage = failureMessage || result.content.find((block) => block.type === "text")?.text || "Tool execution failed";
746
+ const assessment = assessToolFailure(effectiveFailureMessage, "failed", errorClass);
747
+ const record = rememberToolFailure(toolFailureMemory, prepared.toolCall.name, prepared.args, "failed", assessment.failureCode, assessment.guidance, assessment.diagnostic);
748
+ result = { ...createToolFailureResult(record, result.terminate), usage };
699
749
  }
700
750
  else {
701
- resetExecutionFailureTracker(executionFailureTracker);
751
+ clearToolFailure(toolFailureMemory, prepared.toolCall.name, prepared.args);
702
752
  }
703
- const repaired = appendRepairTeachNotes(result, prepared.toolCall, repairTeachTracker, config);
753
+ const repaired = isError
754
+ ? { result, taught: false }
755
+ : appendRepairTeachNotes(result, prepared.toolCall, repairTeachTracker, config);
704
756
  emitToolArgumentValidationTelemetry(config, prepared.validationEvent, isError ? "failed" : "succeeded", repaired.taught ? "note" : "none");
705
757
  return {
706
758
  toolCall: prepared.toolCall,
@@ -714,18 +766,6 @@ function createErrorToolResult(message) {
714
766
  details: {},
715
767
  };
716
768
  }
717
- function createErrorToolResultWithGuidance(message) {
718
- const guidance = getToolExecutionErrorGuidance(message);
719
- if (!guidance)
720
- return createErrorToolResult(message);
721
- return {
722
- content: [
723
- { type: "text", text: message },
724
- { type: "text", text: `[harness] ${guidance}` },
725
- ],
726
- details: {},
727
- };
728
- }
729
769
  async function emitToolExecutionStart(toolCall, emit) {
730
770
  await emit({
731
771
  type: "tool_execution_start",
@@ -761,6 +801,7 @@ function createToolResultMessage(finalized) {
761
801
  toolName: finalized.toolCall.name,
762
802
  content: finalized.result.content,
763
803
  details: finalized.result.details,
804
+ usage: finalized.result.usage,
764
805
  isError: finalized.isError,
765
806
  timestamp: Date.now(),
766
807
  };