@pentoshi/clai 3.12.1 → 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 (39) 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/runner.js +125 -97
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/app/adapters/agent-event-adapter.js +1 -0
  11. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  12. package/dist/app/controllers/session-compact-helper.d.ts +3 -1
  13. package/dist/app/controllers/session-compact-helper.js +86 -42
  14. package/dist/app/controllers/session-compact-helper.js.map +1 -1
  15. package/dist/app/controllers/session-context-usage.js +0 -2
  16. package/dist/app/controllers/session-context-usage.js.map +1 -1
  17. package/dist/app/controllers/session-controller.js +3 -0
  18. package/dist/app/controllers/session-controller.js.map +1 -1
  19. package/dist/app/events/app-event.d.ts +1 -0
  20. package/dist/app/events/app-event.js.map +1 -1
  21. package/dist/tools/registry.js +33 -13
  22. package/dist/tools/registry.js.map +1 -1
  23. package/dist/tui/state.js +5 -5
  24. package/dist/tui/state.js.map +1 -1
  25. package/dist/tui-v2/components/status/status-line.d.ts +1 -0
  26. package/dist/tui-v2/components/status/status-line.js +62 -3
  27. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  28. package/dist/tui-v2/rendering/incremental-strip.js +1 -1
  29. package/dist/tui-v2/rendering/incremental-strip.js.map +1 -1
  30. package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +0 -8
  31. package/dist/tui-v2/rendering/strip-tool-surfaces.js +31 -17
  32. package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -1
  33. package/dist/tui-v2/state/transcript-compaction.js +1 -4
  34. package/dist/tui-v2/state/transcript-compaction.js.map +1 -1
  35. package/dist/tui-v2/state/transcript-reducer.js +3 -1
  36. package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
  37. package/dist/version.generated.d.ts +2 -2
  38. package/dist/version.generated.js +2 -2
  39. 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({
@@ -2773,50 +2783,105 @@ export async function runAgentTurn(prompt, options = {}) {
2773
2783
  const summarizeForCompaction = async (summaryPrompt, stage) => {
2774
2784
  const streamFinalSummary = stage?.phase !== "map";
2775
2785
  const compactionId = streamFinalSummary ? activeCompactionId : undefined;
2776
- const deltaParser = compactionId
2777
- ? 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)
2778
2792
  : undefined;
2779
2793
  const request = {
2780
2794
  provider,
2781
2795
  model,
2782
- messages: [
2783
- { role: "system", content: COMPACTION_SYSTEM_PROMPT },
2784
- { role: "user", content: summaryPrompt },
2785
- ],
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
+ ],
2786
2805
  temperature: 0.1,
2787
- maxTokens: stage?.phase === "map"
2788
- ? COMPACTION_MAP_MAX_COMPLETION_TOKENS
2789
- : COMPACTION_MAX_COMPLETION_TOKENS,
2806
+ maxTokens,
2790
2807
  thinking: { enabled: false, effort: "none" },
2791
2808
  signal: options.signal,
2792
2809
  allowModelFallback: true,
2810
+ ...(compactionTools?.length
2811
+ ? {
2812
+ tools: compactionTools,
2813
+ toolChoice: "none",
2814
+ }
2815
+ : {}),
2793
2816
  };
2794
- const response = await streamWithProvider(request, (token) => deltaParser?.push(token), { onStatus: () => undefined, maxRetries: 0 });
2795
- deltaParser?.finish();
2796
- const parsed = stripThinking(response.text);
2797
- if (parsed.visible.trim() || !parsed.hasThinking)
2798
- return response.text;
2799
- const retry = await completeWithProvider({
2800
- ...request,
2801
- messages: [
2802
- {
2803
- role: "system",
2804
- content: `${COMPACTION_SYSTEM_PROMPT}\nReturn only the continuation-memory summary. Do not include analysis, reasoning, or <think> tags.`,
2805
- },
2806
- { role: "user", content: summaryPrompt },
2807
- ],
2808
- temperature: 0,
2809
- maxTokens: stage?.phase === "map"
2810
- ? COMPACTION_MAP_MAX_COMPLETION_TOKENS
2811
- : COMPACTION_MAX_COMPLETION_TOKENS,
2812
- thinking: { enabled: false, effort: "none" },
2813
- allowModelFallback: true,
2814
- }, { maxRetries: 0 });
2815
- const retryVisible = stripThinking(retry.text).visible.trim();
2816
- if (retryVisible && compactionId) {
2817
- 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
+ }
2818
2883
  }
2819
- return retry.text;
2884
+ return visible;
2820
2885
  };
