@pentoshi/clai 3.12.1 → 3.13.1

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/LICENSE +1 -1
  2. package/dist/agent/compaction-summary.d.ts +20 -12
  3. package/dist/agent/compaction-summary.js +72 -14
  4. package/dist/agent/compaction-summary.js.map +1 -1
  5. package/dist/agent/context-manager.d.ts +2 -0
  6. package/dist/agent/context-manager.js +64 -35
  7. package/dist/agent/context-manager.js.map +1 -1
  8. package/dist/agent/events.d.ts +1 -0
  9. package/dist/agent/message-slim.d.ts +6 -18
  10. package/dist/agent/message-slim.js +41 -23
  11. package/dist/agent/message-slim.js.map +1 -1
  12. package/dist/agent/runner.js +133 -97
  13. package/dist/agent/runner.js.map +1 -1
  14. package/dist/app/adapters/agent-event-adapter.js +1 -0
  15. package/dist/app/adapters/agent-event-adapter.js.map +1 -1
  16. package/dist/app/controllers/session-compact-helper.d.ts +3 -1
  17. package/dist/app/controllers/session-compact-helper.js +86 -42
  18. package/dist/app/controllers/session-compact-helper.js.map +1 -1
  19. package/dist/app/controllers/session-context-usage.js +0 -2
  20. package/dist/app/controllers/session-context-usage.js.map +1 -1
  21. package/dist/app/controllers/session-controller.js +3 -0
  22. package/dist/app/controllers/session-controller.js.map +1 -1
  23. package/dist/app/events/app-event.d.ts +1 -0
  24. package/dist/app/events/app-event.js.map +1 -1
  25. package/dist/prompts/embedded.js +1 -1
  26. package/dist/prompts/embedded.js.map +1 -1
  27. package/dist/prompts/index.js +3 -3
  28. package/dist/prompts/index.js.map +1 -1
  29. package/dist/prompts/system.agent.md +2 -2
  30. package/dist/tools/elevated-shell.d.ts +4 -29
  31. package/dist/tools/elevated-shell.js +46 -71
  32. package/dist/tools/elevated-shell.js.map +1 -1
  33. package/dist/tools/registry.js +47 -17
  34. package/dist/tools/registry.js.map +1 -1
  35. package/dist/tools/shell.d.ts +2 -14
  36. package/dist/tools/shell.js +19 -43
  37. package/dist/tools/shell.js.map +1 -1
  38. package/dist/tui/state.js +5 -5
  39. package/dist/tui/state.js.map +1 -1
  40. package/dist/tui-v2/components/status/status-line.d.ts +1 -0
  41. package/dist/tui-v2/components/status/status-line.js +62 -3
  42. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  43. package/dist/tui-v2/rendering/incremental-strip.js +1 -1
  44. package/dist/tui-v2/rendering/incremental-strip.js.map +1 -1
  45. package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +0 -8
  46. package/dist/tui-v2/rendering/strip-tool-surfaces.js +31 -17
  47. package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -1
  48. package/dist/tui-v2/state/transcript-compaction.js +1 -4
  49. package/dist/tui-v2/state/transcript-compaction.js.map +1 -1
  50. package/dist/tui-v2/state/transcript-reducer.js +3 -1
  51. package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
  52. package/dist/version.generated.d.ts +2 -2
  53. package/dist/version.generated.js +2 -2
  54. package/package.json +1 -1
@@ -1,33 +1,23 @@
1
- /**
2
- * Cap in-memory / re-sent message bulk from native toolCalls.
3
- *
4
- * fs.write / writeMany put entire file bodies in tool-call args. Those were
5
- * stored verbatim in history and LoopGuard signatures, so a scaffold turn
6
- * could hold many multi-MB strings — memory climbed into the multi-GB range
7
- * and Macs heated under GC. Full file bodies remain on disk; tool results
8
- * still report paths/bytes.
9
- */
10
1
  import { createHash } from "node:crypto";
