@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.
Files changed (151) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +1 -0
  3. package/README.md +124 -2
  4. package/THIRD_PARTY_NOTICES.md +66 -0
  5. package/ZCode-LICENSE +202 -0
  6. package/dist/index.mjs +4871 -0
  7. package/dist/native-command-plan.LICENSE.txt +221 -0
  8. package/dist/native-command-plan.json +16 -0
  9. package/dist/native-command-plan.mjs +8 -0
  10. package/dist/rpc-entry.mjs +5682 -0
  11. package/package.json +169 -4
  12. package/src/native/a2a-receipt-source.mjs +19 -0
  13. package/src/native/a2a-tools.mjs +517 -0
  14. package/src/native/activity-status.mjs +30 -0
  15. package/src/native/artifact-codec.mjs +481 -0
  16. package/src/native/artifact-store.mjs +1162 -0
  17. package/src/native/assistant-stream.mjs +76 -0
  18. package/src/native/background-command-port.mjs +95 -0
  19. package/src/native/browser-action-observation.mjs +43 -0
  20. package/src/native/browser-availability.mjs +22 -0
  21. package/src/native/browser-canvas-metrics.mjs +71 -0
  22. package/src/native/browser-dom.mjs +193 -0
  23. package/src/native/browser-playwright.mjs +1249 -0
  24. package/src/native/browser-vision.mjs +369 -0
  25. package/src/native/browser-workspace-policy.mjs +22 -0
  26. package/src/native/browser-workspace-protocol.mjs +95 -0
  27. package/src/native/browser-workspace-server.mjs +164 -0
  28. package/src/native/browser-workspace.mjs +355 -0
  29. package/src/native/captured-source.mjs +3 -0
  30. package/src/native/child-budget-allocation.mjs +87 -0
  31. package/src/native/child-capabilities.mjs +35 -0
  32. package/src/native/child-command-resource.mjs +20 -0
  33. package/src/native/child-history.mjs +188 -0
  34. package/src/native/child-merge.mjs +98 -0
  35. package/src/native/child-model-surface.mjs +19 -0
  36. package/src/native/child-observation.mjs +100 -0
  37. package/src/native/child-owner.mjs +620 -0
  38. package/src/native/child-publication.mjs +76 -0
  39. package/src/native/child-readers.mjs +366 -0
  40. package/src/native/child-tools.mjs +46 -0
  41. package/src/native/child-workspace-binding.mjs +29 -0
  42. package/src/native/child-workspace-tools.mjs +1 -0
  43. package/src/native/child-workspaces.mjs +655 -0
  44. package/src/native/children.mjs +365 -0
  45. package/src/native/command-plan-worker.mjs +14 -0
  46. package/src/native/command-plan.mjs +31 -0
  47. package/src/native/command-progress.mjs +64 -0
  48. package/src/native/command-shell.mjs +9 -0
  49. package/src/native/command-workspace-observation.mjs +36 -0
  50. package/src/native/component-tools.mjs +280 -0
  51. package/src/native/context-counter.mjs +133 -0
  52. package/src/native/context-markers.mjs +51 -0
  53. package/src/native/context-profile.mjs +72 -0
  54. package/src/native/context-status.mjs +27 -0
  55. package/src/native/conversation-command-control.mjs +133 -0
  56. package/src/native/delivery-timeline.mjs +39 -0
  57. package/src/native/dsh-background-ownership.mjs +102 -0
  58. package/src/native/dsh-managed-ownership.mjs +23 -0
  59. package/src/native/dsh-turn-contract.mjs +3 -0
  60. package/src/native/dsh-turn-ownership.mjs +54 -0
  61. package/src/native/dsh-turn-runtime.mjs +498 -0
  62. package/src/native/e2b-background-supervisor.mjs +347 -0
  63. package/src/native/e2b-background.mjs +368 -0
  64. package/src/native/e2b-command-state.mjs +40 -0
  65. package/src/native/e2b-command-tools.mjs +486 -0
  66. package/src/native/e2b-sandbox-owner.mjs +227 -0
  67. package/src/native/hook-executor.mjs +249 -0
  68. package/src/native/instruction-assembly.mjs +60 -0
  69. package/src/native/instruction-context.mjs +54 -0
  70. package/src/native/local-background-supervisor.mjs +108 -0
  71. package/src/native/local-background.mjs +207 -0
  72. package/src/native/local-command-evidence.mjs +57 -0
  73. package/src/native/local-command-policy-worker.mjs +197 -0
  74. package/src/native/local-command-policy.mjs +46 -0
  75. package/src/native/local-command-state.mjs +177 -0
  76. package/src/native/local-command-tools.mjs +302 -0
  77. package/src/native/local-execution-profile.mjs +36 -0
  78. package/src/native/local-output-policy.mjs +92 -0
  79. package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
  80. package/src/native/local-process-bin/darwin-x64/launcher +0 -0
  81. package/src/native/local-process-bin/linux-arm64/launcher +0 -0
  82. package/src/native/local-process-bin/linux-x64/launcher +0 -0
  83. package/src/native/local-process-launcher.c +183 -0
  84. package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
  85. package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
  86. package/src/native/local-process-licenses/NOTICE +21 -0
  87. package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
  88. package/src/native/local-process-manifest.json +32 -0
  89. package/src/native/local-process.mjs +246 -0
  90. package/src/native/local-resource-profile.mjs +32 -0
  91. package/src/native/memory-candidate.mjs +121 -0
  92. package/src/native/memory-extraction-run.mjs +60 -0
  93. package/src/native/memory-extraction-tools.mjs +116 -0
  94. package/src/native/memory-recall.mjs +65 -0
  95. package/src/native/memory-store.mjs +334 -0
  96. package/src/native/memory-tools.mjs +329 -0
  97. package/src/native/model-anthropic-cache.mjs +49 -0
  98. package/src/native/model-anthropic-tool-search.mjs +85 -0
  99. package/src/native/model-anthropic.mjs +753 -0
  100. package/src/native/model-chat-usage.mjs +24 -0
  101. package/src/native/model-inline-reasoning.mjs +111 -0
  102. package/src/native/model-media.mjs +174 -0
  103. package/src/native/model-normalize.mjs +228 -0
  104. package/src/native/model-openai-chat.mjs +561 -0
  105. package/src/native/model-port.mjs +361 -0
  106. package/src/native/model-provider-compat.mjs +129 -0
  107. package/src/native/model-replay.mjs +153 -0
  108. package/src/native/model-request.mjs +15 -0
  109. package/src/native/model-responses.mjs +1205 -0
  110. package/src/native/model-sse.mjs +171 -0
  111. package/src/native/model-surface-host.mjs +27 -0
  112. package/src/native/model-tool-search-policy.mjs +16 -0
  113. package/src/native/model-transport-observation.mjs +27 -0
  114. package/src/native/owned-child.mjs +114 -0
  115. package/src/native/owned-command.mjs +106 -0
  116. package/src/native/python-syntax-worker.mjs +60 -0
  117. package/src/native/python-syntax.mjs +50 -0
  118. package/src/native/read-interruption.mjs +16 -0
  119. package/src/native/result-stream.mjs +63 -0
  120. package/src/native/ruff-ast.mjs +95 -0
  121. package/src/native/run-observation.mjs +128 -0
  122. package/src/native/session-delivery-journal.mjs +96 -0
  123. package/src/native/session-delivery.mjs +434 -0
  124. package/src/native/shell-analysis.mjs +50 -0
  125. package/src/native/skill-disclosure.mjs +404 -0
  126. package/src/native/skill-events.mjs +164 -0
  127. package/src/native/skill-tools.mjs +813 -0
  128. package/src/native/source-page.mjs +73 -0
  129. package/src/native/sqlite-session-inbox.mjs +42 -0
  130. package/src/native/sqlite-store.mjs +617 -0
  131. package/src/native/tool-authority.mjs +99 -0
  132. package/src/native/tool-guidance.mjs +12 -0
  133. package/src/native/tool-observation.mjs +75 -0
  134. package/src/native/tool-states.mjs +14 -0
  135. package/src/native/verification-authority.mjs +37 -0
  136. package/src/native/web-tools.mjs +643 -0
  137. package/src/native/workspace-checkpoints.mjs +312 -0
  138. package/src/native/workspace-context.mjs +61 -0
  139. package/src/native/workspace-display.mjs +39 -0
  140. package/src/native/workspace-edit-match.mjs +19 -0
  141. package/src/native/workspace-edit-worker.mjs +15 -0
  142. package/src/native/workspace-editing.mjs +447 -0
  143. package/src/native/workspace-image.mjs +72 -0
  144. package/src/native/workspace-instructions.mjs +107 -0
  145. package/src/native/workspace-mutation-lock.mjs +74 -0
  146. package/src/native/workspace-patch-worker.mjs +17 -0
  147. package/src/native/workspace-patch.mjs +17 -0
  148. package/src/native/workspace-paths.mjs +225 -0
  149. package/src/native/workspace-state.mjs +272 -0
  150. package/src/native/workspace-subject-observer.mjs +84 -0
  151. package/src/native/workspace-tools.mjs +1740 -0
