@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +1 -0
- package/README.md +124 -2
- package/THIRD_PARTY_NOTICES.md +66 -0
- package/ZCode-LICENSE +202 -0
- package/dist/index.mjs +4871 -0
- package/dist/native-command-plan.LICENSE.txt +221 -0
- package/dist/native-command-plan.json +16 -0
- package/dist/native-command-plan.mjs +8 -0
- package/dist/rpc-entry.mjs +5682 -0
- package/package.json +169 -4
- package/src/native/a2a-receipt-source.mjs +19 -0
- package/src/native/a2a-tools.mjs +517 -0
- package/src/native/activity-status.mjs +30 -0
- package/src/native/artifact-codec.mjs +481 -0
- package/src/native/artifact-store.mjs +1162 -0
- package/src/native/assistant-stream.mjs +76 -0
- package/src/native/background-command-port.mjs +95 -0
- package/src/native/browser-action-observation.mjs +43 -0
- package/src/native/browser-availability.mjs +22 -0
- package/src/native/browser-canvas-metrics.mjs +71 -0
- package/src/native/browser-dom.mjs +193 -0
- package/src/native/browser-playwright.mjs +1249 -0
- package/src/native/browser-vision.mjs +369 -0
- package/src/native/browser-workspace-policy.mjs +22 -0
- package/src/native/browser-workspace-protocol.mjs +95 -0
- package/src/native/browser-workspace-server.mjs +164 -0
- package/src/native/browser-workspace.mjs +355 -0
- package/src/native/captured-source.mjs +3 -0
- package/src/native/child-budget-allocation.mjs +87 -0
- package/src/native/child-capabilities.mjs +35 -0
- package/src/native/child-command-resource.mjs +20 -0
- package/src/native/child-history.mjs +188 -0
- package/src/native/child-merge.mjs +98 -0
- package/src/native/child-model-surface.mjs +19 -0
- package/src/native/child-observation.mjs +100 -0
- package/src/native/child-owner.mjs +620 -0
- package/src/native/child-publication.mjs +76 -0
- package/src/native/child-readers.mjs +366 -0
- package/src/native/child-tools.mjs +46 -0
- package/src/native/child-workspace-binding.mjs +29 -0
- package/src/native/child-workspace-tools.mjs +1 -0
- package/src/native/child-workspaces.mjs +655 -0
- package/src/native/children.mjs +365 -0
- package/src/native/command-plan-worker.mjs +14 -0
- package/src/native/command-plan.mjs +31 -0
- package/src/native/command-progress.mjs +64 -0
- package/src/native/command-shell.mjs +9 -0
- package/src/native/command-workspace-observation.mjs +36 -0
- package/src/native/component-tools.mjs +280 -0
- package/src/native/context-counter.mjs +133 -0
- package/src/native/context-markers.mjs +51 -0
- package/src/native/context-profile.mjs +72 -0
- package/src/native/context-status.mjs +27 -0
- package/src/native/conversation-command-control.mjs +133 -0
- package/src/native/delivery-timeline.mjs +39 -0
- package/src/native/dsh-background-ownership.mjs +102 -0
- package/src/native/dsh-managed-ownership.mjs +23 -0
- package/src/native/dsh-turn-contract.mjs +3 -0
- package/src/native/dsh-turn-ownership.mjs +54 -0
- package/src/native/dsh-turn-runtime.mjs +498 -0
- package/src/native/e2b-background-supervisor.mjs +347 -0
- package/src/native/e2b-background.mjs +368 -0
- package/src/native/e2b-command-state.mjs +40 -0
- package/src/native/e2b-command-tools.mjs +486 -0
- package/src/native/e2b-sandbox-owner.mjs +227 -0
- package/src/native/hook-executor.mjs +249 -0
- package/src/native/instruction-assembly.mjs +60 -0
- package/src/native/instruction-context.mjs +54 -0
- package/src/native/local-background-supervisor.mjs +108 -0
- package/src/native/local-background.mjs +207 -0
- package/src/native/local-command-evidence.mjs +57 -0
- package/src/native/local-command-policy-worker.mjs +197 -0
- package/src/native/local-command-policy.mjs +46 -0
- package/src/native/local-command-state.mjs +177 -0
- package/src/native/local-command-tools.mjs +302 -0
- package/src/native/local-execution-profile.mjs +36 -0
- package/src/native/local-output-policy.mjs +92 -0
- package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
- package/src/native/local-process-bin/darwin-x64/launcher +0 -0
- package/src/native/local-process-bin/linux-arm64/launcher +0 -0
- package/src/native/local-process-bin/linux-x64/launcher +0 -0
- package/src/native/local-process-launcher.c +183 -0
- package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
- package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
- package/src/native/local-process-licenses/NOTICE +21 -0
- package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
- package/src/native/local-process-manifest.json +32 -0
- package/src/native/local-process.mjs +246 -0
- package/src/native/local-resource-profile.mjs +32 -0
- package/src/native/memory-candidate.mjs +121 -0
- package/src/native/memory-extraction-run.mjs +60 -0
- package/src/native/memory-extraction-tools.mjs +116 -0
- package/src/native/memory-recall.mjs +65 -0
- package/src/native/memory-store.mjs +334 -0
- package/src/native/memory-tools.mjs +329 -0
- package/src/native/model-anthropic-cache.mjs +49 -0
- package/src/native/model-anthropic-tool-search.mjs +85 -0
- package/src/native/model-anthropic.mjs +753 -0
- package/src/native/model-chat-usage.mjs +24 -0
- package/src/native/model-inline-reasoning.mjs +111 -0
- package/src/native/model-media.mjs +174 -0
- package/src/native/model-normalize.mjs +228 -0
- package/src/native/model-openai-chat.mjs +561 -0
- package/src/native/model-port.mjs +361 -0
- package/src/native/model-provider-compat.mjs +129 -0
- package/src/native/model-replay.mjs +153 -0
- package/src/native/model-request.mjs +15 -0
- package/src/native/model-responses.mjs +1205 -0
- package/src/native/model-sse.mjs +171 -0
- package/src/native/model-surface-host.mjs +27 -0
- package/src/native/model-tool-search-policy.mjs +16 -0
- package/src/native/model-transport-observation.mjs +27 -0
- package/src/native/owned-child.mjs +114 -0
- package/src/native/owned-command.mjs +106 -0
- package/src/native/python-syntax-worker.mjs +60 -0
- package/src/native/python-syntax.mjs +50 -0
- package/src/native/read-interruption.mjs +16 -0
- package/src/native/result-stream.mjs +63 -0
- package/src/native/ruff-ast.mjs +95 -0
- package/src/native/run-observation.mjs +128 -0
- package/src/native/session-delivery-journal.mjs +96 -0
- package/src/native/session-delivery.mjs +434 -0
- package/src/native/shell-analysis.mjs +50 -0
- package/src/native/skill-disclosure.mjs +404 -0
- package/src/native/skill-events.mjs +164 -0
- package/src/native/skill-tools.mjs +813 -0
- package/src/native/source-page.mjs +73 -0
- package/src/native/sqlite-session-inbox.mjs +42 -0
- package/src/native/sqlite-store.mjs +617 -0
- package/src/native/tool-authority.mjs +99 -0
- package/src/native/tool-guidance.mjs +12 -0
- package/src/native/tool-observation.mjs +75 -0
- package/src/native/tool-states.mjs +14 -0
- package/src/native/verification-authority.mjs +37 -0
- package/src/native/web-tools.mjs +643 -0
- package/src/native/workspace-checkpoints.mjs +312 -0
- package/src/native/workspace-context.mjs +61 -0
- package/src/native/workspace-display.mjs +39 -0
- package/src/native/workspace-edit-match.mjs +19 -0
- package/src/native/workspace-edit-worker.mjs +15 -0
- package/src/native/workspace-editing.mjs +447 -0
- package/src/native/workspace-image.mjs +72 -0
- package/src/native/workspace-instructions.mjs +107 -0
- package/src/native/workspace-mutation-lock.mjs +74 -0
- package/src/native/workspace-patch-worker.mjs +17 -0
- package/src/native/workspace-patch.mjs +17 -0
- package/src/native/workspace-paths.mjs +225 -0
- package/src/native/workspace-state.mjs +272 -0
- package/src/native/workspace-subject-observer.mjs +84 -0
- package/src/native/workspace-tools.mjs +1740 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ProtocolError } from './model-normalize.mjs';
|
|
2
|
+
|
|
3
|
+
export const CUMULATIVE_CHAT_USAGE = 'cumulative-snapshots/v1';
|
|
4
|
+
export const admitChatUsagePolicy = protocol => protocol === 'openai-chat' ? CUMULATIVE_CHAT_USAGE : undefined;
|
|
5
|
+
const fields = ['inputTokens', 'outputTokens', 'cachedInputTokens', 'reasoningTokens'];
|
|
6
|
+
|
|
7
|
+
/** Usage reports describe a single request, never additional token deltas.
|
|
8
|
+
* Optional subdivisions may arrive late; absence never fabricates a zero or
|
|
9
|
+
* erases a known count. Generation sealing is handled separately by the stream.
|
|
10
|
+
* Legacy absence retains exact primary-counter comparison for frozen Runs. */
|
|
11
|
+
export function mergeChatUsage(previous, reported, policy, finalized = false) {
|
|
12
|
+
if (policy !== undefined && policy !== CUMULATIVE_CHAT_USAGE)
|
|
13
|
+
throw new ProtocolError('invalid chat usage policy', 'request_invalid');
|
|
14
|
+
if (!previous) return { ...reported };
|
|
15
|
+
if (policy === undefined) {
|
|
16
|
+
if (previous.inputTokens !== reported.inputTokens || previous.outputTokens !== reported.outputTokens)
|
|
17
|
+
throw new ProtocolError('conflicting usage reports in one attempt', 'usage_conflict');
|
|
18
|
+
return { ...reported };
|
|
19
|
+
}
|
|
20
|
+
for (const field of fields) if (Object.hasOwn(previous, field) && Object.hasOwn(reported, field)
|
|
21
|
+
&& (reported[field] < previous[field] || finalized && reported[field] !== previous[field]))
|
|
22
|
+
throw new ProtocolError('conflicting cumulative usage reports in one attempt', 'usage_conflict');
|
|
23
|
+
return { ...previous, ...reported };
|
|
24
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Versioned projection from ksadk.model_proxy.compat. Policy belongs to the
|
|
2
|
+
// admitted Run; neither recovery nor individual requests read process.env.
|
|
3
|
+
const rules = new Set(['permissive', 'openai', 'strict-openai', 'cerebras', 'groq',
|
|
4
|
+
'deepseek', 'zai', 'kspmas-gateway', 'moonshot', 'mimo']);
|
|
5
|
+
const urlHints = [['kspmas', 'kspmas-gateway'], ['cerebras', 'cerebras'], ['groq', 'groq'],
|
|
6
|
+
['deepseek', 'deepseek'], ['zai', 'zai'], ['bigmodel', 'zai'], ['glm', 'zai'],
|
|
7
|
+
['moonshot', 'moonshot'], ['api.kimi.com', 'moonshot'], ['openai.com', 'openai']];
|
|
8
|
+
const modelHints = [['deepseek', 'deepseek'], ['glm', 'zai'], ['zai', 'zai'],
|
|
9
|
+
['bigmodel', 'zai'], ['mimo', 'mimo'], ['gpt-', 'openai']];
|
|
10
|
+
const normalizeModel = value => String(value ?? '').toLowerCase().replaceAll('_', '-');
|
|
11
|
+
// Python str.lstrip includes NEL/information separators and excludes BOM.
|
|
12
|
+
const pythonLeftSpace = /^[\u0009-\u000d\u001c-\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]*/u;
|
|
13
|
+
const startsThinking = text => text.replace(pythonLeftSpace, '').startsWith('<think>');
|
|
14
|
+
|
|
15
|
+
export function admitInlineReasoningPolicy(override, { contract = 'profiled-content/v2' } = {}) {
|
|
16
|
+
if (!['profiled-content/v1', 'profiled-content/v2'].includes(contract)) throw new Error('invalid_inline_reasoning_policy');
|
|
17
|
+
const rule = typeof override === 'string' ? override.trim() : '';
|
|
18
|
+
return { contract, rule: rule ? (rules.has(rule) ? rule : 'permissive') : 'auto' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function inlineReasoningMarker(policy, binding = {}) {
|
|
22
|
+
if (!policy) return null; // Preserve existing admitted Runs byte for byte.
|
|
23
|
+
if (!['profiled-content/v1', 'profiled-content/v2'].includes(policy.contract) || (policy.rule !== 'auto' && !rules.has(policy.rule))) {
|
|
24
|
+
throw new Error('invalid_inline_reasoning_policy');
|
|
25
|
+
}
|
|
26
|
+
const rule = resolveCompatibilityRule(policy.rule, binding);
|
|
27
|
+
if (['kspmas-gateway', 'moonshot'].includes(rule)
|
|
28
|
+
&& ['kimi-k2.7-code', 'kimi-k2.7-code-highspeed'].includes(String(binding.model ?? '').trim().toLowerCase())) return null;
|
|
29
|
+
return ['zai', 'kspmas-gateway'].includes(rule) ? '</think>' : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveCompatibilityRule(selected, binding = {}) {
|
|
33
|
+
if (selected !== 'auto' && !rules.has(selected)) throw new Error('invalid_provider_compatibility_policy');
|
|
34
|
+
const model = normalizeModel(binding.model), url = String(binding.baseUrl ?? '').toLowerCase();
|
|
35
|
+
return selected === 'auto'
|
|
36
|
+
? urlHints.find(([hint]) => url.includes(hint))?.[1]
|
|
37
|
+
?? modelHints.find(([hint]) => model.includes(hint))?.[1] ?? 'permissive'
|
|
38
|
+
: selected;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function projectInlineReasoning(text, marker, policy) {
|
|
42
|
+
if (!marker) return text;
|
|
43
|
+
if (policy?.contract === 'profiled-content/v2') {
|
|
44
|
+
const projector = createInlineReasoningProjector(marker, policy);
|
|
45
|
+
return projector.push(text) + projector.seal();
|
|
46
|
+
}
|
|
47
|
+
const index = text.lastIndexOf(marker);
|
|
48
|
+
return index >= 0 ? text.slice(index + marker.length) : startsThinking(text) ? '' : text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createInlineReasoningProjector(marker, policy) {
|
|
52
|
+
if (marker && policy?.contract === 'profiled-content/v2') return taggedPrefixProjector(marker);
|
|
53
|
+
let pending = '', released = !marker;
|
|
54
|
+
return {
|
|
55
|
+
push(text) {
|
|
56
|
+
if (released) return text;
|
|
57
|
+
pending += text;
|
|
58
|
+
const index = pending.lastIndexOf(marker);
|
|
59
|
+
if (index < 0) return '';
|
|
60
|
+
const visible = pending.slice(index + marker.length);
|
|
61
|
+
pending = ''; released = true;
|
|
62
|
+
return visible;
|
|
63
|
+
},
|
|
64
|
+
seal() {
|
|
65
|
+
if (released) return '';
|
|
66
|
+
const visible = startsThinking(pending) ? '' : pending;
|
|
67
|
+
pending = ''; released = true;
|
|
68
|
+
return visible;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Content and reasoning_content are separate wire channels. Only an explicit
|
|
74
|
+
* leading reasoning tag changes the content channel's framing. Ordinary prose
|
|
75
|
+
* is released as soon as its prefix is unambiguous; a closing tag quoted later
|
|
76
|
+
* in prose is text, not permission to retract already-published content.
|
|
77
|
+
* Once inside reasoning, retain at most a possible closing-tag suffix. */
|
|
78
|
+
function taggedPrefixProjector(marker) {
|
|
79
|
+
const opening = '<think>';
|
|
80
|
+
let phase = 'prefix', pending = '';
|
|
81
|
+
return {
|
|
82
|
+
push(text) {
|
|
83
|
+
if (phase === 'text') return text;
|
|
84
|
+
pending += text;
|
|
85
|
+
if (phase === 'prefix') {
|
|
86
|
+
const prefix = pending.replace(pythonLeftSpace, '');
|
|
87
|
+
if (!prefix || opening.startsWith(prefix) || marker.startsWith(prefix)) return '';
|
|
88
|
+
if (prefix.startsWith(opening)) {
|
|
89
|
+
pending = prefix.slice(opening.length);phase = 'reasoning';
|
|
90
|
+
} else if (prefix.startsWith(marker)) {
|
|
91
|
+
const visible = prefix.slice(marker.length);pending = '';phase = 'text';return visible;
|
|
92
|
+
} else {
|
|
93
|
+
const visible = pending;pending = '';phase = 'text';return visible;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const end = pending.indexOf(marker);
|
|
97
|
+
if (end >= 0) {
|
|
98
|
+
const visible = pending.slice(end + marker.length);pending = '';phase = 'text';return visible;
|
|
99
|
+
}
|
|
100
|
+
let suffix = Math.min(pending.length, marker.length - 1);
|
|
101
|
+
while (suffix && !marker.startsWith(pending.slice(-suffix))) suffix--;
|
|
102
|
+
pending = suffix ? pending.slice(-suffix) : '';
|
|
103
|
+
return '';
|
|
104
|
+
},
|
|
105
|
+
seal() {
|
|
106
|
+
// Incomplete tag prefixes and unfinished reasoning cannot become prose.
|
|
107
|
+
const visible = phase === 'prefix' && !pending.replace(pythonLeftSpace, '') ? pending : '';
|
|
108
|
+
pending = '';phase = 'text';return visible;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Media validation and projection for the native model adapter.
|
|
2
|
+
//
|
|
3
|
+
// The host supplies image observations as message.media blocks. The adapter
|
|
4
|
+
// validates their shape and byte budget and projects them onto each wire
|
|
5
|
+
// without reading files or fetching remote URLs: a bare path or file:// URL is
|
|
6
|
+
// rejected, only explicit http(s) URLs (without userinfo credentials) and
|
|
7
|
+
// supported image data URLs are admitted.
|
|
8
|
+
//
|
|
9
|
+
// The byte budget is request.maxInlineImageBytes (the kernel passes
|
|
10
|
+
// configuration.limits.maxInlineImageBytes). When absent, a 40 MiB safe default
|
|
11
|
+
// is used. Like the reference request-media policy, this budget counts all
|
|
12
|
+
// encoded inline image URLs in the dispatch view, including their prefixes.
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
const IMAGE_KINDS = new Set(['png', 'jpeg', 'webp', 'gif']);
|
|
17
|
+
const DEFAULT_MAX_IMAGE_BYTES = 40 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
class MediaError extends Error {
|
|
20
|
+
constructor(message) { super(message); this.name = 'MediaError'; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate one host media block and return it normalized to the OpenAI
|
|
24
|
+
// image_url shape: { type: 'image_url', image_url: { url, detail } }. Throws
|
|
25
|
+
// on a local path, file:// URL, userinfo URL, unsupported data URL, bad base64,
|
|
26
|
+
// or an encoded inline URL over maxInlineImageBytes.
|
|
27
|
+
export function validateMediaBlock(block, maxInlineImageBytes = DEFAULT_MAX_IMAGE_BYTES) {
|
|
28
|
+
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
29
|
+
throw new MediaError('media block must be an object');
|
|
30
|
+
}
|
|
31
|
+
if (block.type !== 'image_url') {
|
|
32
|
+
throw new MediaError('unsupported media block type');
|
|
33
|
+
}
|
|
34
|
+
const image = block.image_url;
|
|
35
|
+
if (!image || typeof image !== 'object' || Array.isArray(image)) {
|
|
36
|
+
throw new MediaError('image_url block requires an image_url object');
|
|
37
|
+
}
|
|
38
|
+
const url = String(image.url ?? '');
|
|
39
|
+
if (!url) throw new MediaError('image block requires a url');
|
|
40
|
+
const detail = image.detail == null ? 'auto' : String(image.detail);
|
|
41
|
+
if (!['auto', 'low', 'high'].includes(detail)) {
|
|
42
|
+
throw new MediaError('image detail must be auto, low or high');
|
|
43
|
+
}
|
|
44
|
+
validateImageUrl(url, maxInlineImageBytes);
|
|
45
|
+
return { type: 'image_url', image_url: { url, detail } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateImageUrl(url, maxInlineImageBytes) {
|
|
49
|
+
if (url.startsWith('data:')) {
|
|
50
|
+
const [prefix, separator, payload] = partition(url, ',');
|
|
51
|
+
const kind = matchDataImage(prefix);
|
|
52
|
+
if (!kind || !separator || !payload) {
|
|
53
|
+
throw new MediaError('image data URL requires a supported image and base64 content');
|
|
54
|
+
}
|
|
55
|
+
// Strict base64: only the standard alphabet, length a multiple of 4, with
|
|
56
|
+
// 0-2 padding chars. Rejecting malformed padding avoids accepting a
|
|
57
|
+
// truncated payload that decodes to fewer bytes than the host expected.
|
|
58
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(payload) || payload.length % 4 !== 0) {
|
|
59
|
+
throw new MediaError('invalid image base64 content');
|
|
60
|
+
}
|
|
61
|
+
if (Buffer.byteLength(url, 'utf8') > maxInlineImageBytes) {
|
|
62
|
+
throw new MediaError('image exceeds maxInlineImageBytes');
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try { parsed = new URL(url); } catch { throw new MediaError('image URL must be an explicit HTTP URL or image data URL'); }
|
|
68
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
69
|
+
throw new MediaError('image URL must be an explicit HTTP URL or image data URL');
|
|
70
|
+
}
|
|
71
|
+
// An image URL carrying userinfo credentials would send them to the provider
|
|
72
|
+
// in the request; reject it so credentials never enter the model wire.
|
|
73
|
+
if (parsed.username || parsed.password) {
|
|
74
|
+
throw new MediaError('image URL must not carry userinfo credentials');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function matchDataImage(prefix) {
|
|
79
|
+
for (const kind of IMAGE_KINDS) {
|
|
80
|
+
if (prefix === `data:image/${kind};base64`) return kind;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function partition(s, sep) {
|
|
86
|
+
const i = s.indexOf(sep);
|
|
87
|
+
if (i === -1) return [s, '', ''];
|
|
88
|
+
return [s.slice(0, i), sep, s.slice(i + sep.length)];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// This is a disposable dispatch projection. The canonical transcript and its
|
|
92
|
+
// tool receipts remain unchanged; omitted historical pixels cannot authorize
|
|
93
|
+
// a repeated side effect. The latest actual user input is protected in full.
|
|
94
|
+
export function projectRequestMedia(request) {
|
|
95
|
+
const limit = request.maxInlineImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
|
|
96
|
+
if (!Number.isSafeInteger(limit) || limit < 0) throw new MediaError('invalid inline image budget');
|
|
97
|
+
const messages = (request.messages || []).map((message) => {
|
|
98
|
+
if (message.media == null) return { ...message };
|
|
99
|
+
if (!Array.isArray(message.media)) throw new MediaError('media must be an array');
|
|
100
|
+
const media = message.media.map((block) => validateMediaBlock(block, Infinity));
|
|
101
|
+
if (media.length && message.role !== 'user' && message.role !== 'tool') throw new MediaError('image observations require user or tool content');
|
|
102
|
+
return { ...message, media };
|
|
103
|
+
});
|
|
104
|
+
if (request.profile?.supportsImages === false) {
|
|
105
|
+
for (const message of messages) if (message.media?.length) {
|
|
106
|
+
delete message.providerReplay;
|
|
107
|
+
message.content = `${message.content || ''}\n${JSON.stringify({
|
|
108
|
+
type: 'agentkit.request-media-omission/v1', reason: 'model_does_not_support_images',
|
|
109
|
+
status: 'not_sent_in_this_request',
|
|
110
|
+
observation: 'Image pixels were not sent to this model. Do not claim visual inspection or repeat side effects for missing pixels.',
|
|
111
|
+
})}`;
|
|
112
|
+
message.media = [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const actualUser = messages.findLastIndex(message => message.role === 'user' && !message.history
|
|
116
|
+
&& (message.kind == null || ['user_input', 'guidance', 'steering'].includes(message.kind)
|
|
117
|
+
|| message.kind === 'host_input' && message.persistent === true));
|
|
118
|
+
const inline = [];
|
|
119
|
+
for (const [i, message] of messages.entries()) {
|
|
120
|
+
for (const [j, block] of (message.media || []).entries()) {
|
|
121
|
+
if (block.image_url.url.startsWith('data:')) inline.push({ i, j, bytes: Buffer.byteLength(block.image_url.url, 'utf8') });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (inline.reduce((sum, item) => sum + item.bytes, 0) <= limit) return { ...request, messages };
|
|
125
|
+
const protectedBytes = inline.filter((item) => item.i === actualUser).reduce((sum, item) => sum + item.bytes, 0);
|
|
126
|
+
let remaining = limit - protectedBytes;
|
|
127
|
+
if (remaining < 0) throw new MediaError('request_current_media_over_budget');
|
|
128
|
+
const retained = new Set();
|
|
129
|
+
for (const item of inline.toReversed()) {
|
|
130
|
+
if (item.i === actualUser) retained.add(`${item.i}:${item.j}`);
|
|
131
|
+
else if (item.bytes <= remaining) {
|
|
132
|
+
retained.add(`${item.i}:${item.j}`);
|
|
133
|
+
remaining -= item.bytes;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const item of inline) {
|
|
137
|
+
if (retained.has(`${item.i}:${item.j}`)) continue;
|
|
138
|
+
const message = messages[item.i];
|
|
139
|
+
const block = message.media[item.j];
|
|
140
|
+
const marker = {
|
|
141
|
+
type: 'agentkit.request-media-omission/v1',
|
|
142
|
+
mediaDigest: createHash('sha256').update(JSON.stringify(block)).digest('hex'),
|
|
143
|
+
status: 'not_sent_in_this_request', reason: 'inline_image_budget',
|
|
144
|
+
observation: 'Historical image omitted from this model request. Use the textual receipt; do not claim to have seen the image or repeat executed side effects because its pixels were omitted.',
|
|
145
|
+
};
|
|
146
|
+
message.content = `${message.content || ''}\n${JSON.stringify(marker)}`;
|
|
147
|
+
message.media[item.j] = null;
|
|
148
|
+
delete message.providerReplay;
|
|
149
|
+
}
|
|
150
|
+
for (const message of messages) if (message.media) message.media = message.media.filter(Boolean);
|
|
151
|
+
return { ...request, messages };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Project validated media blocks onto the OpenAI chat wire (unchanged shape).
|
|
155
|
+
export function projectMediaOpenAI(blocks) {
|
|
156
|
+
return blocks.map((b) => ({ type: 'image_url', image_url: { url: b.image_url.url, detail: b.image_url.detail } }));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Project validated media blocks onto the Anthropic messages wire. http(s)
|
|
160
|
+
// URLs become {type:'image', source:{type:'url', url}}; data URLs become a
|
|
161
|
+
// base64 source with the media type derived from the data URL prefix.
|
|
162
|
+
export function projectMediaAnthropic(blocks) {
|
|
163
|
+
return blocks.map((b) => {
|
|
164
|
+
const url = b.image_url.url;
|
|
165
|
+
if (url.startsWith('data:')) {
|
|
166
|
+
const [prefix, , payload] = partition(url, ',');
|
|
167
|
+
const kind = matchDataImage(prefix);
|
|
168
|
+
return { type: 'image', source: { type: 'base64', media_type: `image/${kind}`, data: payload } };
|
|
169
|
+
}
|
|
170
|
+
return { type: 'image', source: { type: 'url', url } };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { MediaError, DEFAULT_MAX_IMAGE_BYTES };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Shared, protocol-neutral normalization for the native model adapter.
|
|
2
|
+
//
|
|
3
|
+
// The host kernel owns retry/backoff policy and call budgets. This module only
|
|
4
|
+
// classifies one sealed attempt: it maps a provider finish reason, visible text
|
|
5
|
+
// and an assembled tool batch to the ModelResult.kind contract. It never invents
|
|
6
|
+
// a finish from partial transport, and never authorizes tool execution.
|
|
7
|
+
//
|
|
8
|
+
// Provider error bodies, credentials and exception text never cross this
|
|
9
|
+
// boundary: only the sanitized failureKind string leaves the adapter.
|
|
10
|
+
|
|
11
|
+
export const emptyUsage = () => ({
|
|
12
|
+
inputTokens: 0, outputTokens: 0, modelCalls: 0, toolCalls: 0, cost: 0,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Canonical finish-reason sets, mirroring ksadk.harness.native.model. A missing
|
|
16
|
+
// finish reason stays unknown; a stream that never observed protocol completion
|
|
17
|
+
// is retryable, never a silent final.
|
|
18
|
+
const USER_ABORTS = new Set(['user_abort', 'canceled', 'cancelled']);
|
|
19
|
+
const POLICY_ABORTS = new Set(['policy_abort', 'content_filter']);
|
|
20
|
+
const CONTEXT_LIMITS = new Set(['context_overflow', 'context_length_exceeded']);
|
|
21
|
+
const OUTPUT_LIMITS = new Set([
|
|
22
|
+
'output_limit', 'length', 'max_tokens', 'max_output_tokens', 'incomplete',
|
|
23
|
+
// A completed generation can fill input + output capacity without its input
|
|
24
|
+
// having been rejected. Use bounded output continuation;
|
|
25
|
+
// real input-overflow errors still take the separate context-overflow path.
|
|
26
|
+
'model_context_window_exceeded',
|
|
27
|
+
]);
|
|
28
|
+
const TOOL_FINISHES = new Set(['tool_calls', 'tool_use']);
|
|
29
|
+
const COMPLETE_FINISHES = new Set([
|
|
30
|
+
null, 'stop', 'end_turn', 'completed', 'final_candidate', ...TOOL_FINISHES,
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Classify one sealed attempt into a ModelResult.kind with validated tool calls.
|
|
35
|
+
*
|
|
36
|
+
* `streamComplete` means the adapter observed protocol completion (a real
|
|
37
|
+
* terminal: [DONE], message_stop, or a buffered 2xx body), not just socket EOF.
|
|
38
|
+
* Partial tool payloads never reach toolCalls; the caller keeps raw evidence for
|
|
39
|
+
* repair separately. Tool calls with undecodable argument bytes keep their
|
|
40
|
+
* identity but carry arguments={} and the contract's `wireArgumentsInvalid`
|
|
41
|
+
* flag so the kernel can emit one bounded tool-error receipt without executing.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeResponse({
|
|
44
|
+
text = '', toolCalls = [], finishReason = null, streamComplete = true,
|
|
45
|
+
} = {}) {
|
|
46
|
+
const out = (kind, calls = []) => ({ kind, text, toolCalls: calls });
|
|
47
|
+
|
|
48
|
+
if (USER_ABORTS.has(finishReason)) return out('user_abort');
|
|
49
|
+
if (POLICY_ABORTS.has(finishReason)) return out('policy_abort');
|
|
50
|
+
if (CONTEXT_LIMITS.has(finishReason)) return out('context_overflow');
|
|
51
|
+
if (finishReason === 'provider_terminal_error') return out('provider_terminal_error');
|
|
52
|
+
if (!streamComplete || finishReason === 'provider_retryable_error') {
|
|
53
|
+
return out('provider_retryable_error');
|
|
54
|
+
}
|
|
55
|
+
if (OUTPUT_LIMITS.has(finishReason)) return { ...out('output_limit'), textCommitVersion: 'provider-finished-text/v1',
|
|
56
|
+
outputBoundary: toolCalls.length ? 'tool-call/v1' : text.trim() ? 'assistant-text/v1' : 'no-visible-output/v1' };
|
|
57
|
+
if (finishReason === 'malformed_tool_call') return out('malformed_tool_call');
|
|
58
|
+
if (finishReason === 'empty_response') return out('empty_response');
|
|
59
|
+
if (!COMPLETE_FINISHES.has(finishReason)) return out('provider_terminal_error');
|
|
60
|
+
|
|
61
|
+
let calls;
|
|
62
|
+
try {
|
|
63
|
+
calls = toolCalls.map(validateToolCall);
|
|
64
|
+
} catch {
|
|
65
|
+
return out('malformed_tool_call');
|
|
66
|
+
}
|
|
67
|
+
if (calls.length) {
|
|
68
|
+
const ids = calls.map((c) => c.callId);
|
|
69
|
+
if (new Set(ids).size !== ids.length) return out('malformed_tool_call');
|
|
70
|
+
return out('tool_calls', calls);
|
|
71
|
+
}
|
|
72
|
+
if (TOOL_FINISHES.has(finishReason)) return out('malformed_tool_call');
|
|
73
|
+
return out(text.trim() ? 'final_candidate' : 'empty_response');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate a single assembled tool call. Throws on a malformed identity or
|
|
77
|
+
// shape; a call with wireArgumentsInvalid keeps arguments={} and passes.
|
|
78
|
+
function validateToolCall(call) {
|
|
79
|
+
if (!call || typeof call !== 'object') throw new Error('invalid tool call');
|
|
80
|
+
const callId = String(call.callId ?? '');
|
|
81
|
+
const name = String(call.name ?? '');
|
|
82
|
+
if (!callId.trim() || !name.trim()) throw new Error('tool call identity blank');
|
|
83
|
+
const arguments_ = call.wireArgumentsInvalid ? {} : call.arguments;
|
|
84
|
+
if (arguments_ == null || typeof arguments_ !== 'object' || Array.isArray(arguments_)) {
|
|
85
|
+
throw new Error('tool arguments must be an object');
|
|
86
|
+
}
|
|
87
|
+
return { callId, name, arguments: arguments_, ...(call.wireArgumentsInvalid ? { wireArgumentsInvalid: true } : {}) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Parse the OpenAI chat usage envelope. Both primary counters must be present;
|
|
91
|
+
// an incomplete report stays unknown, never fabricated zero usage. All supplied
|
|
92
|
+
// fields are validated even in a partial report. Optional subdivisions are
|
|
93
|
+
// retained by source meaning: prompt_tokens_details.cached_tokens ->
|
|
94
|
+
// cachedInputTokens, completion_tokens_details.reasoning_tokens -> reasoningTokens.
|
|
95
|
+
export function parseOpenAIUsage(value) {
|
|
96
|
+
if (value == null) return null;
|
|
97
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
98
|
+
throw new ProtocolError('invalid usage report', 'usage_invalid');
|
|
99
|
+
}
|
|
100
|
+
for (const field of ['prompt_tokens', 'completion_tokens', 'total_tokens']) {
|
|
101
|
+
if (Object.hasOwn(value, field)) toNonNegInt(value[field], field);
|
|
102
|
+
}
|
|
103
|
+
const details = value.prompt_tokens_details;
|
|
104
|
+
const completionDetails = value.completion_tokens_details;
|
|
105
|
+
if (details != null && (typeof details !== 'object' || Array.isArray(details))) {
|
|
106
|
+
throw new ProtocolError('invalid usage details', 'usage_invalid');
|
|
107
|
+
}
|
|
108
|
+
if (completionDetails != null && (typeof completionDetails !== 'object' || Array.isArray(completionDetails))) {
|
|
109
|
+
throw new ProtocolError('invalid usage details', 'usage_invalid');
|
|
110
|
+
}
|
|
111
|
+
const cached = details && Object.hasOwn(details, 'cached_tokens')
|
|
112
|
+
? toNonNegInt(details.cached_tokens, 'cached_tokens') : undefined;
|
|
113
|
+
const reasoning = completionDetails && Object.hasOwn(completionDetails, 'reasoning_tokens')
|
|
114
|
+
? toNonNegInt(completionDetails.reasoning_tokens, 'reasoning_tokens') : undefined;
|
|
115
|
+
if (!Object.hasOwn(value, 'prompt_tokens') || !Object.hasOwn(value, 'completion_tokens')) return null;
|
|
116
|
+
const usage = { ...emptyUsage(), inputTokens: value.prompt_tokens, outputTokens: value.completion_tokens };
|
|
117
|
+
if (cached != null) usage.cachedInputTokens = cached;
|
|
118
|
+
if (reasoning != null) usage.reasoningTokens = reasoning;
|
|
119
|
+
return usage;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Parse an Anthropic usage fragment (message_start or message_delta). Counters
|
|
123
|
+
// are accumulated across the stream. Cache fields are retained by source
|
|
124
|
+
// meaning: cache_read_input_tokens -> cachedInputTokens (tokens served from the
|
|
125
|
+
// cache). reasoning_tokens, when a provider reports it, -> reasoningTokens.
|
|
126
|
+
export function parseAnthropicUsageFragment(value) {
|
|
127
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
128
|
+
throw new ProtocolError('invalid anthropic usage', 'usage_invalid');
|
|
129
|
+
}
|
|
130
|
+
const fragment = {};
|
|
131
|
+
if (Object.hasOwn(value, 'input_tokens')) {
|
|
132
|
+
fragment.inputTokens = toNonNegInt(value.input_tokens, 'input_tokens');
|
|
133
|
+
}
|
|
134
|
+
if (Object.hasOwn(value, 'output_tokens')) {
|
|
135
|
+
fragment.outputTokens = toNonNegInt(value.output_tokens, 'output_tokens');
|
|
136
|
+
}
|
|
137
|
+
if (value.cache_read_input_tokens != null) {
|
|
138
|
+
fragment.cachedInputTokens = toNonNegInt(value.cache_read_input_tokens, 'cache_read_input_tokens');
|
|
139
|
+
}
|
|
140
|
+
if (value.cache_creation_input_tokens != null) {
|
|
141
|
+
fragment.cacheCreationInputTokens = toNonNegInt(value.cache_creation_input_tokens, 'cache_creation_input_tokens');
|
|
142
|
+
}
|
|
143
|
+
if (value.reasoning_tokens != null) {
|
|
144
|
+
fragment.reasoningTokens = toNonNegInt(value.reasoning_tokens, 'reasoning_tokens');
|
|
145
|
+
}
|
|
146
|
+
return fragment;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build a sealed Usage plus usageReported flag. When the provider reported
|
|
150
|
+
// usage, usageReported is true and the counters are the provider's (including
|
|
151
|
+
// cached/reasoning). When it did not, usage is emptyUsage() and usageReported
|
|
152
|
+
// is false — the kernel records providerUsageIncomplete so a zero counter is
|
|
153
|
+
// never mistaken for known free usage.
|
|
154
|
+
export function sealUsage(reportedUsage) {
|
|
155
|
+
if (reportedUsage == null) return { usage: emptyUsage(), usageReported: false };
|
|
156
|
+
return { usage: reportedUsage, usageReported: true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toNonNegInt(value, field) {
|
|
160
|
+
// The kernel's addUsage validates Number.isSafeInteger for token counters;
|
|
161
|
+
// reject NaN/booleans/strings/unsafe integers so a bad report is a protocol
|
|
162
|
+
// error here, not an invalid_usage crash in the kernel.
|
|
163
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
164
|
+
throw new ProtocolError(`invalid ${field} counter`, 'usage_invalid');
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Host generation options may select a smaller output budget, but cannot
|
|
170
|
+
// increase the limit pinned in the run configuration.
|
|
171
|
+
export function boundOutputTokens(value, profileLimit) {
|
|
172
|
+
if (!Number.isSafeInteger(profileLimit) || profileLimit <= 0) throw new ProtocolError('invalid pinned output limit');
|
|
173
|
+
if (value === undefined) return profileLimit;
|
|
174
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new ProtocolError('invalid output limit');
|
|
175
|
+
return Math.min(value, profileLimit);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Parse a Retry-After header into milliseconds. Accepts delta-seconds or an
|
|
179
|
+
// HTTP-date. Non-finite, negative or malformed values yield null (the kernel
|
|
180
|
+
// applies its own cooldown; a bad header is not a scheduler instruction).
|
|
181
|
+
export function parseRetryAfterMs(headerValue) {
|
|
182
|
+
if (headerValue == null || headerValue === '') return null;
|
|
183
|
+
const trimmed = String(headerValue).trim();
|
|
184
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
|
|
185
|
+
const seconds = Number(trimmed);
|
|
186
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const parsed = Date.parse(trimmed);
|
|
190
|
+
if (Number.isNaN(parsed)) return null;
|
|
191
|
+
const delta = parsed - Date.now();
|
|
192
|
+
return Number.isFinite(delta) && delta >= 0 ? Math.round(delta) : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Typed protocol error: a fixed, credential-free classification. The adapter
|
|
196
|
+
// translates these to provider_terminal_error with failureKind 'invalid_response'
|
|
197
|
+
// (or retryable for transport/idle). Provider bytes never enter the message.
|
|
198
|
+
export class ProtocolError extends Error {
|
|
199
|
+
constructor(message, code = 'protocol_invalid') {
|
|
200
|
+
super(message);
|
|
201
|
+
this.name = 'ProtocolError';
|
|
202
|
+
this.code = code;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Resolve a request URL from the host binding's baseUrl and a wire path suffix.
|
|
207
|
+
// If baseUrl already ends with the suffix path, it is used as-is; otherwise the
|
|
208
|
+
// suffix is appended. Trailing slashes on baseUrl are stripped first. This lets
|
|
209
|
+
// the host pass either an API root (e.g. https://host/v1) or a full endpoint.
|
|
210
|
+
export function resolveUrl(baseUrl, suffix) {
|
|
211
|
+
const base = String(baseUrl).replace(/\/+$/, '');
|
|
212
|
+
if (!base) throw new ProtocolError('binding baseUrl is empty');
|
|
213
|
+
if (base.endsWith('/' + suffix) || base === suffix) return base;
|
|
214
|
+
return base + '/' + suffix;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Sanitize an arbitrary thrown value into a safe, non-leaking kind string plus
|
|
218
|
+
// message. Exception text from host hooks or transports may include credentials,
|
|
219
|
+
// so only a stable failureKind leaves the adapter; the original text is dropped.
|
|
220
|
+
export function sanitizeFailureKind(error, fallback = 'host_error') {
|
|
221
|
+
if (error == null) return fallback;
|
|
222
|
+
if (error instanceof DOMException) {
|
|
223
|
+
if (error.name === 'TimeoutError') return 'request_timeout';
|
|
224
|
+
if (error.name === 'AbortError') return 'request_timeout';
|
|
225
|
+
}
|
|
226
|
+
if (error instanceof TypeError) return 'network_error';
|
|
227
|
+
return fallback;
|
|
228
|
+
}
|