11
- /** Strings at or above this are replaced with a length+hash stub. */
12
2
  export const SLIM_ARG_STRING_CHARS = 400;
13
- /** Hard ceiling for a single string kept as-is in history (safety). */
14
3
  export const SLIM_ARG_ABSOLUTE_MAX_CHARS = 8_000;
15
- /** Max depth when walking args trees (writeMany files[]). */
16
4
  const SLIM_MAX_DEPTH = 6;
5
+ const BULK_ARG_KEYS = new Set(["content", "body"]);
6
+ const ELIDED_STUB_PATTERN = /^«\d+ chars sha256=[0-9a-f]{12}(?:\s+—.*)?»$/s;
17
7
  function shortHash(text) {
18
8
  return createHash("sha256").update(text).digest("hex").slice(0, 12);
19
9
  }
20
- /**
21
- * Replace large string values with a compact stub so history/loop-guard
22
- * never retains full write payloads. Small strings and structure stay intact
23
- * so path-based identity still works for loop detection.
24
- */
25
- export function slimValue(value, depth = 0) {
10
+ function slimLimitForKey(key) {
11
+ return key !== undefined && BULK_ARG_KEYS.has(key)
12
+ ? SLIM_ARG_STRING_CHARS
13
+ : SLIM_ARG_ABSOLUTE_MAX_CHARS;
14
+ }
15
+ export function slimValue(value, depth = 0, key) {
26
16
  if (typeof value === "string") {
27
- if (value.length < SLIM_ARG_STRING_CHARS)
17
+ if (value.length < slimLimitForKey(key))
28
18
  return value;
29
19
  const hash = shortHash(value);
30
- return `«${value.length} chars sha256=${hash}»`;
20
+ return `«${value.length} chars sha256=${hash} — elided from history; never reuse this stub, regenerate the full value»`;
31
21
  }
32
22
  if (value === null || value === undefined)
33
23
  return value;
@@ -42,8 +32,8 @@ export function slimValue(value, depth = 0) {
42
32
  return value.map((entry) => slimValue(entry, depth + 1));
43
33
  }
44
34
  const out = {};
45
- for (const key of Object.keys(value).sort()) {
46
- out[key] = slimValue(value[key], depth + 1);
35
+ for (const childKey of Object.keys(value).sort()) {
36
+ out[childKey] = slimValue(value[childKey], depth + 1, childKey);
47
37
  }
48
38
  return out;
49
39
  }
@@ -54,7 +44,35 @@ export function slimToolArgs(args) {
54
44
  }
55
45
  return {};
56
46
  }
57
- /** Approximate UTF-16 code units for token budgeting (same basis as content). */
47
+ export function findElidedStubArg(value, path = "args", depth = 0) {
48
+ if (typeof value === "string") {
49
+ return ELIDED_STUB_PATTERN.test(value) ? { key: path, value } : undefined;
50
+ }
51
+ if (value === null || value === undefined || typeof value !== "object") {
52
+ return undefined;
53
+ }
54
+ if (depth >= SLIM_MAX_DEPTH)
55
+ return undefined;
56
+ if (Array.isArray(value)) {
57
+ for (let i = 0; i < value.length; i += 1) {
58
+ const found = findElidedStubArg(value[i], `${path}[${i}]`, depth + 1);
59
+ if (found)
60
+ return found;
61
+ }
62
+ return undefined;
63
+ }
64
+ for (const [childKey, childValue] of Object.entries(value)) {
65
+ const found = findElidedStubArg(childValue, path ? `${path}.${childKey}` : childKey, depth + 1);
66
+ if (found)
67
+ return found;
68
+ }
69
+ return undefined;
70
+ }
71
+ export function elidedStubReuseMessage(key) {
72
+ return (`Tool call rejected: argument "${key}" is an elided history placeholder («N chars sha256=…»), not a real value. ` +
73
+ "Compressed history replaces long arguments with those stubs and the original text cannot be recovered from them. " +
74
+ "Re-issue the tool call with the complete literal value — never copy «…» stubs from earlier context.");
75
+ }
58
76
  export function measureToolCallsChars(toolCalls) {
59
77
  if (!toolCalls?.length)
60
78
  return 0;
@@ -1 +1 @@
1
- {"version":3,"file":"message-slim.js","sourceRoot":"","sources":["../../src/agent/message-slim.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"message-slim.js","sourceRoot":"","sources":["../../src/agent/message-slim.ts"],"names":[],"mappings":"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"}
@@ -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";
@@ -38,6 +39,7 @@ function safeEngagementActionsForToolCall(call) {
38
39
  }
39
40
  import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
40
41
  import { getToolDefinitions, getCompactToolDefinitions, RUNNER_META_TOOL_NAMES, } from "../tools/definitions.js";
42
+ import { elidedStubReuseMessage, findElidedStubArg, } from "./message-slim.js";
41
43
  import { appendAssistantWithTools, ensureUniqueToolCallIds, toolCallIdsInHistory, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
42
44
  import { formatViewportHint, registerViewport } from "../ui/output-pane.js";
43
45
  import { compactMessagesWithSummary, shouldApplyAutoCompact, COMPACTION_MEMORY_PREFIX, PLAN_IMPLEMENT_MEMORY_PREFIX, isCompactionMemoryMessage, } from "./context-manager.js";
@@ -55,7 +57,7 @@ import { analyzeTask, formatTaskAnalysisHint, isNarrowExplicitNmapOperation, } f
55
57
  import { computeMaxIterations, computeStepBudget } from "./step-budget.js";
56
58
  import { isScratchOnlyWrite } from "./scratch-write.js";
57
59
  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";
60
+ import { buildCompactionRetryPrompt, compactionSinglePassInputBudget, COMPACTION_SYSTEM_PROMPT, COMPACTION_MAX_COMPLETION_TOKENS, COMPACTION_MAP_MAX_COMPLETION_TOKENS, isCompactionCompletionTruncated, looksLikeIncompleteCompactionSummary, looksLikeTranscriptReplay, normalizeCompactionSummary, } from "./compaction-summary.js";
59
61
  import { maybeAppendPlanModeReminder, PLAN_REMINDER_TOAST, } from "./plan-mode-reminders.js";
60
62
  import { LoopGuard } from "./loop-guard.js";
61
63
  import { appendInterruptedReasoning, interruptedReasoningBrief, isMeaningfulResumptionYield, } from "./interrupted-reasoning.js";
@@ -315,12 +317,21 @@ export async function runAgentTurn(prompt, options = {}) {
315
317
  process.stdout.write(chalk.dim(" ✦ Compacted Context · streaming Markdown\n\n"));
316
318
  }
317
319
  };
318
- const writeCompactionDelta = (id, text) => {
319
- if (!text)
320
+ const writeCompactionDelta = (id, text, replace = false) => {
321
+ if (!text && !replace)
320
322
  return;
321
- emit({ type: "compaction-delta", id, text });
322
- if (writesDirectly)
323
- process.stdout.write(text);
323
+ emit({
324
+ type: "compaction-delta",
325
+ id,
326
+ text,
327
+ ...(replace ? { replace: true } : {}),
328
+ });
329
+ if (writesDirectly) {
330
+ if (replace)
331
+ process.stdout.write(chalk.dim("\n ↻ rewriting compacted context\n\n"));
332
+ if (text)
333
+ process.stdout.write(text);
334
+ }
324
335
  };
325
336
  const writeCompactionCompleted = (id, summary, beforeTokens, afterTokens) => {
326
337
  emit({
@@ -1255,6 +1266,13 @@ export async function runAgentTurn(prompt, options = {}) {
1255
1266
  emitToolResult(toolEventId, result, reason);
1256
1267
  return { ok: false, call, result, contextOutput: reason };
1257
1268
  }
1269
+ const elidedStub = findElidedStubArg(call.args);
1270
+ if (elidedStub) {
1271
+ const reason = elidedStubReuseMessage(elidedStub.key);
1272
+ const result = { ok: false, output: reason, exitCode: 1 };
1273
+ emitToolResult(toolEventId, result, reason);
1274
+ return { ok: false, call, result, contextOutput: reason };
1275
+ }
1258
1276
  if (call.name === "image.ocr" && !imageOcrEnabled) {
1259
1277
  writeNotice("info", "skipped OCR because the original image is attached to the vision model", chalk.dim(" ℹ skipped OCR — inspecting the attached image directly\n"));
1260
1278
  const recoveryText = "The original image is attached to this message and you can inspect it directly. " +
@@ -2773,50 +2791,105 @@ export async function runAgentTurn(prompt, options = {}) {
2773
2791
  const summarizeForCompaction = async (summaryPrompt, stage) => {
2774
2792
  const streamFinalSummary = stage?.phase !== "map";
2775
2793
  const compactionId = streamFinalSummary ? activeCompactionId : undefined;
2776
- const deltaParser = compactionId
2777
- ? createThinkingStreamParser((text) => writeCompactionDelta(compactionId, text), undefined, { remember: false })
2794
+ const maxTokens = stage?.phase === "map"
2795
+ ? COMPACTION_MAP_MAX_COMPLETION_TOKENS
2796
+ : COMPACTION_MAX_COMPLETION_TOKENS;
2797
+ const sourceMessages = stage?.sourceMessages;
2798
+ const compactionTools = sourceMessages
2799
+ ? selectToolDefs(nativeToolsActive, useCompactSystemPrompt)
2778
2800
  : undefined;
2779
2801
  const request = {
2780
2802
  provider,
2781
2803
  model,
2782
- messages: [
2783
- { role: "system", content: COMPACTION_SYSTEM_PROMPT },
2784
- { role: "user", content: summaryPrompt },
2785
- ],
2804
+ messages: sourceMessages
2805
+ ? [
2806
+ ...sourceMessages,
2807
+ { role: "user", content: summaryPrompt },
2808
+ ]
2809
+ : [
2810
+ { role: "system", content: COMPACTION_SYSTEM_PROMPT },
2811
+ { role: "user", content: summaryPrompt },
2812
+ ],
2786
2813
  temperature: 0.1,
2787
- maxTokens: stage?.phase === "map"
2788
- ? COMPACTION_MAP_MAX_COMPLETION_TOKENS
2789
- : COMPACTION_MAX_COMPLETION_TOKENS,
2814
+ maxTokens,
2790
2815
  thinking: { enabled: false, effort: "none" },
2791
2816
  signal: options.signal,
2792
2817
  allowModelFallback: true,
2818
+ ...(compactionTools?.length
2819
+ ? {
2820
+ tools: compactionTools,
2821
+ toolChoice: "none",
2822
+ }
2823
+ : {}),
2793
2824
  };
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);
2825
+ const runAttempt = async (attemptRequest, replace = false) => {
2826
+ if (compactionId && replace) {
2827
+ writeCompactionDelta(compactionId, "", true);
2828
+ }
2829
+ const parser = createThinkingStreamParser((text) => {
2830
+ if (compactionId)
2831
+ writeCompactionDelta(compactionId, text);
2832
+ }, undefined, { remember: false });
2833
+ const result = await streamWithProvider(attemptRequest, (token) => parser.push(token), { onStatus: () => undefined, maxRetries: 0 });
2834
+ parser.finish();
2835
+ return result;
2836
+ };
2837
+ const first = await runAttempt(request);
2838
+ let visible = normalizeCompactionSummary(stripThinking(first.text).visible);
2839
+ let retryReason;
2840
+ if (isCompactionCompletionTruncated(first, maxTokens)) {
2841
+ retryReason = "truncated";
2842
+ }
2843
+ else if (!visible) {
2844
+ retryReason = "reasoning-only";
2845
+ }
2846
+ else if (looksLikeTranscriptReplay(visible)) {
2847
+ retryReason = "replayed";
2848
+ }
2849
+ else if (looksLikeIncompleteCompactionSummary(visible)) {
2850
+ retryReason = "incomplete";
2851
+ }
2852
+ if (retryReason) {
2853
+ const retry = await runAttempt({
2854
+ ...request,
2855
+ messages: sourceMessages
2856
+ ? [
2857
+ ...sourceMessages,
2858
+ {
2859
+ role: "user",
2860
+ content: buildCompactionRetryPrompt(summaryPrompt, retryReason),
2861
+ },
2862
+ ]
2863
+ : [
2864
+ {
2865
+ role: "system",
2866
+ content: `${COMPACTION_SYSTEM_PROMPT}\nReturn only a complete continuation-memory summary. Do not include analysis, reasoning, or <think> tags.`,
2867
+ },
2868
+ {
2869
+ role: "user",
2870
+ content: buildCompactionRetryPrompt(summaryPrompt, retryReason),
2871
+ },
2872
+ ],
2873
+ temperature: 0,
2874
+ maxTokens,
2875
+ thinking: { enabled: false, effort: "none" },
2876
+ allowModelFallback: true,
2877
+ }, true);
2878
+ if (isCompactionCompletionTruncated(retry, maxTokens)) {
2879
+ throw new Error("compaction failed: model hit the summary output limit twice — original context retained");
2880
+ }
2881
+ visible = normalizeCompactionSummary(stripThinking(retry.text).visible);
2882
+ if (!visible) {
2883
+ throw new Error("compaction failed: model returned an empty summary");
2884
+ }
2885
+ if (looksLikeTranscriptReplay(visible)) {
2886
+ throw new Error("compaction failed: model replayed the transcript twice — original context retained");
2887
+ }
2888
+ if (looksLikeIncompleteCompactionSummary(visible)) {
2889
+ throw new Error("compaction failed: model returned an incomplete summary twice — original context retained");
2890
+ }
2818
2891
  }
2819
- return retry.text;
2892
+ return visible;
2820
2893
  };
2821
2894
  /**
2822
2895
  * Estimate the complete next model request, including attached native-tool
@@ -2909,9 +2982,12 @@ export async function runAgentTurn(prompt, options = {}) {
2909
2982
  activeCompactionId = compactionId;
2910
2983
  writeCompactionStarted(compactionId, beforeTokens);
2911
2984
  try {
2985
+ const compactionTools = selectToolDefs(nativeToolsActive, useCompactSystemPrompt);
2986
+ const compactionSchemaTokens = buildContextBreakdown([], compactionTools).estimatedTotalTokens;
2912
2987
  const result = await compactMessagesWithSummary(messages, summarizeForCompaction, {
2913
2988
  budgetTokens: 0,
2914
2989
  keepRecent: AUTO_COMPACT_KEEP_RECENT,
2990
+ singlePassInputBudgetTokens: Math.max(0, compactionSinglePassInputBudget(contextLimitTokens ?? modelContextWindow(model, provider)) - compactionSchemaTokens),
2915
2991
  ...(durableEnvelope ? { durableEnvelope } : {}),
2916
2992
  });
2917
2993
  const summaryBody = result.messages.find((m) => isCompactionMemoryMessage(m))?.content ??
@@ -3098,6 +3174,7 @@ export async function runAgentTurn(prompt, options = {}) {
3098
3174
  }, 10_000);
3099
3175
  heartbeat.unref?.();
3100
3176
  const deferredToolCalls = [];
3177
+ const streamedNativeCallNames = new Map();
3101
3178
  const deltaParser = writesDirectly
3102
3179
  ? undefined
3103
3180
  : createThinkingStreamParser((text) => emit({ type: "assistant-delta", text }), (text) => {
@@ -3208,64 +3285,22 @@ export async function runAgentTurn(prompt, options = {}) {
3208
3285
  tools: turnTools,
3209
3286
  toolChoice: "auto",
3210
3287
  parallelToolCalls: true,
3211
- // P2-3: emit tool cards as soon as the function name arrives.
3212
3288
  onToolCallDelta: (delta) => {
3213
3289
  if (!delta.name)
3214
3290
  return;
3215
3291
  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);
3292
+ streamedNativeCallNames.set(delta.index, name);
3261
3293
  if (!writesDirectly) {
3262
- writeToolCall(eventId, call, entry.rendered);
3263
- entry.shown = true;
3264
- emit({ type: "status", text: call.name });
3294
+ emit({
3295
+ type: "status",
3296
+ text: delta.argumentsBytes && delta.argumentsBytes >= 4096
3297
+ ? `${name} (${Math.round(delta.argumentsBytes / 1024)}KB args)`
3298
+ : name,
3299
+ });
3265
3300
  }
3266
3301
  else {
3267
3302
  spinner.stop();
3268
- spinner = startThinkingSpinner(`tool ${call.name}…`, options.signal);
3303
+ spinner = startThinkingSpinner(`tool ${name}…`, options.signal);
3269
3304
  }
3270
3305
  },
3271
3306
  }
@@ -3283,7 +3318,7 @@ export async function runAgentTurn(prompt, options = {}) {
3283
3318
  spinner.stop();
3284
3319
  }
3285
3320
  while (streamedCallsCount < parsedCalls.length) {
3286
- const call = parsedCalls[streamedCallsCount];
3321
+ const call = normalizeToolCall(parsedCalls[streamedCallsCount]);
3287
3322
  const eventId = `tool-${++nextToolEventId}`;
3288
3323
  callIds[streamedCallsCount] = eventId;
3289
3324
  alreadyPrintedIds.add(eventId);
@@ -3788,7 +3823,7 @@ export async function runAgentTurn(prompt, options = {}) {
3788
3823
  continue;
3789
3824
  }
3790
3825
  const incompleteNativeStream = nativeToolCalls.length === 0 &&
3791
- deferredToolCalls.some((entry) => entry.shown && entry.call.name !== "…");
3826
+ streamedNativeCallNames.size > 0;
3792
3827
  if (incompleteNativeStream) {
3793
3828
  const reason = "The provider began this native tool call but never completed it. Nothing ran; reissue a complete call.";
3794
3829
  for (const deferred of deferredToolCalls) {
@@ -4201,13 +4236,14 @@ export async function runAgentTurn(prompt, options = {}) {
4201
4236
  let parsed = parseAllToolCalls(assistantText.visible || assistantText.thinkContent);
4202
4237
  if (parsed.length === 0 && call)
4203
4238
  parsed = [call];
4204
- bound = parsed.map((c, index) => {
4239
+ bound = parsed.map((rawCall, index) => {
4240
+ const call = normalizeToolCall(rawCall);
4205
4241
  const id = syntheticToolCallId(index);
4206
4242
  return {
4207
4243
  index,
4208
4244
  id,
4209
- call: c,
4210
- native: { id, name: c.name, args: c.args },
4245
+ call,
4246
+ native: { id, name: call.name, args: call.args },
4211
4247
  };
4212
4248
  });
4213
4249
  }
@@ -4377,7 +4413,7 @@ export async function runAgentTurn(prompt, options = {}) {
4377
4413
  for (const deferred of activeDeferredToolCalls.slice(0, allCalls.length)) {
4378
4414
  if (!deferred.call.name || deferred.call.name === "…")
4379
4415
  continue;
4380
- if (!deferred.shown || !writesDirectly) {
4416
+ if (!deferred.shown) {
4381
4417
  writeToolCall(deferred.eventId, deferred.call, deferred.rendered);
4382
4418
  deferred.shown = true;
4383
4419
  }