@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +1 -0
- package/README.md +124 -2
- package/THIRD_PARTY_NOTICES.md +66 -0
- package/ZCode-LICENSE +202 -0
- package/dist/index.mjs +4871 -0
- package/dist/native-command-plan.LICENSE.txt +221 -0
- package/dist/native-command-plan.json +16 -0
- package/dist/native-command-plan.mjs +8 -0
- package/dist/rpc-entry.mjs +5682 -0
- package/package.json +169 -4
- package/src/native/a2a-receipt-source.mjs +19 -0
- package/src/native/a2a-tools.mjs +517 -0
- package/src/native/activity-status.mjs +30 -0
- package/src/native/artifact-codec.mjs +481 -0
- package/src/native/artifact-store.mjs +1162 -0
- package/src/native/assistant-stream.mjs +76 -0
- package/src/native/background-command-port.mjs +95 -0
- package/src/native/browser-action-observation.mjs +43 -0
- package/src/native/browser-availability.mjs +22 -0
- package/src/native/browser-canvas-metrics.mjs +71 -0
- package/src/native/browser-dom.mjs +193 -0
- package/src/native/browser-playwright.mjs +1249 -0
- package/src/native/browser-vision.mjs +369 -0
- package/src/native/browser-workspace-policy.mjs +22 -0
- package/src/native/browser-workspace-protocol.mjs +95 -0
- package/src/native/browser-workspace-server.mjs +164 -0
- package/src/native/browser-workspace.mjs +355 -0
- package/src/native/captured-source.mjs +3 -0
- package/src/native/child-budget-allocation.mjs +87 -0
- package/src/native/child-capabilities.mjs +35 -0
- package/src/native/child-command-resource.mjs +20 -0
- package/src/native/child-history.mjs +188 -0
- package/src/native/child-merge.mjs +98 -0
- package/src/native/child-model-surface.mjs +19 -0
- package/src/native/child-observation.mjs +100 -0
- package/src/native/child-owner.mjs +620 -0
- package/src/native/child-publication.mjs +76 -0
- package/src/native/child-readers.mjs +366 -0
- package/src/native/child-tools.mjs +46 -0
- package/src/native/child-workspace-binding.mjs +29 -0
- package/src/native/child-workspace-tools.mjs +1 -0
- package/src/native/child-workspaces.mjs +655 -0
- package/src/native/children.mjs +365 -0
- package/src/native/command-plan-worker.mjs +14 -0
- package/src/native/command-plan.mjs +31 -0
- package/src/native/command-progress.mjs +64 -0
- package/src/native/command-shell.mjs +9 -0
- package/src/native/command-workspace-observation.mjs +36 -0
- package/src/native/component-tools.mjs +280 -0
- package/src/native/context-counter.mjs +133 -0
- package/src/native/context-markers.mjs +51 -0
- package/src/native/context-profile.mjs +72 -0
- package/src/native/context-status.mjs +27 -0
- package/src/native/conversation-command-control.mjs +133 -0
- package/src/native/delivery-timeline.mjs +39 -0
- package/src/native/dsh-background-ownership.mjs +102 -0
- package/src/native/dsh-managed-ownership.mjs +23 -0
- package/src/native/dsh-turn-contract.mjs +3 -0
- package/src/native/dsh-turn-ownership.mjs +54 -0
- package/src/native/dsh-turn-runtime.mjs +498 -0
- package/src/native/e2b-background-supervisor.mjs +347 -0
- package/src/native/e2b-background.mjs +368 -0
- package/src/native/e2b-command-state.mjs +40 -0
- package/src/native/e2b-command-tools.mjs +486 -0
- package/src/native/e2b-sandbox-owner.mjs +227 -0
- package/src/native/hook-executor.mjs +249 -0
- package/src/native/instruction-assembly.mjs +60 -0
- package/src/native/instruction-context.mjs +54 -0
- package/src/native/local-background-supervisor.mjs +108 -0
- package/src/native/local-background.mjs +207 -0
- package/src/native/local-command-evidence.mjs +57 -0
- package/src/native/local-command-policy-worker.mjs +197 -0
- package/src/native/local-command-policy.mjs +46 -0
- package/src/native/local-command-state.mjs +177 -0
- package/src/native/local-command-tools.mjs +302 -0
- package/src/native/local-execution-profile.mjs +36 -0
- package/src/native/local-output-policy.mjs +92 -0
- package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
- package/src/native/local-process-bin/darwin-x64/launcher +0 -0
- package/src/native/local-process-bin/linux-arm64/launcher +0 -0
- package/src/native/local-process-bin/linux-x64/launcher +0 -0
- package/src/native/local-process-launcher.c +183 -0
- package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
- package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
- package/src/native/local-process-licenses/NOTICE +21 -0
- package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
- package/src/native/local-process-manifest.json +32 -0
- package/src/native/local-process.mjs +246 -0
- package/src/native/local-resource-profile.mjs +32 -0
- package/src/native/memory-candidate.mjs +121 -0
- package/src/native/memory-extraction-run.mjs +60 -0
- package/src/native/memory-extraction-tools.mjs +116 -0
- package/src/native/memory-recall.mjs +65 -0
- package/src/native/memory-store.mjs +334 -0
- package/src/native/memory-tools.mjs +329 -0
- package/src/native/model-anthropic-cache.mjs +49 -0
- package/src/native/model-anthropic-tool-search.mjs +85 -0
- package/src/native/model-anthropic.mjs +753 -0
- package/src/native/model-chat-usage.mjs +24 -0
- package/src/native/model-inline-reasoning.mjs +111 -0
- package/src/native/model-media.mjs +174 -0
- package/src/native/model-normalize.mjs +228 -0
- package/src/native/model-openai-chat.mjs +561 -0
- package/src/native/model-port.mjs +361 -0
- package/src/native/model-provider-compat.mjs +129 -0
- package/src/native/model-replay.mjs +153 -0
- package/src/native/model-request.mjs +15 -0
- package/src/native/model-responses.mjs +1205 -0
- package/src/native/model-sse.mjs +171 -0
- package/src/native/model-surface-host.mjs +27 -0
- package/src/native/model-tool-search-policy.mjs +16 -0
- package/src/native/model-transport-observation.mjs +27 -0
- package/src/native/owned-child.mjs +114 -0
- package/src/native/owned-command.mjs +106 -0
- package/src/native/python-syntax-worker.mjs +60 -0
- package/src/native/python-syntax.mjs +50 -0
- package/src/native/read-interruption.mjs +16 -0
- package/src/native/result-stream.mjs +63 -0
- package/src/native/ruff-ast.mjs +95 -0
- package/src/native/run-observation.mjs +128 -0
- package/src/native/session-delivery-journal.mjs +96 -0
- package/src/native/session-delivery.mjs +434 -0
- package/src/native/shell-analysis.mjs +50 -0
- package/src/native/skill-disclosure.mjs +404 -0
- package/src/native/skill-events.mjs +164 -0
- package/src/native/skill-tools.mjs +813 -0
- package/src/native/source-page.mjs +73 -0
- package/src/native/sqlite-session-inbox.mjs +42 -0
- package/src/native/sqlite-store.mjs +617 -0
- package/src/native/tool-authority.mjs +99 -0
- package/src/native/tool-guidance.mjs +12 -0
- package/src/native/tool-observation.mjs +75 -0
- package/src/native/tool-states.mjs +14 -0
- package/src/native/verification-authority.mjs +37 -0
- package/src/native/web-tools.mjs +643 -0
- package/src/native/workspace-checkpoints.mjs +312 -0
- package/src/native/workspace-context.mjs +61 -0
- package/src/native/workspace-display.mjs +39 -0
- package/src/native/workspace-edit-match.mjs +19 -0
- package/src/native/workspace-edit-worker.mjs +15 -0
- package/src/native/workspace-editing.mjs +447 -0
- package/src/native/workspace-image.mjs +72 -0
- package/src/native/workspace-instructions.mjs +107 -0
- package/src/native/workspace-mutation-lock.mjs +74 -0
- package/src/native/workspace-patch-worker.mjs +17 -0
- package/src/native/workspace-patch.mjs +17 -0
- package/src/native/workspace-paths.mjs +225 -0
- package/src/native/workspace-state.mjs +272 -0
- package/src/native/workspace-subject-observer.mjs +84 -0
- package/src/native/workspace-tools.mjs +1740 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
// Anthropic Messages wire adapter.
|
|
2
|
+
//
|
|
3
|
+
// Ports ksadk.studio.model_client._stream_anthropic / model_buffered_wire /
|
|
4
|
+
// model_wire_tools / model_request_projection.anthropic_messages: one HTTP
|
|
5
|
+
// request per attempt over the Anthropic Messages SSE protocol, with sealed
|
|
6
|
+
// content-block termination, signed-thinking + tool_use assembly, usage
|
|
7
|
+
// accumulation and refusal normalization. The adapter never retries and never
|
|
8
|
+
// executes tools; it emits text/thinking/tool_delta display deltas and returns
|
|
9
|
+
// one sealed ModelResult with optional anthropic_content replay.
|
|
10
|
+
//
|
|
11
|
+
// The signed-thinking signature is private replay material: it is captured for
|
|
12
|
+
// the sealed providerReplay but never emitted in a public stream event's
|
|
13
|
+
// metadata, and never concatenated to visible text.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
normalizeResponse, parseAnthropicUsageFragment, parseRetryAfterMs,
|
|
17
|
+
ProtocolError, resolveUrl, boundOutputTokens,
|
|
18
|
+
} from './model-normalize.mjs';
|
|
19
|
+
import { sealAnthropicReplay, anthropicReplayContent, profileFingerprint } from './model-replay.mjs';
|
|
20
|
+
import { validateMediaBlock, projectMediaAnthropic, MediaError } from './model-media.mjs';
|
|
21
|
+
// parseModelJson is the kernel's strict finite JSON parser (rejects duplicate
|
|
22
|
+
// keys and non-finite numbers); using it keeps provider-envelope and tool-input
|
|
23
|
+
// decoding consistent with the kernel's own validation.
|
|
24
|
+
import { parseModelJson, stringifyModelJson, parseNativeJson, modelWireToolCall, FINAL_SUBMISSION_TOOL } from '@kingsoftcloud/agentkit-harness-core';
|
|
25
|
+
import { applyAnthropicThinking, projectProviderToolChoice, resolveProviderCompatibility } from './model-provider-compat.mjs';
|
|
26
|
+
import { projectAnthropicPromptCache } from './model-anthropic-cache.mjs';
|
|
27
|
+
import {
|
|
28
|
+
anthropicToolEntry, isNativeToolSearch, mergeNativeToolSearchBeta, nativeToolReferenceContent,
|
|
29
|
+
} from './model-anthropic-tool-search.mjs';
|
|
30
|
+
|
|
31
|
+
function strictParseJson(text) {
|
|
32
|
+
try { return parseModelJson(text); }
|
|
33
|
+
catch (error) { throw new ProtocolError(error?.code || 'invalid_model_json', 'json_invalid'); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
37
|
+
|
|
38
|
+
const FORBIDDEN_BODY_KEYS = new Set([
|
|
39
|
+
'model', 'messages', 'tools', 'stream', 'system', 'tool_choice', 'max_tokens',
|
|
40
|
+
'provider_thinking_display', 'provider_tool_search',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const CLIENT_ITEM_TYPES = new Set([
|
|
44
|
+
'text', 'thinking', 'redacted_thinking', 'tool_use',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// In-stream error.type → retryable/terminal classification. A 2xx stream that
|
|
48
|
+
// emits an error event is a provider rejection, not a final; unknown types
|
|
49
|
+
// stay terminal so a foreign payload cannot become a silent retry or final.
|
|
50
|
+
const RETRYABLE_ERROR_TYPES = new Set([
|
|
51
|
+
'rate_limit_error', 'overloaded_error', 'api_error', 'service_unavailable_error',
|
|
52
|
+
]);
|
|
53
|
+
const TERMINAL_ERROR_TYPES = new Set([
|
|
54
|
+
'invalid_request_error', 'authentication_error', 'permission_error', 'not_found_error',
|
|
55
|
+
'request_too_large', 'billing_error',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function securityHeaders() {
|
|
59
|
+
return new Set(['authorization', 'x-api-key', 'anthropic-version', 'content-type', 'accept']);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validateAnthropicBinding(binding) {
|
|
63
|
+
if (!binding || typeof binding !== 'object') throw new ProtocolError('anthropic binding missing');
|
|
64
|
+
if (binding.protocol !== 'anthropic-messages') throw new ProtocolError('binding is not anthropic-messages');
|
|
65
|
+
if (typeof binding.baseUrl !== 'string' || !binding.baseUrl) throw new ProtocolError('anthropic binding requires baseUrl');
|
|
66
|
+
if (typeof binding.apiKey !== 'string' || !binding.apiKey) throw new ProtocolError('anthropic binding requires apiKey');
|
|
67
|
+
if (typeof binding.model !== 'string' || !binding.model.trim()) throw new ProtocolError('anthropic binding requires model');
|
|
68
|
+
try { new URL(binding.baseUrl); } catch { throw new ProtocolError('anthropic binding baseUrl is not a URL'); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildAnthropicRequest(binding, request) {
|
|
72
|
+
validateAnthropicBinding(binding);
|
|
73
|
+
const profile = request.profile;
|
|
74
|
+
const native = isNativeToolSearch(profile) && !['compaction', 'delivery_review'].includes(request.purpose);
|
|
75
|
+
const url = resolveUrl(binding.baseUrl, 'messages');
|
|
76
|
+
|
|
77
|
+
const headers = { Accept: profile.streaming === false ? 'application/json' : 'text/event-stream' };
|
|
78
|
+
const skip = securityHeaders();
|
|
79
|
+
for (const [name, value] of Object.entries(binding.headers || {})) {
|
|
80
|
+
if (skip.has(name.toLowerCase())) continue;
|
|
81
|
+
if (typeof value !== 'string') continue;
|
|
82
|
+
headers[name] = value;
|
|
83
|
+
}
|
|
84
|
+
headers['Content-Type'] = 'application/json';
|
|
85
|
+
headers['x-api-key'] = binding.apiKey;
|
|
86
|
+
headers['anthropic-version'] = ANTHROPIC_VERSION;
|
|
87
|
+
// Preserve a host-selected thinking block-binding beta header if provided.
|
|
88
|
+
const beta = (binding.headers && binding.headers['anthropic-beta']) || binding.headers?.['Anthropic-Beta'];
|
|
89
|
+
if (beta && !('anthropic-beta' in headers)) headers['anthropic-beta'] = beta;
|
|
90
|
+
|
|
91
|
+
const { systemParts, items } = projectAnthropicMessages(request, request.maxInlineImageBytes);
|
|
92
|
+
const system = systemParts.join('\n\n');
|
|
93
|
+
const payload = {
|
|
94
|
+
model: binding.model,
|
|
95
|
+
messages: items,
|
|
96
|
+
stream: profile.streaming ?? true,
|
|
97
|
+
max_tokens: boundOutputTokens(binding.parameters?.max_tokens, profile.maxOutputTokens),
|
|
98
|
+
};
|
|
99
|
+
if (system) payload.system = system;
|
|
100
|
+
|
|
101
|
+
const parameters = binding.parameters || {};
|
|
102
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
103
|
+
if (FORBIDDEN_BODY_KEYS.has(key)) continue;
|
|
104
|
+
payload[key] = value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Advertise tools: during a finalization-only turn the kernel has already
|
|
108
|
+
// stripped normal workspace tools, but the adapter filters defensively and
|
|
109
|
+
// keeps the internal submit_final_output tool so the model can submit its
|
|
110
|
+
// final answer. Normal tools are never sent during finalization.
|
|
111
|
+
const tools = request.tools || [];
|
|
112
|
+
const advertised = request.finalizationOnly && profile.toolCatalogPolicy !== 'stable-task/v1' && !native
|
|
113
|
+
? tools.filter((tool) => tool.name === FINAL_SUBMISSION_TOOL)
|
|
114
|
+
: tools;
|
|
115
|
+
if (advertised.length) {
|
|
116
|
+
payload.tools = advertised.map((tool) => (native ? anthropicToolEntry(tool) : {
|
|
117
|
+
name: tool.name,
|
|
118
|
+
description: tool.description,
|
|
119
|
+
input_schema: tool.parameters || { type: 'object', properties: {} },
|
|
120
|
+
}));
|
|
121
|
+
const choice = projectProviderToolChoice(request.toolChoice ?? (profile.providerCompatibilityPolicy && request.finalizationOnly
|
|
122
|
+
&& advertised.some(tool => tool.name === FINAL_SUBMISSION_TOOL)
|
|
123
|
+
? { type: 'function', function: { name: FINAL_SUBMISSION_TOOL } } : 'auto'),
|
|
124
|
+
resolveProviderCompatibility(profile.providerCompatibilityPolicy, binding));
|
|
125
|
+
payload.tool_choice = typeof choice === 'string' ? { type: choice === 'required' ? 'any' : choice }
|
|
126
|
+
: { type: 'tool', name: choice.function.name };
|
|
127
|
+
}
|
|
128
|
+
if (profile.temperature !== undefined) payload.temperature = profile.temperature;
|
|
129
|
+
if (profile.topP !== undefined) payload.top_p = profile.topP;
|
|
130
|
+
if (profile.parallelToolCalls !== undefined && payload.tool_choice) payload.tool_choice.disable_parallel_tool_use = !profile.parallelToolCalls;
|
|
131
|
+
|
|
132
|
+
const projected = applyAnthropicThinking(payload, profile, binding);
|
|
133
|
+
if (projected.thinking?.block_binding) {
|
|
134
|
+
const keys = Object.keys(headers).filter(key => key.toLowerCase() === 'anthropic-beta');
|
|
135
|
+
const betas = new Set(keys.flatMap(key => headers[key].split(',').map(value => value.trim()).filter(Boolean)));
|
|
136
|
+
keys.forEach(key => delete headers[key]);
|
|
137
|
+
betas.add('thinking-binding-controls-2026-08-01');
|
|
138
|
+
headers['anthropic-beta'] = [...betas].join(',');
|
|
139
|
+
}
|
|
140
|
+
// Native deferred-tool-search advertises defer_loading; add its beta once,
|
|
141
|
+
// case-insensitive-deduped against any Host-selected betas above.
|
|
142
|
+
if (native) mergeNativeToolSearchBeta(headers);
|
|
143
|
+
return { url, headers, body: stringifyModelJson(projectAnthropicPromptCache(projected, systemParts, request)) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Project host messages onto the Anthropic Messages wire. System messages are
|
|
147
|
+
// lifted to the top-level `system` field; user/assistant alternate (consecutive
|
|
148
|
+
// same-role content is concatenated); tool receipts become user tool_result
|
|
149
|
+
// blocks. Assistant anthropic_content replay replaces the projected blocks so
|
|
150
|
+
// the model sees its signed thinking and tool_use exactly.
|
|
151
|
+
export function projectMessagesAnthropic(request, maxInlineImageBytes) {
|
|
152
|
+
const { systemParts, items } = projectAnthropicMessages(request, maxInlineImageBytes);
|
|
153
|
+
return { system: systemParts.join('\n\n'), items };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function projectAnthropicMessages(request, maxInlineImageBytes) {
|
|
157
|
+
const profile = request.profile;
|
|
158
|
+
// Summary/review models receive source prose, not task tool capabilities.
|
|
159
|
+
const native = isNativeToolSearch(profile) && !['compaction', 'delivery_review'].includes(request.purpose);
|
|
160
|
+
const systemParts = [];
|
|
161
|
+
const items = [];
|
|
162
|
+
const pending = new Set();
|
|
163
|
+
// Pending call name per receipt callId, so native tool_reference metadata
|
|
164
|
+
// pairs only with a ToolSearch call. The wire id still keys the receipt.
|
|
165
|
+
const pendingNames = new Map();
|
|
166
|
+
const wireIds = new Map();
|
|
167
|
+
const midSystem = profile.anthropicPolicy?.midSystem === true;
|
|
168
|
+
for (const message of request.messages || []) {
|
|
169
|
+
const role = message.role;
|
|
170
|
+
if (native && message.discoveredToolNames !== undefined && role !== 'tool')
|
|
171
|
+
throw new ProtocolError('tool_reference metadata requires a paired tool receipt');
|
|
172
|
+
if (role !== 'tool' && pending.size) throw new ProtocolError('incomplete assistant tool batch before next message');
|
|
173
|
+
if (role === 'assistant' && message.toolCalls?.length) {
|
|
174
|
+
validateAssistantToolIds(message.toolCalls);
|
|
175
|
+
for (const call of message.toolCalls) {
|
|
176
|
+
pending.add(call.callId);
|
|
177
|
+
pendingNames.set(call.callId, modelWireToolCall(call).name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
let pairedName;
|
|
181
|
+
if (role === 'tool') {
|
|
182
|
+
if (!pending.delete(message.callId)) throw new ProtocolError('tool receipt does not match the pending assistant batch');
|
|
183
|
+
pairedName = pendingNames.get(message.callId);
|
|
184
|
+
pendingNames.delete(message.callId);
|
|
185
|
+
}
|
|
186
|
+
if (role === 'system' || role === 'developer') {
|
|
187
|
+
if (midSystem && items.length) items.push({ role: 'system', content: message.content || '' });
|
|
188
|
+
else systemParts.push(message.content || '');
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let item;
|
|
192
|
+
if (role === 'tool') {
|
|
193
|
+
// Native deferred-tool-search: project tool_reference blocks only from
|
|
194
|
+
// kernel-owned discoveredToolNames on a ToolSearch receipt. Undefined
|
|
195
|
+
// (no metadata) or empty names keep the ordinary text/media projection;
|
|
196
|
+
// malformed or unpaired metadata fails preflight rather than widening
|
|
197
|
+
// authority. Non-native policy ignores the metadata entirely.
|
|
198
|
+
const references = native ? nativeToolReferenceContent(message, pairedName, request.tools) : undefined;
|
|
199
|
+
const media = message.media ? message.media.map((b) => validateMediaBlock(b, maxInlineImageBytes)) : null;
|
|
200
|
+
let content;
|
|
201
|
+
if (references) content = [...references, ...(media?.length ? projectMediaAnthropic(media) : [])];
|
|
202
|
+
else if (media && media.length) content = [{ type: 'text', text: message.content || '' }, ...projectMediaAnthropic(media)];
|
|
203
|
+
else content = (message.content || '');
|
|
204
|
+
item = {
|
|
205
|
+
role: 'user',
|
|
206
|
+
content: [{
|
|
207
|
+
type: 'tool_result',
|
|
208
|
+
tool_use_id: wireIds.get(message.callId) ?? String(message.callId ?? ''),
|
|
209
|
+
content,
|
|
210
|
+
}],
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
// anthropicReplayContent applies message.replayToolIds to the original
|
|
214
|
+
// tool_use ids and validates the blocks pair exactly with the sealed
|
|
215
|
+
// toolCalls/text; a cross-profile or inconsistent replay is dropped.
|
|
216
|
+
const replayContent = anthropicReplayContent(message, profile);
|
|
217
|
+
let blocks;
|
|
218
|
+
if (replayContent) {
|
|
219
|
+
// Bound replay keeps original provider ids, including signed tool_use blocks.
|
|
220
|
+
if (profile.providerReplayProjectionPolicy === 'anthropic-client-compaction/v1') {
|
|
221
|
+
const originals = message.providerReplay.anthropicContent.filter(block => block?.type === 'tool_use');
|
|
222
|
+
for (const [index, block] of originals.entries()) wireIds.set(message.toolCalls[index].callId, block.id);
|
|
223
|
+
const inverse = new Map([...wireIds].map(([canonical, original]) => [canonical, original]));
|
|
224
|
+
blocks = replayContent.map(block => block?.type === 'tool_use' ? { ...block, id: inverse.get(block.id) ?? block.id } : block);
|
|
225
|
+
} else blocks = replayContent;
|
|
226
|
+
} else {
|
|
227
|
+
blocks = [];
|
|
228
|
+
const media = message.media ? message.media.map((b) => validateMediaBlock(b, maxInlineImageBytes)) : null;
|
|
229
|
+
if ((message.content || '').length) blocks.push({ type: 'text', text: message.content });
|
|
230
|
+
if (media && media.length) {
|
|
231
|
+
if (role !== 'user') throw new MediaError('image observations require user or tool content');
|
|
232
|
+
blocks.push(...projectMediaAnthropic(media));
|
|
233
|
+
}
|
|
234
|
+
if (role === 'assistant' && message.toolCalls && message.toolCalls.length) {
|
|
235
|
+
validateAssistantToolIds(message.toolCalls);
|
|
236
|
+
for (const call of message.toolCalls.map(modelWireToolCall)) {
|
|
237
|
+
blocks.push({ type: 'tool_use', id: call.callId, name: call.name, input: call.arguments || {} });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!blocks.length) blocks.push({ type: 'text', text: '' });
|
|
241
|
+
}
|
|
242
|
+
item = { role, content: blocks };
|
|
243
|
+
}
|
|
244
|
+
const previous = items[items.length - 1];
|
|
245
|
+
if (previous && previous.role === item.role) {
|
|
246
|
+
previous.content = mergeAnthropicContent(previous.content, item.content);
|
|
247
|
+
} else {
|
|
248
|
+
items.push(item);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (pending.size) throw new ProtocolError('assistant tool batch is missing receipts');
|
|
252
|
+
if (midSystem) for (let index = 0; index < items.length; index++) {
|
|
253
|
+
if (items[index].role !== 'system') continue;
|
|
254
|
+
if (items[index - 1]?.role !== 'user') throw new ProtocolError('mid-conversation system section must follow user or tool results');
|
|
255
|
+
while (items[index + 1]?.role === 'system') index++;
|
|
256
|
+
if (items[index + 1] && items[index + 1].role !== 'assistant') throw new ProtocolError('mid-conversation system section must precede assistant or end of input');
|
|
257
|
+
}
|
|
258
|
+
return { systemParts, items };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function mergeAnthropicContent(first, second) {
|
|
262
|
+
const a = Array.isArray(first) ? first : [{ type: 'text', text: first }];
|
|
263
|
+
const b = Array.isArray(second) ? second : [{ type: 'text', text: second }];
|
|
264
|
+
return [...a, ...b];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function validateAssistantToolIds(calls) {
|
|
268
|
+
const ids = [];
|
|
269
|
+
for (const call of calls) {
|
|
270
|
+
const id = String(call?.callId ?? '');
|
|
271
|
+
if (!id.trim()) throw new ProtocolError('assistant tool calls require stable IDs');
|
|
272
|
+
ids.push(id);
|
|
273
|
+
}
|
|
274
|
+
if (new Set(ids).size !== ids.length) throw new ProtocolError('duplicate assistant tool IDs');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Parse a buffered 2xx Anthropic Messages body into a sealed result.
|
|
278
|
+
export function parseAnthropicBuffered(bodyText, request, { captureReplay = false, onThinking } = {}) {
|
|
279
|
+
let payload;
|
|
280
|
+
try { payload = strictParseJson(bodyText); } catch { throw new ProtocolError('invalid response json', 'json_invalid'); }
|
|
281
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
282
|
+
throw new ProtocolError('invalid response envelope', 'envelope_invalid');
|
|
283
|
+
}
|
|
284
|
+
if ('type' in payload && payload.type !== 'message') throw new ProtocolError('invalid response envelope', 'envelope_invalid');
|
|
285
|
+
if ('error' in payload) throw new ProtocolError('invalid response envelope', 'envelope_invalid');
|
|
286
|
+
if (typeof payload.stop_sequence !== 'undefined' && payload.stop_sequence !== null && typeof payload.stop_sequence !== 'string') {
|
|
287
|
+
throw new ProtocolError('invalid stop_sequence');
|
|
288
|
+
}
|
|
289
|
+
const blocks = payload.content;
|
|
290
|
+
if (!Array.isArray(blocks)) throw new ProtocolError('anthropic content must be an array');
|
|
291
|
+
let text = '';
|
|
292
|
+
const reasoningBlocks = [];
|
|
293
|
+
const replayBlocks = [];
|
|
294
|
+
const calls = [];
|
|
295
|
+
for (const block of blocks) {
|
|
296
|
+
if (!block || typeof block !== 'object') throw new ProtocolError('anthropic content requires typed blocks');
|
|
297
|
+
const kind = String(block.type);
|
|
298
|
+
if (!CLIENT_ITEM_TYPES.has(kind)) throw new ProtocolError('unsupported anthropic block type');
|
|
299
|
+
if (kind === 'text') {
|
|
300
|
+
if (typeof block.text !== 'string') throw new ProtocolError('invalid text block');
|
|
301
|
+
text += block.text;
|
|
302
|
+
replayBlocks.push({ ...block });
|
|
303
|
+
} else if (kind === 'thinking') {
|
|
304
|
+
if (typeof block.thinking !== 'string') throw new ProtocolError('invalid thinking block');
|
|
305
|
+
if ('signature' in block && typeof block.signature !== 'string') throw new ProtocolError('invalid thinking signature');
|
|
306
|
+
reasoningBlocks.push(block);
|
|
307
|
+
replayBlocks.push({ ...block });
|
|
308
|
+
} else if (kind === 'redacted_thinking') {
|
|
309
|
+
if (typeof block.data !== 'string') throw new ProtocolError('invalid redacted thinking block');
|
|
310
|
+
replayBlocks.push({ ...block });
|
|
311
|
+
} else if (kind === 'tool_use') {
|
|
312
|
+
if (typeof block.id !== 'string' || !block.id.trim()) throw new ProtocolError('invalid tool block id');
|
|
313
|
+
if (typeof block.name !== 'string' || !block.name.trim()) throw new ProtocolError('invalid tool block name');
|
|
314
|
+
if (block.input == null || typeof block.input !== 'object' || Array.isArray(block.input)) {
|
|
315
|
+
throw new ProtocolError('invalid tool block input');
|
|
316
|
+
}
|
|
317
|
+
calls.push({ callId: block.id, name: block.name, arguments: block.input });
|
|
318
|
+
replayBlocks.push({ ...block });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
let stopReason = payload.stop_reason;
|
|
322
|
+
if (stopReason != null && typeof stopReason !== 'string') throw new ProtocolError('invalid stop_reason');
|
|
323
|
+
if (anthropicRefusal(payload.stop_details)) {
|
|
324
|
+
stopReason = 'refusal';
|
|
325
|
+
calls.length = 0;
|
|
326
|
+
}
|
|
327
|
+
const finish = mapStopReason(stopReason);
|
|
328
|
+
const counters = payload.usage == null ? {} : parseAnthropicUsageFragment(payload.usage);
|
|
329
|
+
const normalized = normalizeResponse({ text, toolCalls: calls, finishReason: finish, streamComplete: true });
|
|
330
|
+
const result = { ...normalized, usage: buildAnthropicUsage(counters), usageReported: hasCompleteUsage(counters) };
|
|
331
|
+
const sealed = captureReplay ? sealAnthropicReplay(result, request.profile, replayBlocks) : result;
|
|
332
|
+
onThinking?.(reasoningBlocks.map(block => block.thinking).join(''));
|
|
333
|
+
return sealed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function mapStopReason(stopReason) {
|
|
337
|
+
return ({
|
|
338
|
+
end_turn: 'stop', stop_sequence: 'stop', max_tokens: 'length',
|
|
339
|
+
tool_use: 'tool_calls', refusal: 'content_filter',
|
|
340
|
+
})[stopReason] || stopReason || null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function anthropicRefusal(details) {
|
|
344
|
+
if (details == null) return false;
|
|
345
|
+
if (typeof details !== 'object' || Array.isArray(details) || details.type !== 'refusal') {
|
|
346
|
+
throw new ProtocolError('unsupported model stop details');
|
|
347
|
+
}
|
|
348
|
+
for (const field of ['category', 'explanation']) {
|
|
349
|
+
if (details[field] != null && typeof details[field] !== 'string') {
|
|
350
|
+
throw new ProtocolError('invalid model stop details field');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Accumulate Anthropic usage across message_start (input_tokens) and
|
|
357
|
+
// message_delta (output_tokens). The state tracks the two counters and a
|
|
358
|
+
// pair of primary counters so genuine zeroes stay distinct from a partial report.
|
|
359
|
+
function buildAnthropicUsage(state) {
|
|
360
|
+
const inputTokens = (state.inputTokens ?? 0) + (state.cachedInputTokens ?? 0) + (state.cacheCreationInputTokens ?? 0);
|
|
361
|
+
if (!Number.isSafeInteger(inputTokens)) throw new ProtocolError('unsafe inclusive input token counter');
|
|
362
|
+
const usage = {
|
|
363
|
+
inputTokens,
|
|
364
|
+
outputTokens: state.outputTokens ?? 0,
|
|
365
|
+
modelCalls: 0, toolCalls: 0, cost: 0,
|
|
366
|
+
};
|
|
367
|
+
if (state.cachedInputTokens != null) usage.cachedInputTokens = state.cachedInputTokens;
|
|
368
|
+
if (state.cacheCreationInputTokens != null) usage.cacheWriteInputTokens = state.cacheCreationInputTokens;
|
|
369
|
+
if (state.reasoningTokens != null) usage.reasoningTokens = state.reasoningTokens;
|
|
370
|
+
return usage;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function hasCompleteUsage(state) {
|
|
374
|
+
return state.inputTokens != null && state.outputTokens != null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function mergeUsage(state, value) {
|
|
378
|
+
const fragment = parseAnthropicUsageFragment(value);
|
|
379
|
+
buildAnthropicUsage({ ...state, ...fragment });
|
|
380
|
+
Object.assign(state, fragment);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Classify an in-stream `error` event into a retryable/terminal result.
|
|
384
|
+
export function classifyAnthropicStreamError(errorObj) {
|
|
385
|
+
const type = errorObj && typeof errorObj === 'object' ? String(errorObj.type || '') : '';
|
|
386
|
+
if (RETRYABLE_ERROR_TYPES.has(type)) {
|
|
387
|
+
return { kind: 'provider_retryable_error', failureKind: type === 'rate_limit_error' ? 'rate_limit' : 'server_error' };
|
|
388
|
+
}
|
|
389
|
+
if (TERMINAL_ERROR_TYPES.has(type)) {
|
|
390
|
+
return { kind: 'provider_terminal_error', failureKind: 'client_error' };
|
|
391
|
+
}
|
|
392
|
+
return { kind: 'provider_terminal_error', failureKind: 'invalid_response' };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Streaming state for the Anthropic Messages SSE protocol. Accumulates text,
|
|
396
|
+
// thinking (with signatures), an assembled tool batch and usage across
|
|
397
|
+
// content_block_* and message_* events. Frame errors throw ProtocolError.
|
|
398
|
+
export function createAnthropicStreamState(profile) {
|
|
399
|
+
const captureReplay = profile.providerReplayMode !== 'none';
|
|
400
|
+
const emitThinking = profile.providerThinkingDisplay ?? profile.providerReplayMode !== 'none';
|
|
401
|
+
return {
|
|
402
|
+
text: '',
|
|
403
|
+
reasoning: '',
|
|
404
|
+
blockTypes: new Map(), // index -> type
|
|
405
|
+
closed: new Set(), // closed block indexes
|
|
406
|
+
values: new Map(), // index -> accumulated text/thinking string
|
|
407
|
+
signatures: new Map(), // index -> accumulated signature (thinking)
|
|
408
|
+
originals: new Map(), // index -> original content block (for replay)
|
|
409
|
+
tools: new Map(), // index -> { callId, name, arguments, closed, argumentsDone, initialInput }
|
|
410
|
+
initialInputs: new Set(),
|
|
411
|
+
implicitIndexes: new Set(),
|
|
412
|
+
stopReason: null,
|
|
413
|
+
refused: false,
|
|
414
|
+
inputTokens: null,
|
|
415
|
+
outputTokens: null,
|
|
416
|
+
cachedInputTokens: null,
|
|
417
|
+
cacheCreationInputTokens: null,
|
|
418
|
+
reasoningTokens: null,
|
|
419
|
+
done: false,
|
|
420
|
+
contentFinished: false,
|
|
421
|
+
model: null,
|
|
422
|
+
responseId: null,
|
|
423
|
+
|
|
424
|
+
consume(frame, emit) {
|
|
425
|
+
const data = frame.data;
|
|
426
|
+
if (!data.trim()) return;
|
|
427
|
+
if (data.trim() === '[DONE]') return;
|
|
428
|
+
let event;
|
|
429
|
+
try { event = strictParseJson(data); } catch { throw new ProtocolError('invalid stream json', 'json_invalid'); }
|
|
430
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
431
|
+
throw new ProtocolError('invalid stream envelope', 'envelope_invalid');
|
|
432
|
+
}
|
|
433
|
+
const type = String(event.type || '');
|
|
434
|
+
if (this.contentFinished && (type === 'content_block_start' || type === 'content_block_delta')) {
|
|
435
|
+
throw new ProtocolError('content after finish reason');
|
|
436
|
+
}
|
|
437
|
+
if (type === 'error') {
|
|
438
|
+
throw Object.assign(new ProtocolError('anthropic stream error'), {
|
|
439
|
+
streamError: classifyAnthropicStreamError(event.error),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (type === 'ping') return;
|
|
443
|
+
if (type === 'message_start') this.handleMessageStart(event);
|
|
444
|
+
else if (type === 'content_block_start') this.handleBlockStart(event, emit);
|
|
445
|
+
else if (type === 'content_block_delta') this.handleBlockDelta(event, emit);
|
|
446
|
+
else if (type === 'content_block_stop') this.handleBlockStop(event, emit);
|
|
447
|
+
else if (type === 'message_delta') this.handleMessageDelta(event);
|
|
448
|
+
else if (type === 'message_stop') {
|
|
449
|
+
this.validateFinishedBlocks();
|
|
450
|
+
this.drainContent(emit);
|
|
451
|
+
this.done = true;
|
|
452
|
+
this.contentFinished = true;
|
|
453
|
+
}
|
|
454
|
+
// Unknown event types are ignored: the adapter validates the blocks it
|
|
455
|
+
// assembles rather than rejecting foreign extensions terminally.
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
handleMessageStart(event) {
|
|
459
|
+
const message = event.message;
|
|
460
|
+
if (!message || typeof message !== 'object') throw new ProtocolError('invalid message_start');
|
|
461
|
+
const role = message.role;
|
|
462
|
+
if (role != null && role !== 'assistant') throw new ProtocolError('invalid message role');
|
|
463
|
+
this.pinIdentity(message.id, message.model);
|
|
464
|
+
if (message.usage != null) {
|
|
465
|
+
mergeUsage(this, message.usage);
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
handleMessageDelta(event) {
|
|
470
|
+
const delta = event.delta;
|
|
471
|
+
if (!delta || typeof delta !== 'object') throw new ProtocolError('invalid message_delta');
|
|
472
|
+
if (delta.stop_reason != null) {
|
|
473
|
+
if (typeof delta.stop_reason !== 'string') throw new ProtocolError('invalid stop_reason');
|
|
474
|
+
if (this.stopReason != null && delta.stop_reason !== this.stopReason) {
|
|
475
|
+
throw new ProtocolError('conflicting stop reasons');
|
|
476
|
+
}
|
|
477
|
+
this.stopReason = delta.stop_reason;
|
|
478
|
+
this.contentFinished = true;
|
|
479
|
+
}
|
|
480
|
+
if (delta.stop_sequence != null && typeof delta.stop_sequence !== 'string') {
|
|
481
|
+
throw new ProtocolError('invalid stop_sequence');
|
|
482
|
+
}
|
|
483
|
+
if (delta.stop_details != null) {
|
|
484
|
+
if (anthropicRefusal(delta.stop_details)) this.refused = true;
|
|
485
|
+
}
|
|
486
|
+
if (event.usage != null) {
|
|
487
|
+
mergeUsage(this, event.usage);
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
handleBlockStart(event, emit) {
|
|
492
|
+
const block = event.content_block;
|
|
493
|
+
if (!block || typeof block !== 'object') throw new ProtocolError('invalid content_block');
|
|
494
|
+
const blockType = String(block.type);
|
|
495
|
+
if (!CLIENT_ITEM_TYPES.has(block.type)) {
|
|
496
|
+
if (blockType === 'server_tool_use' || blockType.endsWith('_tool_result')) {
|
|
497
|
+
throw new ProtocolError('unsupported hosted tool block');
|
|
498
|
+
}
|
|
499
|
+
throw new ProtocolError('unsupported content block type');
|
|
500
|
+
}
|
|
501
|
+
const index = event.index;
|
|
502
|
+
if (typeof index !== 'number' || !Number.isInteger(index) || index < 0) {
|
|
503
|
+
throw new ProtocolError('invalid block index');
|
|
504
|
+
}
|
|
505
|
+
if (this.blockTypes.has(index) || this.closed.has(index)) {
|
|
506
|
+
throw new ProtocolError('duplicate content block index');
|
|
507
|
+
}
|
|
508
|
+
this.blockTypes.set(index, blockType);
|
|
509
|
+
this.originals.set(index, deepCopy(block));
|
|
510
|
+
for (const field of ['text', 'thinking', 'signature']) {
|
|
511
|
+
if (field in block && typeof block[field] !== 'string') throw new ProtocolError('invalid block field');
|
|
512
|
+
}
|
|
513
|
+
if (blockType === 'text' || blockType === 'thinking') {
|
|
514
|
+
if (block[blockType] != null && typeof block[blockType] !== 'string') {
|
|
515
|
+
throw new ProtocolError(`invalid ${blockType} block`);
|
|
516
|
+
}
|
|
517
|
+
this.values.set(index, String(block[blockType] || ''));
|
|
518
|
+
} else if (blockType === 'redacted_thinking') {
|
|
519
|
+
if (typeof block.data !== 'string') throw new ProtocolError('invalid redacted thinking block');
|
|
520
|
+
} else if (blockType === 'tool_use') {
|
|
521
|
+
const tool = { callId: '', name: '', arguments: '', closed: false, argumentsDone: false, initialInput: false };
|
|
522
|
+
if (block.id != null) {
|
|
523
|
+
if (typeof block.id !== 'string' || !block.id.trim()) throw new ProtocolError('invalid tool block id');
|
|
524
|
+
tool.callId = block.id;
|
|
525
|
+
}
|
|
526
|
+
if (block.name != null) {
|
|
527
|
+
if (typeof block.name !== 'string' || !block.name.trim()) throw new ProtocolError('invalid tool block name');
|
|
528
|
+
tool.name = block.name;
|
|
529
|
+
}
|
|
530
|
+
if ('input' in block) {
|
|
531
|
+
if (block.input == null || typeof block.input !== 'object' || Array.isArray(block.input)) {
|
|
532
|
+
throw new ProtocolError('invalid tool block input');
|
|
533
|
+
}
|
|
534
|
+
// Only a non-empty initial input pins the block; an empty `input: {}`
|
|
535
|
+
// is the usual placeholder and still allows input_json_delta fragments.
|
|
536
|
+
if (Object.keys(block.input).length > 0) {
|
|
537
|
+
tool.arguments = stringifyModelJson(block.input);
|
|
538
|
+
tool.initialInput = true;
|
|
539
|
+
this.initialInputs.add(index);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
this.tools.set(index, tool);
|
|
543
|
+
}
|
|
544
|
+
this.drainContent(emit);
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
handleBlockDelta(event, emit) {
|
|
548
|
+
const delta = event.delta;
|
|
549
|
+
if (!delta || typeof delta !== 'object') throw new ProtocolError('invalid content_block_delta');
|
|
550
|
+
const deltaType = String(delta.type);
|
|
551
|
+
let index = event.index;
|
|
552
|
+
if (index == null && deltaType !== 'input_json_delta') index = 0;
|
|
553
|
+
if (typeof index !== 'number' || !Number.isInteger(index) || index < 0) {
|
|
554
|
+
throw new ProtocolError('invalid block index');
|
|
555
|
+
}
|
|
556
|
+
if (this.closed.has(index)) throw new ProtocolError('delta after closed block');
|
|
557
|
+
const expected = ({
|
|
558
|
+
text_delta: 'text', thinking_delta: 'thinking', signature_delta: 'thinking',
|
|
559
|
+
citations_delta: 'text', input_json_delta: 'tool_use',
|
|
560
|
+
})[deltaType];
|
|
561
|
+
const previous = this.blockTypes.get(index);
|
|
562
|
+
if (expected != null && previous != null && previous !== expected) {
|
|
563
|
+
throw new ProtocolError('delta type does not match block type');
|
|
564
|
+
}
|
|
565
|
+
if (expected != null && previous == null && (expected === 'text' || expected === 'thinking')) {
|
|
566
|
+
// Legacy proxy omitting block_start: pin the slot from the first delta.
|
|
567
|
+
this.blockTypes.set(index, expected);
|
|
568
|
+
this.implicitIndexes.add(index);
|
|
569
|
+
this.originals.set(index, { type: expected });
|
|
570
|
+
this.values.set(index, '');
|
|
571
|
+
}
|
|
572
|
+
if (deltaType === 'text_delta') {
|
|
573
|
+
if (typeof delta.text !== 'string') throw new ProtocolError('invalid text delta');
|
|
574
|
+
this.values.set(index, (this.values.get(index) || '') + delta.text);
|
|
575
|
+
} else if (deltaType === 'thinking_delta') {
|
|
576
|
+
if (typeof delta.thinking !== 'string') throw new ProtocolError('invalid thinking delta');
|
|
577
|
+
this.values.set(index, (this.values.get(index) || '') + delta.thinking);
|
|
578
|
+
} else if (deltaType === 'signature_delta') {
|
|
579
|
+
if (typeof delta.signature !== 'string') throw new ProtocolError('invalid signature delta');
|
|
580
|
+
const original = this.originals.get(index) || { type: 'thinking' };
|
|
581
|
+
original.signature = (original.signature || '') + delta.signature;
|
|
582
|
+
this.originals.set(index, original);
|
|
583
|
+
} else if (deltaType === 'input_json_delta') {
|
|
584
|
+
if (typeof delta.partial_json !== 'string') throw new ProtocolError('invalid input_json delta');
|
|
585
|
+
const tool = this.tools.get(index);
|
|
586
|
+
if (!tool || this.initialInputs.has(index)) throw new ProtocolError('tool delta without an open block');
|
|
587
|
+
tool.arguments += delta.partial_json;
|
|
588
|
+
if (delta.partial_json) {
|
|
589
|
+
emit({ kind: 'tool_delta', text: delta.partial_json, metadata: { index,
|
|
590
|
+
...(tool.callId ? { callId: tool.callId } : {}), ...(tool.name ? { name: tool.name } : {}) } });
|
|
591
|
+
}
|
|
592
|
+
} else if (deltaType === 'citations_delta') {
|
|
593
|
+
// Citations attach to text; their content is not visible model text.
|
|
594
|
+
} else {
|
|
595
|
+
throw new ProtocolError('unsupported delta type');
|
|
596
|
+
}
|
|
597
|
+
this.drainContent(emit);
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
handleBlockStop(event, emit) {
|
|
601
|
+
const index = event.index;
|
|
602
|
+
if (typeof index !== 'number' || !Number.isInteger(index) || index < 0) {
|
|
603
|
+
throw new ProtocolError('invalid block index');
|
|
604
|
+
}
|
|
605
|
+
if (this.closed.has(index) || !this.blockTypes.has(index)) {
|
|
606
|
+
throw new ProtocolError('invalid content_block_stop');
|
|
607
|
+
}
|
|
608
|
+
const tool = this.tools.get(index);
|
|
609
|
+
if (tool) {
|
|
610
|
+
if (!tool.callId.trim() || !tool.name.trim()) throw new ProtocolError('tool block missing identity');
|
|
611
|
+
tool.closed = true;
|
|
612
|
+
}
|
|
613
|
+
this.closed.add(index);
|
|
614
|
+
this.drainContent(emit);
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
drainContent(emit) {
|
|
618
|
+
const prefix = (kind) => {
|
|
619
|
+
const pieces = [];
|
|
620
|
+
let expected = 0;
|
|
621
|
+
for (const [index, type] of [...this.blockTypes.entries()].sort((a, b) => a[0] - b[0])) {
|
|
622
|
+
if (index !== expected++) break;
|
|
623
|
+
if (type !== kind) continue;
|
|
624
|
+
pieces.push(this.values.get(index) || '');
|
|
625
|
+
if (!this.closed.has(index)) break;
|
|
626
|
+
}
|
|
627
|
+
return pieces.join('');
|
|
628
|
+
};
|
|
629
|
+
const text = prefix('text');
|
|
630
|
+
const reasoning = prefix('thinking');
|
|
631
|
+
if (!text.startsWith(this.text) || !reasoning.startsWith(this.reasoning)) throw new ProtocolError('non-monotonic content');
|
|
632
|
+
const textDelta = text.slice(this.text.length);
|
|
633
|
+
const thinkingDelta = reasoning.slice(this.reasoning.length);
|
|
634
|
+
this.text = text;
|
|
635
|
+
this.reasoning = reasoning;
|
|
636
|
+
if (textDelta) emit({ kind: 'text', text: textDelta });
|
|
637
|
+
if (thinkingDelta && emitThinking) emit({ kind: 'thinking', text: thinkingDelta });
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
validateFinishedBlocks() {
|
|
641
|
+
let expected = 0;
|
|
642
|
+
for (const index of [...this.blockTypes.keys()].sort((a, b) => a - b)) {
|
|
643
|
+
if (index !== expected++) throw new ProtocolError('non-contiguous content block indexes');
|
|
644
|
+
if (!this.closed.has(index) && !this.implicitIndexes.has(index)) throw new ProtocolError('unclosed content block');
|
|
645
|
+
}
|
|
646
|
+
for (const index of this.implicitIndexes) this.closed.add(index);
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
finalize({ errorKind = null, failureKind = null } = {}) {
|
|
650
|
+
const usage = buildAnthropicUsage(this);
|
|
651
|
+
const usageReported = hasCompleteUsage(this);
|
|
652
|
+
let finish = this.refused ? 'content_filter' : mapStopReason(this.stopReason);
|
|
653
|
+
if (errorKind && errorKind !== 'provider_retryable_error') {
|
|
654
|
+
return { kind: errorKind, text: this.text, toolCalls: [], usage, usageReported, ...(failureKind ? { failureKind } : {}) };
|
|
655
|
+
}
|
|
656
|
+
if (errorKind === 'provider_retryable_error' || !this.done) {
|
|
657
|
+
const normalized = normalizeResponse({ text: this.text, toolCalls: [], finishReason: finish, streamComplete: false });
|
|
658
|
+
if (normalized.kind === 'provider_retryable_error') {
|
|
659
|
+
return { ...normalized, usage, usageReported, failureKind: failureKind || 'incomplete_response' };
|
|
660
|
+
}
|
|
661
|
+
return this.seal({ ...normalized, usage, usageReported });
|
|
662
|
+
}
|
|
663
|
+
// done, no error: assemble the full response.
|
|
664
|
+
let calls = [];
|
|
665
|
+
let malformed = false;
|
|
666
|
+
try {
|
|
667
|
+
calls = this.buildCalls();
|
|
668
|
+
} catch {
|
|
669
|
+
malformed = true;
|
|
670
|
+
calls = [{}];
|
|
671
|
+
}
|
|
672
|
+
if (this.refused) { finish = 'content_filter'; calls = []; }
|
|
673
|
+
const normalized = normalizeResponse({ text: this.text, toolCalls: malformed ? [{}] : calls, finishReason: finish, streamComplete: true });
|
|
674
|
+
return this.seal({ ...normalized, usage, usageReported });
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
buildCalls() {
|
|
678
|
+
const calls = [];
|
|
679
|
+
const indexes = [...this.tools.keys()].sort((a, b) => a - b);
|
|
680
|
+
for (const index of indexes) {
|
|
681
|
+
const tool = this.tools.get(index);
|
|
682
|
+
if (!tool.closed) throw new ProtocolError('tool block not closed');
|
|
683
|
+
let arguments_;
|
|
684
|
+
let invalid = false;
|
|
685
|
+
try {
|
|
686
|
+
arguments_ = strictParseJson(tool.arguments || '{}');
|
|
687
|
+
if (arguments_ == null || typeof arguments_ !== 'object' || Array.isArray(arguments_)) throw new Error();
|
|
688
|
+
} catch {
|
|
689
|
+
arguments_ = {}; invalid = true;
|
|
690
|
+
}
|
|
691
|
+
calls.push({
|
|
692
|
+
callId: tool.callId, name: tool.name, arguments: arguments_,
|
|
693
|
+
...(invalid ? { wireArgumentsInvalid: true } : {}),
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
return calls;
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
seal(result) {
|
|
700
|
+
if (!captureReplay) return result;
|
|
701
|
+
const replayBlocks = this.replayContent();
|
|
702
|
+
return sealAnthropicReplay(result, profile, replayBlocks);
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
// Rebuild the original content blocks (text, signed thinking, tool_use with
|
|
706
|
+
// parsed input) for anthropic_content replay. The order follows block index.
|
|
707
|
+
replayContent() {
|
|
708
|
+
const blocks = [];
|
|
709
|
+
const indexes = [...this.blockTypes.keys()].sort((a, b) => a - b);
|
|
710
|
+
for (const index of indexes) {
|
|
711
|
+
const kind = this.blockTypes.get(index);
|
|
712
|
+
const original = deepCopy(this.originals.get(index) || { type: kind });
|
|
713
|
+
if (kind === 'text') {
|
|
714
|
+
original.text = this.values.get(index) || '';
|
|
715
|
+
} else if (kind === 'thinking') {
|
|
716
|
+
original.thinking = this.values.get(index) || '';
|
|
717
|
+
} else if (kind === 'tool_use') {
|
|
718
|
+
const tool = this.tools.get(index);
|
|
719
|
+
try {
|
|
720
|
+
original.input = strictParseJson(tool.arguments || '{}');
|
|
721
|
+
if (original.input == null || typeof original.input !== 'object' || Array.isArray(original.input)) throw new Error();
|
|
722
|
+
} catch {
|
|
723
|
+
// Invalid JSON has no complete content to replay; retain the block
|
|
724
|
+
// without fabricating original input (mirrors AnthropicContent).
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
blocks.push(original);
|
|
729
|
+
}
|
|
730
|
+
return blocks;
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
pinIdentity(id, model) {
|
|
734
|
+
if (id != null) {
|
|
735
|
+
if (typeof id !== 'string' || !id.trim()) throw new ProtocolError('invalid message id');
|
|
736
|
+
if (this.responseId != null && id !== this.responseId) throw new ProtocolError('mixed message identities');
|
|
737
|
+
this.responseId = id;
|
|
738
|
+
}
|
|
739
|
+
if (model != null) {
|
|
740
|
+
if (typeof model !== 'string' || !model.trim()) throw new ProtocolError('invalid model');
|
|
741
|
+
if (this.model != null && model !== this.model) throw new ProtocolError('mixed model identities');
|
|
742
|
+
this.model = model;
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function deepCopy(value) {
|
|
749
|
+
if (value == null || typeof value !== 'object') return value;
|
|
750
|
+
return parseNativeJson(stringifyModelJson(value));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export { ANTHROPIC_VERSION, parseRetryAfterMs };
|