@bubblebrain-ai/bubble 0.0.17 → 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 (45) hide show
  1. package/dist/agent/tool-intent.js +0 -1
  2. package/dist/agent.d.ts +1 -0
  3. package/dist/agent.js +54 -21
  4. package/dist/context/prune.d.ts +1 -0
  5. package/dist/context/prune.js +32 -0
  6. package/dist/feishu/agent-host/run-driver.js +2 -2
  7. package/dist/feishu/card/run-state.js +1 -0
  8. package/dist/main.js +11 -9
  9. package/dist/model-pricing.js +2 -1
  10. package/dist/model-selection.d.ts +7 -0
  11. package/dist/model-selection.js +9 -0
  12. package/dist/network/chatgpt-transport.js +1 -0
  13. package/dist/orchestrator/default-hooks.js +1 -1
  14. package/dist/prompt/environment.js +1 -3
  15. package/dist/prompt/runtime.js +1 -1
  16. package/dist/provider-anthropic.d.ts +15 -3
  17. package/dist/provider-anthropic.js +55 -2
  18. package/dist/provider-openai-codex.js +3 -1
  19. package/dist/provider.js +1 -1
  20. package/dist/session-title.js +3 -6
  21. package/dist/slash-commands/commands.js +4 -0
  22. package/dist/stats/usage.d.ts +1 -0
  23. package/dist/stats/usage.js +28 -3
  24. package/dist/tools/edit.js +75 -1
  25. package/dist/tools/glob.js +77 -12
  26. package/dist/tools/index.d.ts +1 -1
  27. package/dist/tools/index.js +1 -3
  28. package/dist/tools/prompt-metadata.d.ts +3 -0
  29. package/dist/tools/prompt-metadata.js +17 -0
  30. package/dist/tools/write.js +14 -0
  31. package/dist/tui/paste-placeholder.d.ts +10 -0
  32. package/dist/tui/paste-placeholder.js +45 -0
  33. package/dist/tui/run.js +23 -0
  34. package/dist/tui-ink/app.js +2 -0
  35. package/dist/tui-ink/input-box.d.ts +1 -8
  36. package/dist/tui-ink/input-box.js +8 -38
  37. package/dist/tui-opentui/app.js +2 -0
  38. package/dist/tui-opentui/input-box.d.ts +1 -3
  39. package/dist/tui-opentui/input-box.js +17 -26
  40. package/dist/types.d.ts +9 -0
  41. package/package.json +7 -3
  42. package/dist/tools/apply-patch.d.ts +0 -9
  43. package/dist/tools/apply-patch.js +0 -330
  44. package/dist/tools/patch-apply.d.ts +0 -41
  45. package/dist/tools/patch-apply.js +0 -312
@@ -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
@@ -9,9 +9,9 @@ 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";
@@ -24,6 +24,7 @@ import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMeta
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;
@@ -398,11 +404,9 @@ export class Agent {
398
404
  };
399
405
  await hookBus.runBeforeModelCall(beforeModelCallCtx);
400
406
  toolEntries = beforeModelCallCtx.toolEntries;
401
- if (this._mode !== "plan") {
402
- toolEntries = toolEntries.filter((t) => t.name !== "exit_plan_mode");
403
- }
404
407
  flushGovernorReminders();
