@pentoshi/clai 3.11.28 → 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.
Files changed (48) hide show
  1. package/dist/agent/loop-guard.d.ts +12 -3
  2. package/dist/agent/loop-guard.js +237 -41
  3. package/dist/agent/loop-guard.js.map +1 -1
  4. package/dist/agent/runner.d.ts +1 -0
  5. package/dist/agent/runner.js +64 -29
  6. package/dist/agent/runner.js.map +1 -1
  7. package/dist/agent/tool-call-parser.js +70 -0
  8. package/dist/agent/tool-call-parser.js.map +1 -1
  9. package/dist/app/adapters/current-agent-adapter.js +3 -0
  10. package/dist/app/adapters/current-agent-adapter.js.map +1 -1
  11. package/dist/app/controllers/session-context-limits.d.ts +1 -7
  12. package/dist/app/controllers/session-context-limits.js +16 -18
  13. package/dist/app/controllers/session-context-limits.js.map +1 -1
  14. package/dist/app/controllers/session-controller.d.ts +0 -2
  15. package/dist/app/controllers/session-controller.js +1 -4
  16. package/dist/app/controllers/session-controller.js.map +1 -1
  17. package/dist/app/controllers/session-turn-request.d.ts +1 -0
  18. package/dist/app/controllers/session-turn-request.js +3 -0
  19. package/dist/app/controllers/session-turn-request.js.map +1 -1
  20. package/dist/app/ports/agent-port.d.ts +1 -0
  21. package/dist/llm/capabilities.d.ts +2 -2
  22. package/dist/llm/capabilities.js +9 -11
  23. package/dist/llm/capabilities.js.map +1 -1
  24. package/dist/llm/http.js +3 -1
  25. package/dist/llm/http.js.map +1 -1
  26. package/dist/llm/router.js +9 -5
  27. package/dist/llm/router.js.map +1 -1
  28. package/dist/tui-v2/app/App.js +8 -2
  29. package/dist/tui-v2/app/App.js.map +1 -1
  30. package/dist/tui-v2/components/status/context-limit-chip.d.ts +1 -1
  31. package/dist/tui-v2/components/status/context-limit-chip.js +23 -3
  32. package/dist/tui-v2/components/status/context-limit-chip.js.map +1 -1
  33. package/dist/tui-v2/components/status/status-line.d.ts +1 -0
  34. package/dist/tui-v2/components/status/status-line.js +3 -3
  35. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  36. package/dist/tui-v2/composer/composer-editor.d.ts +1 -0
  37. package/dist/tui-v2/composer/composer-editor.js +17 -12
  38. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  39. package/dist/tui-v2/composer/use-draft-actions.d.ts +1 -1
  40. package/dist/tui-v2/composer/use-draft-actions.js +2 -2
  41. package/dist/tui-v2/composer/use-draft-actions.js.map +1 -1
  42. package/dist/tui-v2/rendering/incremental-strip.js +1 -1
  43. package/dist/tui-v2/rendering/incremental-strip.js.map +1 -1
  44. package/dist/tui-v2/rendering/strip-tool-surfaces.js +4 -0
  45. package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -1
  46. package/dist/version.generated.d.ts +2 -2
  47. package/dist/version.generated.js +2 -2
  48. package/package.json +1 -1