2821
2886
  /**
2822
2887
  * Estimate the complete next model request, including attached native-tool
@@ -2909,9 +2974,12 @@ export async function runAgentTurn(prompt, options = {}) {
2909
2974
  activeCompactionId = compactionId;
2910
2975
  writeCompactionStarted(compactionId, beforeTokens);
2911
2976
  try {
2977
+ const compactionTools = selectToolDefs(nativeToolsActive, useCompactSystemPrompt);
2978
+ const compactionSchemaTokens = buildContextBreakdown([], compactionTools).estimatedTotalTokens;
2912
2979
  const result = await compactMessagesWithSummary(messages, summarizeForCompaction, {
2913
2980
  budgetTokens: 0,
2914
2981
  keepRecent: AUTO_COMPACT_KEEP_RECENT,
2982
+ singlePassInputBudgetTokens: Math.max(0, compactionSinglePassInputBudget(contextLimitTokens ?? modelContextWindow(model, provider)) - compactionSchemaTokens),
2915
2983
  ...(durableEnvelope ? { durableEnvelope } : {}),
2916
2984
  });
2917
2985
  const summaryBody = result.messages.find((m) => isCompactionMemoryMessage(m))?.content ??
@@ -3098,6 +3166,7 @@ export async function runAgentTurn(prompt, options = {}) {
3098
3166
  }, 10_000);
3099
3167
  heartbeat.unref?.();
3100
3168
  const deferredToolCalls = [];
3169
+ const streamedNativeCallNames = new Map();
3101
3170
  const deltaParser = writesDirectly
3102
3171
  ? undefined
3103
3172
  : createThinkingStreamParser((text) => emit({ type: "assistant-delta", text }), (text) => {
@@ -3208,64 +3277,22 @@ export async function runAgentTurn(prompt, options = {}) {
3208
3277
  tools: turnTools,
3209
3278
  toolChoice: "auto",
3210
3279
  parallelToolCalls: true,
3211
- // P2-3: emit tool cards as soon as the function name arrives.
3212
3280
  onToolCallDelta: (delta) => {
3213
3281
  if (!delta.name)
3214
3282
  return;
3215
3283
  const name = fromWireName(delta.name) ?? delta.name;
3216
- const existing = deferredToolCalls[delta.index];
3217
- if (existing && existing.call.name !== "…") {
3218
- if (delta.argumentsBytes &&
3219
- delta.argumentsBytes >= 4096 &&
3220
- !writesDirectly) {
3221
- emit({
3222
- type: "status",
3223
- text: `${name} (${Math.round(delta.argumentsBytes / 1024)}KB args)`,
3224
- });
3225
- }
3226
- return;
3227
- }
3228
- while (deferredToolCalls.length < delta.index) {
3229
- const slot = deferredToolCalls.length;
3230
- const placeholderId = `tool-${++nextToolEventId}`;
3231
- callIds[slot] = placeholderId;
3232
- deferredToolCalls.push({
3233
- eventId: placeholderId,
3234
- call: { name: "…", args: {} },
3235
- rendered: "",
3236
- shown: false,
3237
- });
3238
- }
3239
- const call = normalizeToolCall({
3240
- name,
3241
- args: {},
3242
- });
3243
- const eventId = existing?.eventId ?? `tool-${++nextToolEventId}`;
3244
- callIds[delta.index] = eventId;
3245
- alreadyPrintedIds.add(eventId);
3246
- const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
3247
- chalk.gray(` ${formatToolArgs(call)}`);
3248
- const entry = {
3249
- eventId,
3250
- call,
3251
- rendered: styleToolChatter(call, toolCallLine) + "\n",
3252
- shown: false,
3253
- };
3254
- if (deferredToolCalls.length === delta.index) {
3255
- deferredToolCalls.push(entry);
3256
- }
3257
- else {
3258
- deferredToolCalls[delta.index] = entry;
3259
- }
3260
- streamedCallsCount = Math.max(streamedCallsCount, deferredToolCalls.length);
3284
+ streamedNativeCallNames.set(delta.index, name);
3261
3285
  if (!writesDirectly) {
3262
- writeToolCall(eventId, call, entry.rendered);
3263
- entry.shown = true;
3264
- 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
+ });
3265
3292
  }
3266
3293
  else {
3267
3294
  spinner.stop();
3268
- spinner = startThinkingSpinner(`tool ${call.name}…`, options.signal);
3295
+ spinner = startThinkingSpinner(`tool ${name}…`, options.signal);
3269
3296
  }
3270
3297
  },
3271
3298
  }
@@ -3283,7 +3310,7 @@ export async function runAgentTurn(prompt, options = {}) {
3283
3310
  spinner.stop();
3284
3311
  }
3285
3312
  while (streamedCallsCount < parsedCalls.length) {
3286
- const call = parsedCalls[streamedCallsCount];
3313
+ const call = normalizeToolCall(parsedCalls[streamedCallsCount]);
3287
3314
  const eventId = `tool-${++nextToolEventId}`;
3288
3315
  callIds[streamedCallsCount] = eventId;
3289
3316
  alreadyPrintedIds.add(eventId);
@@ -3788,7 +3815,7 @@ export async function runAgentTurn(prompt, options = {}) {
3788
3815
  continue;
3789
3816
  }
3790
3817
  const incompleteNativeStream = nativeToolCalls.length === 0 &&
3791
- deferredToolCalls.some((entry) => entry.shown && entry.call.name !== "…");
3818
+ streamedNativeCallNames.size > 0;
3792
3819
  if (incompleteNativeStream) {
3793
3820
  const reason = "The provider began this native tool call but never completed it. Nothing ran; reissue a complete call.";
3794
3821
  for (const deferred of deferredToolCalls) {
@@ -4201,13 +4228,14 @@ export async function runAgentTurn(prompt, options = {}) {
4201
4228
  let parsed = parseAllToolCalls(assistantText.visible || assistantText.thinkContent);
4202
4229
  if (parsed.length === 0 && call)
4203
4230
  parsed = [call];
4204
- bound = parsed.map((c, index) => {
4231
+ bound = parsed.map((rawCall, index) => {
4232
+ const call = normalizeToolCall(rawCall);
4205
4233
  const id = syntheticToolCallId(index);
4206
4234
  return {
4207
4235
  index,
4208
4236
  id,
4209
- call: c,
4210
- native: { id, name: c.name, args: c.args },
4237
+ call,
4238
+ native: { id, name: call.name, args: call.args },
4211
4239
  };
4212
4240
  });
4213
4241
  }
@@ -4377,7 +4405,7 @@ export async function runAgentTurn(prompt, options = {}) {
4377
4405
  for (const deferred of activeDeferredToolCalls.slice(0, allCalls.length)) {
4378
4406
  if (!deferred.call.name || deferred.call.name === "…")
4379
4407
  continue;
4380
- if (!deferred.shown || !writesDirectly) {
4408
+ if (!deferred.shown) {
4381
4409
  writeToolCall(deferred.eventId, deferred.call, deferred.rendered);
4382
4410
  deferred.shown = true;
4383
4411
  }