@pentoshi/clai 3.11.5 → 3.11.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.
Files changed (54) hide show
  1. package/dist/agent/interrupted-reasoning.d.ts +6 -0
  2. package/dist/agent/interrupted-reasoning.js +38 -0
  3. package/dist/agent/interrupted-reasoning.js.map +1 -0
  4. package/dist/agent/runner.js +90 -15
  5. package/dist/agent/runner.js.map +1 -1
  6. package/dist/agent/stream-recovery.d.ts +4 -1
  7. package/dist/agent/stream-recovery.js +20 -9
  8. package/dist/agent/stream-recovery.js.map +1 -1
  9. package/dist/llm/anthropic.js +16 -1
  10. package/dist/llm/anthropic.js.map +1 -1
  11. package/dist/llm/aws-mantle.js +14 -1
  12. package/dist/llm/aws-mantle.js.map +1 -1
  13. package/dist/llm/gemini.js +3 -1
  14. package/dist/llm/gemini.js.map +1 -1
  15. package/dist/llm/http.d.ts +8 -2
  16. package/dist/llm/http.js +62 -21
  17. package/dist/llm/http.js.map +1 -1
  18. package/dist/llm/modal.js +3 -3
  19. package/dist/llm/modal.js.map +1 -1
  20. package/dist/llm/nvidia.js +2 -3
  21. package/dist/llm/nvidia.js.map +1 -1
  22. package/dist/llm/ollama.js +3 -1
  23. package/dist/llm/ollama.js.map +1 -1
  24. package/dist/tui-v2/components/pager/pager-line.js +7 -4
  25. package/dist/tui-v2/components/pager/pager-line.js.map +1 -1
  26. package/dist/tui-v2/components/pager/pager.js +2 -2
  27. package/dist/tui-v2/components/pager/pager.js.map +1 -1
  28. package/dist/tui-v2/components/transcript/assistant-message.js +5 -3
  29. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
  30. package/dist/tui-v2/components/transcript/file-diff-card.js +3 -0
  31. package/dist/tui-v2/components/transcript/file-diff-card.js.map +1 -1
  32. package/dist/tui-v2/components/transcript/selectable-line.d.ts +13 -0
  33. package/dist/tui-v2/components/transcript/selectable-line.js +11 -0
  34. package/dist/tui-v2/components/transcript/selectable-line.js.map +1 -1
  35. package/dist/tui-v2/components/transcript/thinking-block.js +7 -4
  36. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
  37. package/dist/tui-v2/components/transcript/tool-card.js +3 -3
  38. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
  39. package/dist/tui-v2/rendering/pager-chrome.d.ts +14 -3
  40. package/dist/tui-v2/rendering/pager-chrome.js +86 -23
  41. package/dist/tui-v2/rendering/pager-chrome.js.map +1 -1
  42. package/dist/tui-v2/rendering/pager-markdown.js +5 -0
  43. package/dist/tui-v2/rendering/pager-markdown.js.map +1 -1
  44. package/dist/ui/code-block.d.ts +8 -0
  45. package/dist/ui/code-block.js +33 -6
  46. package/dist/ui/code-block.js.map +1 -1
  47. package/dist/ui/markdown.js +37 -3
  48. package/dist/ui/markdown.js.map +1 -1
  49. package/dist/ui/text-width.d.ts +27 -0
  50. package/dist/ui/text-width.js +36 -0
  51. package/dist/ui/text-width.js.map +1 -0
  52. package/dist/version.generated.d.ts +2 -2
  53. package/dist/version.generated.js +2 -2
  54. package/package.json +12 -12