405
- const toolDefinitions = ((hookState.forceTextOnlyReason ? [] : toolEntries))
408
+ const textOnly = !!hookState.forceTextOnlyReason;
409
+ const toolDefinitions = toolEntries
406
410
  .map((t) => ({
407
411
  name: t.name,
408
412
  description: t.description,
@@ -417,6 +421,7 @@ export class Agent {
417
421
  const bufferedStreamingToolCallIds = new Set();
418
422
  const discoveryBarrier = hookState.discoveryBarrier;
419
423
  try {
424
+ markStableCurrentToolResultsForCache(this.messages);
420
425
  const projectedMessages = projectMessages(this.messages, {
421
426
  mode: "budgeted",
422
427
  providerId: this.providerId,
@@ -434,11 +439,12 @@ export class Agent {
434
439
  toolCount: toolDefinitions.length,
435
440
  thinkingLevel: this.thinkingLevel,
436
441
  mode: this._mode,
437
- requestFingerprint: buildProviderRequestFingerprint(projectedMessages, toolDefinitions, this.providerId),
442
+ requestFingerprint: buildProviderRequestFingerprint(projectedMessages, toolDefinitions, this.providerId, toolDefinitions.length > 0 ? (textOnly ? "none" : "auto") : undefined),
438
443
  }, traceContext);
439
444
  const stream = this.provider.streamChat(projectedMessages, {
440
445
  model: this.apiModel,
441
446
  tools: toolDefinitions,
447
+ toolChoice: toolDefinitions.length > 0 ? (textOnly ? "none" : "auto") : undefined,
442
448
  temperature: this.temperature,
443
449
  thinkingLevel: this.thinkingLevel,
444
450
  abortSignal,
@@ -1366,7 +1372,7 @@ export class Agent {
1366
1372
  thinkingLevel: route.thinkingLevel,
1367
1373
  mode: "plan",
1368
1374
  workingDir: cwd,
1369
- tools: childToolNames,
1375
+ ...buildToolPromptOptions(tools),
1370
1376
  memoryPrompt: childToolNames.some((name) => name === "memory_search" || name === "memory_read_summary")
1371
1377
  ? this.memoryPrompt
1372
1378
  : undefined,
@@ -1518,8 +1524,7 @@ export class Agent {
1518
1524
  || heapUsed >= RESIDENT_HISTORY_HEAP_HARD_LIMIT;
1519
1525
  const shouldCompact = !!budget?.shouldCompact
1520
1526
  || candidate.length >= RESIDENT_HISTORY_MESSAGE_LIMIT
1521
- || residentChars >= RESIDENT_HISTORY_CHAR_SOFT_LIMIT
1522
- || heapUsed >= RESIDENT_HISTORY_HEAP_SOFT_LIMIT;
1527
+ || residentChars >= RESIDENT_HISTORY_CHAR_SOFT_LIMIT;
1523
1528
  if (shouldAggressivelyPrune) {
1524
1529
  candidate = aggressivePruneMessages(candidate);
1525
1530
  }
@@ -1627,7 +1632,22 @@ export class Agent {
1627
1632
  metadata: { kind: "security", reason: "args_corrupt" },
1628
1633
  };
1629
1634
  }
1630
- 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);
1631
1651
  if (missingRequired.length > 0) {
1632
1652
  return {
1633
1653
  content: `Error: Tool "${toolCall.name}" was called without required argument${missingRequired.length === 1 ? "" : "s"}: ${missingRequired.map((name) => `"${name}"`).join(", ")}. ` +
@@ -1638,7 +1658,7 @@ export class Agent {
1638
1658
  };
1639
1659
  }
1640
1660
  try {
1641
- return await tool.execute(toolCall.parsedArgs, {
1661
+ return await tool.execute(preparedArgs, {
1642
1662
  cwd,
1643
1663
  sessionID: this.sessionID,
1644
1664
  abortSignal,
@@ -1716,7 +1736,7 @@ function appendProviderContentBlock(message, provider, block) {
1716
1736
  },
1717
1737
  };
1718
1738
  }
1719
- function buildProviderRequestFingerprint(messages, tools, providerId) {
1739
+ function buildProviderRequestFingerprint(messages, tools, providerId, toolChoice) {
1720
1740
  const roleCounts = {};
1721
1741
  let contentChars = 0;
1722
1742
  let reasoningChars = 0;
@@ -1756,11 +1776,24 @@ function buildProviderRequestFingerprint(messages, tools, providerId) {
1756
1776
  contentChars += message.content.length;
1757
1777
  }
1758
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");
1759
1784
  return {
1760
1785
  roleCounts,
1761
1786
  estimatedTokens: estimateContextTokens(messages, providerId),
1762
1787
  projectedJsonBytes: Buffer.byteLength(JSON.stringify(messages), "utf8"),
1763
- toolSchemaJsonBytes: Buffer.byteLength(JSON.stringify(tools), "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,
1764
1797
  contentChars,
1765
1798
  reasoningChars,
1766
1799
  toolResultChars,
@@ -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
@@ -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
  };
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";
@@ -244,18 +245,19 @@ async function main() {
244
245
  }
245
246
  sessionPromptCacheKey = sessionManager.getOrCreatePromptCacheKey();
246
247
  // Model resolution:
247
- // 1. Session metadata 2. User-configured default model 3. CLI flag
248
+ // 1. CLI flag 2. Session metadata 3. User-configured default model
248
249
  // No implicit built-in model fallback.
249
250
  const fallbackProviderId = defaultProvider?.id || "";
250
251
  const sessionModel = sessionManager?.getMetadata().model;
251
- const configuredModel = sessionModel ?? userConfig.getDefaultModel() ?? args.model;
252
+ const defaultModel = userConfig.getDefaultModel();
252
253
  const sessionThinkingLevel = sessionManager?.getMetadata().thinkingLevel;
253
254
  const configuredThinkingLevel = userConfig.getDefaultThinkingLevel();
254
- const normalizedConfiguredModel = configuredModel
255
- ? (configuredModel.includes(":")
256
- ? configuredModel
257
- : (fallbackProviderId ? encodeModel(fallbackProviderId, configuredModel) : ""))
258
- : "";
255
+ const normalizedConfiguredModel = resolveConfiguredModel({
256
+ cliModel: args.model,
257
+ sessionModel,
258
+ defaultModel,
259
+ fallbackProviderId,
260
+ });
259
261
  const { providerId: effectiveProviderId, modelId: effectiveModelId } = normalizedConfiguredModel
260
262
  ? decodeModel(normalizedConfiguredModel)
261
263
  : { providerId: undefined, modelId: "" };
@@ -288,7 +290,7 @@ async function main() {
288
290
  thinkingLevel: initialThinkingLevel,
289
291
  mode: initialMode,
290
292
  workingDir: args.cwd,
291
- tools: tools.map((tool) => tool.name),
293
+ ...buildToolPromptOptions(tools.filter((tool) => !tool.deferred)),
292
294
  memoryPrompt,
293
295
  });
294
296
  const traceInfo = configureDebugTrace({
@@ -38,7 +38,8 @@ export function calculateUsageCost(providerId, modelId, usage) {
38
38
  if (!pricing)
39
39
  return undefined;
40
40
  const hasCacheBreakdown = typeof usage.promptCacheHitTokens === "number"
41
- || typeof usage.promptCacheMissTokens === "number";
41
+ || typeof usage.promptCacheMissTokens === "number"
42
+ || typeof usage.cacheCreationTokens === "number";
42
43
  const hit = usage.promptCacheHitTokens ?? 0;
43
44
  const miss = hasCacheBreakdown
44
45
  ? usage.promptCacheMissTokens ?? Math.max(0, usage.promptTokens - hit)
@@ -0,0 +1,7 @@
1
+ export interface ResolveConfiguredModelInput {
2
+ cliModel?: string;
3
+ sessionModel?: string;
4
+ defaultModel?: string;
5
+ fallbackProviderId?: string;
6
+ }
7
+ export declare function resolveConfiguredModel(input: ResolveConfiguredModelInput): string;
@@ -0,0 +1,9 @@
1
+ import { encodeModel } from "./provider-registry.js";
2
+ export function resolveConfiguredModel(input) {
3
+ const selected = input.cliModel ?? input.sessionModel ?? input.defaultModel;
4
+ if (!selected)
5
+ return "";
6
+ if (selected.includes(":"))
7
+ return selected;
8
+ return input.fallbackProviderId ? encodeModel(input.fallbackProviderId, selected) : "";
9
+ }
@@ -195,6 +195,7 @@ function isChatGptNetworkErrorText(text) {
195
195
  /\bEPIPE\b/i,
196
196
  /\bUND_ERR_/i,
197
197
  /socket hang up/i,
198
+ /Unable to connect\. Is the computer able to access the url\?/i,
198
199
  /certificate/i,
199
200
  /unable to verify/i,
200
201
  /self[- ]signed/i,
@@ -176,7 +176,7 @@ function isCodeWriteResult(_toolCall, result) {
176
176
  return result.metadata?.kind === "write" || result.metadata?.kind === "edit" || result.metadata?.kind === "patch";
177
177
  }
178
178
  function isMutationTool(name) {
179
- return name === "edit" || name === "write" || name === "apply_patch";
179
+ return name === "edit" || name === "write";
180
180
  }
181
181
  function hasSubagentLifecycleActivity(toolCalls, toolResults) {
182
182
  return toolCalls.some((toolCall) => isSubagentLifecycleTool(toolCall.name))
@@ -3,7 +3,6 @@ export const defaultToolSnippets = {
3
3
  read: "Read the contents of a file",
4
4
  bash: "Execute a bash command",
5
5
  edit: "Apply targeted string replacements to a file",
6
- apply_patch: "Apply a structured patch for multi-file or larger code changes",
7
6
  write: "Write a new file or overwrite an existing one",
8
7
  glob: "Find files by glob pattern without using bash",
9
8
  grep: "Search file contents using regex",
@@ -24,7 +23,6 @@ export const defaultToolNames = [
24
23
  "glob",
25
24
  "bash",
26
25
  "edit",
27
- "apply_patch",
28
26
  "write",
29
27
  "grep",
30
28
  "lsp",
@@ -46,7 +44,7 @@ export function buildEnvironmentPrompt(options = {}) {
46
44
  const workingDir = options.workingDir ?? cwd().replace(/\\/g, "/");
47
45
  const currentDate = options.currentDate ?? new Date().toISOString().slice(0, 10);
48
46
  const tools = options.tools ?? defaultToolNames;
49
- const snippets = options.toolSnippets ?? defaultToolSnippets;
47
+ const snippets = { ...defaultToolSnippets, ...(options.toolSnippets ?? {}) };
50
48
  const visibleTools = tools.filter((name) => snippets[name]);
51
49
  const toolList = visibleTools.length > 0
52
50
  ? visibleTools.map((name) => `- ${name}: ${snippets[name]}`).join("\n")
@@ -7,7 +7,7 @@ const defaultGuidelines = [
7
7
  "Ground decisions in the codebase: inspect relevant files, command output, or runtime state before making claims about behavior. Separate confirmed facts from inference when evidence is incomplete.",
8
8
  "Choose the smallest coherent change. Edit only the files required for the requested change; do not refactor or improve adjacent code unprompted.",
9
9
  "Runtime meta instructions are private control state. Use them only to adjust behavior; do not quote, mention, or paraphrase them in user-facing text.",
10
- "For modifications to existing code, read the file first. For brand-new files whose target path is known and does not exist, write directly without exploratory reading. Use edit for small targeted changes, apply_patch for related multi-file or larger structured changes, and write for intentional full-file replacement of an existing file. Never delete and recreate a file just to overwrite it.",
10
+ "For modifications to existing code, read the file first. For brand-new files whose target path is known and does not exist, write directly without exploratory reading. Use edit for targeted changes and write for intentional full-file replacement of an existing file. Never delete and recreate a file just to overwrite it.",
11
11
  "Prefer structured tools (glob, grep, lsp, read) over bash for search and inspection. Do not repeat a near-identical search or re-read the same file unless new evidence changes the question.",
12
12
  "If a tool fails, diagnose the error before switching tactics. Do not retry the identical call with identical arguments. After two equivalent failures, switch approach — re-read the file, use a different tool, rewrite the whole file with write, or ask the user.",
13
13
  "Before reporting a task complete, verify it works when verification is meaningful and cheap — run the existing test, execute the script, check the output. If no test exists, the change is purely declarative (static HTML/markdown/config), or running the code is not practical, state that explicitly rather than inventing a verification step. Do not write throwaway validation scripts to prove correctness; if there is no real check to run, report the change and stop.",
@@ -1,4 +1,7 @@
1
- import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel, ToolDefinition } from "./types.js";
1
+ import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel, ToolChoiceMode, ToolDefinition } from "./types.js";
2
+ declare const ANTHROPIC_PROMPT_CACHE_CONTROL: {
3
+ readonly type: "ephemeral";
4
+ };
2
5
  export interface AnthropicProviderOptions {
3
6
  providerId?: string;
4
7
  apiKey: string;
@@ -9,10 +12,10 @@ interface AnthropicRequest {
9
12
  model: string;
10
13
  max_tokens: number;
11
14
  messages: AnthropicMessage[];
12
- system?: string;
15
+ system?: string | AnthropicSystemBlock[];
13
16
  tools?: AnthropicTool[];
14
17
  tool_choice?: {
15
- type: "auto" | "any";
18
+ type: "auto" | "any" | "none";
16
19
  };
17
20
  stream?: boolean;
18
21
  temperature?: number;
@@ -20,6 +23,12 @@ interface AnthropicRequest {
20
23
  type: "adaptive";
21
24
  };
22
25
  }
26
+ type AnthropicCacheControl = typeof ANTHROPIC_PROMPT_CACHE_CONTROL;
27
+ interface AnthropicSystemBlock {
28
+ type: "text";
29
+ text: string;
30
+ cache_control?: AnthropicCacheControl;
31
+ }
23
32
  type AnthropicContentBlock = {
24
33
  type: "text";
25
34
  text: string;
@@ -59,15 +68,18 @@ interface AnthropicTool {
59
68
  name: string;
60
69
  description: string;
61
70
  input_schema: ToolDefinition["parameters"];
71
+ cache_control?: AnthropicCacheControl;
62
72
  }
63
73
  export declare function createAnthropicMessagesProvider(options: AnthropicProviderOptions): Provider;
64
74
  export declare function buildAnthropicRequest(options: AnthropicProviderOptions, messages: ProviderMessage[], chatOptions: {
65
75
  model: string;
66
76
  tools?: ToolDefinition[];
77
+ toolChoice?: ToolChoiceMode;
67
78
  temperature?: number;
68
79
  thinkingLevel?: ThinkingLevel;
69
80
  stream?: boolean;
70
81
  }): AnthropicRequest;
82
+ export declare function supportsAnthropicPromptCache(options: AnthropicProviderOptions, model: string): boolean;
71
83
  export declare function toAnthropicMessages(messages: ProviderMessage[], echoThinking?: boolean): {
72
84
  system: string;
73
85
  messages: AnthropicMessage[];
@@ -1,11 +1,23 @@
1
1
  import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
2
2
  const ANTHROPIC_VERSION = "2023-06-01";
3
3
  const DEFAULT_MAX_TOKENS = 8192;
4
+ const ANTHROPIC_PROMPT_CACHE_CONTROL = { type: "ephemeral" };
5
+ const MINIMAX_PROMPT_CACHE_MODELS = new Set([
6
+ "minimax-m2.7",
7
+ "minimax-m2.7-highspeed",
8
+ "minimax-m2.5",
9
+ "minimax-m2.5-highspeed",
10
+ "minimax-m2.1",
11
+ "minimax-m2.1-highspeed",
12
+ "minimax-m2",
13
+ "m2-her",
14
+ ]);
4
15
  export function createAnthropicMessagesProvider(options) {
5
16
  async function* streamChat(messages, chatOptions) {
6
17
  const body = buildAnthropicRequest(options, messages, {
7
18
  model: chatOptions.model,
8
19
  tools: chatOptions.tools,
20
+ toolChoice: chatOptions.toolChoice,
9
21
  temperature: chatOptions.temperature,
10
22
  thinkingLevel: chatOptions.thinkingLevel,
11
23
  stream: true,
@@ -41,18 +53,25 @@ export function createAnthropicMessagesProvider(options) {
41
53
  }
42
54
  export function buildAnthropicRequest(options, messages, chatOptions) {
43
55
  const { system, messages: anthropicMessages } = toAnthropicMessages(messages, shouldEchoThinking(options.providerId));
56
+ const enablePromptCache = supportsAnthropicPromptCache(options, chatOptions.model);
44
57
  const tools = chatOptions.tools?.map((tool) => ({
45
58
  name: tool.name,
46
59
  description: tool.description,
47
60
  input_schema: tool.parameters,
48
61
  }));
62
+ if (enablePromptCache && tools && tools.length > 0) {
63
+ tools[tools.length - 1] = {
64
+ ...tools[tools.length - 1],
65
+ cache_control: ANTHROPIC_PROMPT_CACHE_CONTROL,
66
+ };
67
+ }
49
68
  const body = {
50
69
  model: chatOptions.model,
51
70
  max_tokens: DEFAULT_MAX_TOKENS,
52
- system: system || undefined,
71
+ system: buildAnthropicSystem(system, enablePromptCache),
53
72
  messages: anthropicMessages,
54
73
  tools: tools && tools.length > 0 ? tools : undefined,
55
- tool_choice: tools && tools.length > 0 ? { type: "auto" } : undefined,
74
+ tool_choice: tools && tools.length > 0 ? { type: chatOptions.toolChoice ?? "auto" } : undefined,
56
75
  stream: chatOptions.stream || undefined,
57
76
  };
58
77
  if (typeof chatOptions.temperature === "number") {
@@ -64,6 +83,23 @@ export function buildAnthropicRequest(options, messages, chatOptions) {
64
83
  }
65
84
  return body;
66
85
  }
86
+ function buildAnthropicSystem(system, enablePromptCache) {
87
+ if (!system)
88
+ return undefined;
89
+ if (!enablePromptCache)
90
+ return system;
91
+ return [{ type: "text", text: system, cache_control: ANTHROPIC_PROMPT_CACHE_CONTROL }];
92
+ }
93
+ export function supportsAnthropicPromptCache(options, model) {
94
+ const providerId = (options.providerId ?? "").toLowerCase();
95
+ if (providerId === "anthropic" || isOfficialAnthropicBaseUrl(options.baseURL)) {
96
+ return true;
97
+ }
98
+ if (!isMiniMaxAnthropicEndpoint(options)) {
99
+ return false;
100
+ }
101
+ return MINIMAX_PROMPT_CACHE_MODELS.has(model.toLowerCase());
102
+ }
67
103
  export function toAnthropicMessages(messages, echoThinking = false) {
68
104
  const system = [];
69
105
  const out = [];
@@ -512,6 +548,7 @@ function mergeAnthropicUsage(current, raw) {
512
548
  let promptTokens = current?.promptTokens ?? 0;
513
549
  let promptCacheHitTokens = current?.promptCacheHitTokens;
514
550
  let promptCacheMissTokens = current?.promptCacheMissTokens;
551
+ let cacheCreationTokens = current?.cacheCreationTokens;
515
552
  if (hasPromptUsage) {
516
553
  const inputTokens = rawInput ?? promptCacheMissTokens ?? promptTokens;
517
554
  const cacheRead = rawCacheRead ?? promptCacheHitTokens ?? 0;
@@ -519,12 +556,14 @@ function mergeAnthropicUsage(current, raw) {
519
556
  promptTokens = inputTokens + cacheRead + cacheCreation;
520
557
  promptCacheHitTokens = cacheRead;
521
558
  promptCacheMissTokens = inputTokens + cacheCreation;
559
+ cacheCreationTokens = cacheCreation;
522
560
  }
523
561
  return {
524
562
  promptTokens,
525
563
  completionTokens: outputTokens,
526
564
  promptCacheHitTokens,
527
565
  promptCacheMissTokens,
566
+ cacheCreationTokens,
528
567
  totalTokens: promptTokens + outputTokens,
529
568
  };
530
569
  }
@@ -534,6 +573,20 @@ function shouldEchoThinking(providerId) {
534
573
  function shouldSendBearerAuth(options) {
535
574
  return !isOfficialAnthropicBaseUrl(options.baseURL) || options.providerId?.startsWith("minimax") === true;
536
575
  }
576
+ function isMiniMaxAnthropicEndpoint(options) {
577
+ const providerId = (options.providerId ?? "").toLowerCase();
578
+ if (providerId !== "minimax" && providerId !== "minimax-anthropic")
579
+ return false;
580
+ try {
581
+ const url = new URL(options.baseURL);
582
+ const host = url.hostname.toLowerCase();
583
+ const path = url.pathname.toLowerCase();
584
+ return (host === "api.minimax.io" || host === "api.minimaxi.com") && path.includes("/anthropic");
585
+ }
586
+ catch {
587
+ return false;
588
+ }
589
+ }
537
590
  function isOfficialAnthropicBaseUrl(baseURL) {
538
591
  try {
539
592
  return new URL(baseURL).hostname === "api.anthropic.com";
@@ -72,6 +72,7 @@ export function createOpenAICodexProvider(options) {
72
72
  const body = JSON.stringify(buildRequestBody(messages, {
73
73
  model: chatOptions.model,
74
74
  tools: chatOptions.tools,
75
+ toolChoice: chatOptions.toolChoice,
75
76
  reasoningEffort: requestConfig.reasoningEffort,
76
77
  sessionId,
77
78
  providerId: options.providerId,
@@ -314,7 +315,7 @@ function buildRequestBody(messages, options) {
314
315
  providerId: options.providerId,
315
316
  model: options.model,
316
317
  }),
317
- tool_choice: "auto",
318
+ tool_choice: options.tools && options.tools.length > 0 ? options.toolChoice ?? "auto" : undefined,
318
319
  parallel_tool_calls: true,
319
320
  text: { verbosity: "medium" },
320
321
  };
@@ -454,6 +455,7 @@ function isTransientCodexTransportError(error) {
454
455
  /\bEPIPE\b/i,
455
456
  /socket hang up/i,
456
457
  /fetch failed/i,
458
+ /Unable to connect\. Is the computer able to access the url\?/i,
457
459
  /unknown certificate verification error/i,
458
460
  /certificate (?:verify|verification) (?:failed|error)/i,
459
461
  /unable to verify (?:the )?(?:first )?certificate/i,
package/dist/provider.js CHANGED
@@ -106,7 +106,7 @@ export function createProviderInstance(options) {
106
106
  reasoningContentEcho: requestConfig.reasoningContentEcho ?? "tool_calls",
107
107
  })),
108
108
  tools: tools && tools.length > 0 ? tools : undefined,
109
- tool_choice: tools && tools.length > 0 ? "auto" : undefined,
109
+ tool_choice: tools && tools.length > 0 ? chatOptions.toolChoice ?? "auto" : undefined,
110
110
  stream: true,
111
111
  };
112
112
  // DeepSeek and MiniMax only emit final usage in streaming mode when this flag is set.