@pentoshi/clai 3.11.29 → 3.11.31

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.
@@ -6,7 +6,7 @@ import { classifyStreamFailure, planStreamRecovery, recordRecoveryAttempt, creat
6
6
  import { modelSupportsVision, resolveToolDialect } from "../llm/capabilities.js";
7
7
  import { syntheticToolCallId, isTextOnlyModel, markTextOnlyModel, fromWireName, } from "../llm/tool-protocol.js";
8
8
  import { sanitizeAssistantText } from "../ui/ansi-box.js";
9
- import { randomUUID } from "node:crypto";
9
+ import { createHash, randomUUID } from "node:crypto";
10
10
  import { jobManager, } from "../tools/jobs.js";
11
11
  import { isResponderResultLedgerMessage, responderContextMessage, upsertResponderContextMessage, upsertResponderResultLedger, } from "./responder-context.js";
12
12
  import { agentModeDirective, planModeDirective, renderAgentSystemPrompt, renderCompactAgentSystemPrompt, renderRequestEnvironmentContext, scratchDirFor, toolNudge, } from "../prompts/index.js";
@@ -86,7 +86,7 @@ import { buildRichStopSummary } from "./stop-summary.js";
86
86
  import { composeAgentSystemPrompt } from "./prompt-composer.js";
87
87
  import { createGovernorState, governProgress, } from "./evidence-governor.js";
88
88
  import { createTurnState, transitionTurn, } from "./turn-state.js";
89
- import { deriveOutcomeStatus, inferOutcomeKind, openOutcomeState, recordAnswerEvidence, recordFailedHypothesis, recordToolEvidence, saveOutcomeState, validateCriterionEvidence, } from "./outcomes.js";
89
+ import { deriveOutcomeStatus, inferOutcomeKind, openOutcomeState, recordAnswerEvidence, recordFailedHypothesis, recordToolEvidence, completedOperationObservationDigest, saveOutcomeState, validateCriterionEvidence, } from "./outcomes.js";
90
90
  import { createTurnOutcome, normalizeTurnOutcomeInput, renderTurnOutcome, } from "./turn-outcome.js";
91
91
  import { beginEngagementAction, finishEngagementAction, recordEngagementCheckpoint, reconcileEngagementJob, openEngagement, saveEngagement, } from "../store/engagement.js";
92
92
  export * from "./tool-call-parser.js";
@@ -856,6 +856,17 @@ export async function runAgentTurn(prompt, options = {}) {
856
856
  }
857
857
  return message;
858
858
  };
