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.
- package/package.json +1 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- 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
|
|
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,
|
|
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:'
|
|
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
|
-
*
|
|
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)
|
|
57
|
-
//
|
|
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
|
-
&&
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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 {
|