@bubblebrain-ai/bubble 0.0.16 → 0.0.18

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 (62) hide show
  1. package/dist/agent/internal-reminder-sanitizer.d.ts +2 -0
  2. package/dist/agent/internal-reminder-sanitizer.js +27 -0
  3. package/dist/agent/tool-intent.js +0 -1
  4. package/dist/agent.d.ts +1 -0
  5. package/dist/agent.js +148 -23
  6. package/dist/context/budget.js +15 -0
  7. package/dist/context/prune.d.ts +1 -0
  8. package/dist/context/prune.js +32 -0
  9. package/dist/debug-trace.js +14 -0
  10. package/dist/feishu/agent-host/run-driver.js +2 -2
  11. package/dist/feishu/card/run-state.js +1 -0
  12. package/dist/feishu/serve.js +1 -0
  13. package/dist/main.js +13 -9
  14. package/dist/model-catalog.d.ts +3 -0
  15. package/dist/model-catalog.js +38 -0
  16. package/dist/model-config.d.ts +3 -0
  17. package/dist/model-config.js +3 -0
  18. package/dist/model-pricing.js +2 -1
  19. package/dist/model-selection.d.ts +7 -0
  20. package/dist/model-selection.js +9 -0
  21. package/dist/network/chatgpt-transport.js +1 -0
  22. package/dist/orchestrator/default-hooks.js +1 -1
  23. package/dist/prompt/compose.js +1 -1
  24. package/dist/prompt/environment.js +1 -3
  25. package/dist/prompt/reminders.js +3 -3
  26. package/dist/prompt/runtime.js +2 -1
  27. package/dist/provider-anthropic.d.ts +89 -0
  28. package/dist/provider-anthropic.js +597 -0
  29. package/dist/provider-openai-codex.js +3 -1
  30. package/dist/provider-registry.d.ts +2 -0
  31. package/dist/provider-registry.js +29 -3
  32. package/dist/provider-transform.d.ts +1 -1
  33. package/dist/provider-transform.js +14 -0
  34. package/dist/provider.d.ts +4 -1
  35. package/dist/provider.js +120 -41
  36. package/dist/session-log.js +14 -2
  37. package/dist/session-title.js +3 -6
  38. package/dist/slash-commands/commands.js +8 -2
  39. package/dist/stats/usage.d.ts +1 -0
  40. package/dist/stats/usage.js +28 -3
  41. package/dist/tools/edit.js +75 -1
  42. package/dist/tools/glob.js +77 -12
  43. package/dist/tools/index.d.ts +1 -1
  44. package/dist/tools/index.js +1 -3
  45. package/dist/tools/prompt-metadata.d.ts +3 -0
  46. package/dist/tools/prompt-metadata.js +17 -0
  47. package/dist/tools/write.js +14 -0
  48. package/dist/tui/paste-placeholder.d.ts +10 -0
  49. package/dist/tui/paste-placeholder.js +45 -0
  50. package/dist/tui/run.js +23 -0
  51. package/dist/tui-ink/app.js +2 -0
  52. package/dist/tui-ink/input-box.d.ts +1 -8
  53. package/dist/tui-ink/input-box.js +8 -38
  54. package/dist/tui-opentui/app.js +2 -0
  55. package/dist/tui-opentui/input-box.d.ts +1 -3
  56. package/dist/tui-opentui/input-box.js +17 -26
  57. package/dist/types.d.ts +22 -0
  58. package/package.json +7 -3
  59. package/dist/tools/apply-patch.d.ts +0 -9
  60. package/dist/tools/apply-patch.js +0 -330
  61. package/dist/tools/patch-apply.d.ts +0 -41
  62. package/dist/tools/patch-apply.js +0 -312
@@ -1,6 +1,8 @@
1
+ import type { AssistantProviderMetadata } from "../types.js";
1
2
  export declare function formatInternalReminderBlock(kind: string, content: string): string;
2
3
  export declare function formatInternalContextBlock(kind: string, content: string): string;
3
4
  export declare function sanitizeInternalReminderBlocks(text: string): string;
