@pentoshi/clai 3.12.0 → 3.13.0

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/dist/agent/compaction-summary.d.ts +20 -12
  2. package/dist/agent/compaction-summary.js +72 -14
  3. package/dist/agent/compaction-summary.js.map +1 -1
  4. package/dist/agent/context-manager.d.ts +2 -0
  5. package/dist/agent/context-manager.js +64 -35
  6. package/dist/agent/context-manager.js.map +1 -1
  7. package/dist/agent/events.d.ts +1 -0
  8. package/dist/agent/reliability-policy.d.ts +1 -0
  9. package/dist/agent/reliability-policy.js +13 -9
  10. package/dist/agent/reliability-policy.js.map +1 -1
  11. package/dist/agent/runner.js +147 -98
  12. package/dist/agent/runner.js.map +1 -1
  13. package/dist/app/adapters/agent-event-adapter.js +1 -0
  14. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  15. package/dist/app/controllers/session-compact-helper.d.ts +3 -1
  16. package/dist/app/controllers/session-compact-helper.js +86 -42
  17. package/dist/app/controllers/session-compact-helper.js.map +1 -1
  18. package/dist/app/controllers/session-context-usage.js +0 -2
  19. package/dist/app/controllers/session-context-usage.js.map +1 -1
  20. package/dist/app/controllers/session-controller.js +3 -0
  21. package/dist/app/controllers/session-controller.js.map +1 -1
  22. package/dist/app/events/app-event.d.ts +1 -0
  23. package/dist/app/events/app-event.js.map +1 -1
  24. package/dist/tools/registry.js +33 -13
  25. package/dist/tools/registry.js.map +1 -1
  26. package/dist/tui/state.js +5 -5
  27. package/dist/tui/state.js.map +1 -1
  28. package/dist/tui-v2/components/status/status-line.d.ts +1 -0
  29. package/dist/tui-v2/components/status/status-line.js +62 -3
  30. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  31. package/dist/tui-v2/components/transcript/thinking-block.js +2 -2
  32. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
  33. package/dist/tui-v2/components/transcript/transcript-scrollbar.d.ts +7 -11
  34. package/dist/tui-v2/components/transcript/transcript-scrollbar.js +35 -30
  35. package/dist/tui-v2/components/transcript/transcript-scrollbar.js.map +1 -1
  36. package/dist/tui-v2/rendering/incremental-strip.js +1 -1
  37. package/dist/tui-v2/rendering/incremental-strip.js.map +1 -1
  38. package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +0 -8
  39. package/dist/tui-v2/rendering/strip-tool-surfaces.js +32 -14
  40. package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -1
  41. package/dist/tui-v2/rendering/thinking-tail.d.ts +1 -0
  42. package/dist/tui-v2/rendering/thinking-tail.js +21 -0
  43. package/dist/tui-v2/rendering/thinking-tail.js.map +1 -1
  44. package/dist/tui-v2/state/transcript-compaction.js +1 -4
  45. package/dist/tui-v2/state/transcript-compaction.js.map +1 -1
  46. package/dist/tui-v2/state/transcript-reducer.js +3 -1
  47. package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
  48. package/dist/version.generated.d.ts +2 -2
  49. package/dist/version.generated.js +2 -2
  50. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { join } from "node:path";
3
- import { completeWithProvider, streamWithProvider } from "../llm/router.js";
3
+ import { streamWithProvider } from "../llm/router.js";
4
+ import { modelContextWindow } from "../llm/token-usage.js";
4
5
  import { streamAlreadyEmitted } from "../llm/stream-progress.js";
5
6
  import { classifyStreamFailure, planStreamRecovery, recordRecoveryAttempt, createStreamRecoveryState, resetStreamRecoveryState, } from "./stream-recovery.js";
6
7
  import { modelSupportsVision, resolveToolDialect } from "../llm/capabilities.js";
@@ -55,7 +56,7 @@ import { analyzeTask, formatTaskAnalysisHint, isNarrowExplicitNmapOperation, } f
55
56
  import { computeMaxIterations, computeStepBudget } from "./step-budget.js";
