@caupulican/pi-agent-core 0.93.4 → 0.93.7

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.
@@ -7,7 +7,7 @@ const TOOL_FAILURE_EXECUTION_KEY = Symbol("ToolFailureExecutionKey");
7
7
  const MAX_OPERATION_CHARS = 240;
8
8
  const MAX_FAILURE_CODE_CHARS = 48;
9
9
  const MAX_DIAGNOSTIC_CHARS = 240;
10
- const MAX_CORRECTION_CHARS = 320;
10
+ const MAX_CORRECTION_CHARS = 480;
11
11
  const MAX_TOOL_FAILURE_EVIDENCE_CHARS = 1_600;
12
12
  const MAX_ACTIVE_FAILURE_EVIDENCE_CHARS = 2_400;
13
13
  const MAX_TOOL_NAME_CHARS = 64;
@@ -16,8 +16,8 @@ const MAX_ACTIVE_FAILURES = 8;
16
16
  const MAX_TRACKED_FAILURES = 64;
17
17
  const REPAIRABLE_REJECTION_CODES = new Set(["invalid_arguments", "malformed_call", "unknown_tool"]);
18
18
  const LEGACY_GENERIC_EXECUTION_CORRECTION = "Change the arguments or approach before retrying; do not resend the unchanged operation.";
