@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,361 @@
1
+ // Native model protocol adapter: one host-resolved HTTP request per attempt.
2
+ //
3
+ // createNativeModelPort({ resolveProfile, fetchImpl, maxResponseBytes,
4
+ // idleTimeoutMs }) -> ModelPort. generate(request, emit) resolves a host-owned
5
+ // binding, issues exactly ONE HTTP request, streams text/thinking/tool_delta
6
+ // display deltas, and returns one sealed ModelResult. The adapter never retries
7
+ // and never executes tools; kernel policy owns backoff, call budgets and tool
8
+ // execution.
9
+ //
10
+ // The binding's apiKey is placed only in the Authorization/x-api-key header and
11
+ // never persisted in request, messages or result. binding.parameters may add
12
+ // generation fields but never override messages/tools/model/stream or
13
+ // security-sensitive transport values. Caller signal, body size and idle
14
+ // silence are all bounded.
15
+
16
+ import {
17
+ emptyUsage, parseRetryAfterMs, sanitizeFailureKind, ProtocolError,
18
+ } from './model-normalize.mjs';
19
+ import {
20
+ readModelSse, SseFramingError, IdleTimeout,
21
+ } from './model-sse.mjs';
22
+ import {
23
+ parseOpenAIBuffered, createOpenAIStreamTurn,
24
+ } from './model-openai-chat.mjs';
25
+ import {
26
+ parseAnthropicBuffered, createAnthropicStreamState,
27
+ } from './model-anthropic.mjs';
28
+ import {
29
+ parseResponsesBuffered, createResponsesStreamState,
30
+ } from './model-responses.mjs';
31
+ import { MediaError } from './model-media.mjs';
32
+ import { buildNativeModelRequest } from './model-request.mjs';
33
+ // parseModelJson is the kernel's strict finite JSON parser; used here for the
34
+ // 400 context-length error heuristic so a malformed error body is never parsed
35
+ // as a lenient ambiguity.
36
+ import { parseModelJson, MODEL_FAILURE_CODES } from '@kingsoftcloud/agentkit-harness-core';
37
+ import { observeModelTransportPort } from './model-transport-observation.mjs';
38
+
39
+ const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
40
+ const DEFAULT_IDLE_MS = 180000;
41
+
42
+ /** Only the adapter's plain inference request is admitted for cold replay.
43
+ * Profile extensions can enable provider behavior we cannot prove pure. Keep
44
+ * those manual until their semantics have an explicit Host admission contract.
45
+ * A replay remains a new billed attempt; the ledger retains uncertain usage. */
46
+ export function nativeModelRecoveryPolicy({ protocol, parameters = {} } = {}) {
47
+ return ['openai-chat', 'openai-responses', 'anthropic-messages'].includes(protocol)
48
+ && parameters !== null && typeof parameters === 'object' && !Array.isArray(parameters)
49
+ && Object.keys(parameters).length === 0 ? 'pure-inference/v1' : 'manual';
50
+ }
51
+
52
+ // A response body exceeded maxResponseBytes: terminal, never a truncated final.
53
+ class OverflowError extends Error {
54
+ constructor() { super('response body exceeded maxResponseBytes'); this.name = 'OverflowError'; }
55
+ }
56
+
57
+ export function createNativeModelPort({
58
+ resolveProfile,
59
+ fetchImpl = fetch,
60
+ maxResponseBytes = DEFAULT_MAX_BYTES,
61
+ idleTimeoutMs = DEFAULT_IDLE_MS,
62
+ } = {}) {
63
+ if (typeof resolveProfile !== 'function') {
64
+ throw new TypeError('createNativeModelPort requires a resolveProfile function');
65
+ }
66
+ if (typeof fetchImpl !== 'function') {
67
+ throw new TypeError('createNativeModelPort requires a fetchImpl function');
68
+ }
69
+ if (!Number.isSafeInteger(maxResponseBytes) || maxResponseBytes <= 0) throw new RangeError('maxResponseBytes must be a positive safe integer');
70
+ if (!Number.isFinite(idleTimeoutMs) || idleTimeoutMs <= 0) throw new RangeError('idleTimeoutMs must be positive and finite');
71
+ const maxBytes = maxResponseBytes;
72
+ const idleTimeout = idleTimeoutMs;
73
+
74
+ return observeModelTransportPort(async (request, emitEvent, observation) => {
75
+ if (!request || typeof request !== 'object' || !request.profile) {
76
+ return withFailure(terminalResult('invalid_response'), null, 'request', 'request_invalid');
77
+ }
78
+
79
+ // 1. Resolve the host-owned binding. A host hook failure is terminal and
80
+ // sanitized: exception text may carry credentials, so only the kind leaks.
81
+ let binding;
82
+ try {
83
+ binding = await resolveProfile(request.profile, request);
84
+ } catch {
85
+ return withFailure(terminalResult('host_error'), null, 'binding', 'host_binding');
86
+ }
87
+
88
+ // 2. Build the wire request. Pre-flight validation of the binding, profile
89
+ // or media is terminal; the adapter never calls HTTP with a bad binding.
90
+ let built;
91
+ try {
92
+ built = buildNativeModelRequest(binding, request);
93
+ } catch (error) {
94
+ const failureKind = (error instanceof ProtocolError || error instanceof MediaError)
95
+ ? 'invalid_response' : 'client_error';
96
+ return withFailure(terminalResult(failureKind), binding, 'request', 'request_invalid');
97
+ }
98
+
99
+ // 3. One fetch with a combined signal: the caller's signal plus an internal
100
+ // controller that bounds overall wall time. Redirects are disabled so a 3xx
101
+ // is a terminal client error, never a silent second request.
102
+ const internal = new AbortController();
103
+ const signals = [internal.signal];
104
+ if (request.signal) signals.push(request.signal);
105
+ const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
106
+ let overallTimer = null;
107
+ const overallMs = Number(request.profile?.requestTimeoutMs);
108
+ if (Number.isFinite(overallMs) && overallMs > 0) {
109
+ overallTimer = setTimeout(() => internal.abort(new IdleTimeout()), overallMs);
110
+ overallTimer.unref?.();
111
+ }
112
+
113
+ let response;
114
+ try {
115
+ observation.phase('headers');
116
+ response = await fetchImpl(built.url, {
117
+ method: 'POST',
118
+ headers: built.headers,
119
+ body: built.body,
120
+ signal: combined,
121
+ redirect: 'manual',
122
+ });
123
+ observation.headers();
124
+ } catch (error) {
125
+ clearTimer(overallTimer);
126
+ // Honor a caller-initiated abort by propagating it.
127
+ if (request.signal?.aborted) throw error;
128
+ if (internal.signal.aborted) return withFailure(retryableResult('request_timeout'), binding, 'headers', 'stream_timeout');
129
+ return withFailure(retryableResult(sanitizeFailureKind(error, 'network_error')), binding, 'headers', 'transport_error');
130
+ }
131
+
132
+ // 4. Dispatch. The overall timer stays armed through body reads so a
133
+ // server that sends headers then hangs the body is bounded; the finally
134
+ // clears the timer and cancels any unconsumed body so no connection leaks
135
+ // (e.g. a 429 whose body was never read).
136
+ try {
137
+ if (!response.ok) {
138
+ observation.phase('http-error');
139
+ const result = await classifyHttpError(response, { maxBytes });
140
+ request.signal?.throwIfAborted();
141
+ const code = response.status === 401 ? 'http_authentication' : response.status === 403 ? 'http_permission'
142
+ : response.status === 429 ? 'http_rate_limit' : response.status === 408 ? 'http_timeout'
143
+ : response.status >= 500 ? 'http_server' : 'http_client';
144
+ return withFailure(result, binding, 'http-error', code, { httpStatus: response.status });
145
+ }
146
+ const contentType = response.headers.get('content-type') || '';
147
+ if (contentType.includes('text/event-stream')) {
148
+ observation.phase('stream');
149
+ const idlePolicy = request.profile.streaming !== false && request.profile.streamTimeoutPolicy === 'idle/v1';
150
+ if (idlePolicy) { clearTimer(overallTimer); overallTimer = null; }
151
+ return await streamResponse(response, request, binding, emitEvent, internal, maxBytes,
152
+ idlePolicy && overallMs > 0 ? Math.min(idleTimeout, overallMs) : idleTimeout, observation);
153
+ }
154
+ observation.phase('buffered');
155
+ return await bufferedResponse(response, request, binding, maxBytes, emitEvent, observation);
156
+ } catch (error) {
157
+ if (request.signal?.aborted) throw error;
158
+ if (error instanceof IdleTimeout || internal.signal.aborted) {
159
+ return withFailure(retryableResult('request_timeout'), binding, 'buffered', 'stream_timeout');
160
+ }
161
+ // Overflow, bad UTF-8, or a transport error mid-body: a partial body is
162
+ // never sealed as a final; the attempt is terminal invalid_response.
163
+ return withFailure(terminalResult('invalid_response'), binding, 'buffered', error instanceof OverflowError
164
+ ? 'body_limit' : error instanceof TypeError && error.code === 'ERR_ENCODING_INVALID_ENCODED_DATA'
165
+ ? 'body_encoding' : protocolCode(error));
166
+ } finally {
167
+ clearTimer(overallTimer);
168
+ cancelBody(response);
169
+ }
170
+ });
171
+ }
172
+
173
+ async function streamResponse(response, request, binding, emit, internal, maxBytes, idleTimeoutMs, observation) {
174
+ const profile = request.profile;
175
+ const idlePolicy = profile.streaming !== false && profile.streamTimeoutPolicy === 'idle/v1';
176
+ const state = binding.protocol === 'anthropic-messages'
177
+ ? createAnthropicStreamState(profile)
178
+ : binding.protocol === 'openai-responses'
179
+ ? createResponsesStreamState(profile)
180
+ : createOpenAIStreamTurn(profile, binding);
181
+ // Idle silence aborts the fetch via the internal controller so the pending
182
+ // body read rejects cleanly instead of leaving an orphaned read to cancel.
183
+ const abort = (reason) => internal.abort(reason);
184
+ let frameIndex = -1;
185
+ const failed = (result, code) => withFailure(result, binding, 'stream', code,
186
+ { ...(frameIndex < 0 ? {} : { frameIndex }),
187
+ ...(code === 'usage_conflict' && state.usageConflict ? { usageConflict: state.usageConflict } : {}) });
188
+ try {
189
+ for await (const frame of readModelSse(response.body, { maxBytes, idleTimeoutMs, abort, observeBytes: observation.bytes })) {
190
+ if (idlePolicy && !frame.data.trim()) continue;
191
+ frameIndex++;
192
+ observation.frame();
193
+ state.consume(frame, emit);
194
+ if (idlePolicy) emit({ kind: 'activity', text: '' });
195
+ if (state.done) break;
196
+ }
197
+ } catch (error) {
198
+ if (request.signal?.aborted) throw error; // caller cancel propagates
199
+ if (error instanceof IdleTimeout) {
200
+ return failed(state.finalize({ errorKind: 'provider_retryable_error', failureKind: 'request_timeout' }), 'stream_timeout');
201
+ }
202
+ if (error instanceof SseFramingError) {
203
+ return failed(state.finalize({ errorKind: 'provider_terminal_error', failureKind: 'invalid_response' }), protocolCode(error));
204
+ }
205
+ if (error instanceof ProtocolError) {
206
+ // An Anthropic in-stream `error` event carries its own classification.
207
+ if (error.streamError) {
208
+ return failed(state.finalize({ errorKind: error.streamError.kind, failureKind: error.streamError.failureKind }), 'provider_error');
209
+ }
210
+ return failed(state.finalize({ errorKind: 'provider_terminal_error', failureKind: 'invalid_response' }), protocolCode(error));
211
+ }
212
+ if (internal.signal.aborted) {
213
+ // An overall-timeout abort (and any idle abort that surfaced as a
214
+ // transport error) is a retryable request timeout, never a final.
215
+ return failed(state.finalize({ errorKind: 'provider_retryable_error', failureKind: 'request_timeout' }), 'stream_timeout');
216
+ }
217
+ return failed(state.finalize({ errorKind: 'provider_retryable_error', failureKind: sanitizeFailureKind(error, 'network_error') }), 'transport_error');
218
+ }
219
+ const result = state.finalize();
220
+ return failed(result, state.streamError ? 'provider_error' : result.kind === 'malformed_tool_call' ? 'tool_payload_invalid'
221
+ : result.failureKind === 'incomplete_response' ? 'stream_incomplete' : 'protocol_invalid');
222
+ }
223
+
224
+ async function bufferedResponse(response, request, binding, maxBytes, emit, observation) {
225
+ const bodyText = await readBoundedText(response, maxBytes, observation.bytes);
226
+ const captureReplay = request.profile?.providerReplayMode !== 'none';
227
+ // Keep legacy buffered admissions unchanged. New visibility admissions also
228
+ // retain explicitly provided display reasoning when a server returns JSON.
229
+ const onThinking = request.profile.providerThinkingDisplayPolicy === 'provider-visible/v1'
230
+ && request.profile.providerThinkingDisplay === true ? text => { if (text) emit({ kind: 'thinking', text }); } : undefined;
231
+ try {
232
+ if (binding.protocol === 'anthropic-messages') {
233
+ return withFailure(parseAnthropicBuffered(bodyText, request, { captureReplay, onThinking }), binding, 'buffered', 'protocol_invalid');
234
+ }
235
+ if (binding.protocol === 'openai-responses') {
236
+ return withFailure(parseResponsesBuffered(bodyText, request, { captureReplay, onThinking }), binding, 'buffered', 'protocol_invalid');
237
+ }
238
+ return withFailure(parseOpenAIBuffered(bodyText, request, { captureReplay, binding, onThinking }), binding, 'buffered', 'protocol_invalid');
239
+ } catch (error) {
240
+ return withFailure(terminalResult('invalid_response'), binding, 'buffered', protocolCode(error));
241
+ }
242
+ }
243
+
244
+ // Classify a non-2xx HTTP response into a sealed ModelResult. The body is read
245
+ // only for a 400 to detect a context_length_exceeded code; otherwise the
246
+ // status and Retry-After header are the only facts that leave the adapter. A
247
+ // body that cannot be read (timeout/overflow/bad utf-8) cannot erase the HTTP
248
+ // failure fact — the status-based classification still stands.
249
+ async function classifyHttpError(response, { maxBytes }) {
250
+ const status = response.status;
251
+ const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'));
252
+ if (status === 408) return retryableResult('request_timeout', retryAfterMs);
253
+ if (status === 429) return retryableResult('rate_limit', retryAfterMs);
254
+ if (status >= 500 && status <= 599) return retryableResult('server_error', retryAfterMs);
255
+ if (status === 403) return { kind: 'policy_abort', text: '', toolCalls: [], usage: emptyUsage(), usageReported: false };
256
+ if (status === 400) {
257
+ try {
258
+ const bodyText = await readBoundedText(response, maxBytes);
259
+ if (hasContextLengthExceeded(bodyText)) {
260
+ return { kind: 'context_overflow', text: '', toolCalls: [], usage: emptyUsage(), usageReported: false };
261
+ }
262
+ } catch { /* an unreadable 400 body cannot erase the 400 failure fact */ }
263
+ return terminalResult('client_error');
264
+ }
265
+ return terminalResult('client_error');
266
+ }
267
+
268
+ function hasContextLengthExceeded(bodyText) {
269
+ try {
270
+ const payload = parseModelJson(bodyText);
271
+ const error = payload && typeof payload === 'object' ? payload.error : null;
272
+ if (error && typeof error === 'object') {
273
+ const code = String(error.code || '');
274
+ const type = String(error.type || '');
275
+ if (code === 'context_length_exceeded' || type === 'context_length_exceeded') return true;
276
+ }
277
+ } catch { /* an unreadable error body cannot erase the HTTP failure fact */ }
278
+ return false;
279
+ }
280
+
281
+ function terminalResult(failureKind) {
282
+ return { kind: 'provider_terminal_error', text: '', toolCalls: [], usage: emptyUsage(), usageReported: false, failureKind };
283
+ }
284
+
285
+ // A closed enum, rather than exception messages, crosses the model boundary.
286
+ // This is diagnostic evidence only; original classification/retry policy stays intact.
287
+ function protocolCode(error) {
288
+ return (error instanceof ProtocolError || error instanceof SseFramingError) && MODEL_FAILURE_CODES.includes(error.code)
289
+ ? error.code : 'protocol_invalid';
290
+ }
291
+ function withFailure(result, binding, phase, code, fields = {}) {
292
+ if (!['provider_terminal_error', 'provider_retryable_error', 'policy_abort', 'malformed_tool_call'].includes(result.kind)) return result;
293
+ const protocol = ['openai-chat', 'openai-responses', 'anthropic-messages'].includes(binding?.protocol) ? binding.protocol : 'unknown';
294
+ return { ...result, failureDiagnostic: { contract: fields.usageConflict
295
+ ? 'model-failure-diagnostic/v2' : 'model-failure-diagnostic/v1', protocol, phase, code, ...fields } };
296
+ }
297
+
298
+ function retryableResult(failureKind, retryAfterMs = null) {
299
+ const result = { kind: 'provider_retryable_error', text: '', toolCalls: [], usage: emptyUsage(), usageReported: false, failureKind };
300
+ if (Number.isFinite(retryAfterMs) && retryAfterMs != null && retryAfterMs >= 0) {
301
+ result.retryAfterMs = retryAfterMs;
302
+ }
303
+ return result;
304
+ }
305
+
306
+ function clearTimer(timer) {
307
+ if (timer) clearTimeout(timer);
308
+ }
309
+
310
+ // Cancel an unconsumed response body so the underlying connection is released
311
+ // (e.g. a 429 whose body was never read). Safe to call on a consumed/locked/
312
+ // errored body: a cancel that cannot proceed, or rejects because the stream
313
+ // was aborted, is swallowed so it never surfaces as an unhandled rejection.
314
+ function cancelBody(response) {
315
+ try {
316
+ const result = response?.body?.cancel?.();
317
+ if (result && typeof result.then === 'function') result.catch(() => {});
318
+ } catch { /* already locked/consumed */ }
319
+ }
320
+
321
+ // Read a response body as strict UTF-8 text, failing on overflow rather than
322
+ // truncating a prefix (a JSON prefix + overflow suffix must not masquerade as a
323
+ // complete response). Reads to EOF before sealing. Throws OverflowError on
324
+ // overflow; propagates abort and bad-UTF-8 errors for the caller to classify.
325
+ async function readBoundedText(response, maxBytes, observeBytes) {
326
+ const body = response.body;
327
+ if (body == null) return '';
328
+ const reader = body.getReader ? body.getReader() : null;
329
+ const decoder = new TextDecoder('utf-8', { fatal: true });
330
+ let total = 0;
331
+ const chunks = [];
332
+ if (reader) {
333
+ try {
334
+ while (true) {
335
+ const { value, done } = await reader.read();
336
+ if (done) break;
337
+ if (!value) continue;
338
+ if (!(value instanceof Uint8Array) && !(value instanceof ArrayBuffer)) throw new ProtocolError('body requires bytes');
339
+ total += value.byteLength;
340
+ observeBytes?.(value.byteLength);
341
+ if (total > maxBytes) throw new OverflowError();
342
+ chunks.push(decoder.decode(value, { stream: true }));
343
+ }
344
+ chunks.push(decoder.decode());
345
+ return chunks.join('');
346
+ } finally {
347
+ reader.releaseLock?.();
348
+ }
349
+ }
350
+ // Async-iterable fallback.
351
+ for await (const value of body) {
352
+ if (!value) continue;
353
+ if (!(value instanceof Uint8Array) && !(value instanceof ArrayBuffer)) throw new ProtocolError('body requires bytes');
354
+ total += value.byteLength;
355
+ observeBytes?.(value.byteLength);
356
+ if (total > maxBytes) throw new OverflowError();
357
+ chunks.push(decoder.decode(value, { stream: true }));
358
+ }
359
+ chunks.push(decoder.decode());
360
+ return chunks.join('');
361
+ }
@@ -0,0 +1,129 @@
1
+ // Frozen Chat Completions request policy, ported from the pinned Python
2
+ // model_proxy.compat and transform.model-effort-map/v2. No IO or retries here.
3
+ import { admitInlineReasoningPolicy, resolveCompatibilityRule } from './model-inline-reasoning.mjs';
4
+ import { ProtocolError } from './model-normalize.mjs';
5
+
6
+ const strict = new Set(['openai', 'strict-openai', 'cerebras', 'groq']);
7
+ const requiredAuto = new Set(['claude-opus-5-5', 'claude-sonnet-5-5', 'claude-fable-5-1', 'claude-mythos-5-1']);
8
+ const mandatory = new Set(['claude-fable-5-1', 'claude-opus-5-5', 'claude-sonnet-5-5']);
9
+ const ranks = { none: 0, minimal: 1, low: 2, medium: 3, high: 4, xhigh: 5, max: 5 };
10
+ const normalized = model => String(model ?? '').trim().toLowerCase().replaceAll('_', '-');
11
+ const object = value => value && typeof value === 'object' && !Array.isArray(value);
12
+ // Provider capability, independent of text streaming. Without tool_stream,
13
+ // these services may buffer a large argument until generation completes.
14
+ // https://docs.bigmodel.cn/cn/guide/capabilities/stream-tool
15
+ // Unknown models retain their configured wire behavior, including explicit
16
+ // tool_stream. Freeze v2 at admission; v1 recovery must retain its old bytes.
17
+ const incrementalToolModels = new Set(['glm-5', 'glm-5.1', 'glm-5.2', 'glm-5.2-fast-preview',
18
+ 'glm-5.3', 'glm-5.3-flash', 'glm-5.3-flashx']);
19
+
20
+ // Exact pinned catalog entries. Aliases and future names retain legacy wire behavior.
21
+ export function admitAnthropicPolicies(protocol, model) {
22
+ if (protocol !== 'anthropic-messages') return {};
23
+ const known = [...mandatory, 'claude-haiku-5-5'].includes(String(model).trim().toLowerCase());
24
+ return { anthropicPolicy: { contract: 'anthropic-wire/v1', midSystem: known, blockBinding: known ? 'drop_block' : 'disabled' },
25
+ ...(known ? { promptCachePolicy: 'anthropic-prefix/v1', operatorGuidancePolicy: 'journaled-system/v1', toolCatalogPolicy: 'stable-task/v1',
26
+ providerReplayProjectionPolicy: 'anthropic-client-compaction/v1' } : {}) };
27
+ }
28
+
29
+ export function applyAnthropicThinking(payload, profile, binding) {
30
+ if (!profile.anthropicPolicy) return payload;
31
+ const compat = resolveProviderCompatibility(profile.providerCompatibilityPolicy, binding);
32
+ const effort = profile.reasoningEffort ?? payload.reasoning_effort ?? null;
33
+ const mode = profile.thinkingMode ?? payload.thinking?.type;
34
+ if (profile.thinkingMode === 'enabled') throw new ProtocolError('model_thinking_mode_unsupported');
35
+ const disable = compat?.supportsThinking && (compat.disabledEfforts === null || compat.disabledEfforts.includes(effort))
36
+ && (mode === 'disabled' || compat.disabledEfforts?.includes(effort)
37
+ || compat.toolThinkingOff && !!payload.tools?.length
38
+ || compat.thinkingBudget !== null && payload.max_tokens <= compat.thinkingBudget);
39
+ if (disable) return { ...payload, thinking: { type: 'disabled' } };
40
+ if (profile.anthropicPolicy.blockBinding !== 'drop_block') return payload;
41
+ const thinking = { ...(payload.thinking ?? { type: 'adaptive' }) };
42
+ if (['adaptive', 'enabled'].includes(thinking.type)) thinking.block_binding = { prefix_mismatch_behavior: 'drop_block' };
43
+ return { ...payload, thinking };
44
+ }
45
+
46
+ export function admitProviderCompatibilityPolicy(override) {
47
+ return { contract: 'provider-compat/v2', rule: admitInlineReasoningPolicy(override).rule };
48
+ }
49
+
50
+ export function resolveProviderCompatibility(policy, binding) {
51
+ if (!policy) return null;
52
+ if (!['provider-compat/v1', 'provider-compat/v2'].includes(policy.contract)) throw new ProtocolError('invalid_provider_compatibility_policy');
53
+ const rule = resolveCompatibilityRule(policy.rule, binding);
54
+ const name = String(binding.model ?? '').trim().toLowerCase();
55
+ const kimi = ['kspmas-gateway', 'moonshot'].includes(rule)
56
+ && ['kimi-k2.7-code', 'kimi-k2.7-code-highspeed'].includes(name);
57
+ const disabledEfforts = mandatory.has(name) ? [] : name === 'claude-haiku-5-5' ? ['low', 'medium', 'high'] : null;
58
+ return { rule, supportsUsage: !strict.has(rule), supportsThinking: !strict.has(rule) && !kimi,
59
+ supportsToolArgumentStreaming: policy.contract === 'provider-compat/v2'
60
+ && ['zai', 'kspmas-gateway'].includes(rule) && incrementalToolModels.has(normalized(binding.model)),
61
+ supportsEffort: !kimi, toolThinkingOff: ['zai', 'kspmas-gateway'].includes(rule) && !kimi,
62
+ thinkingBudget: rule === 'zai' && !kimi && !disabledEfforts ? 32000 : null,
63
+ disabledEfforts, requiredAuto: kimi || requiredAuto.has(name),
64
+ echo: kimi || ['deepseek', 'mimo'].includes(rule) ? 'preserve' : strict.has(rule) ? 'strip' : 'thinking',
65
+ hints: kimi ? ['kimi-k2.7-code'] : { moonshot: ['kimi'], deepseek: ['deepseek'],
66
+ zai: ['glm', 'zai', 'bigmodel'], 'kspmas-gateway': ['glm', 'deepseek'], mimo: ['mimo'] }[rule] ?? [] };
67
+ }
68
+
69
+ export function clampReasoningEffort(model, effort) {
70
+ if (typeof effort !== 'string' || typeof model !== 'string') return effort;
71
+ if (model.startsWith('glm-5.3-flash') && !['low', 'high', 'max'].includes(effort)) {
72
+ const rank = ranks[effort];
73
+ effort = rank === undefined ? 'low' : ['low', 'high', 'max'].find(value => ranks[value] >= rank) ?? 'max';
74
+ }
75
+ if (['qwen3.7', 'qwen3.6', 'qwen3.5'].some(prefix => model.startsWith(prefix)) && ranks[effort] > ranks.high) return 'high';
76
+ return effort;
77
+ }
78
+
79
+ export function projectProviderToolChoice(choice, compat) {
80
+ const named = object(choice) && Object.keys(choice).length === 2 && choice.type === 'function'
81
+ && object(choice.function) && Object.keys(choice.function).length === 1
82
+ && typeof choice.function.name === 'string' && choice.function.name.trim();
83
+ return compat?.requiredAuto && (choice === 'required' || named) ? 'auto' : choice;
84
+ }
85
+
86
+ export function applyProviderChatCompatibility(payload, request, binding) {
87
+ const compat = resolveProviderCompatibility(request.profile.providerCompatibilityPolicy, binding);
88
+ if (!compat) return payload;
89
+ const result = { ...payload }, options = request.profile;
90
+ if (compat.supportsToolArgumentStreaming && result.stream && result.tools?.length && result.tool_choice !== 'none') {
91
+ if (result.tool_stream !== undefined && typeof result.tool_stream !== 'boolean') throw new ProtocolError('invalid_tool_stream');
92
+ result.tool_stream ??= true;
93
+ }
94
+ const model = binding.model, name = normalized(model);
95
+ const requested = options.reasoningEffort ?? result.reasoning_effort ?? null;
96
+ let effort = requested;
97
+ if (requested === null && options.controllerReasoningPolicy === 'disclosed-delegate/v1'
98
+ && ['zai', 'kspmas-gateway'].includes(compat.rule) && name.startsWith('glm-5.3')
99
+ && result.tools?.some(tool => tool.type === 'function' && tool.function?.name === 'delegate_task')) effort = 'low';
100
+ if (requested === null && request.purpose === 'compaction' && compat.rule === 'kspmas-gateway'
101
+ && ['glm-5.2', 'deepseek-v4-flash'].includes(name)) effort = 'none';
102
+ effort = compat.supportsEffort ? clampReasoningEffort(model, effort) : null;
103
+ if (effort !== null) result.reasoning_effort = effort;
104
+ if (result.stream) result.stream_options = { ...(object(result.stream_options) ? result.stream_options : {}), include_usage: true };
105
+ if (!compat.supportsUsage && object(result.stream_options)) {
106
+ result.stream_options = { ...result.stream_options }; delete result.stream_options.include_usage;
107
+ if (!Object.keys(result.stream_options).length) delete result.stream_options;
108
+ }
109
+ const mode = options.thinkingMode ?? result.thinking?.type;
110
+ const mayDisable = compat.supportsThinking && (compat.disabledEfforts === null || compat.disabledEfforts.includes(effort));
111
+ const summaryOff = request.purpose === 'compaction' && requested === null && compat.rule === 'kspmas-gateway' && name === 'glm-5.3-flash';
112
+ const budget = result.max_completion_tokens ?? result.max_tokens;
113
+ if (mode === 'enabled') {
114
+ if (!compat.supportsThinking) throw new ProtocolError('model_thinking_mode_unsupported');
115
+ result.thinking = { type: 'enabled' };
116
+ } else if (mayDisable && (mode === 'disabled' || summaryOff
117
+ || compat.toolThinkingOff && !!result.tools?.length && result.tool_choice !== 'none'
118
+ || compat.thinkingBudget !== null && budget <= compat.thinkingBudget)) result.thinking = { type: 'disabled' };
119
+ if (!compat.supportsThinking) delete result.thinking;
120
+ if (!compat.supportsEffort) delete result.reasoning_effort;
121
+ if (result.tool_choice !== undefined) result.tool_choice = projectProviderToolChoice(result.tool_choice, compat);
122
+ if (compat.echo === 'strip' || compat.echo === 'thinking' && !compat.hints.some(hint => normalized(model).includes(hint))) {
123
+ result.messages = result.messages.map(message => {
124
+ if (message.role !== 'assistant') return message;
125
+ const copy = { ...message }; delete copy.reasoning_content; return copy;
126
+ });
127
+ }
128
+ return result;
129
+ }
@@ -0,0 +1,153 @@
1
+ // Private provider replay sealing for the native model adapter.
2
+ //
3
+ // Ports ksadk.harness.native.model_replay. Private protocol material (chat
4
+ // reasoning_content, or Anthropic signed-thinking + tool_use content blocks)
5
+ // is bound to the host's pinned profile and echoed only to the same profile on
6
+ // the next attempt. It authorizes neither a tool nor a final answer, and never
7
+ // enters a public stream event's payload — only ModelResult.providerReplay.
8
+ //
9
+ // The binding fingerprint is profileId + profileVersion + compatibilityDigest.
10
+ // The sealed replay also carries compatibilityDigest explicitly: the kernel's
11
+ // boundReplay checks `result.providerReplay.compatibilityDigest ===
12
+ // config.profile.compatibilityDigest` and rejects a mismatch, so a replay that
13
+ // does not prove its profile lineage cannot authorize history. On input
14
+ // re-projection the adapter re-checks the full fingerprint so cross-profile
15
+ // private material is dropped, never leaked onto another model's wire.
16
+
17
+ import { createHash } from 'node:crypto';
18
+ import { canonical, modelWireToolCall } from '@kingsoftcloud/agentkit-harness-core';
19
+ import { ProtocolError } from './model-normalize.mjs';
20
+
21
+ const SEALED_KINDS = new Set(['tool_calls', 'final_candidate', 'output_limit']);
22
+
23
+ export function profileFingerprint(profile) {
24
+ return `${String(profile?.profileId ?? '')}\0${String(profile?.profileVersion ?? '')}\0${String(profile?.compatibilityDigest ?? '')}`;
25
+ }
26
+
27
+ // Seal chat reasoning_content replay. The replay carries compatibilityDigest
28
+ // for the kernel's boundReplay check and the fingerprint for input re-projection.
29
+ export function sealModelReplay(result, profile, reasoningContent) {
30
+ if (profile?.providerReplayMode !== 'reasoning_content') return result;
31
+ if (!SEALED_KINDS.has(result.kind)) return result;
32
+ return {
33
+ ...result,
34
+ providerReplay: {
35
+ version: 'model-replay/v1',
36
+ profileFingerprint: profileFingerprint(profile),
37
+ compatibilityDigest: String(profile.compatibilityDigest ?? ''),
38
+ reasoningContent: String(reasoningContent || ''),
39
+ },
40
+ usageReported: result.usageReported,
41
+ };
42
+ }
43
+
44
+ // Seal Anthropic original content blocks (text + signed thinking + tool_use
45
+ // with original provider ids) for anthropic_content replay. The blocks keep
46
+ // the provider's original tool_use ids; the kernel later adds replayToolIds to
47
+ // the ModelResult if it remaps colliding ids, and the adapter applies that map
48
+ // on input re-projection.
49
+ export function sealAnthropicReplay(result, profile, contentBlocks) {
50
+ if (profile?.providerReplayMode !== 'anthropic_content') return result;
51
+ if (!SEALED_KINDS.has(result.kind)) return result;
52
+ return {
53
+ ...result,
54
+ providerReplay: {
55
+ version: 'model-replay/v2',
56
+ profileFingerprint: profileFingerprint(profile),
57
+ compatibilityDigest: String(profile.compatibilityDigest ?? ''),
58
+ anthropicContent: Array.isArray(contentBlocks) ? contentBlocks : [],
59
+ },
60
+ usageReported: result.usageReported,
61
+ };
62
+ }
63
+
64
+ // Re-project an assistant message's providerReplay onto the OpenAI chat wire.
65
+ // The replay must belong to the current profile (full fingerprint match) or it
66
+ // is dropped — cross-profile private reasoning never leaks onto another wire.
67
+ // Returns { reasoning_content } to merge into the assistant message, or {}.
68
+ export function openAIReplayFields(message, profile) {
69
+ if (profile?.providerReplayMode !== 'reasoning_content') return {};
70
+ const replay = message?.providerReplay;
71
+ if (!replay || replay.version !== 'model-replay/v1') return {};
72
+ if (replay.profileFingerprint !== profileFingerprint(profile)) return {};
73
+ if (!replay.reasoningContent) return {};
74
+ return { reasoning_content: replay.reasoningContent };
75
+ }
76
+
77
+ // Re-project an assistant message's anthropic_content replay onto the Anthropic
78
+ // Messages wire. The replay must belong to the current profile; its tool_use
79
+ // ids are mapped through message.replayToolIds (original -> canonical) without
80
+ // touching the signature or private thinking text. The mapped tool_use blocks
81
+ // must pair exactly with the message's sealed toolCalls (id/name/arguments) and
82
+ // the text blocks must reconstruct message.content — otherwise the replay is
83
+ // inconsistent and dropped (never allowed to inject extra tools or replace a
84
+ // sealed call). The original providerReplay object is never mutated.
85
+ export function anthropicReplayContent(message, profile) {
86
+ if (profile?.providerReplayMode !== 'anthropic_content') return null;
87
+ const replay = message?.providerReplay;
88
+ if (!replay || replay.version !== 'model-replay/v2') return null;
89
+ if (replay.profileFingerprint !== profileFingerprint(profile)) return null;
90
+ if (!Array.isArray(replay.anthropicContent)) return null;
91
+ const idMap = message.replayToolIds || {};
92
+ let blocks = structuredClone(replay.anthropicContent).map((block) => {
93
+ if (block && block.type === 'tool_use') {
94
+ const mappedId = Object.prototype.hasOwnProperty.call(idMap, block.id) ? idMap[block.id] : block.id;
95
+ return { ...block, id: mappedId };
96
+ }
97
+ return block == null ? block : { ...block };
98
+ });
99
+ if (!validateReplayPairing(blocks, message)) return null;
100
+ const view = message.providerReplayProjection;
101
+ if (view) {
102
+ const contentDigest = createHash('sha256').update(canonical(replay.anthropicContent)).digest('hex');
103
+ if (profile.providerReplayProjectionPolicy !== 'anthropic-client-compaction/v1'
104
+ || view.version !== 'anthropic-client-compaction/v1' || view.profileDigest !== profile.compatibilityDigest
105
+ || view.originalContentDigest !== contentDigest || !/^[0-9a-f]{64}$/.test(view.prefixDigest))
106
+ throw new ProtocolError('anthropic_replay_projection_mismatch');
107
+ blocks = blocks.filter(block => !['thinking', 'redacted_thinking'].includes(block?.type));
108
+ }
109
+ return blocks;
110
+ }
111
+
112
+ // Validate that the replay blocks pair exactly with the sealed message facts:
113
+ // text blocks reconstruct message.content, and tool_use blocks (in order)
114
+ // match message.toolCalls by callId/name/arguments. A mismatch means the replay
115
+ // is not a faithful original of this sealed turn; drop it rather than let it
116
+ // inject or replace tool facts.
117
+ function validateReplayPairing(blocks, message) {
118
+ const text = blocks.filter((b) => b && b.type === 'text').map((b) => b.text || '').join('');
119
+ if (text !== (message.content || '')) return false;
120
+ const toolUses = blocks.filter((b) => b && b.type === 'tool_use');
121
+ const calls = message.toolCalls || [];
122
+ if (toolUses.length !== calls.length) return false;
123
+ for (let i = 0; i < toolUses.length; i++) {
124
+ const tu = toolUses[i];
125
+ const call = calls[i] && modelWireToolCall(calls[i]);
126
+ if (!tu || !call) return false;
127
+ if (tu.id !== call.callId) return false;
128
+ if (tu.name !== call.name) return false;
129
+ if (!jsonEqual(tu.input, call.arguments)) return false;
130
+ }
131
+ return true;
132
+ }
133
+
134
+ function jsonEqual(a, b) {
135
+ if (a === b) return true;
136
+ if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object') return false;
137
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
138
+ if (Array.isArray(a)) {
139
+ if (a.length !== b.length) return false;
140
+ return a.every((v, i) => jsonEqual(v, b[i]));
141
+ }
142
+ const ka = Object.keys(a).sort();
143
+ const kb = Object.keys(b).sort();
144
+ if (ka.length !== kb.length || !ka.every((k, i) => k === kb[i])) return false;
145
+ return ka.every((k) => jsonEqual(a[k], b[k]));
146
+ }
147
+
148
+ // True when the message carries a replay that belongs to the given profile.
149
+ export function replayMatchesProfile(message, profile) {
150
+ const replay = message?.providerReplay;
151
+ if (!replay) return true;
152
+ return replay.profileFingerprint === profileFingerprint(profile);
153
+ }
@@ -0,0 +1,15 @@
1
+ import { buildOpenAIRequest } from './model-openai-chat.mjs';
2
+ import { buildAnthropicRequest } from './model-anthropic.mjs';
3
+ import { buildResponsesRequest } from './model-responses.mjs';
4
+ import { ProtocolError } from './model-normalize.mjs';
5
+ import { projectRequestMedia } from './model-media.mjs';
6
+
7
+ /** One pure dispatch projection shared by request admission and actual HTTP. */
8
+ export function buildNativeModelRequest(binding, request) {
9
+ if (!binding || typeof binding !== 'object') throw new ProtocolError('binding missing');
10
+ const view = projectRequestMedia(request);
11
+ if (binding.protocol === 'anthropic-messages') return buildAnthropicRequest(binding, view);
12
+ if (binding.protocol === 'openai-chat') return buildOpenAIRequest(binding, view);
13
+ if (binding.protocol === 'openai-responses') return buildResponsesRequest(binding, view);
14
+ throw new ProtocolError('unsupported binding protocol');
15
+ }