56
57
  import { isScratchOnlyWrite } from "./scratch-write.js";
57
58
  import { buildDurableEnvelope, WorkLedger, } from "./durable-envelope.js";
58
- import { COMPACTION_SYSTEM_PROMPT, COMPACTION_MAX_COMPLETION_TOKENS, COMPACTION_MAP_MAX_COMPLETION_TOKENS, } from "./compaction-summary.js";
59
+ import { buildCompactionRetryPrompt, compactionSinglePassInputBudget, COMPACTION_SYSTEM_PROMPT, COMPACTION_MAX_COMPLETION_TOKENS, COMPACTION_MAP_MAX_COMPLETION_TOKENS, isCompactionCompletionTruncated, looksLikeIncompleteCompactionSummary, looksLikeTranscriptReplay, normalizeCompactionSummary, } from "./compaction-summary.js";
59
60
  import { maybeAppendPlanModeReminder, PLAN_REMINDER_TOAST, } from "./plan-mode-reminders.js";
60
61
  import { LoopGuard } from "./loop-guard.js";
61
62
  import { appendInterruptedReasoning, interruptedReasoningBrief, isMeaningfulResumptionYield, } from "./interrupted-reasoning.js";
@@ -315,12 +316,21 @@ export async function runAgentTurn(prompt, options = {}) {
315
316
  process.stdout.write(chalk.dim(" ✦ Compacted Context · streaming Markdown\n\n"));
316
317
  }
317
318
  };