@@ -0,0 +1,6 @@
1
+ export declare const INTERRUPTED_REASONING_LIMIT = 4000;
2
+ export declare const INTERRUPTED_REASONING_MIN = 160;
3
+ export declare const MIN_RESUMPTION_YIELD = 240;
4
+ export declare function isMeaningfulResumptionYield(producedChars: number): boolean;
5
+ export declare function appendInterruptedReasoning(previous: string, next: string): string;
6
+ export declare function interruptedReasoningBrief(reasoning: string): string | undefined;
@@ -0,0 +1,38 @@
1
+ export const INTERRUPTED_REASONING_LIMIT = 4_000;
2
+ export const INTERRUPTED_REASONING_MIN = 160;
3
+ export const MIN_RESUMPTION_YIELD = 240;
4
+ export function isMeaningfulResumptionYield(producedChars) {
5
+ return producedChars >= MIN_RESUMPTION_YIELD;
6
+ }
7
+ function normalize(reasoning) {
8
+ return reasoning.replace(/\r/g, "").replace(/[ \t]+$/gm, "").trim();
9
+ }
10
+ function clampReasoning(reasoning) {
11
+ if (reasoning.length <= INTERRUPTED_REASONING_LIMIT)
12
+ return reasoning;
13
+ const tail = reasoning.slice(reasoning.length - INTERRUPTED_REASONING_LIMIT);
14
+ const breakAt = tail.indexOf("\n");
15
+ return breakAt > 0 && breakAt < 400 ? tail.slice(breakAt + 1) : tail;
16
+ }
17
+ export function appendInterruptedReasoning(previous, next) {
18
+ const addition = normalize(next);
19
+ if (!addition)
20
+ return previous;
21
+ const earlier = normalize(previous);
22
+ if (!earlier)
23
+ return clampReasoning(addition);
24
+ if (earlier.includes(addition))
25
+ return clampReasoning(earlier);
26
+ if (addition.includes(earlier))
27
+ return clampReasoning(addition);
28
+ return clampReasoning(`${earlier}\n\n${addition}`);
29
+ }
30
+ export function interruptedReasoningBrief(reasoning) {
31
+ const body = normalize(reasoning);
32
+ if (body.length < INTERRUPTED_REASONING_MIN)
33
+ return undefined;
34
+ return ("Your own reasoning from the interrupted attempt is preserved below. " +
35
+ "Build on these conclusions instead of re-deriving them, and go straight to the next concrete action.\n\n" +
36
+ `<preserved_reasoning>\n${body}\n</preserved_reasoning>`);
37
+ }
38
+ //# sourceMappingURL=interrupted-reasoning.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interrupted-reasoning.js","sourceRoot":"","sources":["../../src/agent/interrupted-reasoning.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AAEjD,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAE7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC,MAAM,UAAU,2BAA2B,CAAC,aAAqB;IAC/D,OAAO,aAAa,IAAI,oBAAoB,CAAC;AAC/C,CAAC;AAED,SAAS,SAAS,CAAC,SAAiB;IAClC,OAAO,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACtE,CAAC;AAED,SAAS,cAAc,CAAC,SAAiB;IACvC,IAAI,SAAS,CAAC,MAAM,IAAI,2BAA2B;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,2BAA2B,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,OAAO,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,QAAgB,EAChB,IAAY;IAEZ,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC/B,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IACpC,IAAI,CAAC,OAAO;QAAE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,cAAc,CAAC,OAAO,CAAC,CAAC;IAC/D,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;IAChE,OAAO,cAAc,CAAC,GAAG,OAAO,OAAO,QAAQ,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,SAAiB;IAEjB,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,IAAI,CAAC,MAAM,GAAG,yBAAyB;QAAE,OAAO,SAAS,CAAC;IAC9D,OAAO,CACL,sEAAsE;QACtE,0GAA0G;QAC1G,0BAA0B,IAAI,0BAA0B,CACzD,CAAC;AACJ,CAAC"}
@@ -58,6 +58,7 @@ import { buildDurableEnvelope, WorkLedger, } from "./durable-envelope.js";
58
58
  import { COMPACTION_SYSTEM_PROMPT, } from "./compaction-summary.js";
59
59
  import { maybeAppendPlanModeReminder, PLAN_REMINDER_TOAST, } from "./plan-mode-reminders.js";
60
60
  import { LoopGuard } from "./loop-guard.js";
61
+ import { appendInterruptedReasoning, interruptedReasoningBrief, isMeaningfulResumptionYield, } from "./interrupted-reasoning.js";
61
62
  import { CompactionAttemptLedger, compactionAttemptKey, } from "./compaction-attempt.js";
62
63
  import { resolveRequestBudget } from "./request-budget.js";
