@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,561 @@
1
+ // OpenAI-compatible chat completions wire adapter.
2
+ //
3
+ // Ports ksadk.harness.model_adapters.native_openai: one HTTP request per attempt,
4
+ // sealed SSE turns with partial-chunk tool assembly, refusal/usage/error
5
+ // normalization, and private reasoning_content replay. The adapter never retries
6
+ // and never executes tools; it emits text/thinking/tool_delta display deltas and
7
+ // returns one sealed ModelResult. Kernel policy owns backoff and call budgets.
8
+ //
9
+ // Profiled inline reasoning is projected under the Run's frozen policy.
10
+
11
+ import {
12
+ normalizeResponse, parseOpenAIUsage, sealUsage, ProtocolError, resolveUrl, boundOutputTokens,
13
+ } from './model-normalize.mjs';
14
+ import { sealModelReplay, openAIReplayFields } from './model-replay.mjs';
15
+ import { validateMediaBlock, projectMediaOpenAI, MediaError } from './model-media.mjs';
16
+ import { inlineReasoningMarker, projectInlineReasoning, createInlineReasoningProjector } from './model-inline-reasoning.mjs';
17
+ import { applyProviderChatCompatibility } from './model-provider-compat.mjs';
18
+ import { mergeChatUsage } from './model-chat-usage.mjs';
19
+ // parseModelJson is the kernel's strict finite JSON parser: it rejects
20
+ // duplicate object keys and non-finite numbers (e.g. 1e400). Using it here keeps
21
+ // provider-envelope and tool-argument decoding consistent with the kernel's
22
+ // own validation, so a malformed provider payload is a terminal protocol error
23
+ // rather than a silently-accepted ambiguity.
24
+ import { parseModelJson, stringifyModelJson, modelWireToolCall, FINAL_SUBMISSION_TOOL } from '@kingsoftcloud/agentkit-harness-core';
25
+
26
+ // Wrap parseModelJson so its NativeError codes become the adapter's ProtocolError.
27
+ function strictParseJson(text) {
28
+ try { return parseModelJson(text); }
29
+ catch (error) { throw new ProtocolError(error?.code || 'invalid_model_json', 'json_invalid'); }
30
+ }
31
+
32
+ // Body keys the host binding parameters may never override: messages, tools,
33
+ // model and stream are request/adapter owned; the rest are security-sensitive
34
+ // transport values that belong to the adapter, not the binding parameters.
35
+ const FORBIDDEN_BODY_KEYS = new Set([
36
+ 'model', 'messages', 'tools', 'stream', 'n', 'tool_choice', 'parallel_tool_calls', 'max_tokens', 'max_completion_tokens',
37
+ 'provider_thinking_display',
38
+ 'provider_tool_search',
39
+ ]);
40
+
41
+ // Validate the host binding shape before any HTTP attempt.
42
+ export function validateOpenAIBinding(binding) {
43
+ if (!binding || typeof binding !== 'object') throw new ProtocolError('openai binding missing');
44
+ if (binding.protocol !== 'openai-chat') throw new ProtocolError('binding is not openai-chat');
45
+ if (typeof binding.baseUrl !== 'string' || !binding.baseUrl) throw new ProtocolError('openai binding requires baseUrl');
46
+ if (typeof binding.apiKey !== 'string' || !binding.apiKey) throw new ProtocolError('openai binding requires apiKey');
47
+ if (typeof binding.model !== 'string' || !binding.model.trim()) throw new ProtocolError('openai binding requires model');
48
+ try { new URL(binding.baseUrl); } catch { throw new ProtocolError('openai binding baseUrl is not a URL'); }
49
+ }
50
+
51
+ function securityHeaders() {
52
+ return new Set(['authorization', 'x-api-key', 'anthropic-version', 'content-type', 'accept']);
53
+ }
54
+
55
+ // Build the chat completions request. The adapter controls model, messages,
56
+ // stream and tools; binding.parameters may only add generation fields, never
57
+ // override the forbidden keys or security-sensitive headers.
58
+ export function buildOpenAIRequest(binding, request) {
59
+ validateOpenAIBinding(binding);
60
+ const profile = request.profile;
61
+ const url = resolveUrl(binding.baseUrl, 'chat/completions');
62
+
63
+ const headers = { Accept: profile.streaming === false ? 'application/json' : 'text/event-stream' };
64
+ const skip = securityHeaders();
65
+ for (const [name, value] of Object.entries(binding.headers || {})) {
66
+ if (skip.has(name.toLowerCase())) continue;
67
+ if (typeof value !== 'string') continue;
68
+ headers[name] = value;
69
+ }
70
+ headers['Content-Type'] = 'application/json';
71
+ headers.Authorization = `Bearer ${binding.apiKey}`;
72
+
73
+ const payload = {
74
+ model: binding.model,
75
+ messages: projectMessagesOpenAI(request, request.maxInlineImageBytes),
76
+ stream: profile.streaming ?? true,
77
+ n: 1,
78
+ };
79
+ payload.max_tokens = boundOutputTokens(undefined, profile.maxOutputTokens);
80
+
81
+ const parameters = binding.parameters || {};
82
+ for (const [key, value] of Object.entries(parameters)) {
83
+ if (FORBIDDEN_BODY_KEYS.has(key)) continue;
84
+ payload[key] = value;
85
+ }
86
+ // max_completion_tokens replaces max_tokens; a binding that selects the newer
87
+ // chat parameter must not send both.
88
+ if (profile.outputTokenParameter === 'max_completion_tokens'
89
+ || profile.outputTokenParameter === undefined && 'max_completion_tokens' in parameters) {
90
+ payload.max_completion_tokens = boundOutputTokens(parameters.max_completion_tokens, profile.maxOutputTokens);
91
+ delete payload.max_tokens;
92
+ } else if ('max_tokens' in parameters) {
93
+ payload.max_tokens = boundOutputTokens(parameters.max_tokens, profile.maxOutputTokens);
94
+ }
95
+
96
+ // Advertise tools: during a finalization-only turn the kernel has already
97
+ // stripped normal workspace tools, but the adapter filters defensively and
98
+ // keeps the internal submit_final_output tool so the model can submit its
99
+ // final answer. Normal tools are never sent during finalization.
100
+ const tools = request.tools || [];
101
+ const advertised = request.finalizationOnly
102
+ ? tools.filter((tool) => tool.name === FINAL_SUBMISSION_TOOL)
103
+ : tools;
104
+ if (advertised.length) {
105
+ payload.tools = advertised.map((tool) => ({
106
+ type: 'function',
107
+ function: { name: tool.name, description: tool.description, parameters: tool.parameters || { type: 'object', properties: {} } },
108
+ }));
109
+ payload.tool_choice = request.toolChoice ?? (profile.providerCompatibilityPolicy && request.finalizationOnly
110
+ ? { type: 'function', function: { name: FINAL_SUBMISSION_TOOL } } : 'auto');
111
+ }
112
+ if (profile.parallelToolCalls !== undefined) payload.parallel_tool_calls = profile.parallelToolCalls;
113
+ if (profile.temperature !== undefined) payload.temperature = profile.temperature;
114
+ if (profile.topP !== undefined) payload.top_p = profile.topP;
115
+ if (profile.reasoningEffort !== undefined) payload.reasoning_effort = profile.reasoningEffort;
116
+ if (profile.thinkingMode !== undefined) payload.thinking = { type: profile.thinkingMode };
117
+ return { url, headers, body: stringifyModelJson(applyProviderChatCompatibility(payload, request, binding)) };
118
+ }
119
+
120
+ // Project host messages onto the chat wire, admitting media only on user/tool
121
+ // content and collecting tool-receipt images into a following user message.
122
+ // Assistant tool batches require stable, unique IDs and matching receipts.
123
+ // maxInlineImageBytes bounds encoded inline URLs; a cross-profile replay is
124
+ // dropped before it reaches the wire.
125
+ export function projectMessagesOpenAI(request, maxInlineImageBytes) {
126
+ const profile = request.profile;
127
+ const projected = [];
128
+ const pending = new Set();
129
+ let images = [];
130
+ for (const message of request.messages || []) {
131
+ const role = message.role;
132
+ if (role !== 'tool' && pending.size) {
133
+ throw new ProtocolError('incomplete assistant tool batch before next message');
134
+ }
135
+ if (role === 'assistant' && message.toolCalls && message.toolCalls.length) {
136
+ validateAssistantToolIds(message.toolCalls);
137
+ for (const call of message.toolCalls) pending.add(call.callId);
138
+ }
139
+ if (role === 'tool') {
140
+ if (!pending.has(message.callId)) {
141
+ throw new ProtocolError('tool receipt does not match the pending assistant batch');
142
+ }
143
+ pending.delete(message.callId);
144
+ }
145
+ const media = message.media ? message.media.map((b) => validateMediaBlock(b, maxInlineImageBytes)) : null;
146
+ const hasMedia = media && media.length;
147
+ if (hasMedia && role !== 'user' && role !== 'tool') {
148
+ throw new MediaError('image observations require user or tool content');
149
+ }
150
+ if (hasMedia && role === 'tool') {
151
+ const text = message.content || 'Image observation follows the tool batch.';
152
+ projected.push({ role: 'tool', tool_call_id: message.callId, content: text });
153
+ images.push(
154
+ { type: 'text', text: `Image observation for tool call ${message.callId}:` },
155
+ ...projectMediaOpenAI(media),
156
+ );
157
+ } else if (hasMedia && role === 'user') {
158
+ projected.push({
159
+ role: 'user',
160
+ content: [{ type: 'text', text: message.content || '' }, ...projectMediaOpenAI(media)],
161
+ });
162
+ } else if (role === 'assistant') {
163
+ const msg = { role: 'assistant', content: message.content || '' };
164
+ if (message.toolCalls && message.toolCalls.length) {
165
+ msg.tool_calls = message.toolCalls.map(modelWireToolCall).map((call) => ({
166
+ id: call.callId, type: 'function',
167
+ function: { name: call.name, arguments: safeStringifyArguments(call.arguments) },
168
+ }));
169
+ }
170
+ Object.assign(msg, openAIReplayFields(message, profile));
171
+ projected.push(msg);
172
+ } else if (role === 'tool') {
173
+ projected.push({ role: 'tool', tool_call_id: message.callId, content: message.content || '' });
174
+ } else {
175
+ projected.push({ role, content: message.content || '' });
176
+ }
177
+ if (role === 'tool' && pending.size === 0 && images.length) {
178
+ projected.push({ role: 'user', content: images });
179
+ images = [];
180
+ }
181
+ }
182
+ if (pending.size) throw new ProtocolError('assistant tool batch is missing receipts');
183
+ return projected;
184
+ }
185
+
186
+ function safeStringifyArguments(arguments_) {
187
+ if (arguments_ == null || typeof arguments_ !== 'object') return '{}';
188
+ try { return stringifyModelJson(arguments_); } catch { return '{}'; }
189
+ }
190
+
191
+ function validateAssistantToolIds(calls) {
192
+ const ids = [];
193
+ for (const call of calls) {
194
+ const id = String(call?.callId ?? '');
195
+ if (!id.trim()) throw new ProtocolError('assistant tool calls require stable IDs');
196
+ ids.push(id);
197
+ }
198
+ if (new Set(ids).size !== ids.length) {
199
+ throw new ProtocolError('duplicate assistant tool IDs');
200
+ }
201
+ }
202
+
203
+ // Parse a buffered 2xx chat completions body into a sealed result. Used when a
204
+ // 2xx response is not SSE (rare for stream:true); also the basis for envelope
205
+ // validation. Throws ProtocolError on a malformed envelope.
206
+ export function parseOpenAIBuffered(bodyText, request, { captureReplay = false, binding, onThinking } = {}) {
207
+ let payload;
208
+ try { payload = strictParseJson(bodyText); } catch { throw new ProtocolError('invalid response json', 'json_invalid'); }
209
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
210
+ throw new ProtocolError('invalid response envelope', 'envelope_invalid');
211
+ }
212
+ if ('error' in payload) throw new ProtocolError('invalid response envelope', 'provider_error');
213
+ const choices = payload.choices;
214
+ if (!Array.isArray(choices) || choices.length > 1) {
215
+ throw new ProtocolError('expected one completion choice', 'choice_invalid');
216
+ }
217
+ // parseOpenAIUsage returns null when no usage was reported; an invalid report
218
+ // throws a protocol error (terminal), never a fabricated zero-cost claim.
219
+ const reportedUsage = parseOpenAIUsage(payload.usage);
220
+ if (!choices.length) {
221
+ return finalizeResult({ kind: 'empty_response', text: '', toolCalls: [], reportedUsage, request, reasoning: '', captureReplay });
222
+ }
223
+ const choice = choices[0];
224
+ if (!choice || typeof choice !== 'object') throw new ProtocolError('invalid completion choice', 'choice_invalid');
225
+ let finish = choice.finish_reason;
226
+ if (finish != null && typeof finish !== 'string') throw new ProtocolError('invalid finish reason', 'termination_conflict');
227
+ const message = choice.message;
228
+ if (!message || typeof message !== 'object' || message.role !== 'assistant') {
229
+ throw new ProtocolError('invalid assistant message');
230
+ }
231
+ if (chatRefusal(message.refusal)) finish = 'content_filter';
232
+ let text = message.content;
233
+ if (text == null) text = '';
234
+ if (typeof text !== 'string') throw new ProtocolError('unsupported assistant content');
235
+ text = projectInlineReasoning(text, inlineReasoningMarker(request.profile.inlineReasoningPolicy, binding), request.profile.inlineReasoningPolicy);
236
+ let calls;
237
+ // tool_calls truthiness mirrors Python: an empty array carries no calls.
238
+ const rawCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
239
+ try {
240
+ calls = parseToolCalls(message.tool_calls);
241
+ // A legacy function_call envelope is an unsupported, half-built batch: the
242
+ // normalizer owns precedence, so it becomes malformed_tool_call, not terminal.
243
+ if ('function_call' in message) throw new ProtocolError('legacy function_call protocol is not supported');
244
+ } catch {
245
+ // length/content_filter still wins over a half-built tool batch below.
246
+ calls = [{}];
247
+ }
248
+ if (finish == null && (text.trim() || rawCalls || calls.length)) {
249
+ return finalizeResult({
250
+ kind: 'provider_retryable_error', text, toolCalls: [], reportedUsage, request,
251
+ failureKind: 'incomplete_response', reasoning: '', captureReplay,
252
+ });
253
+ }
254
+ const reasoning = captureReplay || onThinking ? chatReasoning(message) : '';
255
+ const result = finalizeResult({
256
+ kind: null, text, toolCalls: calls, reportedUsage, request, finishReason: finish,
257
+ streamComplete: true, reasoning, captureReplay,
258
+ });
259
+ onThinking?.(reasoning);
260
+ return result;
261
+ }
262
+
263
+ function finalizeResult({
264
+ kind, text, toolCalls, reportedUsage, request, finishReason = null, streamComplete = true,
265
+ failureKind = null, retryAfterMs = null, reasoning = '', captureReplay = false,
266
+ }) {
267
+ const { usage, usageReported } = sealUsage(reportedUsage);
268
+ if (kind) {
269
+ return {
270
+ kind, text, toolCalls, usage, usageReported,
271
+ ...(failureKind ? { failureKind } : {}),
272
+ ...(retryAfterMs != null ? { retryAfterMs } : {}),
273
+ };
274
+ }
275
+ const normalized = normalizeResponse({ text, toolCalls, finishReason, streamComplete });
276
+ const result = {
277
+ ...normalized, usage, usageReported,
278
+ ...(normalized.kind === 'provider_retryable_error' && failureKind ? { failureKind } : {}),
279
+ ...(retryAfterMs != null ? { retryAfterMs } : {}),
280
+ };
281
+ return captureReplay ? sealModelReplay(result, request.profile, reasoning) : result;
282
+ }
283
+
284
+ // Assemble a tool batch from accumulated stream fragments. Invalid argument
285
+ // JSON becomes a non-executable call (arguments={}, wireArgumentsInvalid);
286
+ // a bad envelope throws and the caller marks the whole batch malformed.
287
+ function parseToolCalls(raw) {
288
+ if (raw == null) return [];
289
+ if (!Array.isArray(raw)) throw new ProtocolError('tool_calls must be an array');
290
+ const calls = [];
291
+ for (const entry of raw) {
292
+ if (!entry || typeof entry !== 'object' || entry.type !== 'function') {
293
+ throw new ProtocolError('unsupported tool call envelope');
294
+ }
295
+ const fn = entry.function;
296
+ if (!fn || typeof fn !== 'object' || typeof fn.arguments !== 'string') {
297
+ throw new ProtocolError('function arguments must be encoded JSON');
298
+ }
299
+ let arguments_;
300
+ let invalid = false;
301
+ try {
302
+ arguments_ = strictParseJson(fn.arguments);
303
+ if (arguments_ == null || typeof arguments_ !== 'object' || Array.isArray(arguments_)) throw new Error();
304
+ } catch {
305
+ // Invalid or non-finite argument bytes become a non-executable tool call:
306
+ // arguments={} with wireArgumentsInvalid=true. The kernel rejects
307
+ // execution and emits one bounded tool-error receipt; the flag is never
308
+ // silently cleared here.
309
+ arguments_ = {}; invalid = true;
310
+ }
311
+ calls.push({
312
+ callId: entry.id != null ? String(entry.id) : '',
313
+ name: fn.name != null ? String(fn.name) : '',
314
+ arguments: arguments_,
315
+ ...(invalid ? { wireArgumentsInvalid: true } : {}),
316
+ });
317
+ }
318
+ return calls;
319
+ }
320
+
321
+ function chatRefusal(value) {
322
+ if (value == null) return false;
323
+ if (typeof value !== 'string') throw new ProtocolError('invalid model refusal field', 'delta_invalid');
324
+ return value.length > 0;
325
+ }
326
+
327
+ function chatReasoning(message) {
328
+ const native = message.reasoning_content;
329
+ const alias = message.reasoning;
330
+ for (const value of [native, alias]) {
331
+ if (value != null && typeof value !== 'string') throw new ProtocolError('invalid private reasoning field', 'delta_invalid');
332
+ }
333
+ if (native && alias && native !== alias) throw new ProtocolError('conflicting private reasoning fields', 'delta_invalid');
334
+ return native || alias || '';
335
+ }
336
+
337
+ // Streaming turn state: accumulates visible text, private reasoning, an
338
+ // assembled tool batch and reported usage across SSE frames. Mirrors
339
+ // ksadk._StreamTurn. Frame errors throw ProtocolError; finalize preserves the
340
+ // accumulated text/usage and classifies the attempt.
341
+ export function createOpenAIStreamTurn(profile, binding) {
342
+ const captureReplay = profile.providerReplayMode !== 'none';
343
+ const emitThinking = profile.providerThinkingDisplay ?? profile.providerReplayMode !== 'none';
344
+ const marker = inlineReasoningMarker(profile.inlineReasoningPolicy, binding);
345
+ const projector = createInlineReasoningProjector(marker, profile.inlineReasoningPolicy);
346
+ let emitContent;
347
+ return {
348
+ text: '',
349
+ reasoning: '',
350
+ finish: null,
351
+ usage: null,
352
+ usageFrameIndex: null,
353
+ usageFinalized: false,
354
+ usageAtFinish: false,
355
+ frameIndex: -1,
356
+ completionId: null,
357
+ tools: new Map(),
358
+ malformedTools: false,
359
+ refused: false,
360
+ done: false,
361
+
362
+ consume(frame, emit) {
363
+ this.frameIndex++;
364
+ emitContent = emit;
365
+ const data = frame.data;
366
+ if (data.trim() === '[DONE]') { this.done = true; return; }
367
+ let payload;
368
+ try { payload = strictParseJson(data); } catch { throw new ProtocolError('invalid stream json', 'json_invalid'); }
369
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
370
+ throw new ProtocolError('invalid stream envelope', 'envelope_invalid');
371
+ }
372
+ if ('error' in payload) throw new ProtocolError('invalid stream envelope', 'provider_error');
373
+ if (payload.id != null) {
374
+ if (typeof payload.id !== 'string' || !payload.id) throw new ProtocolError('invalid completion ID', 'identity_conflict');
375
+ if (this.completionId != null && payload.id !== this.completionId) {
376
+ throw new ProtocolError('stream mixed completion identities', 'identity_conflict');
377
+ }
378
+ this.completionId = payload.id;
379
+ }
380
+ const reported = parseOpenAIUsage(payload.usage);
381
+ if (reported != null) {
382
+ let next;
383
+ try { next = mergeChatUsage(this.usage, reported, profile.chatUsagePolicy, this.usageFinalized); }
384
+ catch (error) {
385
+ if (error.code !== 'usage_conflict') throw error;
386
+ this.usageConflict = { previousFrameIndex: this.usageFrameIndex,
387
+ previous: usageCounters(this.usage), reported: usageCounters(reported) };
388
+ throw error;
389
+ }
390
+ this.usage = next;
391
+ this.usageFrameIndex = this.frameIndex;
392
+ }
393
+ const choices = payload.choices;
394
+ if (!Array.isArray(choices) || choices.length > 1) throw new ProtocolError('stream requires one choice', 'choice_invalid');
395
+ if (!choices.length) {
396
+ if (reported && this.finish != null) { this.usageFinalized = true; this.usageAtFinish = true; }
397
+ return;
398
+ }
399
+ const choice = choices[0];
400
+ if (!choice || typeof choice !== 'object' || typeof choice.index !== 'number' || choice.index !== 0 || 'message' in choice) {
401
+ throw new ProtocolError('snapshot or alternate choice cannot be merged into a delta stream', 'choice_invalid');
402
+ }
403
+ let delta = choice.delta;
404
+ if (delta == null) delta = {};
405
+ if (typeof delta !== 'object' || Array.isArray(delta)) throw new ProtocolError('invalid stream delta', 'delta_invalid');
406
+ if (delta.role != null && delta.role !== 'assistant') {
407
+ throw new ProtocolError('invalid stream delta', 'delta_invalid');
408
+ }
409
+ const refusal = chatRefusal(delta.refusal);
410
+ if (this.finish != null && refusal) throw new ProtocolError('refusal continued after finish', 'termination_conflict');
411
+ this.refused = refusal || this.refused;
412
+ let content = delta.content;
413
+ if (content == null) content = '';
414
+ if (typeof content !== 'string') throw new ProtocolError('invalid visible content delta', 'delta_invalid');
415
+ const reasoning = chatReasoning(delta);
416
+ if (this.finish != null && reasoning) throw new ProtocolError('private reasoning continued after finish', 'termination_conflict');
417
+ if (captureReplay) this.reasoning += reasoning;
418
+ if (this.finish != null && (content || delta.tool_calls)) {
419
+ throw new ProtocolError('content continued after finish', 'termination_conflict');
420
+ }
421
+ const finish = choice.finish_reason;
422
+ if (finish != null) {
423
+ if (typeof finish !== 'string' || (this.finish != null && finish !== this.finish)) {
424
+ throw new ProtocolError('conflicting finish reasons', 'termination_conflict');
425
+ }
426
+ this.finish = finish;
427
+ if (reported) this.usageAtFinish = true;
428
+ }
429
+ this.text += content;
430
+ const publicContent = projector.push(content);
431
+ if (publicContent) emit({ kind: 'text', text: publicContent });
432
+ if (reasoning && emitThinking) emit({ kind: 'thinking', text: reasoning });
433
+ try {
434
+ if ('function_call' in delta) throw new ProtocolError('legacy function calls are unsupported');
435
+ this.addToolFragments(delta.tool_calls, emit);
436
+ } catch {
437
+ this.malformedTools = true;
438
+ }
439
+ },
440
+
441
+ addToolFragments(fragments, emit) {
442
+ if (fragments == null) return;
443
+ if (!Array.isArray(fragments)) throw new ProtocolError('tool deltas require a list');
444
+ for (const fragment of fragments) {
445
+ if (!fragment || typeof fragment !== 'object' || typeof fragment.index !== 'number' || fragment.index < 0) {
446
+ throw new ProtocolError('invalid tool fragment index');
447
+ }
448
+ const call = this.tools.get(fragment.index) || { id: null, type: null, function: { name: '', arguments: '' } };
449
+ for (const field of ['id', 'type']) {
450
+ if (field in fragment) {
451
+ const value = fragment[field];
452
+ // Some OpenAI-compatible SSE providers emit null for unchanged
453
+ // identity fields in later fragments. Only non-null deltas can
454
+ // establish or conflict with the original tool identity.
455
+ if (value == null) continue;
456
+ if (typeof value !== 'string' || !value || (call[field] != null && call[field] !== value)) {
457
+ throw new ProtocolError('conflicting tool identity', 'identity_conflict');
458
+ }
459
+ call[field] = value;
460
+ }
461
+ }
462
+ const fn = fragment.function;
463
+ if (fn != null) {
464
+ if (typeof fn !== 'object' || Array.isArray(fn)) throw new ProtocolError('invalid function delta');
465
+ for (const field of ['name', 'arguments']) {
466
+ if (field in fn) {
467
+ if (fn[field] == null) continue;
468
+ if (typeof fn[field] !== 'string') throw new ProtocolError('function fragment must be text');
469
+ call.function[field] += fn[field];
470
+ }
471
+ }
472
+ }
473
+ if (!this.tools.has(fragment.index)) this.tools.set(fragment.index, call);
474
+ // Emit a display-only tool_delta for the argument fragment. The sealed
475
+ // batch in ModelResult is authoritative; this never executes a tool.
476
+ if (fn && 'arguments' in fn && typeof fn.arguments === 'string' && fn.arguments) {
477
+ emit({ kind: 'tool_delta', text: fn.arguments, metadata: { index: fragment.index,
478
+ ...(call.id ? { callId: call.id } : {}), ...(call.function.name ? { name: call.function.name } : {}) } });
479
+ }
480
+ }
481
+ },
482
+
483
+ finalize({ errorKind = null, failureKind = null } = {}) {
484
+ const result = this.rawResult({ errorKind, failureKind });
485
+ if (!['provider_retryable_error', 'provider_terminal_error'].includes(result.kind)) {
486
+ const tail = projector.seal();
487
+ if (tail) emitContent?.({ kind: 'text', text: tail });
488
+ }
489
+ const text = projectInlineReasoning(result.text, marker, profile.inlineReasoningPolicy);
490
+ return text === result.text ? result : { ...result, text,
491
+ ...(result.outputBoundary === 'assistant-text/v1' && !text.trim() ? { outputBoundary: 'no-visible-output/v1' } : {}),
492
+ kind: result.kind === 'final_candidate' && !text.trim() ? 'empty_response' : result.kind };
493
+ },
494
+
495
+ rawResult({ errorKind = null, failureKind = null } = {}) {
496
+ const finish = this.refused ? 'content_filter' : this.finish;
497
+ const { usage, usageReported: observedUsage } = sealUsage(this.usage);
498
+ const usageReported = observedUsage && (profile.chatUsagePolicy === undefined
499
+ || this.usageAtFinish && !this.usageConflict);
500
+ if (errorKind && errorKind !== 'provider_retryable_error') {
501
+ return {
502
+ kind: errorKind, text: this.text, toolCalls: [], usage, usageReported,
503
+ ...(failureKind ? { failureKind } : {}),
504
+ };
505
+ }
506
+ if (errorKind === 'provider_retryable_error' || !this.done) {
507
+ const normalized = normalizeResponse({ text: this.text, toolCalls: [], finishReason: finish, streamComplete: false });
508
+ if (normalized.kind === 'provider_retryable_error') {
509
+ return { ...normalized, usage, usageReported, failureKind: failureKind || 'incomplete_response' };
510
+ }
511
+ return sealModelReplay({ ...normalized, usage, usageReported }, profile, this.reasoning);
512
+ }
513
+ // done with no error
514
+ if (!this.refused && this.finish == null && (this.text.trim() || this.tools.size || this.malformedTools)) {
515
+ return {
516
+ kind: 'provider_retryable_error', text: this.text, toolCalls: [],
517
+ usage, usageReported, failureKind: 'incomplete_response',
518
+ };
519
+ }
520
+ let calls;
521
+ try {
522
+ if (this.malformedTools) throw new ProtocolError('malformed tool fragments');
523
+ calls = this.buildCalls();
524
+ } catch {
525
+ calls = [{}];
526
+ }
527
+ const normalized = normalizeResponse({ text: this.text, toolCalls: calls, finishReason: finish, streamComplete: true });
528
+ return sealModelReplay({ ...normalized, usage, usageReported }, profile, this.reasoning);
529
+ },
530
+
531
+ buildCalls() {
532
+ const calls = [];
533
+ const indexes = [...this.tools.keys()].sort((a, b) => a - b);
534
+ for (const index of indexes) {
535
+ const tool = this.tools.get(index);
536
+ if (tool.type !== 'function') throw new ProtocolError('tool type must be function');
537
+ let arguments_;
538
+ let invalid = false;
539
+ try {
540
+ arguments_ = strictParseJson(tool.function.arguments);
541
+ if (arguments_ == null || typeof arguments_ !== 'object' || Array.isArray(arguments_)) throw new Error();
542
+ } catch {
543
+ arguments_ = {}; invalid = true;
544
+ }
545
+ calls.push({
546
+ callId: tool.id != null ? String(tool.id) : '',
547
+ name: tool.function.name || '',
548
+ arguments: arguments_,
549
+ ...(invalid ? { wireArgumentsInvalid: true } : {}),
550
+ });
551
+ }
552
+ return calls;
553
+ },
554
+ };
555
+ }
556
+
557
+ // Copy only validated numeric facts, never the provider envelope or its text.
558
+ function usageCounters(usage) {
559
+ return Object.fromEntries(['inputTokens', 'outputTokens', 'cachedInputTokens', 'reasoningTokens']
560
+ .filter(key => Object.hasOwn(usage, key)).map(key => [key, usage[key]]));
561
+ }