318
- const writeCompactionDelta = (id, text) => {
319
- if (!text)
319
+ const writeCompactionDelta = (id, text, replace = false) => {
320
+ if (!text && !replace)
320
321
  return;
321
- emit({ type: "compaction-delta", id, text });
322
- if (writesDirectly)
323
- process.stdout.write(text);
322
+ emit({
323
+ type: "compaction-delta",
324
+ id,
325
+ text,
326
+ ...(replace ? { replace: true } : {}),
327
+ });
328
+ if (writesDirectly) {
329
+ if (replace)
330
+ process.stdout.write(chalk.dim("\n ↻ rewriting compacted context\n\n"));
331
+ if (text)
332
+ process.stdout.write(text);
333
+ }
324
334
  };
325
335
  const writeCompactionCompleted = (id, summary, beforeTokens, afterTokens) => {
326
336
  emit({
@@ -913,6 +923,7 @@ export async function runAgentTurn(prompt, options = {}) {
913
923
  // Track consecutive thinking-only responses so we can nudge the model
914
924
  // to actually act instead of silently returning an empty answer.
915
925
  let emptyVisibleRetries = 0;
926
+ let truncatedBudgetRounds = 0;
916
927
  let retryWithoutThinking = false;
917
928
  // Robust stream-failure recovery. When a provider stream/complete fails we
918
929
  // try working approaches (backoff, compaction, thinking-off, provider
@@ -1164,6 +1175,7 @@ export async function runAgentTurn(prompt, options = {}) {
1164
1175
  /** Successful file mutation this turn — kills false "error diagnosed but not fixed". */
1165
1176
  let sawSuccessfulMutation = false;
1166
1177
  let step = -1;
1178
+ let stepMaxTokens = 0;
1167
1179
  let nextToolEventId = 0;
1168
1180
  const alreadyPrintedIds = new Set();
1169
1181
  const promptMutex = {
@@ -2771,50 +2783,105 @@ export async function runAgentTurn(prompt, options = {}) {
2771
2783
  const summarizeForCompaction = async (summaryPrompt, stage) => {
2772
2784
  const streamFinalSummary = stage?.phase !== "map";
2773
2785
  const compactionId = streamFinalSummary ? activeCompactionId : undefined;
2774
- const deltaParser = compactionId
2775
- ? createThinkingStreamParser((text) => writeCompactionDelta(compactionId, text), undefined, { remember: false })
2786
+ const maxTokens = stage?.phase === "map"
2787
+ ? COMPACTION_MAP_MAX_COMPLETION_TOKENS
2788
+ : COMPACTION_MAX_COMPLETION_TOKENS;
2789
+ const sourceMessages = stage?.sourceMessages;
2790
+ const compactionTools = sourceMessages
2791
+ ? selectToolDefs(nativeToolsActive, useCompactSystemPrompt)
2776
2792
  : undefined;
2777
2793
  const request = {
2778
2794
  provider,
2779
2795
  model,
2780
- messages: [
2781
- { role: "system", content: COMPACTION_SYSTEM_PROMPT },
2782
- { role: "user", content: summaryPrompt },
2783
- ],
2796
+ messages: sourceMessages
2797
+ ? [
2798
+ ...sourceMessages,
2799
+ { role: "user", content: summaryPrompt },
2800
+ ]
2801
+ : [
2802
+ { role: "system", content: COMPACTION_SYSTEM_PROMPT },
2803
+ { role: "user", content: summaryPrompt },
2804
+ ],
2784
2805
  temperature: 0.1,
2785
- maxTokens: stage?.phase === "map"
2786
- ? COMPACTION_MAP_MAX_COMPLETION_TOKENS
2787
- : COMPACTION_MAX_COMPLETION_TOKENS,
2806
+ maxTokens,
2788
2807
  thinking: { enabled: false, effort: "none" },
2789
2808
  signal: options.signal,
2790
2809
  allowModelFallback: true,
2810
+ ...(compactionTools?.length
2811
+ ? {
2812
+ tools: compactionTools,
2813
+ toolChoice: "none",
2814
+ }
2815
+ : {}),
2791
2816
  };
2792
- const response = await streamWithProvider(request, (token) => deltaParser?.push(token), { onStatus: () => undefined, maxRetries: 0 });
2793
- deltaParser?.finish();
2794
- const parsed = stripThinking(response.text);
2795
- if (parsed.visible.trim() || !parsed.hasThinking)
2796
- return response.text;
2797
- const retry = await completeWithProvider({
2798
- ...request,
2799
- messages: [
2800
- {
2801
- role: "system",
2802
- content: `${COMPACTION_SYSTEM_PROMPT}\nReturn only the continuation-memory summary. Do not include analysis, reasoning, or <think> tags.`,
2803
- },
2804
- { role: "user", content: summaryPrompt },
2805
- ],
2806
- temperature: 0,
2807
- maxTokens: stage?.phase === "map"
2808
- ? COMPACTION_MAP_MAX_COMPLETION_TOKENS
2809
- : COMPACTION_MAX_COMPLETION_TOKENS,
2810
- thinking: { enabled: false, effort: "none" },
2811
- allowModelFallback: true,
2812
- }, { maxRetries: 0 });
2813
- const retryVisible = stripThinking(retry.text).visible.trim();
2814
- if (retryVisible && compactionId) {
2815
- writeCompactionDelta(compactionId, retryVisible);
2817
+ const runAttempt = async (attemptRequest, replace = false) => {
2818
+ if (compactionId && replace) {
2819
+ writeCompactionDelta(compactionId, "", true);
2820
+ }
2821
+ const parser = createThinkingStreamParser((text) => {
2822
+ if (compactionId)
2823
+ writeCompactionDelta(compactionId, text);
2824
+ }, undefined, { remember: false });
2825
+ const result = await streamWithProvider(attemptRequest, (token) => parser.push(token), { onStatus: () => undefined, maxRetries: 0 });
2826
+ parser.finish();
2827
+ return result;
2828
+ };
2829
+ const first = await runAttempt(request);
2830
+ let visible = normalizeCompactionSummary(stripThinking(first.text).visible);
2831
+ let retryReason;
2832
+ if (isCompactionCompletionTruncated(first, maxTokens)) {
2833
+ retryReason = "truncated";
2834
+ }
2835
+ else if (!visible) {
2836
+ retryReason = "reasoning-only";
2837
+ }
2838
+ else if (looksLikeTranscriptReplay(visible)) {
2839
+ retryReason = "replayed";
2840
+ }
2841
+ else if (looksLikeIncompleteCompactionSummary(visible)) {
2842
+ retryReason = "incomplete";
2843
+ }
2844
+ if (retryReason) {
2845
+ const retry = await runAttempt({
2846
+ ...request,
2847
+ messages: sourceMessages
2848
+ ? [
2849
+ ...sourceMessages,
2850
+ {
2851
+ role: "user",
2852
+ content: buildCompactionRetryPrompt(summaryPrompt, retryReason),
2853
+ },
2854
+ ]
2855
+ : [
2856
+ {
2857
+ role: "system",
2858
+ content: `${COMPACTION_SYSTEM_PROMPT}\nReturn only a complete continuation-memory summary. Do not include analysis, reasoning, or <think> tags.`,
2859
+ },
2860
+ {
2861
+ role: "user",
2862
+ content: buildCompactionRetryPrompt(summaryPrompt, retryReason),
2863
+ },
2864
+ ],
2865
+ temperature: 0,
2866
+ maxTokens,
2867
+ thinking: { enabled: false, effort: "none" },
2868
+ allowModelFallback: true,
2869
+ }, true);
2870
+ if (isCompactionCompletionTruncated(retry, maxTokens)) {
2871
+ throw new Error("compaction failed: model hit the summary output limit twice — original context retained");
2872
+ }
2873
+ visible = normalizeCompactionSummary(stripThinking(retry.text).visible);
2874
+ if (!visible) {
2875
+ throw new Error("compaction failed: model returned an empty summary");
2876
+ }
2877
+ if (looksLikeTranscriptReplay(visible)) {
2878
+ throw new Error("compaction failed: model replayed the transcript twice — original context retained");
2879
+ }
2880
+ if (looksLikeIncompleteCompactionSummary(visible)) {
2881
+ throw new Error("compaction failed: model returned an incomplete summary twice — original context retained");
2882
+ }
2816
2883
  }
2817
- return retry.text;
2884
+ return visible;
2818
2885
  };
2819
2886
  /**
2820
2887
  * Estimate the complete next model request, including attached native-tool
@@ -2907,9 +2974,12 @@ export async function runAgentTurn(prompt, options = {}) {
2907
2974
  activeCompactionId = compactionId;
2908
2975
  writeCompactionStarted(compactionId, beforeTokens);
2909
2976
  try {
2977
+ const compactionTools = selectToolDefs(nativeToolsActive, useCompactSystemPrompt);
2978
+ const compactionSchemaTokens = buildContextBreakdown([], compactionTools).estimatedTotalTokens;
2910
2979
  const result = await compactMessagesWithSummary(messages, summarizeForCompaction, {
2911
2980
  budgetTokens: 0,
2912
2981
  keepRecent: AUTO_COMPACT_KEEP_RECENT,
2982
+ singlePassInputBudgetTokens: Math.max(0, compactionSinglePassInputBudget(contextLimitTokens ?? modelContextWindow(model, provider)) - compactionSchemaTokens),
2913
2983
  ...(durableEnvelope ? { durableEnvelope } : {}),
2914
2984
  });
2915
2985
  const summaryBody = result.messages.find((m) => isCompactionMemoryMessage(m))?.content ??
@@ -3096,6 +3166,7 @@ export async function runAgentTurn(prompt, options = {}) {
3096
3166
  }, 10_000);
3097
3167
  heartbeat.unref?.();
3098
3168
  const deferredToolCalls = [];
3169
+ const streamedNativeCallNames = new Map();
3099
3170
  const deltaParser = writesDirectly
3100
3171
  ? undefined
3101
3172
  : createThinkingStreamParser((text) => emit({ type: "assistant-delta", text }), (text) => {
@@ -3165,6 +3236,7 @@ export async function runAgentTurn(prompt, options = {}) {
3165
3236
  nativeToolsActive,
3166
3237
  toolsAttached,
3167
3238
  recoveryNudge: retryWithoutThinking,
3239
+ truncationDepth: truncatedBudgetRounds,
3168
3240
  }),
3169
3241
  });
3170
3242
  // Resume / mid-turn abort can leave orphan tool rows or a user
@@ -3175,10 +3247,11 @@ export async function runAgentTurn(prompt, options = {}) {
3175
3247
  repairToolProtocol(messages);
3176
3248
  assertValidToolProtocol(messages);
3177
3249
  // E3: adaptive completion budget (still large enough for writes).
3178
- const stepMaxTokens = resolveStepMaxTokens({
3250
+ stepMaxTokens = resolveStepMaxTokens({
3179
3251
  nativeToolsActive,
3180
3252
  toolsAttached,
3181
3253
  recoveryNudge: retryWithoutThinking,
3254
+ truncationDepth: truncatedBudgetRounds,
3182
3255
  });
3183
3256
  try {
3184
3257
  if (responderDelivery &&
@@ -3204,64 +3277,22 @@ export async function runAgentTurn(prompt, options = {}) {
3204
3277
  tools: turnTools,
3205
3278
  toolChoice: "auto",
3206
3279
  parallelToolCalls: true,
3207
- // P2-3: emit tool cards as soon as the function name arrives.
3208
3280
  onToolCallDelta: (delta) => {
3209
3281
  if (!delta.name)
3210
3282
  return;
3211
3283
  const name = fromWireName(delta.name) ?? delta.name;
3212
- const existing = deferredToolCalls[delta.index];
3213
- if (existing && existing.call.name !== "…") {
3214
- if (delta.argumentsBytes &&
3215
- delta.argumentsBytes >= 4096 &&
3216
- !writesDirectly) {
3217
- emit({
3218
- type: "status",
3219
- text: `${name} (${Math.round(delta.argumentsBytes / 1024)}KB args)`,
3220
- });
3221
- }
3222
- return;
3223
- }
3224
- while (deferredToolCalls.length < delta.index) {
3225
- const slot = deferredToolCalls.length;
3226
- const placeholderId = `tool-${++nextToolEventId}`;
3227
- callIds[slot] = placeholderId;
3228
- deferredToolCalls.push({
3229
- eventId: placeholderId,
3230
- call: { name: "…", args: {} },
3231
- rendered: "",
3232
- shown: false,
3233
- });
3234
- }
3235
- const call = normalizeToolCall({
3236
- name,
3237
- args: {},
3238
- });
3239
- const eventId = existing?.eventId ?? `tool-${++nextToolEventId}`;
3240
- callIds[delta.index] = eventId;
3241
- alreadyPrintedIds.add(eventId);
3242
- const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
3243
- chalk.gray(` ${formatToolArgs(call)}`);
3244
- const entry = {
3245
- eventId,
3246
- call,
3247
- rendered: styleToolChatter(call, toolCallLine) + "\n",
3248
- shown: false,
3249
- };
3250
- if (deferredToolCalls.length === delta.index) {
3251
- deferredToolCalls.push(entry);
3252
- }
3253
- else {
3254
- deferredToolCalls[delta.index] = entry;
3255
- }
3256
- streamedCallsCount = Math.max(streamedCallsCount, deferredToolCalls.length);
3284
+ streamedNativeCallNames.set(delta.index, name);
3257
3285
  if (!writesDirectly) {
3258
- writeToolCall(eventId, call, entry.rendered);
3259
- entry.shown = true;
3260
- emit({ type: "status", text: call.name });
3286
+ emit({
3287
+ type: "status",
3288
+ text: delta.argumentsBytes && delta.argumentsBytes >= 4096
3289
+ ? `${name} (${Math.round(delta.argumentsBytes / 1024)}KB args)`
3290
+ : name,
3291
+ });
3261
3292
  }
3262
3293
  else {
3263
3294
  spinner.stop();
3264
- spinner = startThinkingSpinner(`tool ${call.name}…`, options.signal);
3295
+ spinner = startThinkingSpinner(`tool ${name}…`, options.signal);
3265
3296
  }
3266
3297
  },
3267
3298
  }
@@ -3279,7 +3310,7 @@ export async function runAgentTurn(prompt, options = {}) {
3279
3310
  spinner.stop();
3280
3311
  }
3281
3312
  while (streamedCallsCount < parsedCalls.length) {
3282
- const call = parsedCalls[streamedCallsCount];
3313
+ const call = normalizeToolCall(parsedCalls[streamedCallsCount]);
3283
3314
  const eventId = `tool-${++nextToolEventId}`;
3284
3315
  callIds[streamedCallsCount] = eventId;
3285
3316
  alreadyPrintedIds.add(eventId);
@@ -3767,8 +3798,24 @@ export async function runAgentTurn(prompt, options = {}) {
3767
3798
  }
3768
3799
  }
3769
3800
  if (!canonicalAssistantVisible.trim() && !call) {
3801
+ const completionTokens = completion.usage?.completionTokens ?? 0;
3802
+ const hitOutputLimit = completion.finishReason === "length" ||
3803
+ (completionTokens > 0 && stepMaxTokens > 0 && completionTokens >= stepMaxTokens - 64);
3804
+ const truncatedRoundText = collapseRepeatedText(completion.text ?? "");
3805
+ if (hitOutputLimit && truncatedRoundText.trim() && truncatedBudgetRounds < 2) {
3806
+ truncatedBudgetRounds += 1;
3807
+ writeNotice("warn", "response hit the output token limit — continuing from where it stopped", chalk.yellow(" ⚠ response hit the output token limit — continuing from where it stopped\n"));
3808
+ messages.push({
3809
+ role: "assistant",
3810
+ content: sanitizeAssistantText(truncatedRoundText),
3811
+ });
3812
+ messages.push(recoveryUserMessage("Your previous response was cut off by the output token limit before it completed. " +
3813
+ "Continue directly from where it stopped — do not restart the analysis or repeat prior text. " +
3814
+ "Finish briefly: emit the next tool call, or the final answer if the task is complete."));
3815
+ continue;
3816
+ }
3770
3817
  const incompleteNativeStream = nativeToolCalls.length === 0 &&
3771
- deferredToolCalls.some((entry) => entry.shown && entry.call.name !== "…");
3818
+ streamedNativeCallNames.size > 0;
3772
3819
  if (incompleteNativeStream) {
3773
3820
  const reason = "The provider began this native tool call but never completed it. Nothing ran; reissue a complete call.";
3774
3821
  for (const deferred of deferredToolCalls) {
@@ -3811,6 +3858,7 @@ export async function runAgentTurn(prompt, options = {}) {
3811
3858
  else {
3812
3859
  // Reset the counter on any successful visible output or recovered call.
3813
3860
  emptyVisibleRetries = 0;
3861
+ truncatedBudgetRounds = 0;
3814
3862
  retryWithoutThinking = false;
3815
3863
  }
3816
3864
  let bareArgsOnly = false;
@@ -4180,13 +4228,14 @@ export async function runAgentTurn(prompt, options = {}) {
4180
4228
  let parsed = parseAllToolCalls(assistantText.visible || assistantText.thinkContent);
4181
4229
  if (parsed.length === 0 && call)
4182
4230
  parsed = [call];
4183
- bound = parsed.map((c, index) => {
4231
+ bound = parsed.map((rawCall, index) => {
4232
+ const call = normalizeToolCall(rawCall);
4184
4233
  const id = syntheticToolCallId(index);
4185
4234
  return {
4186
4235
  index,
4187
4236
  id,
4188
- call: c,
4189
- native: { id, name: c.name, args: c.args },
4237
+ call,
4238
+ native: { id, name: call.name, args: call.args },
4190
4239
  };
4191
4240
  });
4192
4241
  }
@@ -4356,7 +4405,7 @@ export async function runAgentTurn(prompt, options = {}) {
4356
4405
  for (const deferred of activeDeferredToolCalls.slice(0, allCalls.length)) {
4357
4406
  if (!deferred.call.name || deferred.call.name === "…")
4358
4407
  continue;
4359
- if (!deferred.shown || !writesDirectly) {
4408
+ if (!deferred.shown) {
4360
4409
  writeToolCall(deferred.eventId, deferred.call, deferred.rendered);
4361
4410
  deferred.shown = true;
4362
4411
  }