63
64
  import { loadPlan, mutatePlan, markTask, appendPlanTask, readyPlanTasks, foregroundRemaining, responderOpenTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
@@ -169,6 +170,8 @@ export async function runAgentTurn(prompt, options = {}) {
169
170
  // at the top of every loop iteration.
170
171
  let visibleCommitted = false;
171
172
  let interruptedVisible = "";
173
+ let interruptedReasoning = "";
174
+ let lowYieldResumptions = 0;
172
175
  const trimExactContinuationOverlap = (previous, current, minLength = 32) => {
173
176
  if (previous.length > 0 && current.startsWith(previous)) {
174
177
  return current.slice(previous.length);
@@ -3095,6 +3098,30 @@ export async function runAgentTurn(prompt, options = {}) {
3095
3098
  let accumulatedText = "";
3096
3099
  const callIds = [];
3097
3100
  let streamedCallsCount = 0;
3101
+ // A model can think silently for minutes. Without a heartbeat the UI
3102
+ // shows a frozen label and the turn looks hung, so surface elapsed time
3103
+ // and the current phase on a timer rather than only on token arrival.
3104
+ const streamStartedAt = Date.now();
3105
+ const streamPhase = () => {
3106
+ const seconds = Math.round((Date.now() - streamStartedAt) / 1000);
3107
+ const elapsed = seconds < 60
3108
+ ? `${seconds}s`
3109
+ : `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s`;
3110
+ if (generatedTokens > 0 && !inThinking) {
3111
+ return `generating response · ${generatedTokens} tokens · ${elapsed}`;
3112
+ }
3113
+ if (sawReasoning)
3114
+ return `thinking · ${elapsed}`;
3115
+ return `waiting for model · ${elapsed}`;
3116
+ };
3117
+ const heartbeat = setInterval(() => {
3118
+ const text = streamPhase();
3119
+ if (writesDirectly)
3120
+ spinner.setLabel(text);
3121
+ else
3122
+ emit({ type: "status", text });
3123
+ }, 10_000);
3124
+ heartbeat.unref?.();
3098
3125
  const deferredToolCalls = [];
3099
3126
  const deltaParser = writesDirectly
3100
3127
  ? undefined
@@ -3336,6 +3363,7 @@ export async function runAgentTurn(prompt, options = {}) {
3336
3363
  resetStreamRecoveryState(recoveryState);
3337
3364
  allowModelFallback = false;
3338
3365
  preferModelFallback = false;
3366
+ lowYieldResumptions = 0;
3339
3367
  }
3340
3368
  catch (streamError) {
3341
3369
  // User cancelled (double-Esc) — never try to recover, just stop.
@@ -3363,47 +3391,89 @@ export async function runAgentTurn(prompt, options = {}) {
3363
3391
  // We only rethrow (stop the turn) in the worst case: every approach
3364
3392
  // for that failure class is exhausted or the total budget is spent.
3365
3393
  const failureKind = classifyStreamFailure(streamError);
3394
+ const partialStream = streamAlreadyEmitted(streamError) || accumulatedText.length > 0;
3395
+ const partial = rememberThinkingFromText(accumulatedText);
3396
+ const rawPartialVisible = partialStream
3397
+ ? textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible)
3398
+ : "";
3399
+ const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
3400
+ const partialVisible = normalizedPartialVisible.trim();
3401
+ // A route that drops after a handful of characters is not making
3402
+ // progress, however many times it is retried. Only a substantial
3403
+ // yield unlocks the generous resumption budget; anything less is
3404
+ // charged to the failure class and escalates to another route.
3405
+ const meaningfulProgress = partialStream &&
3406
+ isMeaningfulResumptionYield(normalizedPartialVisible.length + partial.thinkContent.length);
3407
+ if (partialStream) {
3408
+ lowYieldResumptions = meaningfulProgress
3409
+ ? 0
3410
+ : lowYieldResumptions + 1;
3411
+ }
3366
3412
  const plan = planStreamRecovery({
3367
3413
  kind: failureKind,
3368
3414
  state: recoveryState,
3415
+ progressed: meaningfulProgress,
3369
3416
  });
3370
- const partialStream = streamAlreadyEmitted(streamError) || accumulatedText.length > 0;
3417
+ const terminalFailure = plan.action === "give-up";
3371
3418
  let continuationNudge = "";
3372
3419
  if (partialStream) {
3373
3420
  spinner.stop();
3374
3421
  deltaParser?.finish();
3375
- const partial = rememberThinkingFromText(accumulatedText);
3376
3422
  const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
3377
- const rawPartialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible);
3378
- const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
3379
- const partialVisible = normalizedPartialVisible.trim();
3380
3423
  if (partialVisible) {
3381
- writeAssistantMessage(partialVisible);
3382
- pushAssistantHistory(partialVisible);
3424
+ // Finalizing here would close the streaming card and split one
3425
+ // answer across a card per interruption. Keep it open and let
3426
+ // the single commit below paint the stitched text; only a
3427
+ // terminal failure has to flush it now.
3428
+ if (terminalFailure) {
3429
+ writeAssistantMessage(interruptedVisible + normalizedPartialVisible);
3430
+ }
3431
+ else {
3432
+ visibleCommitted = true;
3433
+ }
3434
+ messages.push({
3435
+ role: "assistant",
3436
+ content: sanitizeAssistantText(partialVisible),
3437
+ });
3383
3438
  interruptedVisible += normalizedPartialVisible;
3384
3439
  }
3385
- else if (!writesDirectly) {
3440
+ else if (terminalFailure && !writesDirectly) {
3386
3441
  emit({ type: "assistant-message", text: "" });
3387
3442
  }
3388
3443
  if (partial.hasThinking && !hasShownToolCall) {
3389
3444
  writeThinkingBlock(partial.thinkContent);
3390
3445
  }
3446
+ if (partial.hasThinking) {
3447
+ interruptedReasoning = appendInterruptedReasoning(interruptedReasoning, partial.thinkContent);
3448
+ }
3391
3449
  for (const deferred of deferredToolCalls) {
3392
3450
  if (!deferred.shown || deferred.call.name === "…")
3393
3451
  continue;
3394
3452
  writeToolBlocked(deferred.eventId, deferred.call.name, "Incomplete tool call discarded after the provider stream was interrupted.", chalk.yellow(" ⚠ incomplete tool call discarded after stream interruption\n"));
3395
3453
  }
3396
- continuationNudge =
3397
- "The provider stream was interrupted after partial output. Continue from the exact stopping point without repeating prior text. Any incomplete tool call was discarded and must be reissued in full.";
3398
- const restartNotice = plan.action === "give-up"
3454
+ continuationNudge = [
3455
+ partialVisible
3456
+ ? "The provider stream was interrupted after partial output. Continue from the exact stopping point without repeating prior text. Any incomplete tool call was discarded and must be reissued in full."
3457
+ : "The provider stream was interrupted before any answer was produced. Any incomplete tool call was discarded and must be reissued in full. Do not restart your analysis from the beginning.",
3458
+ interruptedReasoningBrief(interruptedReasoning),
3459
+ ]
3460
+ .filter((part) => Boolean(part))
3461
+ .join("\n\n");
3462
+ const restartNotice = terminalFailure
3399
3463
  ? "partial response preserved before terminal provider failure"
3400
- : "partial response preserved — resuming from the interruption";
3464
+ : lowYieldResumptions > 1
3465
+ ? `route is dropping after almost no output (${lowYieldResumptions} in a row) — switching model`
3466
+ : "partial response preserved — resuming from the interruption";
3401
3467
  writeNotice("warn", restartNotice, chalk.yellow(` ⚠ ${restartNotice}\n`));
3402
3468
  }
3403
- if (plan.action === "give-up") {
3469
+ if (terminalFailure) {
3404
3470
  throw streamError;
3405
3471
  }
3406
- recordRecoveryAttempt(recoveryState, failureKind);
3472
+ recordRecoveryAttempt(recoveryState, failureKind, meaningfulProgress);
3473
+ if (lowYieldResumptions > 1) {
3474
+ allowModelFallback = true;
3475
+ preferModelFallback = true;
3476
+ }
3407
3477
  if (plan.notice) {
3408
3478
  writeNotice("warn", plan.notice, chalk.yellow(` ⚠ ${plan.notice}\n`));
3409
3479
  }
@@ -3434,6 +3504,7 @@ export async function runAgentTurn(prompt, options = {}) {
3434
3504
  }
3435
3505
  finally {
3436
3506
  // Always clear the spinner — abort, network error, or success.
3507
+ clearInterval(heartbeat);
3437
3508
  spinner.stop();
3438
3509
  }
3439
3510
  if (responderDelivery) {
@@ -3487,7 +3558,7 @@ export async function runAgentTurn(prompt, options = {}) {
3487
3558
  const commitAssistantRetry = (historyText) => {
3488
3559
  const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
3489
3560
  if (!hasShownToolCall) {
3490
- const displayText = textBeforeToolCall(collapseRepeatedText(assistantText.visible)).trim();
3561
+ const displayText = textBeforeToolCall(collapseRepeatedText(canonicalAssistantVisible)).trim();
3491
3562
  if (displayText) {
3492
3563
  writeAssistantMessage(displayText);
3493
3564
  }
@@ -3503,6 +3574,8 @@ export async function runAgentTurn(prompt, options = {}) {
3503
3574
  }
3504
3575
  pushAssistantHistory(historyText);
3505
3576
  interruptedVisible = "";
3577
+ interruptedReasoning = "";
3578
+ lowYieldResumptions = 0;
3506
3579
  };
3507
3580
  // Only emit a thinking-block event when the classic renderer is
3508
3581
  // active (writesDirectly / no deltaParser). In TUI v2 the
@@ -4186,6 +4259,8 @@ export async function runAgentTurn(prompt, options = {}) {
4186
4259
  emit({ type: "assistant-message", text: "" });
4187
4260
  }
4188
4261
  interruptedVisible = "";
4262
+ interruptedReasoning = "";
4263
+ lowYieldResumptions = 0;
4189
4264
  let bound = [];
4190
4265
  if (nativeToolCalls.length) {
4191
4266
  bound = nativeToolCalls.map((tc, index) => {