mixdog 0.9.52 → 0.9.53

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 (111) hide show
  1. package/package.json +1 -1
  2. package/scripts/bench-run.mjs +2 -2
  3. package/scripts/compact-pressure-test.mjs +104 -0
  4. package/scripts/desktop-session-bridge-test.mjs +704 -0
  5. package/scripts/freevar-smoke.mjs +7 -4
  6. package/scripts/lifecycle-api-test.mjs +65 -4
  7. package/scripts/max-output-recovery-test.mjs +31 -0
  8. package/scripts/memory-core-input-test.mjs +10 -0
  9. package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
  10. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  11. package/scripts/parent-abort-link-test.mjs +24 -0
  12. package/scripts/process-lifecycle-test.mjs +80 -22
  13. package/scripts/provider-contract-test.mjs +257 -0
  14. package/scripts/provider-toolcall-test.mjs +172 -10
  15. package/scripts/session-orphan-sweep-test.mjs +27 -1
  16. package/src/lib/keychain-cjs.cjs +36 -23
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
  18. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  19. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
  22. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
  24. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
  27. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  28. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  29. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  30. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
  41. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
  42. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  43. package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
  44. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  45. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
  46. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  47. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  48. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
  49. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  50. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  53. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
  55. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  62. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
  63. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  64. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
  65. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  66. package/src/runtime/channels/backends/discord.mjs +6 -6
  67. package/src/runtime/channels/lib/config.mjs +15 -2
  68. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  69. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  70. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  72. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  73. package/src/runtime/memory/index.mjs +24 -198
  74. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  75. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  76. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  77. package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
  78. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  79. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  80. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  81. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  82. package/src/runtime/shared/config.mjs +58 -13
  83. package/src/runtime/shared/llm/cost.mjs +14 -4
  84. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  85. package/src/runtime/shared/process-lifecycle.mjs +92 -19
  86. package/src/runtime/shared/process-shutdown.mjs +6 -0
  87. package/src/runtime/shared/resource-admission.mjs +7 -2
  88. package/src/session-runtime/channel-config-api.mjs +7 -7
  89. package/src/session-runtime/config-lifecycle.mjs +20 -17
  90. package/src/session-runtime/cwd-plugins.mjs +9 -7
  91. package/src/session-runtime/env.mjs +1 -2
  92. package/src/session-runtime/hitch-profile.mjs +45 -0
  93. package/src/session-runtime/lifecycle-api.mjs +36 -9
  94. package/src/session-runtime/mcp-glue.mjs +6 -11
  95. package/src/session-runtime/provider-init-key.mjs +17 -0
  96. package/src/session-runtime/runtime-core.mjs +44 -103
  97. package/src/session-runtime/runtime-paths.mjs +20 -0
  98. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  99. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  100. package/src/session-runtime/tool-catalog.mjs +15 -89
  101. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  102. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  103. package/src/standalone/agent-tool.mjs +12 -162
  104. package/src/standalone/channel-admin.mjs +29 -0
  105. package/src/tui/App.jsx +11 -5
  106. package/src/tui/dist/index.mjs +202 -65
  107. package/src/tui/engine/session-api-ext.mjs +37 -4
  108. package/src/tui/index.jsx +1 -0
  109. package/src/tui/lib/voice-setup.mjs +5 -5
  110. package/scripts/_devtools-stub.mjs +0 -1
  111. package/src/runtime/lib/keychain-cjs.cjs +0 -288