@@ -0,0 +1,1205 @@
1
+ // OpenAI Responses wire adapter.
2
+ //
3
+ // Ports ksadk.studio.model_client._stream_responses / _responses_payload /
4
+ // model_request_projection.responses_messages / model_responses_content /
5
+ // model_wire_tools.ResponsesToolCalls+ResponsesItems / model_buffered_wire
6
+ // (responses branch) / model_wire_snapshot.reconcile_responses_snapshot: one
7
+ // HTTP request per attempt over the Responses SSE protocol, with ordered
8
+ // output items, partial tool-argument assembly, refusal/usage/error
9
+ // normalization, response.completed/incomplete/failed classification, and
10
+ // reasoning_content replay sealing. The adapter never retries and never
11
+ // executes tools; it emits text/thinking/tool_delta display deltas and
12
+ // returns one sealed ModelResult. Kernel policy owns backoff and budgets.
13
+ //
14
+ // The Responses wire has no separate replay enum: the host profile's
15
+ // providerReplayMode is 'none' | 'reasoning_content' | 'anthropic_content'.
16
+ // reasoning_content replay (model-replay/v1) is sealed from the wire's
17
+ // reasoning items, matching the chat adapter. anthropic_content replay is
18
+ // the Anthropic Messages wire's contract: the Responses wire produces no
19
+ // anthropic content blocks, so that mode is rejected at build time (an
20
+ // explicit, non-silent configuration mismatch) rather than downgraded.
21
+ // Reasoning_content replay is sealed but, like the Python source, not
22
+ // re-projected onto a subsequent Responses input: responses_messages emits
23
+ // no reasoning items in its input array. See delivery evidence for the
24
+ // audited gap.
25
+
26
+ import {
27
+ normalizeResponse, sealUsage, ProtocolError, resolveUrl, boundOutputTokens,
28
+ emptyUsage,
29
+ } from './model-normalize.mjs';
30
+ import { sealModelReplay } from './model-replay.mjs';
31
+ import { validateMediaBlock, MediaError } from './model-media.mjs';
32
+ // parseModelJson is the kernel's strict finite JSON parser (rejects duplicate
33
+ // object keys and non-finite numbers); using it keeps provider-envelope and
34
+ // tool-argument decoding consistent with the kernel's own validation, so a
35
+ // malformed provider payload is a terminal protocol error rather than a
36
+ // silently-accepted ambiguity.
37
+ import { parseModelJson, stringifyModelJson, modelWireToolCall, FINAL_SUBMISSION_TOOL } from '@kingsoftcloud/agentkit-harness-core';
38
+
39
+ function strictParseJson(text) {
40
+ try { return parseModelJson(text); }
41
+ catch (error) { throw new ProtocolError(error?.code || 'invalid_model_json', 'json_invalid'); }
42
+ }
43
+
44
+ // Body keys the host binding parameters may never override: model, input,
45
+ // tools, stream and the security-sensitive transport values belong to the
46
+ // adapter, not the binding parameters. instructions is request-owned.
47
+ const FORBIDDEN_BODY_KEYS = new Set([
48
+ 'model', 'input', 'tools', 'stream', 'tool_choice', 'parallel_tool_calls',
49
+ 'max_output_tokens', 'instructions', 'n',
50
+ 'previous_response_id', 'conversation', 'background', 'truncation',
51
+ 'provider_thinking_display',
52
+ 'provider_tool_search',
53
+ ]);
54
+
55
+ // Hosted provider effects that must not be silently dropped as client prose.
56
+ const HOSTED_ITEM_TYPES = new Set([
57
+ 'web_search_call', 'web_search_preview_call', 'file_search_call',
58
+ 'code_interpreter_call', 'computer_use_call', 'image_generation_call',
59
+ 'mcp_call', 'local_shell_call',
60
+ ]);
61
+
62
+ // In-stream error code/type → retryable/terminal classification. A 2xx stream
63
+ // that emits an error/response.failed event is a provider rejection, never a
64
+ // final; unknown codes stay terminal so a foreign payload cannot become a
65
+ // silent retry or final. Mirrors the Anthropic stream classifier and the
66
+ // Python provider_error code map.
67
+ const RETRYABLE_ERROR_CODES = new Set([
68
+ 'rate_limit_error', 'rate_limit_exceeded', 'overloaded_error', 'server_error',
69
+ 'api_error', 'service_unavailable_error', 'server_is_overloaded', 'timeout_error',
70
+ 'MODEL_PROVIDER_RATE_LIMIT', 'MODEL_PROVIDER_UNAVAILABLE',
71
+ ]);
72
+ const TERMINAL_ERROR_CODES = new Set([
73
+ 'invalid_request_error', 'invalid_api_key', 'authentication_error',
74
+ 'permission_error', 'not_found_error', 'request_too_large', 'billing_error',
75
+ 'insufficient_quota', 'MODEL_PROVIDER_AUTHENTICATION', 'MODEL_PROVIDER_PERMISSION',
76
+ 'MODEL_PROVIDER_INVALID_REQUEST', 'MODEL_PROVIDER_NOT_FOUND',
77
+ 'MODEL_PROVIDER_QUOTA_EXHAUSTED',
78
+ ]);
79
+
80
+ function securityHeaders() {
81
+ return new Set(['authorization', 'x-api-key', 'anthropic-version', 'content-type', 'accept']);
82
+ }
83
+
84
+ // --- wire-shape helpers (ports model_wire_tools._object/_text/_identity/_index) ---
85
+
86
+ function objectOf(value) {
87
+ if (value == null) return {};
88
+ if (typeof value !== 'object' || Array.isArray(value)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
89
+ return value;
90
+ }
91
+
92
+ function textOf(value) {
93
+ if (value != null && typeof value !== 'string') throw new ProtocolError('invalid model frame', 'envelope_invalid');
94
+ return value;
95
+ }
96
+
97
+ function identityOf(value) {
98
+ const result = textOf(value);
99
+ if (result == null || !result.trim()) throw new ProtocolError('invalid model frame', 'envelope_invalid');
100
+ return result;
101
+ }
102
+
103
+ function optionalText(value) {
104
+ if (value == null) return '';
105
+ if (typeof value !== 'string') throw new ProtocolError('invalid model frame', 'envelope_invalid');
106
+ return value;
107
+ }
108
+
109
+ function indexOf(event, name, { required = false } = {}) {
110
+ if (!(name in event)) {
111
+ if (required) throw new ProtocolError('invalid model frame', 'envelope_invalid');
112
+ return null;
113
+ }
114
+ const value = event[name];
115
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
116
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
117
+ }
118
+ return value;
119
+ }
120
+
121
+ function validateItemKind(kind) {
122
+ if (HOSTED_ITEM_TYPES.has(kind)) throw new ProtocolError('unsupported hosted tool item');
123
+ }
124
+
125
+ // --- binding validation + request build (ports _responses_payload) ---
126
+
127
+ export function validateResponsesBinding(binding) {
128
+ if (!binding || typeof binding !== 'object') throw new ProtocolError('responses binding missing');
129
+ if (binding.protocol !== 'openai-responses') throw new ProtocolError('binding is not openai-responses');
130
+ if (typeof binding.baseUrl !== 'string' || !binding.baseUrl) throw new ProtocolError('responses binding requires baseUrl');
131
+ if (typeof binding.apiKey !== 'string' || !binding.apiKey) throw new ProtocolError('responses binding requires apiKey');
132
+ if (typeof binding.model !== 'string' || !binding.model.trim()) throw new ProtocolError('responses binding requires model');
133
+ try { new URL(binding.baseUrl); } catch { throw new ProtocolError('responses binding baseUrl is not a URL'); }
134
+ }
135
+
136
+ // Build the Responses request. The adapter controls model, input, stream,
137
+ // tools and instructions; binding.parameters may only add generation fields,
138
+ // never override the forbidden keys or security-sensitive headers.
139
+ export function buildResponsesRequest(binding, request) {
140
+ validateResponsesBinding(binding);
141
+ const profile = request.profile;
142
+ // The Responses wire carries no Anthropic content blocks; an
143
+ // anthropic_content replay profile is an explicit mismatch, rejected here
144
+ // rather than silently downgraded to no replay.
145
+ if (profile && profile.providerReplayMode === 'anthropic_content') {
146
+ throw new ProtocolError('anthropic_content replay requires its declared wire adapter');
147
+ }
148
+ const url = resolveUrl(binding.baseUrl, 'responses');
149
+
150
+ const headers = { Accept: profile.streaming === false ? 'application/json' : 'text/event-stream' };
151
+ const skip = securityHeaders();
152
+ for (const [name, value] of Object.entries(binding.headers || {})) {
153
+ if (skip.has(name.toLowerCase())) continue;
154
+ if (typeof value !== 'string') continue;
155
+ headers[name] = value;
156
+ }
157
+ headers['Content-Type'] = 'application/json';
158
+ headers.Authorization = `Bearer ${binding.apiKey}`;
159
+
160
+ const { instructions, items } = projectMessagesResponses(request, request.maxInlineImageBytes);
161
+ const payload = {
162
+ model: binding.model,
163
+ input: items,
164
+ stream: profile.streaming ?? true,
165
+ truncation: 'disabled',
166
+ };
167
+ if (instructions) payload.instructions = instructions;
168
+ payload.max_output_tokens = boundOutputTokens(binding.parameters?.max_output_tokens, profile.maxOutputTokens);
169
+
170
+ const parameters = binding.parameters || {};
171
+ for (const [key, value] of Object.entries(parameters)) {
172
+ if (FORBIDDEN_BODY_KEYS.has(key)) continue;
173
+ payload[key] = value;
174
+ }
175
+ // A binding that selects the output-limit field must not also carry the
176
+ // adapter-owned default.
177
+ if ('max_output_tokens' in parameters) {
178
+ payload.max_output_tokens = boundOutputTokens(parameters.max_output_tokens, profile.maxOutputTokens);
179
+ }
180
+
181
+ // Advertise tools: during a finalization-only turn the kernel has already
182
+ // stripped normal workspace tools, but the adapter filters defensively and
183
+ // keeps the internal submit_final_output tool so the model can submit its
184
+ // final answer. Normal tools are never sent during finalization. Responses
185
+ // tools are flat objects (no nested function envelope); accept both shapes.
186
+ const tools = request.tools || [];
187
+ const advertised = request.finalizationOnly
188
+ ? tools.filter((tool) => tool.name === FINAL_SUBMISSION_TOOL)
189
+ : tools;
190
+ if (advertised.length) {
191
+ payload.tools = advertised.map((tool) => {
192
+ const flat = { type: 'function', name: tool.name, description: tool.description,
193
+ parameters: tool.parameters || { type: 'object', properties: {} } };
194
+ return flat;
195
+ });
196
+ const choice = request.toolChoice ?? (profile.providerCompatibilityPolicy && request.finalizationOnly
197
+ ? { type: 'function', function: { name: FINAL_SUBMISSION_TOOL } } : 'auto');
198
+ payload.tool_choice = typeof choice === 'string' ? choice : { type: 'function', name: choice.function.name };
199
+ payload.parallel_tool_calls = profile.parallelToolCalls ?? true;
200
+ }
201
+ if (profile.temperature !== undefined) payload.temperature = profile.temperature;
202
+ if (profile.topP !== undefined) payload.top_p = profile.topP;
203
+ if (profile.reasoningEffort !== undefined) payload.reasoning = { ...(payload.reasoning ?? {}), effort: profile.reasoningEffort };
204
+
205
+ return { url, headers, body: stringifyModelJson(payload) };
206
+ }
207
+
208
+ // Project host messages onto the Responses input wire. system/developer
209
+ // messages are lifted to the top-level `instructions` field; tool receipts
210
+ // become function_call_output items; user/assistant content becomes role
211
+ // items (input_text/output_text/input_image) and assistant tool calls become
212
+ // function_call items. Canonical call-id pairing is enforced: an assistant
213
+ // tool batch must be followed by matching function_call_output receipts.
214
+ // anthropic_content replay is rejected (the Responses wire carries none).
215
+ export function projectMessagesResponses(request, maxInlineImageBytes) {
216
+ const profile = request.profile;
217
+ const instructions = [];
218
+ const items = [];
219
+ const pending = new Set();
220
+ for (const message of request.messages || []) {
221
+ const role = message.role;
222
+ if (role !== 'tool' && pending.size) {
223
+ throw new ProtocolError('incomplete assistant tool batch before next message');
224
+ }
225
+ if (role === 'assistant' && message.toolCalls && message.toolCalls.length) {
226
+ validateAssistantToolIds(message.toolCalls);
227
+ for (const call of message.toolCalls) pending.add(call.callId);
228
+ }
229
+ if (role === 'tool') {
230
+ if (!pending.has(message.callId)) {
231
+ throw new ProtocolError('tool receipt does not match the pending assistant batch');
232
+ }
233
+ pending.delete(message.callId);
234
+ }
235
+ // The Responses wire carries no Anthropic content; a cross-wire replay is
236
+ // a configuration error, not a silent drop.
237
+ if (message.providerReplay && message.providerReplay.version === 'model-replay/v2') {
238
+ throw new ProtocolError('anthropic_content replay cannot cross the responses wire');
239
+ }
240
+ if (role === 'system' || role === 'developer') {
241
+ instructions.push(message.content || '');
242
+ continue;
243
+ }
244
+ if (role === 'tool') {
245
+ const media = message.media ? message.media.map((b) => validateMediaBlock(b, maxInlineImageBytes)) : null;
246
+ const output = media && media.length
247
+ ? [{ type: 'input_text', text: message.content || '' }, ...projectMediaResponses(media)]
248
+ : (message.content || '');
249
+ items.push({ type: 'function_call_output', call_id: message.callId, output });
250
+ continue;
251
+ }
252
+ // user / assistant
253
+ const media = message.media ? message.media.map((b) => validateMediaBlock(b, maxInlineImageBytes)) : null;
254
+ if (media && media.length && role !== 'user') {
255
+ throw new MediaError('image observations require user or tool content');
256
+ }
257
+ const parts = [];
258
+ if ((message.content || '').length) {
259
+ parts.push({ type: role === 'assistant' ? 'output_text' : 'input_text', text: message.content });
260
+ }
261
+ if (media && media.length) parts.push(...projectMediaResponses(media));
262
+ // Emit the role/content item only when there is content or no calls, so an
263
+ // assistant tool-only turn does not carry an empty content item.
264
+ if (parts.length || !(message.toolCalls && message.toolCalls.length)) {
265
+ items.push({ role, content: parts });
266
+ }
267
+ if (message.toolCalls && message.toolCalls.length) {
268
+ validateAssistantToolIds(message.toolCalls);
269
+ for (const call of message.toolCalls.map(modelWireToolCall)) {
270
+ items.push({ type: 'function_call', call_id: call.callId, name: call.name,
271
+ arguments: safeStringifyArguments(call.arguments) });
272
+ }
273
+ }
274
+ }
275
+ if (pending.size) throw new ProtocolError('assistant tool batch is missing receipts');
276
+ return { instructions: instructions.join('\n\n'), items };
277
+ }
278
+
279
+ function safeStringifyArguments(arguments_) {
280
+ if (arguments_ == null || typeof arguments_ !== 'object' || Array.isArray(arguments_)) {
281
+ throw new ProtocolError('invalid historical tool arguments');
282
+ }
283
+ const visit = (value, seen = new Set()) => {
284
+ if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'bigint') return;
285
+ if (typeof value === 'number' && Number.isFinite(value)) return;
286
+ if (typeof value !== 'object' || seen.has(value)
287
+ || (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
288
+ throw new ProtocolError('invalid historical tool arguments');
289
+ }
290
+ seen.add(value);
291
+ for (const entry of Object.values(value)) visit(entry, seen);
292
+ seen.delete(value);
293
+ };
294
+ visit(arguments_);
295
+ try { return stringifyModelJson(arguments_); }
296
+ catch { throw new ProtocolError('invalid historical tool arguments'); }
297
+ }
298
+
299
+ function validateAssistantToolIds(calls) {
300
+ const ids = [];
301
+ for (const call of calls) {
302
+ const id = String(call?.callId ?? '');
303
+ if (!id.trim()) throw new ProtocolError('assistant tool calls require stable IDs');
304
+ ids.push(id);
305
+ }
306
+ if (new Set(ids).size !== ids.length) throw new ProtocolError('duplicate assistant tool IDs');
307
+ }
308
+
309
+ // Project validated media blocks onto the Responses input wire: input_image
310
+ // with the bare image_url string and optional detail (no nested object).
311
+ function projectMediaResponses(blocks) {
312
+ return blocks.map((b) => {
313
+ const item = { type: 'input_image', image_url: b.image_url.url };
314
+ if (b.image_url.detail != null) item.detail = b.image_url.detail;
315
+ return item;
316
+ });
317
+ }
318
+
319
+ // --- usage (ports model_usage.provider_usage, wire_api="responses") ---
320
+
321
+ // Parse the Responses usage envelope. Both primary counters (input_tokens,
322
+ // output_tokens) must be present for a complete report; an incomplete report
323
+ // stays unknown (null), never a fabricated zero-cost claim. All supplied
324
+ // fields are validated even in a partial report. input_tokens_details.
325
+ // cached_tokens -> cachedInputTokens; output_tokens_details.reasoning_tokens
326
+ // -> reasoningTokens.
327
+ function parseResponsesUsage(value) {
328
+ if (value == null) return null;
329
+ if (typeof value !== 'object' || Array.isArray(value)) throw new ProtocolError('invalid usage report', 'usage_invalid');
330
+ for (const field of ['input_tokens', 'output_tokens', 'total_tokens']) {
331
+ if (Object.hasOwn(value, field)) toNonNegInt(value[field], field);
332
+ }
333
+ const inputDetails = value.input_tokens_details;
334
+ const outputDetails = value.output_tokens_details;
335
+ if (inputDetails != null && (typeof inputDetails !== 'object' || Array.isArray(inputDetails))) {
336
+ throw new ProtocolError('invalid usage details', 'usage_invalid');
337
+ }
338
+ if (outputDetails != null && (typeof outputDetails !== 'object' || Array.isArray(outputDetails))) {
339
+ throw new ProtocolError('invalid usage details', 'usage_invalid');
340
+ }
341
+ const cached = inputDetails && Object.hasOwn(inputDetails, 'cached_tokens')
342
+ ? toNonNegInt(inputDetails.cached_tokens, 'cached_tokens') : undefined;
343
+ const reasoning = outputDetails && Object.hasOwn(outputDetails, 'reasoning_tokens')
344
+ ? toNonNegInt(outputDetails.reasoning_tokens, 'reasoning_tokens') : undefined;
345
+ if (!Object.hasOwn(value, 'input_tokens') || !Object.hasOwn(value, 'output_tokens')) return null;
346
+ const usage = { ...emptyUsage(), inputTokens: value.input_tokens, outputTokens: value.output_tokens };
347
+ if (cached != null) usage.cachedInputTokens = cached;
348
+ if (reasoning != null) usage.reasoningTokens = reasoning;
349
+ return usage;
350
+ }
351
+
352
+ function toNonNegInt(value, field) {
353
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
354
+ throw new ProtocolError(`invalid ${field} counter`, 'usage_invalid');
355
+ }
356
+ return value;
357
+ }
358
+
359
+ // --- buffered output projection (ports model_buffered_wire, responses branch) ---
360
+
361
+ // Project a complete Responses output array into { finish, content, reasoning,
362
+ // calls, refused } for both the buffered 2xx parse and the stream snapshot
363
+ // reconcile. The output array carries message/reasoning/function_call items.
364
+ function projectResponsesOutput(payload, { status }) {
365
+ if (payload.status != null && payload.status !== status) throw new ProtocolError('invalid model frame', 'envelope_invalid');
366
+ if ('output' in payload && !Array.isArray(payload.output)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
367
+ const output = Array.isArray(payload.output) ? payload.output : [];
368
+ const messages = [];
369
+ const reasoning = [];
370
+ const calls = [];
371
+ let refused = false;
372
+ for (const raw of output) {
373
+ const item = objectOf(raw);
374
+ const kind = identityOf(item.type);
375
+ validateItemKind(kind);
376
+ if ('status' in item) optionalText(item.status);
377
+ if (status === 'completed' && (kind === 'message' || kind === 'function_call')
378
+ && item.status != null && item.status !== 'completed') {
379
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
380
+ }
381
+ if (kind === 'function_call') {
382
+ calls.push({
383
+ callId: identityOf(item.call_id), name: identityOf(item.name),
384
+ arguments: optionalText(item.arguments), raw: item,
385
+ });
386
+ } else if (kind === 'message') {
387
+ if (item.role != null && item.role !== 'assistant') throw new ProtocolError('invalid model frame', 'envelope_invalid');
388
+ const [text, itemRefused] = responseMessageContent(item.content);
389
+ messages.push(text);
390
+ refused = refused || itemRefused;
391
+ } else if (kind === 'reasoning') {
392
+ reasoning.push(partsText(item.summary, new Set(['summary_text', 'text'])));
393
+ reasoning.push(partsText(item.content, new Set(['reasoning_text', 'text'])));
394
+ }
395
+ }
396
+ const canonical = messages.join('');
397
+ const convenience = optionalText(payload.output_text);
398
+ if ('output' in payload && payload.output_text != null && canonical !== convenience) {
399
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
400
+ }
401
+ let finish;
402
+ if (status === 'completed') {
403
+ finish = calls.length ? 'tool_calls' : 'stop';
404
+ } else {
405
+ finish = status;
406
+ }
407
+ if (status === 'incomplete') {
408
+ const details = payload.incomplete_details;
409
+ const reason = details != null ? optionalText(objectOf(details).reason) : '';
410
+ if (reason === 'content_filter') finish = 'content_filter';
411
+ else if (reason === 'max_output_tokens' && !calls.length) finish = 'length';
412
+ else finish = 'incomplete';
413
+ }
414
+ if (refused) { finish = 'content_filter'; calls.length = 0; }
415
+ return { finish, content: canonical || convenience, reasoning: reasoning.join(''), calls, refused };
416
+ }
417
+
418
+ // Concatenate output_text/text parts of a message; a refusal part marks the
419
+ // message refused (ports model_buffered_wire.response_message_content).
420
+ function responseMessageContent(value) {
421
+ if (!Array.isArray(value)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
422
+ const texts = [];
423
+ let refused = false;
424
+ for (const raw of Array.isArray(value) ? value : []) {
425
+ const part = objectOf(raw);
426
+ const kind = identityOf(part.type);
427
+ if (kind === 'output_text' || kind === 'text') {
428
+ texts.push(optionalText(part.text));
429
+ } else if (kind === 'refusal') {
430
+ optionalText(part.refusal);
431
+ refused = true;
432
+ } else {
433
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
434
+ }
435
+ }
436
+ return [texts.join(''), refused];
437
+ }
438
+
439
+ function partsText(value, types) {
440
+ if (value != null && !Array.isArray(value)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
441
+ const result = [];
442
+ for (const raw of Array.isArray(value) ? value : []) {
443
+ const part = objectOf(raw);
444
+ const kind = identityOf(part.type);
445
+ if (types.has(kind)) result.push(optionalText(part.text));
446
+ }
447
+ return result.join('');
448
+ }
449
+
450
+ // Parse a buffered 2xx Responses body into a sealed result. Used when a 2xx
451
+ // response is not SSE (rare for stream:true); also the basis for envelope
452
+ // validation. Throws ProtocolError on a malformed envelope or a failed/error
453
+ // status (a 2xx body that claims failure is a protocol conflict, terminal).
454
+ export function parseResponsesBuffered(bodyText, request, { captureReplay = false, onThinking } = {}) {
455
+ let payload;
456
+ try { payload = strictParseJson(bodyText); } catch { throw new ProtocolError('invalid response json', 'json_invalid'); }
457
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
458
+ throw new ProtocolError('invalid response envelope', 'envelope_invalid');
459
+ }
460
+ const status = identityOf(payload.status);
461
+ // A 2xx body that carries an error alongside a purported successful answer,
462
+ // or an explicit failed status, is a protocol conflict, never a final.
463
+ if (payload.error != null) throw new ProtocolError('invalid response envelope', 'envelope_invalid');
464
+ if (status === 'failed') throw new ProtocolError('invalid response envelope', 'envelope_invalid');
465
+ validateBufferedIdentity(payload);
466
+ const reportedUsage = parseResponsesUsage(payload.usage);
467
+ const projection = projectResponsesOutput(payload, { status });
468
+ const calls = projection.calls.map((call) => buildCall(call));
469
+ const normalized = normalizeResponse({
470
+ text: projection.content, toolCalls: calls, finishReason: projection.finish, streamComplete: true,
471
+ });
472
+ const result = sealResult({ normalized, reportedUsage, request, reasoning: projection.reasoning, captureReplay });
473
+ onThinking?.(projection.reasoning);
474
+ return result;
475
+ }
476
+
477
+ function buildCall(buffered) {
478
+ let arguments_;
479
+ let invalid = false;
480
+ try {
481
+ arguments_ = strictParseJson(buffered.arguments || '{}');
482
+ if (arguments_ == null || typeof arguments_ !== 'object' || Array.isArray(arguments_)) throw new Error();
483
+ } catch {
484
+ arguments_ = {}; invalid = true;
485
+ }
486
+ return {
487
+ callId: buffered.callId, name: buffered.name, arguments: arguments_,
488
+ ...(invalid ? { wireArgumentsInvalid: true } : {}),
489
+ };
490
+ }
491
+
492
+ function sealResult({ normalized, reportedUsage, request, reasoning, captureReplay }) {
493
+ const { usage, usageReported } = sealUsage(reportedUsage);
494
+ const result = { ...normalized, usage, usageReported };
495
+ return captureReplay ? sealModelReplay(result, request.profile, reasoning) : result;
496
+ }
497
+
498
+ // Validate the buffered identity: the response id/model cannot change within
499
+ // one attempt (ports model_buffered_wire.validate_buffered_identity, responses).
500
+ function validateBufferedIdentity(payload) {
501
+ const identity = createWireIdentity();
502
+ identity.accept({ type: 'response.created', response: payload });
503
+ }
504
+
505
+ // --- streaming: shared item slot namespace (ports ResponsesItems) ---
506
+
507
+ function createResponsesItems() {
508
+ // One shared slot namespace for message, reasoning and function_call items.
509
+ return {
510
+ indexes: new Map(), // output_index -> item_id
511
+ itemIndexes: new Map(), // item_id -> output_index
512
+ kinds: new Map(), // item_id -> kind
513
+
514
+ pin(event, item, kind = '') {
515
+ const itemId = item != null ? identityOf(item.id) : identityOf(event.item_id);
516
+ if ('item_id' in event && identityOf(event.item_id) !== itemId) throw new ProtocolError('invalid model frame', 'envelope_invalid');
517
+ const index = indexOf(event, 'output_index');
518
+ if (index != null) {
519
+ if (this.indexes.get(index) != null && this.indexes.get(index) !== itemId) throw new ProtocolError('invalid model frame', 'envelope_invalid');
520
+ if (this.itemIndexes.get(itemId) != null && this.itemIndexes.get(itemId) !== index) throw new ProtocolError('invalid model frame', 'envelope_invalid');
521
+ this.indexes.set(index, itemId);
522
+ this.itemIndexes.set(itemId, index);
523
+ }
524
+ if (item != null) kind = identityOf(item.type);
525
+ if (kind) {
526
+ if (this.kinds.has(itemId) && this.kinds.get(itemId) !== kind) throw new ProtocolError('invalid model frame', 'envelope_invalid');
527
+ this.kinds.set(itemId, kind);
528
+ }
529
+ return itemId;
530
+ },
531
+ };
532
+ }
533
+
534
+ // --- streaming: content items (ports model_responses_content.ResponsesContent) ---
535
+
536
+ const TEXT_KINDS = new Set(['output_text', 'text']);
537
+ const DELTA_KINDS = {
538
+ 'response.output_text': ['content', 'output_text', 'content_index'],
539
+ 'response.refusal': ['content', 'refusal', 'content_index'],
540
+ 'response.reasoning_summary_text': ['summary', 'summary_text', 'summary_index'],
541
+ 'response.reasoning_text': ['content', 'reasoning_text', 'content_index'],
542
+ };
543
+
544
+ function createContentPart(kind) {
545
+ return { kind, text: '', closed: false };
546
+ }
547
+
548
+ // A content item (message or reasoning) holds indexed parts grouped by
549
+ // (group, index). Parts snapshot or accumulate text; closure is validated.
550
+ function createResponsesContent(items) {
551
+ const state = {
552
+ items, // shared ResponsesItems
553
+ itemState: new Map(), // key -> { kind, parts, closed }
554
+ emittedText: '',
555
+ emittedReasoning: '',
556
+ legacyText: '',
557
+ legacyReasoning: '',
558
+ legacyClosed: new Set(),
559
+ refused: false,
560
+
561
+ getItem(event, kind, item) {
562
+ const key = items.pin(event, item, kind);
563
+ if (!this.itemState.has(key)) this.itemState.set(key, { kind, parts: new Map(), closed: false });
564
+ return this.itemState.get(key);
565
+ },
566
+
567
+ getPart(item, group, index, kind) {
568
+ const partKey = `${group}\0${index}`;
569
+ const previous = item.parts.get(partKey);
570
+ if (previous != null) {
571
+ if (previous.kind !== kind) throw new ProtocolError('invalid model frame', 'envelope_invalid');
572
+ return previous;
573
+ }
574
+ if (item.closed) throw new ProtocolError('invalid model frame', 'envelope_invalid');
575
+ const part = createContentPart(kind);
576
+ item.parts.set(partKey, part);
577
+ return part;
578
+ },
579
+
580
+ partEvent(event, group, kind, indexField, text, { closed, delta = false }) {
581
+ const index = indexOf(event, indexField, { required: true });
582
+ indexOf(event, 'output_index', { required: true });
583
+ const item = this.getItem(event, kind === 'summary_text' || kind === 'reasoning_text'
584
+ ? 'reasoning' : 'message');
585
+ if (delta && item.closed) throw new ProtocolError('invalid model frame', 'envelope_invalid');
586
+ const part = this.getPart(item, group, index, kind);
587
+ if (delta) {
588
+ if (part.closed) throw new ProtocolError('invalid model frame', 'envelope_invalid');
589
+ part.text += text;
590
+ } else {
591
+ this.snapshotPart(part, text, closed);
592
+ }
593
+ this.refused = this.refused || kind === 'refusal';
594
+ },
595
+
596
+ snapshotPart(part, text, closed) {
597
+ if ((part.closed && text !== part.text) || !text.startsWith(part.text)) {
598
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
599
+ }
600
+ part.text = text;
601
+ part.closed = part.closed || closed;
602
+ },
603
+
604
+ itemSnapshot(event, { closed }) {
605
+ const raw = objectOf(event.item);
606
+ const kind = identityOf(raw.type);
607
+ if (kind === 'message' && raw.role != null && raw.role !== 'assistant') throw new ProtocolError('invalid model frame', 'envelope_invalid');
608
+ const item = this.getItem(event, kind, raw);
609
+ if (!closed && item.closed) throw new ProtocolError('invalid model frame', 'envelope_invalid');
610
+ const groups = kind === 'message' ? ['content']
611
+ : kind === 'reasoning' ? ['summary', 'content'] : [];
612
+ const seen = new Set();
613
+ for (const group of groups) {
614
+ const values = Object.hasOwn(raw, group) ? raw[group] : [];
615
+ if (!Array.isArray(values)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
616
+ for (let index = 0; index < values.length; index++) {
617
+ const value = objectOf(values[index]);
618
+ let partKind = identityOf(value.type);
619
+ if (kind === 'message' && !TEXT_KINDS.has(partKind) && partKind !== 'refusal') {
620
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
621
+ }
622
+ if (kind === 'reasoning') {
623
+ const allowed = group === 'summary' ? ['summary_text', 'text'] : ['reasoning_text', 'text'];
624
+ if (!allowed.includes(partKind)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
625
+ }
626
+ // Legacy alias 'text' shares the canonical part type.
627
+ if (partKind === 'text') {
628
+ partKind = kind === 'message' ? 'output_text'
629
+ : group === 'summary' ? 'summary_text' : 'reasoning_text';
630
+ }
631
+ const text = optionalText(value[partKind === 'refusal' ? 'refusal' : 'text']);
632
+ this.getPart(item, group, index, partKind);
633
+ this.snapshotPart(item.parts.get(`${group}\0${index}`), text, closed);
634
+ this.refused = this.refused || partKind === 'refusal';
635
+ seen.add(`${group}\0${index}`);
636
+ }
637
+ }
638
+ if (closed) {
639
+ for (const key of item.parts.keys()) if (!seen.has(key)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
640
+ item.closed = true;
641
+ }
642
+ },
643
+
644
+ prefix({ reasoning }) {
645
+ const pieces = [];
646
+ let expected = 0;
647
+ for (const [index, key] of [...this.items.indexes.entries()].sort((a, b) => a[0] - b[0])) {
648
+ if (index !== expected) break;
649
+ expected += 1;
650
+ const kind = this.items.kinds.get(key);
651
+ const wanted = reasoning ? 'reasoning' : 'message';
652
+ if (kind !== wanted) continue;
653
+ const item = this.itemState.get(key);
654
+ if (item == null) break;
655
+ if (reasoning && !item.closed) break;
656
+ const groups = reasoning ? ['summary', 'content'] : ['content'];
657
+ for (const group of groups) {
658
+ let partIndex = 0;
659
+ const parts = [...item.parts.entries()]
660
+ .filter(([pk]) => pk.startsWith(`${group}\0`))
661
+ .sort((a, b) => Number(a[0].split('\0')[1]) - Number(b[0].split('\0')[1]));
662
+ for (const [pk, part] of parts) {
663
+ const i = Number(pk.split('\0')[1]);
664
+ if (i !== partIndex) return pieces.join('');
665
+ partIndex += 1;
666
+ if (part.kind !== 'refusal') pieces.push(part.text);
667
+ if (!part.closed) return pieces.join('');
668
+ }
669
+ }
670
+ if (!item.closed) break;
671
+ }
672
+ return pieces.join('');
673
+ },
674
+
675
+ drain() {
676
+ const text = this.prefix({ reasoning: false });
677
+ const reasoning = this.prefix({ reasoning: true });
678
+ if ((text && this.legacyText) || (reasoning && this.legacyReasoning)) {
679
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
680
+ }
681
+ if (!text.startsWith(this.emittedText) || !reasoning.startsWith(this.emittedReasoning)) {
682
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
683
+ }
684
+ const tails = [text.slice(this.emittedText.length), reasoning.slice(this.emittedReasoning.length)];
685
+ this.emittedText = text;
686
+ this.emittedReasoning = reasoning;
687
+ return tails;
688
+ },
689
+
690
+ accept(event) {
691
+ const type = event.type;
692
+ if (type === 'response.output_item.added' || type === 'response.output_item.done') {
693
+ this.itemSnapshot(event, { closed: type.endsWith('.done') });
694
+ } else if (type === 'response.content_part.added' || type === 'response.content_part.done'
695
+ || type === 'response.reasoning_summary_part.added'
696
+ || type === 'response.reasoning_summary_part.done') {
697
+ const part = objectOf(event.part);
698
+ const kind = identityOf(part.type);
699
+ const summary = type.includes('reasoning_summary_part');
700
+ const allowed = summary ? ['summary_text'] : ['output_text', 'refusal', 'reasoning_text'];
701
+ if (!allowed.includes(kind)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
702
+ this.partEvent(event, summary ? 'summary' : 'content', kind,
703
+ summary ? 'summary_index' : 'content_index',
704
+ optionalText(part[kind === 'refusal' ? 'refusal' : 'text']),
705
+ { closed: type.endsWith('.done') });
706
+ } else {
707
+ const stem = type.lastIndexOf('.');
708
+ const head = stem === -1 ? type : type.slice(0, stem);
709
+ const suffix = stem === -1 ? '' : type.slice(stem + 1);
710
+ const spec = DELTA_KINDS[head];
711
+ if (!spec || (suffix !== 'delta' && suffix !== 'done')) return this.drain();
712
+ const [group, kind, indexField] = spec;
713
+ const text = optionalText(event[suffix === 'delta' ? 'delta'
714
+ : kind === 'refusal' ? 'refusal' : 'text']);
715
+ if (!('item_id' in event) && !('output_index' in event) && !(indexField in event)) {
716
+ // Deliberate compatibility for old proxies that omit part identity:
717
+ // unindexed text/reasoning cannot be assigned to an indexed part.
718
+ this.refused = this.refused || kind === 'refusal';
719
+ if (kind === 'refusal') return ['', ''];
720
+ const attr = kind === 'output_text' ? 'legacyText' : 'legacyReasoning';
721
+ const previous = this[attr];
722
+ let tail;
723
+ if (suffix === 'done') {
724
+ if (!text.startsWith(previous) || (this.legacyClosed.has(attr) && text !== previous)) {
725
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
726
+ }
727
+ tail = text.slice(previous.length);
728
+ this[attr] = text;
729
+ this.legacyClosed.add(attr);
730
+ } else {
731
+ if (this.legacyClosed.has(attr)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
732
+ tail = text;
733
+ this[attr] = previous + text;
734
+ }
735
+ const indexed = attr === 'legacyText' ? this.emittedText : this.emittedReasoning;
736
+ if (indexed) throw new ProtocolError('invalid model frame', 'envelope_invalid');
737
+ return attr === 'legacyText' ? [tail, ''] : ['', tail];
738
+ }
739
+ this.partEvent(event, group, kind, indexField, text,
740
+ { closed: suffix === 'done', delta: suffix === 'delta' });
741
+ }
742
+ return this.drain();
743
+ },
744
+
745
+ // Reconcile a terminal `output` array with the observed stream: complete
746
+ // snapshots may fill gaps, never remove or rewrite closed parts.
747
+ reconcileOutput(output) {
748
+ const seen = new Set();
749
+ for (let index = 0; index < output.length; index++) {
750
+ const item = output[index];
751
+ if (!('id' in item) && !this.items.indexes.has(index)) continue;
752
+ const key = identityOf(item.id);
753
+ if (seen.has(key)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
754
+ seen.add(key);
755
+ const kind = item.type;
756
+ if (!this.itemState.has(key) && (
757
+ (kind === 'message' && this.legacyText)
758
+ || (kind === 'reasoning' && this.legacyReasoning))) {
759
+ this.items.pin({ output_index: index }, item);
760
+ continue;
761
+ }
762
+ this.itemSnapshot({ output_index: index, item }, { closed: true });
763
+ }
764
+ for (const key of this.itemState.keys()) if (!seen.has(key)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
765
+ return this.drain();
766
+ },
767
+
768
+ finish({ requireClosed = true } = {}) {
769
+ if (requireClosed) {
770
+ for (const item of this.itemState.values()) {
771
+ if ((item.kind === 'message' || item.kind === 'reasoning') && !item.closed) {
772
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
773
+ }
774
+ }
775
+ }
776
+ const text = this.prefix({ reasoning: false });
777
+ const reasoning = this.prefix({ reasoning: true });
778
+ if (text !== this.emittedText || reasoning !== this.emittedReasoning) {
779
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
780
+ }
781
+ for (const [key, item] of this.itemState) {
782
+ if ((item.kind === 'message' || item.kind === 'reasoning')
783
+ && !this.items.itemIndexes.has(key)) {
784
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
785
+ }
786
+ }
787
+ const indexes = [...this.items.indexes.keys()].sort((a, b) => a - b);
788
+ if (indexes.some((index, expected) => index !== expected)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
789
+ for (const [kind, expected] of [['message', this.emittedText], ['reasoning', this.emittedReasoning]]) {
790
+ const pieces = [];
791
+ for (const [, key] of [...this.items.indexes.entries()].sort((a, b) => a[0] - b[0])) {
792
+ const item = this.itemState.get(key);
793
+ if (item == null || item.kind !== kind) continue;
794
+ for (const group of (kind === 'message' ? ['content'] : ['summary', 'content'])) {
795
+ const parts = [...item.parts.entries()]
796
+ .filter(([pk]) => pk.startsWith(`${group}\0`))
797
+ .sort((a, b) => Number(a[0].split('\0')[1]) - Number(b[0].split('\0')[1]));
798
+ for (const [, part] of parts) if (part.kind !== 'refusal') pieces.push(part.text);
799
+ }
800
+ }
801
+ if (pieces.join('') !== expected) throw new ProtocolError('invalid model frame', 'envelope_invalid');
802
+ }
803
+ },
804
+ };
805
+ return state;
806
+ }
807
+
808
+ // --- streaming: tool calls (ports model_wire_tools.ResponsesToolCalls) ---
809
+
810
+ function createWireTool() {
811
+ return { callId: '', name: '', arguments: '', closed: false, argumentsDone: false };
812
+ }
813
+
814
+ function pinToolIdentity(tool, item, idName) {
815
+ for (const [source, target] of [[idName, 'callId'], ['name', 'name']]) {
816
+ if (!(source in item)) continue;
817
+ const value = identityOf(item[source]);
818
+ if (tool[target] && tool[target] !== value) throw new ProtocolError('invalid model frame', 'envelope_invalid');
819
+ tool[target] = value;
820
+ }
821
+ }
822
+
823
+ function sealTool(tool) {
824
+ identityOf(tool.callId);
825
+ identityOf(tool.name);
826
+ tool.closed = true;
827
+ }
828
+
829
+ function createResponsesToolCalls() {
830
+ const items = createResponsesItems();
831
+ return {
832
+ items,
833
+ calls: new Map(), // item_id -> WireTool
834
+
835
+ key(event, item) {
836
+ return items.pin(event, item, 'function_call');
837
+ },
838
+
839
+ accept(event) {
840
+ const type = event.type;
841
+ if (type === 'response.output_item.added' || type === 'response.output_item.done') {
842
+ const item = objectOf(event.item);
843
+ const itemType = identityOf(item.type);
844
+ validateItemKind(itemType);
845
+ items.pin(event, item);
846
+ if (itemType !== 'function_call') return null;
847
+ const key = this.key(event, item);
848
+ let current = this.calls.get(key);
849
+ if (type === 'response.output_item.added') {
850
+ if (current != null) throw new ProtocolError('invalid model frame', 'envelope_invalid');
851
+ current = createWireTool();
852
+ pinToolIdentity(current, item, 'call_id');
853
+ if ('arguments' in item) current.arguments = optionalText(item.arguments);
854
+ this.calls.set(key, current);
855
+ return null;
856
+ }
857
+ // output_item.done: a complete snapshot. Deltas still require a
858
+ // previously declared item, never a fabricated slot.
859
+ if (current == null) {
860
+ current = createWireTool();
861
+ this.calls.set(key, current);
862
+ }
863
+ pinToolIdentity(current, item, 'call_id');
864
+ if ('arguments' in item) {
865
+ const arguments_ = optionalText(item.arguments);
866
+ if (!arguments_.startsWith(current.arguments)
867
+ || (current.closed || current.argumentsDone) && arguments_ !== current.arguments) {
868
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
869
+ }
870
+ current.arguments = arguments_;
871
+ }
872
+ if (current.closed) return null;
873
+ sealTool(current);
874
+ return current;
875
+ }
876
+ if (type === 'response.function_call_arguments.delta' || type === 'response.function_call_arguments.done') {
877
+ const key = this.key(event);
878
+ const current = this.calls.get(key);
879
+ if (current == null || current.closed || current.argumentsDone) throw new ProtocolError('invalid model frame', 'envelope_invalid');
880
+ if (type.endsWith('.delta')) {
881
+ current.arguments += optionalText(event.delta);
882
+ } else {
883
+ const arguments_ = optionalText(event.arguments);
884
+ if (!arguments_.startsWith(current.arguments)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
885
+ current.arguments = arguments_;
886
+ current.argumentsDone = true;
887
+ }
888
+ }
889
+ return null;
890
+ },
891
+
892
+ finish() {
893
+ for (const tool of this.calls.values()) if (!tool.closed) throw new ProtocolError('invalid model frame', 'envelope_invalid');
894
+ },
895
+
896
+ // Complete snapshots may fill gaps, never remove or rewrite closed calls.
897
+ reconcileOutput(output) {
898
+ const seen = new Set();
899
+ const completed = [];
900
+ for (let index = 0; index < output.length; index++) {
901
+ const item = output[index];
902
+ if (item.type !== 'function_call') continue;
903
+ const key = identityOf(item.id);
904
+ if (seen.has(key)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
905
+ seen.add(key);
906
+ const current = this.calls.get(key);
907
+ if (current != null && !current.closed && current.arguments) {
908
+ if (!item.arguments.startsWith(current.arguments)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
909
+ }
910
+ const tool = this.accept({ type: 'response.output_item.done', item, output_index: index });
911
+ if (tool != null) completed.push(tool);
912
+ }
913
+ const callsKeys = new Set(this.calls.keys());
914
+ if (callsKeys.size !== seen.size || [...callsKeys].some((k) => !seen.has(k))) {
915
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
916
+ }
917
+ this.finish();
918
+ return completed;
919
+ },
920
+ };
921
+ }
922
+
923
+ // --- streaming: wire identity (ports model_wire_identity.ModelWireIdentity) ---
924
+
925
+ function createWireIdentity() {
926
+ return {
927
+ responseId: null,
928
+ model: null,
929
+ pin(field, value) {
930
+ if (value == null) return;
931
+ if (typeof value !== 'string' || !value.trim()) throw new ProtocolError('invalid model frame', 'envelope_invalid');
932
+ if (this[field] != null && this[field] !== value) throw new ProtocolError('invalid model frame', 'envelope_invalid');
933
+ this[field] = value;
934
+ },
935
+ accept(event) {
936
+ if (typeof event !== 'object' || Array.isArray(event)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
937
+ const type = event.type;
938
+ if (typeof type !== 'string' || !type.trim()) throw new ProtocolError('invalid model frame', 'envelope_invalid');
939
+ const response = objectOf(event.response);
940
+ this.pin('responseId', response.id);
941
+ this.pin('responseId', event.response_id);
942
+ this.pin('model', response.model);
943
+ if (type === 'response.output_text.delta' || type === 'response.reasoning_summary_text.delta'
944
+ || type === 'response.reasoning_text.delta' || type === 'response.function_call_arguments.delta'
945
+ || type === 'response.refusal.delta') {
946
+ textOf(event.delta);
947
+ }
948
+ },
949
+ };
950
+ }
951
+
952
+ // --- streaming state (ports _stream_responses) ---
953
+
954
+ // Assemble a sealed ModelResult from the Responses SSE stream. Frame errors
955
+ // throw ProtocolError; finalize preserves the accumulated text/usage and
956
+ // classifies the attempt. One terminal event (response.completed/incomplete/
957
+ // failed/error/cancelled) seals the attempt.
958
+ export function createResponsesStreamState(profile) {
959
+ const captureReplay = profile.providerReplayMode !== 'none';
960
+ const emitThinking = profile.providerThinkingDisplay ?? profile.providerReplayMode !== 'none';
961
+ const toolItems = createResponsesToolCalls();
962
+ const contentItems = createResponsesContent(toolItems.items);
963
+ const identity = createWireIdentity();
964
+ return {
965
+ text: '',
966
+ reasoning: '',
967
+ finish: null,
968
+ usage: null,
969
+ refused: false,
970
+ done: false,
971
+ completed: false,
972
+ streamError: null, // { kind, failureKind } from an in-stream error
973
+ status: null, // 'completed' | 'incomplete'
974
+
975
+ consume(frame, emit) {
976
+ const data = frame.data;
977
+ if (!data.trim()) return;
978
+ if (data.trim() === '[DONE]') return; // [DONE] is a transport marker; the
979
+ let event; // terminal is response.completed/incomplete.
980
+ try { event = strictParseJson(data); } catch { throw new ProtocolError('invalid stream json', 'json_invalid'); }
981
+ if (!event || typeof event !== 'object' || Array.isArray(event)) {
982
+ throw new ProtocolError('invalid stream envelope', 'envelope_invalid');
983
+ }
984
+ identity.accept(event);
985
+ const type = String(event.type || '');
986
+
987
+ if (type === 'error' || type === 'response.failed') {
988
+ this.handleStreamError(event);
989
+ this.done = true;
990
+ return;
991
+ }
992
+ if (type === 'response.cancelled' || type === 'response.canceled') {
993
+ // A provider that cancels mid-stream is a terminal protocol fact, not a
994
+ // user abort and not a retryable incomplete stream.
995
+ this.streamError = { kind: 'provider_terminal_error', failureKind: 'invalid_response' };
996
+ this.done = true;
997
+ return;
998
+ }
999
+
1000
+ toolItems.accept(event);
1001
+ const [text, reasoning] = contentItems.accept(event);
1002
+ this.refused = this.refused || contentItems.refused;
1003
+
1004
+ if (type === 'response.completed' || type === 'response.incomplete') {
1005
+ this.handleTerminal(event, { text, reasoning }, emit);
1006
+ this.done = true;
1007
+ return;
1008
+ }
1009
+
1010
+ if (text) { this.text += text; emit({ kind: 'text', text }); }
1011
+ if (reasoning) {
1012
+ this.reasoning += reasoning;
1013
+ if (emitThinking) emit({ kind: 'thinking', text: reasoning });
1014
+ }
1015
+ // Emit display-only tool argument fragments. The sealed batch in the
1016
+ // ModelResult is authoritative; this never executes a tool.
1017
+ if (type === 'response.function_call_arguments.delta' && event.delta) {
1018
+ const index = event.output_index ?? toolItems.items.itemIndexes.get(event.item_id);
1019
+ const name = toolItems.calls.get(event.item_id)?.name;
1020
+ emit({ kind: 'tool_delta', text: event.delta,
1021
+ metadata: { ...(index != null ? { index } : {}), ...(name ? { name } : {}),
1022
+ ...(event.item_id ? { callId: event.item_id } : {}) } });
1023
+ }
1024
+ },
1025
+
1026
+ handleStreamError(event) {
1027
+ // Build the failed body and classify. response.failed carries its own
1028
+ // status; a generic error wraps the error/usage.
1029
+ let failedBody;
1030
+ if (event.type === 'response.failed') {
1031
+ failedBody = objectOf(event.response);
1032
+ if (failedBody.status != null && failedBody.status !== 'failed') throw new ProtocolError('invalid model frame', 'envelope_invalid');
1033
+ failedBody = { ...failedBody, status: 'failed' };
1034
+ } else {
1035
+ failedBody = { error: event.error != null ? event.error : event, usage: event.usage };
1036
+ }
1037
+ const usage = parseResponsesUsage(failedBody.usage);
1038
+ if (usage != null) this.usage = usage;
1039
+ const errorObj = failedBody.error != null && typeof failedBody.error === 'object' ? failedBody.error : {};
1040
+ const code = String(errorObj.code || errorObj.type || '');
1041
+ let kind; let failureKind;
1042
+ if (RETRYABLE_ERROR_CODES.has(code)) {
1043
+ kind = 'provider_retryable_error';
1044
+ failureKind = code === 'rate_limit_error' || code === 'rate_limit_exceeded' ? 'rate_limit' : 'server_error';
1045
+ } else if (TERMINAL_ERROR_CODES.has(code)) {
1046
+ kind = 'provider_terminal_error';
1047
+ failureKind = 'client_error';
1048
+ } else {
1049
+ kind = 'provider_terminal_error';
1050
+ failureKind = 'invalid_response';
1051
+ }
1052
+ this.streamError = { kind, failureKind };
1053
+ },
1054
+
1055
+ handleTerminal(event, { text: drainText, reasoning: drainReasoning }, emit) {
1056
+ const responsePayload = objectOf(event.response);
1057
+ this.status = event.type === 'response.completed' ? 'completed' : 'incomplete';
1058
+ // A terminal `status` field that disagrees with the event type is a
1059
+ // protocol conflict, checked before any metadata-only short-circuit.
1060
+ if (responsePayload.status != null && responsePayload.status !== this.status) {
1061
+ throw new ProtocolError('invalid model frame', 'envelope_invalid');
1062
+ }
1063
+ const usageValue = parseResponsesUsage(responsePayload.usage);
1064
+ if (usageValue != null) this.usage = usageValue;
1065
+ // A successful terminal cannot simultaneously carry an error. In
1066
+ // particular, no function_call from that conflicting body may dispatch.
1067
+ if (responsePayload.error != null) throw new ProtocolError('invalid model frame', 'envelope_invalid');
1068
+
1069
+ // Reconcile the terminal body with the observed stream. A metadata-only
1070
+ // terminal (no output) keeps the streamed text/tools as observed.
1071
+ let textTail = '';
1072
+ let reasoningTail = '';
1073
+ let completedCalls = [];
1074
+ const hasOutput = 'output' in responsePayload;
1075
+ const hasOutputText = 'output_text' in responsePayload;
1076
+ if (hasOutput || hasOutputText) {
1077
+ let projection;
1078
+ try {
1079
+ projection = projectResponsesOutput(responsePayload, { status: this.status });
1080
+ } catch { throw new ProtocolError('invalid model frame', 'envelope_invalid'); }
1081
+ if (projection.refused) this.refused = true;
1082
+ if (!this.refused) {
1083
+ if (hasOutput && contentItems) {
1084
+ try {
1085
+ const [ct, cr] = contentItems.reconcileOutput(responsePayload.output || []);
1086
+ textTail = ct; reasoningTail = cr;
1087
+ } catch { throw new ProtocolError('invalid model frame', 'envelope_invalid'); }
1088
+ }
1089
+ // The projection's content must extend the observed text+tail.
1090
+ const observedText = this.text + drainText + textTail;
1091
+ if (!projection.content.startsWith(observedText)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
1092
+ textTail += projection.content.slice(observedText.length);
1093
+ if (hasOutput) {
1094
+ const observedReasoning = this.reasoning + drainReasoning + reasoningTail;
1095
+ if (!projection.reasoning.startsWith(observedReasoning)) throw new ProtocolError('invalid model frame', 'envelope_invalid');
1096
+ reasoningTail += projection.reasoning.slice(observedReasoning.length);
1097
+ }
1098
+ if (hasOutput && this.status === 'completed') {
1099
+ try { completedCalls = toolItems.reconcileOutput(responsePayload.output || []); }
1100
+ catch { throw new ProtocolError('invalid model frame', 'envelope_invalid'); }
1101
+ }
1102
+ }
1103
+ }
1104
+
1105
+ // Emit the reconciled tails.
1106
+ if (textTail) { this.text += drainText + textTail; emit({ kind: 'text', text: drainText + textTail }); }
1107
+ else if (drainText) { this.text += drainText; emit({ kind: 'text', text: drainText }); }
1108
+ const privateTail = drainReasoning + reasoningTail;
1109
+ if (privateTail) {
1110
+ this.reasoning += privateTail;
1111
+ if (emitThinking) emit({ kind: 'thinking', text: privateTail });
1112
+ }
1113
+
1114
+ // Determine the finish reason.
1115
+ let finish;
1116
+ if (this.refused) {
1117
+ finish = 'content_filter';
1118
+ } else if (this.status === 'completed') {
1119
+ try { toolItems.finish(); contentItems.finish(); } catch { throw new ProtocolError('invalid model frame', 'envelope_invalid'); }
1120
+ finish = toolItems.calls.size ? 'tool_calls' : 'stop';
1121
+ } else {
1122
+ const details = responsePayload.incomplete_details;
1123
+ const reason = details != null ? optionalText(objectOf(details).reason) : '';
1124
+ if (reason === 'max_output_tokens') {
1125
+ try { contentItems.finish({ requireClosed: false }); } catch { throw new ProtocolError('invalid model frame', 'envelope_invalid'); }
1126
+ }
1127
+ // An unclosed tool input must not grant bounded-text commit authority
1128
+ // even when output exhaustion is confirmed.
1129
+ const openInput = [...toolItems.calls.values()].some((tool) => !tool.closed);
1130
+ if (reason === 'content_filter') finish = 'content_filter';
1131
+ else if (reason === 'max_output_tokens' && !openInput) finish = 'length';
1132
+ else finish = 'incomplete';
1133
+ if (hasOutput && completedCalls.length) finish = 'tool_calls';
1134
+ }
1135
+ this.finish = finish;
1136
+ this.completed = true;
1137
+ },
1138
+
1139
+ finalize({ errorKind = null, failureKind = null } = {}) {
1140
+ const { usage, usageReported } = sealUsage(this.usage);
1141
+ const reasoning = this.reasoning;
1142
+ // An in-stream error or a transport error during the attempt seals the
1143
+ // observed text with no tools; usage is preserved when reported.
1144
+ if (this.streamError) {
1145
+ return {
1146
+ kind: this.streamError.kind, text: this.text, toolCalls: [], usage, usageReported,
1147
+ ...(this.streamError.failureKind ? { failureKind: this.streamError.failureKind } : {}),
1148
+ };
1149
+ }
1150
+ if (errorKind && errorKind !== 'provider_retryable_error') {
1151
+ return { kind: errorKind, text: this.text, toolCalls: [], usage, usageReported,
1152
+ ...(failureKind ? { failureKind } : {}) };
1153
+ }
1154
+ if (errorKind === 'provider_retryable_error' || (!this.done && !this.completed)) {
1155
+ const normalized = normalizeResponse({ text: this.text, toolCalls: [], finishReason: this.finish, streamComplete: false });
1156
+ if (normalized.kind === 'provider_retryable_error') {
1157
+ return { ...normalized, usage, usageReported, failureKind: failureKind || 'incomplete_response' };
1158
+ }
1159
+ return sealModelReplay({ ...normalized, usage, usageReported }, profile, reasoning);
1160
+ }
1161
+ // done with a terminal event: assemble the full response.
1162
+ const finish = this.refused ? 'content_filter' : this.finish;
1163
+ let calls;
1164
+ let malformed = false;
1165
+ try {
1166
+ calls = this.buildCalls();
1167
+ } catch {
1168
+ malformed = true;
1169
+ calls = [{}];
1170
+ }
1171
+ if (this.refused) { calls = []; }
1172
+ const normalized = normalizeResponse({
1173
+ text: this.text, toolCalls: malformed ? [{}] : calls,
1174
+ finishReason: finish, streamComplete: true,
1175
+ });
1176
+ return sealModelReplay({ ...normalized, usage, usageReported }, profile, reasoning);
1177
+ },
1178
+
1179
+ buildCalls() {
1180
+ const calls = [];
1181
+ const indexes = [...toolItems.items.indexes.entries()].sort((a, b) => a[0] - b[0]);
1182
+ for (const [, key] of indexes) {
1183
+ const tool = toolItems.calls.get(key);
1184
+ if (!tool) continue;
1185
+ if (!tool.closed) throw new ProtocolError('invalid model frame', 'envelope_invalid');
1186
+ let arguments_;
1187
+ let invalid = false;
1188
+ try {
1189
+ arguments_ = strictParseJson(tool.arguments || '{}');
1190
+ if (arguments_ == null || typeof arguments_ !== 'object' || Array.isArray(arguments_)) throw new Error();
1191
+ } catch {
1192
+ arguments_ = {}; invalid = true;
1193
+ }
1194
+ calls.push({
1195
+ callId: tool.callId, name: tool.name, arguments: arguments_,
1196
+ ...(invalid ? { wireArgumentsInvalid: true } : {}),
1197
+ });
1198
+ }
1199
+ // Reject duplicate call ids in the sealed batch.
1200
+ const ids = calls.map((c) => c.callId);
1201
+ if (new Set(ids).size !== ids.length) throw new ProtocolError('duplicate tool call ids');
1202
+ return calls;
1203
+ },
1204
+ };
1205
+ }