5
+ export declare function sanitizeAssistantProviderMetadata(metadata: AssistantProviderMetadata | undefined): AssistantProviderMetadata | undefined;
4
6
  export declare function createStreamingInternalReminderSanitizer(): {
5
7
  push(delta: string): string;
6
8
  flush(): string;
@@ -37,6 +37,33 @@ export function sanitizeInternalReminderBlocks(text) {
37
37
  const sanitizer = createStreamingInternalReminderSanitizer();
38
38
  return sanitizer.push(text) + sanitizer.flush();
39
39
  }
40
+ export function sanitizeAssistantProviderMetadata(metadata) {
41
+ const anthropic = metadata?.anthropic;
42
+ const blocks = anthropic?.contentBlocks;
43
+ if (!metadata || !anthropic || !blocks?.length)
44
+ return metadata;
45
+ let changed = false;
46
+ const sanitizedBlocks = blocks.map((block) => {
47
+ if (block.type !== "text" || typeof block.text !== "string") {
48
+ return block;
49
+ }
50
+ const sanitizedText = sanitizeInternalReminderBlocks(block.text);
51
+ if (sanitizedText === block.text) {
52
+ return block;
53
+ }
54
+ changed = true;
55
+ return { ...block, text: sanitizedText };
56
+ });
57
+ if (!changed)
58
+ return metadata;
59
+ return {
60
+ ...metadata,
61
+ anthropic: {
62
+ ...anthropic,
63
+ contentBlocks: sanitizedBlocks,
64
+ },
65
+ };
66
+ }
40
67
  export function createStreamingInternalReminderSanitizer() {
41
68
  let pending = "";
42
69
  const drain = (final) => {
@@ -55,7 +55,6 @@ export function analyzeToolIntent(toolCall) {
55
55
  case "write":
56
56
  return { family: "write" };
57
57
  case "edit":
58
- case "apply_patch":
59
58
  return { family: "edit" };
60
59
  case "web_search":
61
60
  case "web_fetch":
package/dist/agent.d.ts CHANGED
@@ -87,6 +87,7 @@ export declare class Agent {
87
87
  unlockDeferredTools(names: string[]): void;
88
88
  /** All deferred tools in this session (for tool_search to inspect). */
89
89
  listDeferredTools(): ToolRegistryEntry[];
90
+ getSystemPromptToolOptions(): Pick<import("./system-prompt.js").SystemPromptOptions, "tools" | "toolSnippets" | "guidelines">;
90
91
  getContextUsageSnapshot(): ContextUsageSnapshot;
91
92
  resetContextUsageAnchor(): void;
92
93
  /** Whether a given tool is deferred and not yet unlocked. */
package/dist/agent.js CHANGED
@@ -5,13 +5,13 @@
5
5
  import { compactMessages } from "./context/compact.js";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { compactMessagesWithLLM } from "./context/compact-llm.js";
8
- import { getContextBudget } from "./context/budget.js";
8
+ import { estimateContextTokens, getContextBudget } from "./context/budget.js";
9
9
  import { buildContextUsageSnapshot } from "./context/usage.js";
10
10
  import { isContextOverflowError } from "./context/overflow.js";
11
11
  import { projectMessages } from "./context/projector.js";
12
- import { aggressivePruneMessages } from "./context/prune.js";
12
+ import { aggressivePruneMessages, markStableCurrentToolResultsForCache } from "./context/prune.js";
13
13
  import { truncateToolOutputForModel } from "./context/tool-output-truncate.js";
14
- import { buildDeferredToolsReminder, buildToolFreezeReminder, isPermissionModeReminder, reminderForMode } from "./prompt/reminders.js";
14
+ import { buildDeferredToolsReminder, buildToolFreezeReminder, reminderForMode } from "./prompt/reminders.js";
15
15
  import { HookBus } from "./orchestrator/hooks.js";
16
16
  import { createDefaultHooks } from "./orchestrator/default-hooks.js";
17
17
  import { resolveModelRoute, resolveSubagentRoute } from "./agent/categories.js";
@@ -20,10 +20,11 @@ import { composeAbortSignals } from "./agent/budget-ledger.js";
20
20
  import { assignAgentNickname, builtinAgentProfiles, mergeUsage, selectToolsForAgentProfile, validateAgentProfileTools } from "./agent/profiles.js";
21
21
  import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
22
22
  import { isHiddenToolResult } from "./agent/discovery-barrier.js";
23
- import { createStreamingInternalReminderSanitizer, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
23
+ import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMetadata, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
24
24
  import { buildSystemPrompt } from "./system-prompt.js";
25
25
  import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
26
26
  import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
27
+ import { buildToolPromptOptions } from "./tools/prompt-metadata.js";
27
28
  import { stopAutoServersForSession } from "./tools/server-manager.js";
28
29
  import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceMessage, summarizeTraceToolResult, summarizeTraceValue, traceEvent, } from "./debug-trace.js";
29
30
  const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
@@ -31,7 +32,6 @@ const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
31
32
  const RESIDENT_HISTORY_MESSAGE_LIMIT = 160;
32
33
  const RESIDENT_HISTORY_CHAR_SOFT_LIMIT = 256 * 1024;
33
34
  const RESIDENT_HISTORY_CHAR_HARD_LIMIT = 512 * 1024;
34
- const RESIDENT_HISTORY_HEAP_SOFT_LIMIT = 512 * 1024 * 1024;
35
35
  const RESIDENT_HISTORY_HEAP_HARD_LIMIT = 768 * 1024 * 1024;
36
36
  const MAX_EMPTY_ASSISTANT_RECOVERIES = 1;
37
37
  const EMPTY_ASSISTANT_RECOVERY_REMINDER = "The previous model response contained no user-visible assistant content and no tool calls. " +
@@ -131,6 +131,9 @@ export class Agent {
131
131
  listDeferredTools() {
132
132
  return [...this.tools.values()].filter((t) => t.deferred);
133
133
  }
134
+ getSystemPromptToolOptions() {
135
+ return buildToolPromptOptions(this.getActiveToolEntries());
136
+ }
134
137
  getContextUsageSnapshot() {
135
138
  return buildContextUsageSnapshot({
136
139
  providerId: this.providerId,
@@ -153,17 +156,20 @@ export class Agent {
153
156
  }
154
157
  getActiveToolEntries() {
155
158
  return [...this.tools.values()]
156
- .filter((tool) => !tool.deferred || this.unlockedDeferred.has(tool.name))
157
- .filter((tool) => this._mode === "plan" || tool.name !== "exit_plan_mode");
159
+ .filter((tool) => !tool.deferred || this.unlockedDeferred.has(tool.name));
158
160
  }
159
161
  injectSystemReminder(content) {
160
162
  this.appendMessage({ role: "meta", kind: "system-reminder", content });
161
163
  }
162
164
  injectModeReminder() {
163
- this.messages = this.messages.filter((message) => !(message.role === "meta"
164
- && message.kind === "system-reminder"
165
- && isPermissionModeReminder(message.content)));
166
- this.injectSystemReminder(reminderForMode(this._mode));
165
+ const reminder = reminderForMode(this._mode);
166
+ const last = this.messages.at(-1);
167
+ if (last?.role === "meta"
168
+ && last.kind === "system-reminder"
169
+ && last.content === reminder) {
170
+ return;
171
+ }
172
+ this.injectSystemReminder(reminder);
167
173
  }
168
174
  get model() {
169
175
  return this._model;
@@ -376,6 +382,7 @@ export class Agent {
376
382
  modelId: this.apiModel,
377
383
  };
378
384
  const streamingToolCalls = new Map();
385
+ const textSanitizer = createStreamingInternalReminderSanitizer();
379
386
  const reasoningSanitizer = createStreamingInternalReminderSanitizer();
380
387
  let turnUsage;
381
388
  let assistantAppended = false;
@@ -397,11 +404,9 @@ export class Agent {
397
404
  };
398
405
  await hookBus.runBeforeModelCall(beforeModelCallCtx);
399
406
  toolEntries = beforeModelCallCtx.toolEntries;
400
- if (this._mode !== "plan") {
401
- toolEntries = toolEntries.filter((t) => t.name !== "exit_plan_mode");
402
- }
403
407
  flushGovernorReminders();
404
- const toolDefinitions = ((hookState.forceTextOnlyReason ? [] : toolEntries))
408
+ const textOnly = !!hookState.forceTextOnlyReason;
409
+ const toolDefinitions = toolEntries
405
410
  .map((t) => ({
406
411
  name: t.name,
407
412
  description: t.description,
@@ -416,6 +421,7 @@ export class Agent {
416
421
  const bufferedStreamingToolCallIds = new Set();
417
422
  const discoveryBarrier = hookState.discoveryBarrier;
418
423
  try {
424
+ markStableCurrentToolResultsForCache(this.messages);
419
425
  const projectedMessages = projectMessages(this.messages, {
420
426
  mode: "budgeted",
421
427
  providerId: this.providerId,
@@ -433,10 +439,12 @@ export class Agent {
433
439
  toolCount: toolDefinitions.length,
434
440
  thinkingLevel: this.thinkingLevel,
435
441
  mode: this._mode,
442
+ requestFingerprint: buildProviderRequestFingerprint(projectedMessages, toolDefinitions, this.providerId, toolDefinitions.length > 0 ? (textOnly ? "none" : "auto") : undefined),
436
443
  }, traceContext);
437
444
  const stream = this.provider.streamChat(projectedMessages, {
438
445
  model: this.apiModel,
439
446
  tools: toolDefinitions,
447
+ toolChoice: toolDefinitions.length > 0 ? (textOnly ? "none" : "auto") : undefined,
440
448
  temperature: this.temperature,
441
449
  thinkingLevel: this.thinkingLevel,
442
450
  abortSignal,
@@ -445,9 +453,14 @@ export class Agent {
445
453
  throwIfAborted(abortSignal);
446
454
  switch (chunk.type) {
447
455
  case "text":
448
- assistantMsg.content += chunk.content;
449
- streamTextChars += chunk.content.length;
450
- yield emit({ type: "text_delta", content: chunk.content });
456
+ {
457
+ const sanitizedDelta = textSanitizer.push(chunk.content);
458
+ if (sanitizedDelta) {
459
+ assistantMsg.content += sanitizedDelta;
460
+ streamTextChars += sanitizedDelta.length;
461
+ yield emit({ type: "text_delta", content: sanitizedDelta });
462
+ }
463
+ }
451
464
  break;
452
465
  case "reasoning_delta":
453
466
  {
@@ -468,6 +481,9 @@ export class Agent {
468
481
  }
469
482
  }
470
483
  break;
484
+ case "provider_content_block":
485
+ appendProviderContentBlock(assistantMsg, chunk.provider, chunk.block);
486
+ break;
471
487
  case "tool_call":
472
488
  if (discoveryBarrier?.isEnabled()
473
489
  && (bufferedStreamingToolCallIds.has(chunk.id) || discoveryBarrier.shouldBufferStreamingToolCall(chunk.name))) {
@@ -540,6 +556,12 @@ export class Agent {
540
556
  for (const update of this.drainSubagentToolUpdates())
541
557
  yield emit(update);
542
558
  }
559
+ const flushedText = textSanitizer.flush();
560
+ if (flushedText) {
561
+ assistantMsg.content += flushedText;
562
+ streamTextChars += flushedText.length;
563
+ yield emit({ type: "text_delta", content: flushedText });
564
+ }
543
565
  const flushedReasoning = reasoningSanitizer.flush();
544
566
  if (flushedReasoning) {
545
567
  debugReasoningStream({
@@ -1350,7 +1372,7 @@ export class Agent {
1350
1372
  thinkingLevel: route.thinkingLevel,
1351
1373
  mode: "plan",
1352
1374
  workingDir: cwd,
1353
- tools: childToolNames,
1375
+ ...buildToolPromptOptions(tools),
1354
1376
  memoryPrompt: childToolNames.some((name) => name === "memory_search" || name === "memory_read_summary")
1355
1377
  ? this.memoryPrompt
1356
1378
  : undefined,
@@ -1502,8 +1524,7 @@ export class Agent {
1502
1524
  || heapUsed >= RESIDENT_HISTORY_HEAP_HARD_LIMIT;
1503
1525
  const shouldCompact = !!budget?.shouldCompact
1504
1526
  || candidate.length >= RESIDENT_HISTORY_MESSAGE_LIMIT
1505
- || residentChars >= RESIDENT_HISTORY_CHAR_SOFT_LIMIT
1506
- || heapUsed >= RESIDENT_HISTORY_HEAP_SOFT_LIMIT;
1527
+ || residentChars >= RESIDENT_HISTORY_CHAR_SOFT_LIMIT;
1507
1528
  if (shouldAggressivelyPrune) {
1508
1529
  candidate = aggressivePruneMessages(candidate);
1509
1530
  }
@@ -1525,9 +1546,15 @@ export class Agent {
1525
1546
  }
1526
1547
  }
1527
1548
  appendMessage(message) {
1549
+ if (message.role === "assistant" && message.content) {
1550
+ message.content = sanitizeInternalReminderBlocks(message.content);
1551
+ }
1528
1552
  if (message.role === "assistant" && message.reasoning) {
1529
1553
  message.reasoning = sanitizeInternalReminderBlocks(message.reasoning);
1530
1554
  }
1555
+ if (message.role === "assistant" && message.providerMetadata) {
1556
+ message.providerMetadata = sanitizeAssistantProviderMetadata(message.providerMetadata);
1557
+ }
1531
1558
  this.messages.push(message);
1532
1559
  traceEvent("agent_message_append", {
1533
1560
  message: summarizeTraceMessage(message),
@@ -1605,7 +1632,22 @@ export class Agent {
1605
1632
  metadata: { kind: "security", reason: "args_corrupt" },
1606
1633
  };
1607
1634
  }
1608
- const missingRequired = findMissingRequiredArgs(tool.parameters, toolCall.parsedArgs);
1635
+ let preparedArgs = toolCall.parsedArgs;
1636
+ if (tool.prepareArguments) {
1637
+ try {
1638
+ preparedArgs = tool.prepareArguments(preparedArgs);
1639
+ }
1640
+ catch (err) {
1641
+ return {
1642
+ content: `Error: Tool "${toolCall.name}" arguments could not be normalized before execution: ` +
1643
+ `${err instanceof Error ? err.message : String(err)}. Re-issue the call with valid arguments.`,
1644
+ isError: true,
1645
+ status: "blocked",
1646
+ metadata: { kind: "security", reason: "args_prepare_failed" },
1647
+ };
1648
+ }
1649
+ }
1650
+ const missingRequired = findMissingRequiredArgs(tool.parameters, preparedArgs);
1609
1651
  if (missingRequired.length > 0) {
1610
1652
  return {
1611
1653
  content: `Error: Tool "${toolCall.name}" was called without required argument${missingRequired.length === 1 ? "" : "s"}: ${missingRequired.map((name) => `"${name}"`).join(", ")}. ` +
@@ -1616,7 +1658,7 @@ export class Agent {
1616
1658
  };
1617
1659
  }
1618
1660
  try {
1619
- return await tool.execute(toolCall.parsedArgs, {
1661
+ return await tool.execute(preparedArgs, {
1620
1662
  cwd,
1621
1663
  sessionID: this.sessionID,
1622
1664
  abortSignal,
@@ -1682,6 +1724,89 @@ function estimateResidentChars(messages) {
1682
1724
  }
1683
1725
  return total;
1684
1726
  }
1727
+ function appendProviderContentBlock(message, provider, block) {
1728
+ if (provider !== "anthropic")
1729
+ return;
1730
+ const current = message.providerMetadata?.anthropic?.contentBlocks ?? [];
1731
+ message.providerMetadata = {
1732
+ ...message.providerMetadata,
1733
+ anthropic: {
1734
+ ...message.providerMetadata?.anthropic,
1735
+ contentBlocks: [...current, cloneProviderRawContentBlock(block)],
1736
+ },
1737
+ };
1738
+ }
1739
+ function buildProviderRequestFingerprint(messages, tools, providerId, toolChoice) {
1740
+ const roleCounts = {};
1741
+ let contentChars = 0;
1742
+ let reasoningChars = 0;
1743
+ let toolResultChars = 0;
1744
+ let maxToolResultChars = 0;
1745
+ let assistantToolCalls = 0;
1746
+ let rawAnthropicBlocks = 0;
1747
+ let rawAnthropicThinkingBlocks = 0;
1748
+ let rawAnthropicSignatureChars = 0;
1749
+ for (const message of messages) {
1750
+ roleCounts[message.role] = (roleCounts[message.role] ?? 0) + 1;
1751
+ if (message.role === "assistant") {
1752
+ contentChars += message.content.length;
1753
+ reasoningChars += message.reasoning?.length ?? 0;
1754
+ assistantToolCalls += message.toolCalls?.length ?? 0;
1755
+ const blocks = message.providerMetadata?.anthropic?.contentBlocks ?? [];
1756
+ rawAnthropicBlocks += blocks.length;
1757
+ for (const block of blocks) {
1758
+ if (block.type === "thinking" || block.type === "redacted_thinking") {
1759
+ rawAnthropicThinkingBlocks += 1;
1760
+ }
1761
+ if (typeof block.signature === "string") {
1762
+ rawAnthropicSignatureChars += block.signature.length;
1763
+ }
1764
+ }
1765
+ }
1766
+ else if (message.role === "tool") {
1767
+ toolResultChars += message.content.length;
1768
+ maxToolResultChars = Math.max(maxToolResultChars, message.content.length);
1769
+ }
1770
+ else if (message.role === "user") {
1771
+ contentChars += typeof message.content === "string"
1772
+ ? message.content.length
1773
+ : message.content.reduce((sum, part) => sum + (part.type === "text" ? part.text.length : part.image_url.url.length), 0);
1774
+ }
1775
+ else {
1776
+ contentChars += message.content.length;
1777
+ }
1778
+ }
1779
+ const systemMessages = messages.filter((message) => message.role === "system");
1780
+ const bodyMessages = messages.filter((message) => message.role !== "system");
1781
+ const systemJsonBytes = Buffer.byteLength(JSON.stringify(systemMessages), "utf8");
1782
+ const bodyJsonBytes = Buffer.byteLength(JSON.stringify(bodyMessages), "utf8");
1783
+ const toolSchemaJsonBytes = Buffer.byteLength(JSON.stringify(tools), "utf8");
1784
+ return {
1785
+ roleCounts,
1786
+ estimatedTokens: estimateContextTokens(messages, providerId),
1787
+ projectedJsonBytes: Buffer.byteLength(JSON.stringify(messages), "utf8"),
1788
+ systemJsonBytes,
1789
+ bodyJsonBytes,
1790
+ toolSchemaJsonBytes,
1791
+ staticPrefixJsonBytes: Buffer.byteLength(JSON.stringify({
1792
+ system: systemMessages,
1793
+ tools,
1794
+ tool_choice: toolChoice,
1795
+ }), "utf8"),
1796
+ toolChoice,
1797
+ contentChars,
1798
+ reasoningChars,
1799
+ toolResultChars,
1800
+ maxToolResultChars,
1801
+ assistantToolCalls,
1802
+ rawAnthropicBlocks,
1803
+ rawAnthropicThinkingBlocks,
1804
+ rawAnthropicSignatureChars,
1805
+ };
1806
+ }
1807
+ function cloneProviderRawContentBlock(block) {
1808
+ return JSON.parse(JSON.stringify(block));
1809
+ }
1685
1810
  function throwIfAborted(signal) {
1686
1811
  if (!signal?.aborted)
1687
1812
  return;
@@ -20,6 +20,7 @@ export function estimateMessageTokens(message, providerId) {
20
20
  case "assistant":
21
21
  return estimate(message.content)
22
22
  + estimate(message.reasoning ?? "")
23
+ + estimateProviderMetadataOverhead(message.providerMetadata, providerId)
23
24
  + (message.toolCalls?.reduce((sum, toolCall) => sum + estimate(toolCall.arguments) + 12, 0) ?? 0)
24
25
  + 8;
25
26
  case "user":
@@ -34,6 +35,20 @@ export function estimateMessageTokens(message, providerId) {
34
35
  }, 8);
35
36
  }
36
37
  }
38
+ function estimateProviderMetadataOverhead(metadata, providerId) {
39
+ const blocks = metadata?.anthropic?.contentBlocks;
40
+ if (!blocks || blocks.length === 0)
41
+ return 0;
42
+ const estimate = (text) => estimateTextTokens(text, providerId);
43
+ return blocks.reduce((sum, block) => {
44
+ let overhead = 0;
45
+ if (typeof block.signature === "string")
46
+ overhead += estimate(block.signature);
47
+ if (block.type === "redacted_thinking" && typeof block.data === "string")
48
+ overhead += estimate(block.data);
49
+ return sum + overhead;
50
+ }, 0);
51
+ }
37
52
  export function estimateContextTokens(messages, providerId) {
38
53
  return messages.reduce((sum, message) => sum + estimateMessageTokens(message, providerId), 0);
39
54
  }
@@ -1,5 +1,6 @@
1
1
  import type { Message } from "../types.js";
2
2
  export declare function pruneMessages<T extends Message>(messages: T[]): T[];
3
+ export declare function markStableCurrentToolResultsForCache(messages: Message[]): void;
3
4
  /**
4
5
  * Aggressive variant of pruneMessages: drops the content of every prunable
5
6
  * tool output except the latest unresolved tool turn that the model still
@@ -3,6 +3,8 @@ const PRUNEABLE_TOOLS = new Set([
3
3
  ]);
4
4
  const TOOL_RESULT_KEEP_COUNT = 2;
5
5
  const MIN_PRUNE_LENGTH = 240;
6
+ const CACHE_STABLE_PROJECTION_KEY = "cacheStableProjection";
7
+ const CACHE_STABLE_FULL_PROJECTION = "full";
6
8
  export function pruneMessages(messages) {
7
9
  const toolNameByCallId = new Map();
8
10
  const pruneCandidates = [];
@@ -19,6 +21,9 @@ export function pruneMessages(messages) {
19
21
  if (message.role !== "tool") {
20
22
  continue;
21
23
  }
24
+ if (isCacheStableFullToolResult(message)) {
25
+ continue;
26
+ }
22
27
  if (protectedToolCallIds.has(message.toolCallId)) {
23
28
  const toolName = toolNameByCallId.get(message.toolCallId);
24
29
  if (toolName && shouldPruneToolResult(toolName, message.content)) {
@@ -49,6 +54,30 @@ export function pruneMessages(messages) {
49
54
  };
50
55
  });
51
56
  }
57
+ export function markStableCurrentToolResultsForCache(messages) {
58
+ const protectedToolCallIds = collectProtectedToolCallIds(messages);
59
+ if (protectedToolCallIds.size === 0)
60
+ return;
61
+ const toolNameByCallId = new Map();
62
+ for (const message of messages) {
63
+ if (message.role !== "assistant" || !message.toolCalls)
64
+ continue;
65
+ for (const toolCall of message.toolCalls) {
66
+ toolNameByCallId.set(toolCall.id, toolCall.name);
67
+ }
68
+ }
69
+ for (const message of messages) {
70
+ if (message.role !== "tool" || !protectedToolCallIds.has(message.toolCallId))
71
+ continue;
72
+ const toolName = toolNameByCallId.get(message.toolCallId);
73
+ if (!toolName || !shouldPruneToolResult(toolName, message.content))
74
+ continue;
75
+ message.metadata = {
76
+ ...message.metadata,
77
+ [CACHE_STABLE_PROJECTION_KEY]: CACHE_STABLE_FULL_PROJECTION,
78
+ };
79
+ }
80
+ }
52
81
  function shouldPruneToolResult(toolName, content) {
53
82
  if (!PRUNEABLE_TOOLS.has(toolName)) {
54
83
  return false;
@@ -64,6 +93,9 @@ function shouldPruneToolResult(toolName, content) {
64
93
  function summarizePrunedToolResult(toolName, content) {
65
94
  return `[${toolName} output omitted to control context size; original length ${content.length} chars]`;
66
95
  }
96
+ function isCacheStableFullToolResult(message) {
97
+ return message.metadata?.[CACHE_STABLE_PROJECTION_KEY] === CACHE_STABLE_FULL_PROJECTION;
98
+ }
67
99
  /**
68
100
  * Aggressive variant of pruneMessages: drops the content of every prunable
69
101
  * tool output except the latest unresolved tool turn that the model still
@@ -112,6 +112,7 @@ export function summarizeTraceMessage(message) {
112
112
  content: summarizeTraceText(message.content),
113
113
  reasoning: summarizeTraceText(message.reasoning ?? ""),
114
114
  error: message.error,
115
+ providerMetadata: summarizeAssistantProviderMetadata(message),
115
116
  toolCalls: message.toolCalls?.map((call) => ({
116
117
  id: call.id,
117
118
  name: call.name,
@@ -141,6 +142,19 @@ export function summarizeTraceMessage(message) {
141
142
  content: summarizeTraceText(message.content),
142
143
  };
143
144
  }
145
+ function summarizeAssistantProviderMetadata(message) {
146
+ const blocks = message.providerMetadata?.anthropic?.contentBlocks;
147
+ if (!blocks || blocks.length === 0)
148
+ return undefined;
149
+ return {
150
+ anthropic: {
151
+ contentBlocks: blocks.length,
152
+ thinkingBlocks: blocks.filter((block) => block.type === "thinking" || block.type === "redacted_thinking").length,
153
+ signatureChars: blocks.reduce((sum, block) => sum + (typeof block.signature === "string" ? block.signature.length : 0), 0),
154
+ types: blocks.map((block) => block.type).slice(0, 32),
155
+ },
156
+ };
157
+ }
144
158
  export function summarizeTraceToolResult(result) {
145
159
  return {
146
160
  content: summarizeTraceText(result.content),
@@ -19,7 +19,7 @@ import { BashAllowlist } from "../../approval/session-cache.js";
19
19
  import { getLspService } from "../../lsp/index.js";
20
20
  import { buildSystemPrompt } from "../../system-prompt.js";
21
21
  import { FileStateTracker } from "../../tools/file-state.js";
22
- import { createAllTools } from "../../tools/index.js";
22
+ import { buildToolPromptOptions, createAllTools } from "../../tools/index.js";
23
23
  import { displayModel, encodeModel, decodeModel } from "../../provider-registry.js";
24
24
  import { buildMemoryPrompt, recordMemoryCitations } from "../../memory/index.js";
25
25
  import { getDefaultThinkingLevel } from "../../provider-transform.js";
@@ -94,7 +94,7 @@ export class RunDriver {
94
94
  thinkingLevel,
95
95
  mode: initialMode,
96
96
  workingDir: session.cwd,
97
- tools: tools.map((t) => t.name),
97
+ ...buildToolPromptOptions(tools.filter((tool) => !tool.deferred)),
98
98
  memoryPrompt,
99
99
  });
100
100
  const budgetLedger = new BudgetLedger();
@@ -211,6 +211,7 @@ function mergeUsage(prev, next) {
211
211
  completionTokens: prev.completionTokens + (next.completionTokens ?? 0),
212
212
  promptCacheHitTokens: (prev.promptCacheHitTokens ?? 0) + (next.promptCacheHitTokens ?? 0),
213
213
  promptCacheMissTokens: (prev.promptCacheMissTokens ?? 0) + (next.promptCacheMissTokens ?? 0),
214
+ cacheCreationTokens: (prev.cacheCreationTokens ?? 0) + (next.cacheCreationTokens ?? 0),
214
215
  reasoningTokens: (prev.reasoningTokens ?? 0) + (next.reasoningTokens ?? 0),
215
216
  totalTokens: (prev.totalTokens ?? 0) + (next.totalTokens ?? 0),
216
217
  };
@@ -96,6 +96,7 @@ export async function serveFeishu(opts = {}) {
96
96
  apiKey,
97
97
  baseURL,
98
98
  promptCacheKey,
99
+ protocol: providerRegistry.getConfigured().find((provider) => provider.id === providerId)?.protocol,
99
100
  openAICodexAuth: providerRegistry.createOpenAICodexAuthAdapter(providerId),
100
101
  });
101
102
  const createProviderForRoute = async (route, promptCacheKey) => {
package/dist/main.js CHANGED
@@ -8,13 +8,14 @@ import { BudgetLedger } from "./agent/budget-ledger.js";
8
8
  import { parseArgs, printHelp } from "./cli.js";
9
9
  import { UserConfig } from "./config.js";
10
10
  import { createProviderInstance, createUnavailableProvider } from "./provider.js";
11
+ import { resolveConfiguredModel } from "./model-selection.js";
11
12
  import { getDefaultThinkingLevel } from "./provider-transform.js";
12
13
  import { ProviderRegistry, displayModel, encodeModel, decodeModel } from "./provider-registry.js";
13
14
  import { SessionManager } from "./session.js";
14
15
  import { createSessionTitleUpdater } from "./session-title.js";
15
16
  import { buildSystemPrompt } from "./system-prompt.js";
16
17
  import { SkillRegistry } from "./skills/registry.js";
17
- import { createAllTools } from "./tools/index.js";
18
+ import { buildToolPromptOptions, createAllTools } from "./tools/index.js";
18
19
  import { FileStateTracker } from "./tools/file-state.js";
19
20
  import { PermissionAwareApprovalController } from "./approval/controller.js";
20
21
  import { BashAllowlist } from "./approval/session-cache.js";
@@ -83,6 +84,7 @@ async function main() {
83
84
  baseURL: defaultProvider.baseURL,
84
85
  thinkingLevel: args.thinkingLevel,
85
86
  promptCacheKey: sessionPromptCacheKey,
87
+ protocol: defaultProvider.protocol,
86
88
  openAICodexAuth: registry.createOpenAICodexAuthAdapter(defaultProvider.id),
87
89
  })
88
90
  : createUnavailableProvider(unavailableProviderMessage);
@@ -92,6 +94,7 @@ async function main() {
92
94
  baseURL,
93
95
  thinkingLevel: args.thinkingLevel,
94
96
  promptCacheKey: sessionPromptCacheKey,
97
+ protocol: registry.getConfigured().find((provider) => provider.id === providerId)?.protocol,
95
98
  openAICodexAuth: registry.createOpenAICodexAuthAdapter(providerId),
96
99
  });
97
100
  const createProviderForRoute = async (route) => {
@@ -242,18 +245,19 @@ async function main() {
242
245
  }
243
246
  sessionPromptCacheKey = sessionManager.getOrCreatePromptCacheKey();
244
247
  // Model resolution:
245
- // 1. Session metadata 2. User-configured default model 3. CLI flag
248
+ // 1. CLI flag 2. Session metadata 3. User-configured default model
246
249
  // No implicit built-in model fallback.
247
250
  const fallbackProviderId = defaultProvider?.id || "";
248
251
  const sessionModel = sessionManager?.getMetadata().model;
249
- const configuredModel = sessionModel ?? userConfig.getDefaultModel() ?? args.model;
252
+ const defaultModel = userConfig.getDefaultModel();
250
253
  const sessionThinkingLevel = sessionManager?.getMetadata().thinkingLevel;
251
254
  const configuredThinkingLevel = userConfig.getDefaultThinkingLevel();
252
- const normalizedConfiguredModel = configuredModel
253
- ? (configuredModel.includes(":")
254
- ? configuredModel
255
- : (fallbackProviderId ? encodeModel(fallbackProviderId, configuredModel) : ""))
256
- : "";
255
+ const normalizedConfiguredModel = resolveConfiguredModel({
256
+ cliModel: args.model,
257
+ sessionModel,
258
+ defaultModel,
259
+ fallbackProviderId,
260
+ });
257
261
  const { providerId: effectiveProviderId, modelId: effectiveModelId } = normalizedConfiguredModel
258
262
  ? decodeModel(normalizedConfiguredModel)
259
263
  : { providerId: undefined, modelId: "" };
@@ -286,7 +290,7 @@ async function main() {
286
290
  thinkingLevel: initialThinkingLevel,
287
291
  mode: initialMode,
288
292
  workingDir: args.cwd,
289
- tools: tools.map((tool) => tool.name),
293
+ ...buildToolPromptOptions(tools.filter((tool) => !tool.deferred)),
290
294
  memoryPrompt,
291
295
  });
292
296
  const traceInfo = configureDebugTrace({
@@ -1,8 +1,11 @@
1
1
  import type { ReasoningEffort } from "./types.js";
2
+ export type ProviderProtocol = "openai-chat" | "anthropic-messages";
2
3
  export interface BuiltinProviderDefinition {
3
4
  id: string;
4
5
  name: string;
5
6
  baseURL: string;
7
+ protocol?: ProviderProtocol;
8
+ hidden?: boolean;
6
9
  supportsOAuth?: boolean;
7
10
  }
8
11
  export interface BuiltinModelDefinition {