@@ -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";
@@ -458,6 +458,9 @@ export async function runAgentTurn(prompt, options = {}) {
458
458
  let provider = initialProvider;
459
459
  await ensureProviderConfigured(provider);
460
460
  let model = initialModel;
461
+ const currentContextLimitTokens = () => options.getContextLimitTokens
462
+ ? options.getContextLimitTokens(provider, model)
463
+ : options.contextLimitTokens;
461
464
  // Some Groq free-tier models have a per-request/per-minute input budget
462
465
  // below the normal agent prompt alone. Select a purpose-built compact
463
466
  // instruction set before the request is made, rather than treating the
@@ -853,6 +856,17 @@ export async function runAgentTurn(prompt, options = {}) {
853
856
  }
854
857
  return message;
855
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
+ };
856
870
  const recoveryProse = (content) => {
857
871
  const text = textBeforeToolCall(stripSentinelTokens(content)).trim();
858
872
  if (!text ||
@@ -1373,8 +1387,14 @@ export async function runAgentTurn(prompt, options = {}) {
1373
1387
  ...(retryReason ? { retryReason } : {}),
1374
1388
  });
1375
1389
  if (loopCheck.block) {
1376
- const reason = loopCheck.reason ??
1390
+ const baseReason = loopCheck.reason ??
1377
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;
1378
1398
  if (loopCheck.kind === "unchanged-success") {
1379
1399
  const result = { ok: true, output: reason, exitCode: 0 };
1380
1400
  emitVisibleSyntheticReceipt(result, reason);
@@ -2931,11 +2951,12 @@ export async function runAgentTurn(prompt, options = {}) {
2931
2951
  }
2932
2952
  async function maybeAutoCompact(reason, force = false) {
2933
2953
  const beforeTokens = estimateNextRequestTokens(messages);
2954
+ const contextLimitTokens = currentContextLimitTokens();
2934
2955
  const compactTrigger = autoCompactTriggerTokens(getReliabilityPolicy(), {
2935
2956
  provider,
2936
2957
  model,
2937
- ...(options.contextLimitTokens
2938
- ? { contextLimitTokens: options.contextLimitTokens }
2958
+ ...(contextLimitTokens !== undefined
2959
+ ? { contextLimitTokens }
2939
2960
  : {}),
2940
2961
  });
2941
2962
  if (!force && beforeTokens < compactTrigger)
@@ -3196,6 +3217,7 @@ export async function runAgentTurn(prompt, options = {}) {
3196
3217
  writeNotice("info", notice, chalk.dim(` ℹ ${notice}\n`));
3197
3218
  }
3198
3219
  }
3220
+ const contextLimitTokens = currentContextLimitTokens();
3199
3221
  await auditLog("agent.turn", {
3200
3222
  provider,
3201
3223
  model,
@@ -3204,7 +3226,13 @@ export async function runAgentTurn(prompt, options = {}) {
3204
3226
  step,
3205
3227
  // Metadata-only composition metrics (no prompt/tool text).
3206
3228
  ...contextBreakdownAuditPayload(contextBreakdown),
3207
- compactTriggerTokens: autoCompactTriggerTokens(),
3229
+ compactTriggerTokens: autoCompactTriggerTokens(getReliabilityPolicy(), {
3230
+ provider,
3231
+ model,
3232
+ ...(contextLimitTokens !== undefined
3233
+ ? { contextLimitTokens }
3234
+ : {}),
3235
+ }),
3208
3236
  maxTokensBudget: resolveStepMaxTokens({
3209
3237
  nativeToolsActive,
3210
3238
  toolsAttached,
@@ -3873,7 +3901,7 @@ export async function runAgentTurn(prompt, options = {}) {
3873
3901
  }
3874
3902
  // Exhausted retries — fall through to the normal answer path.
3875
3903
  }
3876
- 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)) {
3877
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"));
3878
3906
  commitAssistantRetry(assistantText.visible);
3879
3907
  messages.push(recoveryUserMessage(toolsAttached
@@ -4280,11 +4308,20 @@ export async function runAgentTurn(prompt, options = {}) {
4280
4308
  writeNotice("warn", reason, chalk.yellow(` ⚠ ${reason}\n`));
4281
4309
  const suppressedResults = bound.map((b) => {
4282
4310
  const duplicate = runIds.has(b.id);
4283
- 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;
4284
4320
  const result = {
4285
- ok: false,
4321
+ ok: duplicate,
4286
4322
  output: resultReason,
4287
- exitCode: duplicate ? 409 : 130,
4323
+ exitCode: duplicate ? 0 : 130,
4324
+ ...(duplicate ? { suppressedRepeat: true } : {}),
4288
4325
  };
4289
4326
  return { b, resultReason, result };
4290
4327
  });
@@ -4305,22 +4342,6 @@ export async function runAgentTurn(prompt, options = {}) {
4305
4342
  writeToolOutput(eventId, output, chalk.dim(` ${output}`));
4306
4343
  emitToolResult(eventId, result, resultReason);
4307
4344
  }
4308
- if (historyNativeCalls.length) {
4309
- appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock ??
4310
- (assistantText.hasThinking && assistantText.thinkContent
4311
- ? { text: assistantText.thinkContent }
4312
- : undefined));
4313
- for (const { b, resultReason, result } of suppressedResults) {
4314
- appendToolResult(messages, b.id, `Tool ${b.call.name} result (exit=${result.exitCode}, ok=false):\n${resultReason}`, b.call.name, false);
4315
- }
4316
- }
4317
- else {
4318
- const standardizedContent = (beforeTool ? beforeTool.trim() + "\n\n" : "") +
4319
- allCalls
4320
- .map((candidate) => `\`\`\`tool\n${JSON.stringify(candidate)}\n\`\`\``)
4321
- .join("\n\n");
4322
- pushAssistantHistory(standardizedContent);
4323
- }
4324
4345
  if (sequenceDecision.terminal) {
4325
4346
  const remainingCriteria = unreadResponderNotificationIds.size > 0
4326
4347
  ? ["Analyze and acknowledge the delivered Responder result without repeating completed foreground work."]
@@ -4330,10 +4351,10 @@ export async function runAgentTurn(prompt, options = {}) {
4330
4351
  moveTurn("partial", "repeated identical action sequence");
4331
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.");
4332
4353
  }
4333
- messages.push(recoveryUserMessage(reason +
4354
+ upsertActionCycleRecovery(reason +
4334
4355
  (unreadResponderNotificationIds.size > 0
4335
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."
4336
- : " 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."));
4337
4358
  continue;
4338
4359
  }
4339
4360
  if (sequenceDecision.warn && sequenceDecision.warnMessage) {
@@ -4397,6 +4418,7 @@ export async function runAgentTurn(prompt, options = {}) {
4397
4418
  let planCreatedThisTurn = Boolean(activePlan && activePlan.tasks.length > 0);
4398
4419
  let actionSequenceExecuted = 0;
4399
4420
  let actionSequenceEligible = allCalls.length > 0;
4421
+ const actionSequenceOutcomes = new Map();
4400
4422
  /**
4401
4423
  * Record a tool result into history. Failures / user declines are
4402
4424
  * always returned to the model — we never cancel later siblings or
@@ -4407,6 +4429,15 @@ export async function runAgentTurn(prompt, options = {}) {
4407
4429
  consecutiveModelOnlyRounds = 0;
4408
4430
  recordedNativeIds.add(boundCall.id);
4409
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
+ }));
4410
4441
  // A policy-suppressed call is deterministic: replaying it verbatim
4411
4442
  // returns the identical receipt. It must therefore keep the sequence
4412
4443
  // eligible, otherwise the tool-level suppression and the sequence
@@ -4746,12 +4777,16 @@ export async function runAgentTurn(prompt, options = {}) {
4746
4777
  }
4747
4778
  fillMissingToolResults(messages, historyNativeCalls, "Cancelled — not executed this turn.");
4748
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);
4749
4784
  loopGuard.completeActionSequence(actionSequenceCalls, actionSequenceEligible &&
4750
4785
  toRun.length === bound.length &&
4751
4786
  actionSequenceExecuted === allCalls.length &&
4752
4787
  !aborted &&
4753
4788
  !awaitingPlanApproval &&
4754
- !governorPauseReason);
4789
+ !governorPauseReason, actionSequenceOutcome);
4755
4790
  // Keep ledger system rows outside the native assistant→tool group so
4756
4791
  // protocol repair preserves the real successful job.read body.
4757
4792
  for (const notification of deferredResponderLedgerNotifications.splice(0)) {