@@ -101,8 +101,7 @@ function agentCompactEventDetail(event = {}) {
101
101
  // recursion break)
102
102
  // Hidden-agent exceptions are declarative: defaults/agents.json may set
103
103
  // toolSchemaProfile when first-turn routing quality is worth a separate tool
104
- // prefix. Standard profiles are none/read/full/read-write-search; legacy names
105
- // stay as aliases.
104
+ // prefix. Standard profiles are none/read/full/read-write-search.
106
105
  // See manager.mjs resolveSessionTools for the single source of truth;
107
106
  // agent visibility is declared via annotations.agentHidden on each tool def.
108
107
  const HIDDEN_ROLE_TOOL_SCHEMA_PROFILES = Object.freeze({
@@ -127,17 +126,6 @@ const HIDDEN_ROLE_TOOL_SCHEMA_PROFILES = Object.freeze({
127
126
  'search',
128
127
  'web_fetch',
129
128
  ]),
130
- // Backward-compatible aliases for older hidden-role definitions.
131
- unified: null,
132
- 'llm-only': Object.freeze([]),
133
- 'filesystem-read': Object.freeze([
134
- 'code_graph',
135
- 'find',
136
- 'glob',
137
- 'list',
138
- 'grep',
139
- 'read',
140
- ]),
141
129
  });
142
130
 
143
131
  export function resolveHiddenRoleSchemaAllowedTools(hidden) {
@@ -39,27 +39,26 @@ import { createHash } from 'crypto';
39
39
  import { getHiddenAgent } from '../internal-agents.mjs';
40
40
 
41
41
  /**
42
- * One-shot, LLM-only maintenance hidden roles (cycle1/cycle2/cycle3-agent):
42
+ * One-shot, tool-free maintenance hidden roles (cycle1/cycle2/cycle3-agent):
43
43
  * a fresh stateless session is created per call, asked exactly once, and
44
44
  * closed (agent-dispatch.mjs) — the per-batch user prompt can NEVER be reused.
45
45
  * Writing a message-tail cache breakpoint on it just pays the 1.25x write
46
46
  * premium for content read back 0 times. Identified by the declarative
47
- * (kind:'maintenance' + toolSchemaProfile:'llm-only') pair rather than
47
+ * (kind:'maintenance' + toolSchemaProfile:'none') pair rather than
48
48
  * hardcoded names, so new roles sharing the pattern are covered for free.
49
49
  * Multi-turn maintenance roles (scheduler-task / webhook-handler) are
50
- * 'unified' and therefore excluded — they run a tool loop whose tail caches
51
- * legitimately reuse across iterations.
50
+ * read-write-search and therefore excluded — they run a tool loop whose tail
51
+ * caches legitimately reuse across iterations.
52
52
  */
53
53
  function isOneShotMaintenanceAgent(agent) {
54
54
  const hidden = getHiddenAgent(agent);
55
55
  // Shipped cycle1/2/3 declare toolSchemaProfile:'none' (no tool schema at
56
- // all); 'llm-only' is the legacy alias for the same one-shot LLM-only
57
- // pattern. Multi-turn maintenance roles (scheduler-task/webhook-handler)
58
- // are 'read-write-search' and stay excluded.
56
+ // all). Multi-turn maintenance roles (scheduler-task/webhook-handler) are
57
+ // 'read-write-search' and stay excluded.
59
58
  return Boolean(
60
59
  hidden
61
60
  && hidden.kind === 'maintenance'
62
- && (hidden.toolSchemaProfile === 'llm-only' || hidden.toolSchemaProfile === 'none'),
61
+ && hidden.toolSchemaProfile === 'none',
63
62
  );
64
63
  }
65
64
 
@@ -155,6 +155,27 @@ function extractThinkingTokens(rawUsage) {
155
155
  return null;
156
156
  }
157
157
 
158
+ function resolveTraceUsageInput({
159
+ provider,
160
+ inputTokens,
161
+ cachedTokens,
162
+ cacheWriteTokens,
163
+ inputTokensInclusive,
164
+ }) {
165
+ const inclusive = typeof inputTokensInclusive === 'boolean'
166
+ ? inputTokensInclusive
167
+ : isInclusiveProvider(provider);
168
+ const input = inputTokens || 0;
169
+ const cacheRead = cachedTokens || 0;
170
+ const cacheWrite = cacheWriteTokens || 0;
171
+ return {
172
+ uncachedInputTokens: inclusive ? Math.max(input - cacheRead - cacheWrite, 0) : input,
173
+ promptTokens: inclusive
174
+ ? Math.max(input, cacheRead + cacheWrite)
175
+ : input + cacheRead + cacheWrite,
176
+ };
177
+ }
178
+
158
179
  /** xAI Responses cache-chain diagnosis for usage_raw rows (measurement only). */
159
180
  function grokCacheChainTraceFields(providerState, requestPrevResponseId, continuationResetReason = null) {
160
181
  const lastReceived = typeof providerState?.xaiResponses?.previousResponseId === 'string'
@@ -190,17 +211,19 @@ function traceAgentUsage({
190
211
  requestPrevResponseId,
191
212
  chainContinuous,
192
213
  continuationResetReason,
214
+ inputTokensInclusive,
193
215
  }) {
194
- const inclusive = isInclusiveProvider(provider);
195
- const inTok = inputTokens || 0;
196
- const cacheRead = cachedTokens || 0;
216
+ const accounting = resolveTraceUsageInput({
217
+ provider,
218
+ inputTokens,
219
+ cachedTokens,
220
+ cacheWriteTokens,
221
+ inputTokensInclusive,
222
+ });
197
223
  const cacheWrite = cacheWriteTokens || 0;
198
- const uncachedInputTokens = inclusive ? Math.max(inTok - cacheRead - cacheWrite, 0) : inTok;
199
224
  const promptTotal = typeof promptTokens === 'number'
200
225
  ? promptTokens
201
- : (inclusive
202
- ? Math.max(inTok, cacheRead + cacheWrite)
203
- : inTok + cacheRead + cacheWrite);
226
+ : accounting.promptTokens;
204
227
  const resolvedServiceTier = serviceTier || rawUsage?.service_tier || rawUsage?.serviceTier || null;
205
228
  const thinkingTokens = extractThinkingTokens(rawUsage);
206
229
  appendAgentTrace({
@@ -212,7 +235,7 @@ function traceAgentUsage({
212
235
  thinking_tokens: thinkingTokens,
213
236
  cached_tokens: cachedTokens,
214
237
  cache_write_tokens: cacheWrite,
215
- uncached_input_tokens: uncachedInputTokens,
238
+ uncached_input_tokens: accounting.uncachedInputTokens,
216
239
  // Unified total-prompt field. Anthropic = input+cache_read+cache_write,
217
240
  // OpenAI/Gemini = input_tokens (cached is already a subset).
218
241
  prompt_tokens: promptTotal,
@@ -227,7 +250,7 @@ function traceAgentUsage({
227
250
  payload: {
228
251
  provider: provider || null,
229
252
  prompt_tokens: promptTotal,
230
- uncached_input_tokens: uncachedInputTokens,
253
+ uncached_input_tokens: accounting.uncachedInputTokens,
231
254
  thinking_tokens: thinkingTokens,
232
255
  model_display: modelDisplay || null,
233
256
  response_id: responseId || null,
@@ -254,6 +277,7 @@ export {
254
277
  traceAgentToolFailure,
255
278
  traceAgentCompact,
256
279
  traceAgentUsage,
280
+ resolveTraceUsageInput,
257
281
  grokCacheChainTraceFields,
258
282
  traceAgentCompress,
259
283
  traceAgentBatch,
@@ -28,6 +28,7 @@ export const LEGACY_EFFORT_BUDGET = Object.freeze({
28
28
  });
29
29
  const _LOGGED_EFFORT_NORMALIZATION = new Set();
30
30
  const _LOGGED_UNKNOWN_EFFORT = new Set();
31
+ const MAX_LOGGED_UNKNOWN_EFFORTS = 100;
31
32
 
32
33
  function normalizeModelId(model) {
33
34
  return String(model || '').toLowerCase().replace(/\./g, '-');
@@ -223,8 +224,12 @@ export function applyAnthropicEffortToBody(
223
224
  return;
224
225
  }
225
226
 
226
- if (opts.effort && normalized === undefined && !_LOGGED_UNKNOWN_EFFORT.has(opts.effort)) {
227
- _LOGGED_UNKNOWN_EFFORT.add(opts.effort);
227
+ const unknownEffort = String(opts.effort ?? '');
228
+ if (opts.effort && normalized === undefined && !_LOGGED_UNKNOWN_EFFORT.has(unknownEffort)) {
229
+ if (_LOGGED_UNKNOWN_EFFORT.size >= MAX_LOGGED_UNKNOWN_EFFORTS) {
230
+ _LOGGED_UNKNOWN_EFFORT.delete(_LOGGED_UNKNOWN_EFFORT.values().next().value);
231
+ }
232
+ _LOGGED_UNKNOWN_EFFORT.add(unknownEffort);
228
233
  try {
229
234
  process.stderr.write(
230
235
  `[${logTag}] unknown effort=${opts.effort} ignored (known legacy: ${Object.keys(LEGACY_EFFORT_BUDGET).join(', ')})\n`,
@@ -25,7 +25,6 @@ import {
25
25
  ensureLatestAnthropicModel,
26
26
  } from './anthropic-model-resolve.mjs';
27
27
  import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
28
- import { providerNativeToolPrefixCount } from '../../../../session-runtime/provider-request-tools.mjs';
29
28
  import {
30
29
  TOKEN_REFRESH_SKEW_MS,
31
30
  isAnthropicOAuthRefreshDisabled,
@@ -70,8 +69,18 @@ import {
70
69
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
71
70
  import { normalizeContentForAnthropic } from './media-normalization.mjs';
72
71
  import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
72
+ import {
73
+ ANTHROPIC_CACHE_TTL_STABLE as CACHE_TTL_STABLE,
74
+ ANTHROPIC_CACHE_TTL_VOLATILE as CACHE_TTL_VOLATILE,
75
+ applyAnthropicCacheMarkers,
76
+ clampAnthropicThinkingBudget as clampThinkingBudgetTokens,
77
+ deferredAnthropicTools as sharedDeferredAnthropicTools,
78
+ requestAnthropicTools as sharedRequestAnthropicTools,
79
+ resolveAnthropicCacheTtls as resolveCacheTtls,
80
+ sanitizeAnthropicInputSchema,
81
+ toAnthropicToolChoice,
82
+ } from './lib/anthropic-request-utils.mjs';
73
83
 
74
- // --- Model catalog cache helpers: extracted to anthropic-model-resolve.mjs ---
75
84
  // SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
76
85
  const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
77
86
 
@@ -202,113 +211,12 @@ function resolveMaxTokens(model) {
202
211
  return resolveAnthropicMaxTokens(model, { catalogLookup: _catalogOutputTokens });
203
212
  }
204
213
 
205
- const MIN_THINKING_BUDGET = 1024;
206
- const THINKING_OUTPUT_RESERVE = 1024;
207
-
208
- function clampThinkingBudgetTokens(value, maxTokens) {
209
- const desired = Math.floor(Number(value));
210
- const max = Math.floor(Number(maxTokens));
211
- if (!Number.isFinite(desired) || desired <= 0 || !Number.isFinite(max)) return null;
212
- const ceiling = max - THINKING_OUTPUT_RESERVE;
213
- if (ceiling < MIN_THINKING_BUDGET) return null;
214
- return Math.max(MIN_THINKING_BUDGET, Math.min(desired, ceiling));
215
- }
216
-
217
- // Layered cache TTLs — stable layers get 1h, volatile layers get 5m.
218
- // Anthropic requires 1h entries to appear before 5m entries in the request.
219
- const CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' }; // tools, system, tier3, messages
220
- const CACHE_TTL_VOLATILE = { type: 'ephemeral' }; // explicit 5m override
221
-
222
214
  // --- Message conversion ---
223
215
 
224
- function withCacheControl(block, ttl = CACHE_TTL_VOLATILE) {
225
- if (!block || typeof block !== 'object' || block.cache_control) return block;
226
- return { ...block, cache_control: ttl };
227
- }
228
-
229
- function appendCacheControl(content, ttl = CACHE_TTL_VOLATILE) {
230
- if (Array.isArray(content)) {
231
- if (content.length === 0) return content;
232
- const next = [...content];
233
- next[next.length - 1] = withCacheControl(next[next.length - 1], ttl);
234
- return next;
235
- }
236
- if (typeof content === 'string') {
237
- return [withCacheControl({ type: 'text', text: content }, ttl)];
238
- }
239
- return content;
240
- }
241
-
242
216
  // Anthropic's tool spec forbids oneOf / allOf / anyOf at the TOP level of
243
217
  // input_schema (nested usage inside properties is allowed). External MCP
244
218
  // servers sometimes emit such schemas.
245
219
  // Convert them to a flat object schema so the API never sees a 400.
246
- function _sanitizeInputSchema(schema, toolName) {
247
- if (!schema || typeof schema !== 'object') {
248
- return { type: 'object', properties: {} };
249
- }
250
- const compound = schema.oneOf || schema.anyOf || schema.allOf;
251
- if (!compound) return structuredClone(schema);
252
- // Merge all branch properties into one permissive object schema.
253
- // None of the branches' required lists are hoisted — callers that relied
254
- // on discriminated-union semantics will still function; the model simply
255
- // receives a union of the property surface with no hard-required constraint.
256
- const mergedProps = { ...(schema.properties && typeof schema.properties === 'object' ? schema.properties : {}) };
257
- const branchDescs = [];
258
- for (const branch of Array.isArray(compound) ? compound : []) {
259
- if (branch && typeof branch === 'object' && branch.properties) {
260
- Object.assign(mergedProps, branch.properties);
261
- }
262
- if (branch && typeof branch === 'object') {
263
- const parts = [];
264
- if (branch.description) parts.push(branch.description);
265
- else if (branch.type) parts.push(`type:${branch.type}`);
266
- if (parts.length) branchDescs.push(parts.join(' '));
267
- }
268
- }
269
- const compoundKey = schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : 'allOf';
270
- let description = schema.description || '';
271
- if (branchDescs.length) {
272
- const parts = [];
273
- let used = 0;
274
- for (let i = 0; i < branchDescs.length; i++) {
275
- const v = `(variant ${i + 1}: ${branchDescs[i]})`;
276
- if (used + v.length + (parts.length ? 1 : 0) > 500) break;
277
- parts.push(v);
278
- used += v.length + (parts.length > 1 ? 1 : 0);
279
- }
280
- const addition = parts.join(' ');
281
- if (addition) description = description ? `${description} ${addition}` : addition;
282
- }
283
- const mergedPropsCount = Object.keys(mergedProps).length;
284
- if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
285
- process.stderr.write(
286
- `[anthropic-oauth-sanitizer] tool="${toolName ?? ''}" compound="${compoundKey}" branches=${Array.isArray(compound) ? compound.length : 0} mergedProps=${mergedPropsCount}\n`
287
- );
288
- }
289
- return {
290
- type: 'object',
291
- ...(description ? { description } : {}),
292
- properties: mergedProps,
293
- };
294
- }
295
-
296
- function toAnthropicTools(tools) {
297
- return tools.map(t => {
298
- const out = {
299
- name: t.name,
300
- description: t.description,
301
- input_schema: _sanitizeInputSchema(t.inputSchema, t.name),
302
- };
303
- if (t.deferLoading === true || t.defer_loading === true) out.defer_loading = true;
304
- return out;
305
- });
306
- }
307
- function nativeAnthropicTools(opts) {
308
- return Array.isArray(opts?.nativeTools)
309
- ? opts.nativeTools.filter(t => t && typeof t === 'object')
310
- : [];
311
- }
312
220
  // Map the orchestrator-level opts.toolChoice into Anthropic's tool_choice.
313
221
  // Only 'none' is activated: it lets the hard-cap final turn keep the tool
314
222
  // DEFINITIONS in-request (so the tools->system->messages prefix — and its
@@ -321,59 +229,11 @@ function nativeAnthropicTools(opts) {
321
229
  // effort/thinking active on reasoning models — activating it would convert a
322
230
  // previously-harmless no-op into a hard 400 on exactly that turn. Attached
323
231
  // only when the request actually carries tools (see buildRequestBody).
324
- function toAnthropicToolChoice(toolChoice) {
325
- return toolChoice === 'none' ? { type: 'none' } : undefined;
326
- }
327
- function discoveredAnthropicToolNames(messages, opts, provider) {
328
- const anthropicNative = new Set(['anthropic', 'anthropic-oauth']);
329
- const discovered = new Set(
330
- Array.isArray(opts?.session?.deferredDiscoveredTools)
331
- ? opts.session.deferredDiscoveredTools.map((name) => String(name || '').trim()).filter(Boolean)
332
- : [],
333
- );
334
- for (const message of Array.isArray(messages) ? messages : []) {
335
- const native = message?.nativeToolSearch;
336
- const source = String(native?.provider || '').toLowerCase();
337
- if (source && source !== provider
338
- && !(anthropicNative.has(source) && anthropicNative.has(provider))) continue;
339
- for (const name of Array.isArray(native?.toolReferences) ? native.toolReferences : []) {
340
- const key = String(name || '').trim();
341
- if (key) discovered.add(key);
342
- }
343
- }
344
- return discovered;
345
- }
346
232
  function deferredAnthropicTools(activeTools, messages, opts) {
347
- if (opts?.session?.deferredNativeTools !== true) return [];
348
- // A request whose ONLY tools are deferred is rejected by the API with
349
- // `400: At least one tool must have defer_loading=false` — happens on the
350
- // iteration-cap final turn (loop sends tools: [] to force a text answer).
351
- // No active tools ⇒ send no deferred catalog either.
352
- if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
353
- const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
354
- const discovered = discoveredAnthropicToolNames(messages, opts, 'anthropic-oauth');
355
- const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
356
- return catalog
357
- .filter((tool) => tool?.name && discovered.has(String(tool.name)) && !active.has(String(tool.name)))
358
- .map((tool) => ({ ...tool, deferLoading: true }));
233
+ return sharedDeferredAnthropicTools(activeTools, messages, opts, 'anthropic-oauth');
359
234
  }
360
235
  function requestAnthropicTools(tools, messages, opts) {
361
- const activeTools = Array.isArray(tools) ? tools : [];
362
- if (opts?.providerToolSnapshotAuthoritative === true) {
363
- const nativePrefixCount = providerNativeToolPrefixCount(
364
- activeTools,
365
- opts.providerNativeToolPrefixCount,
366
- );
367
- return [
368
- ...activeTools.slice(0, nativePrefixCount),
369
- ...toAnthropicTools(activeTools.slice(nativePrefixCount)),
370
- ];
371
- }
372
- const deferredTools = deferredAnthropicTools(activeTools, messages, opts);
373
- return [
374
- ...nativeAnthropicTools(opts),
375
- ...toAnthropicTools([...activeTools, ...deferredTools]),
376
- ];
236
+ return sharedRequestAnthropicTools(tools, messages, opts, 'anthropic-oauth');
377
237
  }
378
238
 
379
239
  function toAnthropicMessages(messages) {
@@ -475,154 +335,8 @@ function toAnthropicMessages(messages) {
475
335
  // messageTtl === null disables the tail. BP3 (tier3) now rides a system block,
476
336
  // so it is no longer marked here.
477
337
  // ANTHROPIC_MSG_SLOTS=0 is honoured upstream by passing messageTtl = null.
478
- function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_VOLATILE, messageSlots = 1 } = {}) {
479
- if (!Array.isArray(sanitizedMessages) || sanitizedMessages.length === 0) {
480
- return sanitizedMessages;
481
- }
482
-
483
- const firstText = (content) => {
484
- if (typeof content === 'string') return content;
485
- if (Array.isArray(content)) {
486
- const first = content.find((b) => b?.type === 'text');
487
- return first && typeof first.text === 'string' ? first.text : '';
488
- }
489
- return '';
490
- };
491
- const isSystemReminder = (content) => firstText(content).startsWith('<system-reminder>');
492
-
493
- const markLast = (msg, ttl) => {
494
- if (!msg) return;
495
- msg.content = appendCacheControl(msg.content, ttl);
496
- };
497
- const ttlRank = (ttl) => ttl?.ttl === '1h' ? 2 : 1;
498
- const canMarkMessageIdx = (idx) => {
499
- // System-reminder messages (volatileTail / roleSpecific BP4) vary
500
- // per-call, so never pin them with a 1h marker. The 1h system blocks
501
- // (BP1/BP2/BP3) already satisfy Anthropic's "1h before 5m" ordering.
502
- if (idx < 0) return false;
503
- const msg = sanitizedMessages[idx];
504
- if (ttlRank(messageTtl) > ttlRank(CACHE_TTL_VOLATILE)
505
- && isSystemReminder(msg?.content)) {
506
- return false;
507
- }
508
- return true;
509
- };
510
- const hasUserText = (msg) => {
511
- if (msg?.role !== 'user') return false;
512
- if (isSystemReminder(msg.content)) return false;
513
- if (typeof msg.content === 'string') return msg.content.trim().length > 0;
514
- if (!Array.isArray(msg.content)) return false;
515
- return msg.content.some(b => b?.type === 'text' && typeof b.text === 'string' && b.text.trim().length > 0);
516
- };
517
- const previousUserTextAnchorIdx = () => {
518
- // Prefer the user text turn before the current tail. In a normal
519
- // user->assistant->tool loop this is the last prompt that was already
520
- // present in the previous API request, so its prefix can overlap.
521
- const tailIdx = sanitizedMessages.length - 1;
522
- for (let i = tailIdx - 1; i >= 0; i--) {
523
- if (hasUserText(sanitizedMessages[i])) return i;
524
- }
525
- return -1;
526
- };
527
- const latestToolResultTailIdx = () => {
528
- // Claude/pi refs allow cache_control on tool_result blocks. Keep this
529
- // narrower than "last message" so a fresh user prompt or steering text
530
- // never becomes a 1h breakpoint.
531
- for (let i = sanitizedMessages.length - 1; i >= 0; i--) {
532
- const msg = sanitizedMessages[i];
533
- if (msg?.role !== 'user' || !Array.isArray(msg.content) || msg.content.length === 0) continue;
534
- const lastBlock = msg.content[msg.content.length - 1];
535
- if (lastBlock?.type === 'tool_result') return i;
536
- }
537
- return -1;
538
- };
539
-
540
- const firstRequestUserPromptIdx = () => {
541
- // Iteration-1 fallback: on the very first request a session has only the
542
- // current user prompt — no tool_result tail and no earlier user turn, so
543
- // both anchors above return -1 and NO message breakpoint is placed. That
544
- // left the whole tools+system prefix (~4.2k) uncached on iter1: nothing
545
- // was written, so iter2 re-sent it as a fresh full write instead of a
546
- // read hit. Anchor the current prompt's tail so the stable prefix is
547
- // cache-written on the first ask and read back on the next. Only used
548
- // when neither real anchor exists, so later turns still prefer the
549
- // tool_result / previous-user-text anchors (never the volatile new
550
- // prompt). Synthetic <system-reminder> turns are excluded by hasUserText.
551
- if (latestToolResultTailIdx() !== -1 || previousUserTextAnchorIdx() !== -1) return -1;
552
- const tailIdx = sanitizedMessages.length - 1;
553
- return hasUserText(sanitizedMessages[tailIdx]) ? tailIdx : -1;
554
- };
555
- if (messageTtl !== null) {
556
- const slots = Math.max(0, Math.min(4, Number(messageSlots) || 0));
557
- const marked = new Set();
558
- const candidates = [latestToolResultTailIdx(), previousUserTextAnchorIdx(), firstRequestUserPromptIdx()];
559
- for (const idx of candidates) {
560
- if (slots <= 0) break;
561
- if (idx < 0 || marked.has(idx) || !canMarkMessageIdx(idx)) continue;
562
- markLast(sanitizedMessages[idx], messageTtl);
563
- marked.add(idx);
564
- if (marked.size >= slots) break;
565
- }
566
- }
567
-
568
- return sanitizedMessages;
569
- }
570
-
571
- // --- SSE parser + midstream retry policy: extracted to anthropic-sse.mjs ---
572
-
573
338
  // --- Build request body ---
574
339
 
575
- function resolveCacheTtls(opts) {
576
- // Layered cache strategy — caller may override per-layer via opts.cacheStrategy.
577
- // Anthropic enforces: 1h entries must appear before 5m entries in the request.
578
- const strategy = opts.cacheStrategy || {};
579
- const pick = (layer, fallback) => {
580
- const v = strategy[layer];
581
- if (v === '1h') return CACHE_TTL_STABLE;
582
- if (v === '5m') return CACHE_TTL_VOLATILE;
583
- if (v === 'none') return null;
584
- return fallback;
585
- };
586
- // BP budget (4 total):
587
- // BP1 baseRules — 1h (shared tool policy + compact skill manifest)
588
- // BP2 stableSystem — 1h (role/system rules)
589
- // BP3 tier3 — 1h (sessionMarker: stable memory/meta body)
590
- // BP4 messages — 1h sliding tail (tool_result cache across iter)
591
- // tools BP is dropped — system BP covers the tools prefix via
592
- // Anthropic's prompt cache prefix semantics (order: tools → system
593
- // → messages).
594
- // tier3 defaults to 1h (stable) — sessionMarker content is stable per
595
- // memory/meta tuple and the BP slot is only spent when a 3rd
596
- // (cacheTier:'tier3') system block is actually present, so this default is
597
- // free for sessions that don't carry one. Previously null here meant any
598
- // caller that skipped agent runtime resolve (CLI, raw agent spawn)
599
- // silently lost the tier3 cache layer even though it supported one.
600
- const resolved = {
601
- tools: pick('tools', null),
602
- system: pick('system', CACHE_TTL_STABLE),
603
- tier3: pick('tier3', CACHE_TTL_STABLE),
604
- messages: pick('messages', CACHE_TTL_STABLE),
605
- };
606
- // A partial cacheStrategy override (e.g. {system:'5m'} while tier3/
607
- // messages default to '1h') can put a longer TTL after a shorter one in
608
- // request order, which Anthropic rejects: 1h breakpoints must all appear
609
- // before any 5m breakpoint. Normalize left-to-right in wire order
610
- // (system -> tier3 -> messages; tools is emitted before system and is
611
- // excluded from the run) so a later layer is downgraded to the earliest
612
- // shorter TTL seen so far — never re-promoted. Layers set to null ('none')
613
- // emit no breakpoint at all, so they neither violate nor constrain
614
- // ordering and are skipped.
615
- const ttlRank = (ttl) => (ttl === CACHE_TTL_STABLE ? 2 : 1); // 1h=2, 5m=1
616
- let minRank = Infinity;
617
- for (const layer of ['system', 'tier3', 'messages']) {
618
- if (!resolved[layer]) continue;
619
- const rank = ttlRank(resolved[layer]);
620
- if (rank > minRank) resolved[layer] = CACHE_TTL_VOLATILE;
621
- else minRank = rank;
622
- }
623
- return resolved;
624
- }
625
-
626
340
  // BP3 (tier3) is injected by session/manager as its own `system` role block —
627
341
  // the 3rd system block, tagged `cacheTier:'tier3'`. buildSystemBlocks applies
628
342
  // the tier3 1h cache_control to that block; BP1/BP2 take the system TTL. No
@@ -1444,5 +1158,5 @@ export const _test = {
1444
1158
  resolveMaxTokens,
1445
1159
  deferredAnthropicTools,
1446
1160
  requestAnthropicTools,
1447
- sanitizeInputSchema: _sanitizeInputSchema,
1161
+ sanitizeInputSchema: (schema, toolName) => sanitizeAnthropicInputSchema(schema, toolName, 'anthropic-oauth'),
1448
1162
  };
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * anthropic-sse.mjs — Anthropic SSE stream parser + mid-stream retry policy.
3
3
  *
4
- * Extracted from anthropic-oauth.mjs. Shared by BOTH Anthropic providers:
5
- * anthropic-oauth.mjs (OAuth) and anthropic.mjs (API key) import
6
- * parseSSEStream / _classifyMidstreamError / ANTHROPIC_MAX_MIDSTREAM_RETRIES
7
- * from here (via the anthropic-oauth.mjs re-export for legacy paths).
4
+ * Shared by both Anthropic providers. anthropic-oauth.mjs retains its public
5
+ * re-exports for provider test and integration entry points.
8
6
  */
9
7
  import { randomBytes } from 'crypto';
10
8
  import {