19
- const BLOCKED_REPLAY_CAVEMAN_CORRECTION = "CAVEMAN MODE - MANDATORY. SAME OPERATION BLOCKED. Do not repeat an unchanged operation. NEVER call it again with the same arguments in this run. This is not a harness loop or failure.";
20
- const CLOSED_OPERATION_CAVEMAN_CORRECTION = "CAVEMAN MODE - MANDATORY: OPERATION CLOSED; not executed. Do not repeat an unchanged operation. NEVER call it again with the same arguments in this run. Use a different operation/tool or continue independent work. The recovery guard prevented a loop; this is not harness failure.";
19
+ const BLOCKED_REPLAY_CAVEMAN_CORRECTION = "Blocked: this exact operation will not run again this session change the operation or continue other work.";
20
+ const CLOSED_OPERATION_CAVEMAN_CORRECTION = "Closed: this exact operation was not executed and will not run again this session use a different operation or continue other work.";
21
21
  function truncate(value, maxChars) {
22
22
  if (value.length <= maxChars)
23
23
  return value;
@@ -438,6 +438,19 @@ function boundedFailureCode(value) {
438
438
  * always the last line it appends — outranks any earlier, non-authoritative line that matches.
439
439
  */
440
440
  const EXIT_STATUS_LINE_PATTERN = /^(?:\S+\s+)?(?:exit(?:ed)?(?:\s+with)?(?:\s+code)?|exitcode)\s*[:=]?\s*(-?\d+)\b/i;
441
+ /**
442
+ * bash.ts appends `cwd: <dir>` after its exit trailer on failures. The line is harness status, not
443
+ * a cause: it must never displace a real diagnostic (a path may contain words like "error"), and it
444
+ * reaches the model through the process-exit evidence tail instead.
445
+ */
446
+ const CWD_STATUS_LINE_PATTERN = /^cwd:\s/i;
447
+ /** Tool-owned status/marker lines that carry no cause and never belong in diagnostic or evidence. */
448
+ function isFailureStatusLine(line) {
449
+ return (/^command (?:exited with code|timed out after|aborted|killed after)\b/i.test(line) ||
450
+ /^outcome:\s*(?:failed|aborted|timeout|output_limit)\b/i.test(line) ||
451
+ /^exitcode:\s*-?\d+\b/i.test(line) ||
452
+ /^(?:stdout|stderr):(?:\s*\(empty\))?$/i.test(line));
453
+ }
441
454
  function processExitFailureCode(message) {
442
455
  const lines = message.split(/\r\n|\n/);
443
456
  for (let index = lines.length - 1; index >= 0; index--) {
@@ -522,7 +535,9 @@ function extractFailureDiagnostic(message, allowUnclassifiedFallback, requireStr
522
535
  const stderrLines = rawLines
523
536
  .slice(stderrMarkerIndex + 1)
524
537
  .map((line) => line.trim())
525
- .filter((line) => line.length > 0 && !/^command (?:exited with code|timed out after|aborted|killed after)\b/i.test(line));
538
+ .filter((line) => line.length > 0 &&
539
+ !/^command (?:exited with code|timed out after|aborted|killed after)\b/i.test(line) &&
540
+ !CWD_STATUS_LINE_PATTERN.test(line));
526
541
  if (stderrLines.length > 0) {
527
542
  const classified = stderrLines.find((line) => strongDiagnosticPattern.test(line));
528
543
  if (classified)
@@ -539,11 +554,7 @@ function extractFailureDiagnostic(message, allowUnclassifiedFallback, requireStr
539
554
  }
540
555
  const lines = rawLines
541
556
  .map((line) => line.trim())
542
- .filter((line) => line.length > 0 &&
543
- !/^command (?:exited with code|timed out after|aborted|killed after)\b/i.test(line) &&
544
- !/^outcome:\s*(?:failed|aborted|timeout|output_limit)\b/i.test(line) &&
545
- !/^exitcode:\s*-?\d+\b/i.test(line) &&
546
- !/^(?:stdout|stderr):(?:\s*\(empty\))?$/i.test(line));
557
+ .filter((line) => line.length > 0 && !isFailureStatusLine(line) && !CWD_STATUS_LINE_PATTERN.test(line));
547
558
  if (lines.length === 0)
548
559
  return undefined;
549
560
  const diagnosticPattern = requireStrongSignal
@@ -555,6 +566,35 @@ function extractFailureDiagnostic(message, allowUnclassifiedFallback, requireStr
555
566
  const diagnostic = classified ?? (allowUnclassifiedFallback ? lines.at(-1) : undefined);
556
567
  return diagnostic ? truncateMiddle(diagnostic, MAX_DIAGNOSTIC_CHARS) : undefined;
557
568
  }
569
+ /**
570
+ * Bounded raw-output tail of an executed process-exit failure. Evidence is the raw-data channel:
571
+ * it keeps the trailing lines that strong-signal diagnostic classification refuses to promote, so
572
+ * strictness never destroys the output needed to construct a changed operation.
573
+ */
574
+ function extractProcessExitEvidence(message) {
575
+ const lines = sanitizeBinaryOutput(message)
576
+ .replaceAll("\r\n", "\n")
577
+ .split("\n")
578
+ .map((line) => line.trim())
579
+ .filter((line) => line.length > 0 && !isFailureStatusLine(line));
580
+ if (lines.length === 0)
581
+ return undefined;
582
+ let start = lines.length - 1;
583
+ let retainedChars = lines[start].length;
584
+ while (start > 0 && retainedChars + 1 + lines[start - 1].length <= MAX_TOOL_FAILURE_EVIDENCE_CHARS) {
585
+ start--;
586
+ retainedChars += 1 + lines[start].length;
587
+ }
588
+ let tail = lines.slice(start).join("\n");
589
+ if (tail.length > MAX_TOOL_FAILURE_EVIDENCE_CHARS) {
590
+ let tailStart = tail.length - MAX_TOOL_FAILURE_EVIDENCE_CHARS;
591
+ const firstCode = tail.charCodeAt(tailStart);
592
+ if (firstCode >= 0xdc00 && firstCode <= 0xdfff)
593
+ tailStart++;
594
+ tail = tail.slice(tailStart);
595
+ }
596
+ return sanitizeToolFailureEvidence(tail);
597
+ }
558
598
  export function assessToolFailure(message, state, errorClass) {
559
599
  const policy = getToolExecutionErrorPolicy(message);
560
600
  const exitFailureCode = processExitFailureCode(message);
@@ -562,14 +602,17 @@ export function assessToolFailure(message, state, errorClass) {
562
602
  const diagnostic = state === "failed" && (!policy || policy.retainDiagnostic)
563
603
  ? extractFailureDiagnostic(message, exitFailureCode === undefined && (errorClass !== undefined || retainPolicyDiagnostic), exitFailureCode !== undefined && !retainPolicyDiagnostic)
564
604
  : undefined;
605
+ const evidence = state === "failed" && exitFailureCode !== undefined ? extractProcessExitEvidence(message) : undefined;
565
606
  const failureCode = policy?.failureCode ?? classifyToolFailure(message, errorClass);
566
607
  return {
567
608
  failureCode,
568
609
  phase: policy?.phase ?? inferToolFailurePhase(state, failureCode),
569
610
  ...(diagnostic ? { diagnostic } : {}),
611
+ ...(evidence ? { evidence } : {}),
570
612
  guidance: policy
571
613
  ? truncate(policy.guidance, MAX_CORRECTION_CHARS)
572
614
  : fallbackFailureGuidance(state, diagnostic !== undefined, inferToolFailurePhase(state, failureCode)),
615
+ ...(policy ? { policyGuidance: truncate(policy.guidance, MAX_CORRECTION_CHARS) } : {}),
573
616
  ...(policy?.attemptMemory === "discard" ? { attemptMemory: "discard" } : {}),
574
617
  };
575
618
  }
@@ -592,6 +635,7 @@ function readFailureRecord(details) {
592
635
  return undefined;
593
636
  }
594
637
  const diagnostic = typeof candidate.diagnostic === "string" ? truncate(candidate.diagnostic, MAX_DIAGNOSTIC_CHARS) : undefined;
638
+ const note = typeof candidate.note === "string" ? truncate(candidate.note, MAX_DIAGNOSTIC_CHARS) : undefined;
595
639
  const evidence = sanitizeToolFailureEvidence(candidate.evidence);
596
640
  const retainedCorrection = typeof candidate.correction === "string" ? truncate(candidate.correction, MAX_CORRECTION_CHARS) : undefined;
597
641
  const correction = candidate.state === "failed" && retainedCorrection === LEGACY_GENERIC_EXECUTION_CORRECTION
@@ -616,6 +660,7 @@ function readFailureRecord(details) {
616
660
  phase,
617
661
  failureCode: boundedFailureCode(candidate.failureCode),
618
662
  diagnostic,
663
+ note,
619
664
  evidence,
620
665
  correction,
621
666
  };
@@ -925,6 +970,7 @@ function formatRecordJson(record, includeOperation = false, evidence = record.ev
925
970
  ...(includeOperation ? { operation: record.operation } : {}),
926
971
  failure_code: record.failureCode,
927
972
  ...(record.diagnostic ? { diagnostic: record.diagnostic } : {}),
973
+ ...(record.note ? { note: record.note } : {}),
928
974
  ...(evidence ? { evidence } : {}),
929
975
  ...guidance,
930
976
  ...(record.attemptMemory === "discard" ? { attempt_memory: "discarded" } : {}),
@@ -955,32 +1001,31 @@ export function createToolFailureResult(record, terminate) {
955
1001
  };
956
1002
  }
957
1003
  export function createRepeatedToolFailureResult(record) {
958
- const retainedRecord = {
959
- ...retainBlockedToolFailure(record),
960
- correction: BLOCKED_REPLAY_CAVEMAN_CORRECTION,
961
- };
962
- const diagnostic = truncateMiddle(`The unchanged operation was not executed. Unchanged replay blocked after ${record.failureCode}`, MAX_DIAGNOSTIC_CHARS);
1004
+ const retainedRecord = retainBlockedToolFailure(record);
1005
+ const replayNotice = truncateMiddle(`The unchanged operation was not executed. Unchanged replay blocked after ${record.failureCode}`, MAX_DIAGNOSTIC_CHARS);
963
1006
  const blockedResult = createToolFailureResult({
964
1007
  ...retainedRecord,
965
1008
  state: "rejected",
966
1009
  failureCode: "repeated_failed_operation",
967
- diagnostic,
968
- correction: retainedRecord.correction,
1010
+ ...(retainedRecord.diagnostic
1011
+ ? { note: replayNotice }
1012
+ : { diagnostic: replayNotice, correction: BLOCKED_REPLAY_CAVEMAN_CORRECTION }),
969
1013
  });
970
1014
  return {
971
1015
  ...blockedResult,
972
- // The visible result describes this rejected replay, while retained memory keeps the
973
- // authoritative cause so later blocks do not recursively wrap synthetic failures.
1016
+ // The visible result leads with the retained root cause and carries the replay notice as a
1017
+ // note, while retained memory keeps the authoritative cause so later blocks do not
1018
+ // recursively wrap synthetic failures.
974
1019
  details: { piToolFailureMemory: retainedRecord },
975
1020
  };
976
1021
  }
977
- export function createToolFailureRecoveryExhaustedResult(record, diagnostic) {
1022
+ export function createToolFailureRecoveryExhaustedResult(record, note) {
978
1023
  const retainedRecord = retainBlockedToolFailure(record);
979
1024
  const exhaustedResult = createToolFailureResult({
980
1025
  ...retainedRecord,
981
1026
  state: "rejected",
982
1027
  failureCode: "recovery_exhausted",
983
- diagnostic: truncateMiddle(diagnostic, MAX_DIAGNOSTIC_CHARS),
1028
+ note: truncateMiddle(note, MAX_DIAGNOSTIC_CHARS),
984
1029
  correction: "Stop retrying tools in this run. Report the unresolved failure and the user or environment action required to continue.",
985
1030
  }, true);
986
1031
  return {
@@ -988,7 +1033,7 @@ export function createToolFailureRecoveryExhaustedResult(record, diagnostic) {
988
1033
  details: { piToolFailureMemory: retainedRecord },
989
1034
  };
990
1035
  }
991
- export function createToolFailureOperationExhaustedResult(record, diagnostic) {
1036
+ export function createToolFailureOperationExhaustedResult(record, note) {
992
1037
  const retainedRecord = {
993
1038
  ...retainBlockedToolFailure(record),
994
1039
  correction: CLOSED_OPERATION_CAVEMAN_CORRECTION,
@@ -997,7 +1042,7 @@ export function createToolFailureOperationExhaustedResult(record, diagnostic) {
997
1042
  ...retainedRecord,
998
1043
  state: "rejected",
999
1044
  failureCode: "operation_recovery_exhausted",
1000
- diagnostic: truncateMiddle(diagnostic, MAX_DIAGNOSTIC_CHARS),
1045
+ note: truncateMiddle(note, MAX_DIAGNOSTIC_CHARS),
1001
1046
  }, false);
1002
1047
  return {
1003
1048
  ...exhaustedResult,