859
+ const upsertActionCycleRecovery = (content) => {
860
+ const prefix = "[ACTION CYCLE RECOVERY] ";
861
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
862
+ const message = messages[index];
863
+ if (message.role === "user" && message.internal && message.content.startsWith(prefix)) {
864
+ messages.splice(index, 1);
865
+ break;
866
+ }
867
+ }
868
+ messages.push(recoveryUserMessage(prefix + content));
869
+ };
859
870
  const recoveryProse = (content) => {
860
871
  const text = textBeforeToolCall(stripSentinelTokens(content)).trim();
861
872
  if (!text ||
@@ -1376,8 +1387,14 @@ export async function runAgentTurn(prompt, options = {}) {
1376
1387
  ...(retryReason ? { retryReason } : {}),
1377
1388
  });
1378
1389
  if (loopCheck.block) {
1379
- const reason = loopCheck.reason ??
1390
+ const baseReason = loopCheck.reason ??
1380
1391
  `${call.name} previously failed with identical arguments. Change the command/args and retry.`;
1392
+ const priorObservation = loopCheck.kind === "unchanged-success"
1393
+ ? loopGuard.getPriorObservation(call.name, call.args)
1394
+ : undefined;
1395
+ const reason = priorObservation
1396
+ ? `${baseReason}\n\nPrior successful result (reuse this; it is the result of the requested call):\n${priorObservation}`
1397
+ : baseReason;
1381
1398
  if (loopCheck.kind === "unchanged-success") {
1382
1399
  const result = { ok: true, output: reason, exitCode: 0 };
1383
1400
  emitVisibleSyntheticReceipt(result, reason);
@@ -3884,7 +3901,7 @@ export async function runAgentTurn(prompt, options = {}) {
3884
3901
  }
3885
3902
  // Exhausted retries — fall through to the normal answer path.
3886
3903
  }
3887
- if (/<\|tool_call(?:s_section)?_begin\|>|<\|tool_call_argument_begin\|>/i.test(assistantText.visible)) {
3904
+ if (/<\|tool_call(?:s_section)?_begin\|>|<\|tool_call_argument_begin\|>|<[||]DSML[||](?:tool_calls|invoke|parameter)\b/i.test(assistantText.visible)) {
3888
3905
  writeNotice("warn", "tool call was malformed or cut off — asking the model to retry in JSON form", chalk.yellow(" ⚠ tool call was malformed or cut off — asking the model to retry in JSON form\n"));
3889
3906
  commitAssistantRetry(assistantText.visible);
3890
3907
  messages.push(recoveryUserMessage(toolsAttached
@@ -4291,11 +4308,20 @@ export async function runAgentTurn(prompt, options = {}) {
4291
4308
  writeNotice("warn", reason, chalk.yellow(` ⚠ ${reason}\n`));
4292
4309
  const suppressedResults = bound.map((b) => {
4293
4310
  const duplicate = runIds.has(b.id);
4294
- const resultReason = duplicate ? reason : deferReason;
4311
+ const priorObservation = duplicate
4312
+ ? loopGuard.getPriorObservation(b.call.name, b.call.args)
4313
+ : undefined;
4314
+ const resultReason = duplicate
4315
+ ? reason +
4316
+ (priorObservation
4317
+ ? `\n\nPrior successful result for ${b.call.name}:\n${priorObservation}`
4318
+ : "")
4319
+ : deferReason;
4295
4320
  const result = {
4296
- ok: false,
4321
+ ok: duplicate,
4297
4322
  output: resultReason,
4298
- exitCode: duplicate ? 409 : 130,
4323
+ exitCode: duplicate ? 0 : 130,
4324
+ ...(duplicate ? { suppressedRepeat: true } : {}),
4299
4325
  };
4300
4326
  return { b, resultReason, result };
4301
4327
  });
@@ -4316,22 +4342,6 @@ export async function runAgentTurn(prompt, options = {}) {
4316
4342
  writeToolOutput(eventId, output, chalk.dim(` ${output}`));
4317
4343
  emitToolResult(eventId, result, resultReason);
4318
4344
  }
4319
- if (historyNativeCalls.length) {
4320
- appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock ??
4321
- (assistantText.hasThinking && assistantText.thinkContent
4322
- ? { text: assistantText.thinkContent }
4323
- : undefined));
4324
- for (const { b, resultReason, result } of suppressedResults) {
4325
- appendToolResult(messages, b.id, `Tool ${b.call.name} result (exit=${result.exitCode}, ok=false):\n${resultReason}`, b.call.name, false);
4326
- }
4327
- }
4328
- else {
4329
- const standardizedContent = (beforeTool ? beforeTool.trim() + "\n\n" : "") +
4330
- allCalls
4331
- .map((candidate) => `\`\`\`tool\n${JSON.stringify(candidate)}\n\`\`\``)
4332
- .join("\n\n");
4333
- pushAssistantHistory(standardizedContent);
4334
- }
4335
4345
  if (sequenceDecision.terminal) {
4336
4346
  const remainingCriteria = unreadResponderNotificationIds.size > 0
4337
4347
  ? ["Analyze and acknowledge the delivered Responder result without repeating completed foreground work."]
@@ -4341,10 +4351,10 @@ export async function runAgentTurn(prompt, options = {}) {
4341
4351
  moveTurn("partial", "repeated identical action sequence");
4342
4352
  return finishTurn("Stopped an identical action cycle before it could execute again.", productiveSteps, "partial", remainingCriteria, "The model repeated an identical action sequence without a new premise or state change.");
4343
4353
  }
4344
- messages.push(recoveryUserMessage(reason +
4354
+ upsertActionCycleRecovery(reason +
4345
4355
  (unreadResponderNotificationIds.size > 0
4346
4356
  ? " A delivered Responder result is still unread: analyze the available result, gather only genuinely necessary bounded evidence, then call job.read before returning to foreground work."
4347
- : " Reassess the evidence and select the next action yourself; do not replay completed work.")));
4357
+ : " The original successful tool result remains in context. Reassess that evidence and either finish or select a materially different action; do not replay completed work."));
4348
4358
  continue;
4349
4359
  }
4350
4360
  if (sequenceDecision.warn && sequenceDecision.warnMessage) {
@@ -4408,6 +4418,7 @@ export async function runAgentTurn(prompt, options = {}) {
4408
4418
  let planCreatedThisTurn = Boolean(activePlan && activePlan.tasks.length > 0);
4409
4419
  let actionSequenceExecuted = 0;
4410
4420
  let actionSequenceEligible = allCalls.length > 0;
4421
+ const actionSequenceOutcomes = new Map();
4411
4422
  /**
4412
4423
  * Record a tool result into history. Failures / user declines are
4413
4424
  * always returned to the model — we never cancel later siblings or
@@ -4418,6 +4429,15 @@ export async function runAgentTurn(prompt, options = {}) {
4418
4429
  consecutiveModelOnlyRounds = 0;
4419
4430
  recordedNativeIds.add(boundCall.id);
4420
4431
  actionSequenceExecuted += 1;
4432
+ const sequenceObservation = res.suppressedRepeat
4433
+ ? loopGuard.getPriorObservation(res.call.name, res.call.args) ??
4434
+ res.contextOutput
4435
+ : res.result.output ?? res.contextOutput;
4436
+ actionSequenceOutcomes.set(boundCall.id, JSON.stringify({
4437
+ ok: res.ok,
4438
+ exitCode: res.result.exitCode ?? null,
4439
+ digest: completedOperationObservationDigest(res.call.name, sequenceObservation),
4440
+ }));
4421
4441
  // A policy-suppressed call is deterministic: replaying it verbatim
4422
4442
  // returns the identical receipt. It must therefore keep the sequence
4423
4443
  // eligible, otherwise the tool-level suppression and the sequence
@@ -4757,12 +4777,16 @@ export async function runAgentTurn(prompt, options = {}) {
4757
4777
  }
4758
4778
  fillMissingToolResults(messages, historyNativeCalls, "Cancelled — not executed this turn.");
4759
4779
  }
4780
+ const actionSequenceOutcome = createHash("sha256")
4781
+ .update(JSON.stringify(bound.map((entry) => actionSequenceOutcomes.get(entry.id) ?? null)))
4782
+ .digest("hex")
4783
+ .slice(0, 24);
4760
4784
  loopGuard.completeActionSequence(actionSequenceCalls, actionSequenceEligible &&
4761
4785
  toRun.length === bound.length &&
4762
4786
  actionSequenceExecuted === allCalls.length &&
4763
4787
  !aborted &&
4764
4788
  !awaitingPlanApproval &&
4765
- !governorPauseReason);
4789
+ !governorPauseReason, actionSequenceOutcome);
4766
4790
  // Keep ledger system rows outside the native assistant→tool group so
4767
4791
  // protocol repair preserves the real successful job.read body.
4768
4792
  for (const notification of deferredResponderLedgerNotifications.splice(0)) {