@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,171 @@
1
+ // Strict SSE byte framing for the native model transport.
2
+ //
3
+ // Ports ksadk._model_sse: UTF-8 is deliberately strict (replacing corrupt bytes
4
+ // inside JSON changes model facts), a byte budget bounds the whole body, and a
5
+ // blank line is the only frame dispatch. A discarded terminal fragment at EOF
6
+ // is an unfinished frame, never a complete response. Idle silence is bounded.
7
+ //
8
+ // This framer yields { data, event } frames and never parses JSON: the protocol
9
+ // handlers own envelope validation. [DONE] is yielded as a data frame for the
10
+ // handler to seal termination.
11
+
12
+ export class SseFramingError extends Error {
13
+ constructor(message, code = 'framing_invalid') { super(message); this.name = 'SseFramingError'; this.code = code; }
14
+ }
15
+
16
+ // No nonempty model data arrived within the host's read interval.
17
+ export class IdleTimeout extends Error {
18
+ constructor(message = 'model stream idle timeout') { super(message); this.name = 'IdleTimeout'; }
19
+ }
20
+
21
+ // Normalize a fetch body (web ReadableStream or async iterable) to an async
22
+ // iterator of Uint8Array chunks. Fetch's response.body is a web ReadableStream
23
+ // in Node; both shapes are accepted so tests can feed a plain async iterable.
24
+ function toAsyncIterator(body) {
25
+ if (body && typeof body[Symbol.asyncIterator] === 'function') return body[Symbol.asyncIterator]();
26
+ if (body && typeof body.getReader === 'function') {
27
+ const reader = body.getReader();
28
+ const next = () => reader.read();
29
+ const return_ = async () => {
30
+ try { await reader.cancel(); } catch { /* already closed or released */ }
31
+ try { reader.releaseLock(); } catch { /* ignore */ }
32
+ };
33
+ return { next, return: return_, [Symbol.asyncIterator]() { return this; } };
34
+ }
35
+ throw new SseFramingError('model SSE requires a byte stream');
36
+ }
37
+
38
+ // Stateful line framer mirroring ksadk._model_sse._Framer. CRLF/CR/LF all
39
+ // terminate lines; a blank line dispatches a frame from accumulated data lines.
40
+ class SseFramer {
41
+ constructor() {
42
+ this.lineParts = [];
43
+ this.dataLines = [];
44
+ this.eventName = '';
45
+ this.afterCr = false;
46
+ }
47
+
48
+ feed(text, frames) {
49
+ if (!text) return;
50
+ if (this.afterCr) {
51
+ if (text.startsWith('\n')) text = text.slice(1);
52
+ this.afterCr = false;
53
+ }
54
+ let start = 0;
55
+ for (const m of text.matchAll(/\r\n|\r|\n/g)) {
56
+ this.lineParts.push(text.slice(start, m.index));
57
+ const line = this.lineParts.join('');
58
+ this.lineParts = [];
59
+ start = m.index + m[0].length;
60
+ this.afterCr = m[0] === '\r' && start === text.length;
61
+ const frame = this.handleLine(line);
62
+ if (frame) frames.push(frame);
63
+ }
64
+ if (start < text.length) this.lineParts.push(text.slice(start));
65
+ }
66
+
67
+ handleLine(line) {
68
+ if (line === '') {
69
+ if (this.dataLines.length === 0) { this.eventName = ''; return null; }
70
+ const frame = { data: this.dataLines.join('\n'), event: this.eventName || '' };
71
+ this.dataLines = [];
72
+ this.eventName = '';
73
+ return frame;
74
+ }
75
+ if (line.startsWith(':')) return null; // comment / heartbeat
76
+ const sep = line.indexOf(':');
77
+ const name = sep === -1 ? line : line.slice(0, sep);
78
+ let value = sep === -1 ? '' : line.slice(sep + 1);
79
+ if (value.startsWith(' ')) value = value.slice(1);
80
+ if (name === 'data') this.dataLines.push(value);
81
+ else if (name === 'event') this.eventName = value;
82
+ // `id` and `retry` are transport controls, not model payloads; ignore them.
83
+ return null;
84
+ }
85
+
86
+ finish() {
87
+ const pending = this.lineParts.join('');
88
+ // A trailing data fragment without a blank-line dispatch is unfinished.
89
+ if (this.dataLines.length || pending === 'data' || pending.startsWith('data:')) {
90
+ throw new SseFramingError('unfinished model SSE data');
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Yield { data, event } SSE frames from a byte stream with strict framing.
97
+ *
98
+ * @param body fetch response body (ReadableStream or async iterable of bytes)
99
+ * @param {maxBytes, idleTimeoutMs, abort} bounds. maxBytes bounds total bytes;
100
+ * idleTimeoutMs bounds silence between nonempty frames: when it fires, abort
101
+ * is called with an IdleTimeout so the underlying fetch aborts and the
102
+ * pending read rejects cleanly (no orphaned read to cancel). Throws
103
+ * SseFramingError (terminal) or propagates transport/abort errors.
104
+ */
105
+ export async function* readModelSse(body, { maxBytes = 8 * 1024 * 1024, idleTimeoutMs, abort, observeBytes } = {}) {
106
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
107
+ throw new SseFramingError('model SSE needs a positive byte budget');
108
+ }
109
+ const iter = toAsyncIterator(body);
110
+ const decoder = new TextDecoder('utf-8', { fatal: true });
111
+ const framer = new SseFramer();
112
+ let total = 0;
113
+ const idleEnabled = typeof idleTimeoutMs === 'number' && idleTimeoutMs > 0 && typeof abort === 'function';
114
+ let idleTimer = null;
115
+ const armIdle = () => {
116
+ if (!idleEnabled) return;
117
+ if (idleTimer) clearTimeout(idleTimer);
118
+ idleTimer = setTimeout(() => abort(new IdleTimeout()), idleTimeoutMs);
119
+ idleTimer.unref?.();
120
+ };
121
+ const disarmIdle = () => { if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; } };
122
+ armIdle();
123
+ const frames = [];
124
+ try {
125
+ while (true) {
126
+ let result;
127
+ result = await iter.next();
128
+ if (result.done) break;
129
+ const chunk = result.value;
130
+ if (!(chunk instanceof Uint8Array) && !(chunk instanceof ArrayBuffer)) {
131
+ throw new SseFramingError('model SSE requires byte chunks');
132
+ }
133
+ total += chunk.byteLength;
134
+ observeBytes?.(chunk.byteLength);
135
+ if (total > maxBytes) throw new SseFramingError('model SSE byte budget exceeded', 'body_limit');
136
+ let text;
137
+ try {
138
+ text = decoder.decode(chunk, { stream: true });
139
+ } catch {
140
+ throw new SseFramingError('invalid utf-8 in model stream', 'body_encoding');
141
+ }
142
+ frames.length = 0;
143
+ framer.feed(text, frames);
144
+ for (const frame of frames) {
145
+ yield frame;
146
+ // Time spent by the consumer is not provider idle time. Comments and
147
+ // blank data do not reset the deadline; only nonempty data does.
148
+ if (frame.data && frame.data.trim()) armIdle();
149
+ }
150
+ }
151
+ let tail;
152
+ try {
153
+ tail = decoder.decode();
154
+ } catch {
155
+ throw new SseFramingError('invalid utf-8 in model stream', 'body_encoding');
156
+ }
157
+ if (tail) {
158
+ frames.length = 0;
159
+ framer.feed(tail, frames);
160
+ for (const frame of frames) yield frame;
161
+ }
162
+ framer.finish();
163
+ } finally {
164
+ disarmIdle();
165
+ // Fire-and-forget: the fetch abort already errored the body stream, and
166
+ // awaiting cancel here must not block propagating the IdleTimeout/abort.
167
+ if (typeof iter.return === 'function') {
168
+ try { Promise.resolve(iter.return()).catch(() => {}); } catch { /* already closed */ }
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,27 @@
1
+ import { validateModelSurface } from '@kingsoftcloud/agentkit-harness-core';
2
+
3
+ /** The tool owner supplies actual roots; neither process.cwd nor ambient host prompt data is a fallback. */
4
+ export function bindNativeModelSurface({ environment, description, original, executionCapabilities } = {}) {
5
+ const modelSurface = { contract: 'bound-model-surface/v1',
6
+ ...(original === undefined ? {verificationProjection:'readable-evidence/v1'}
7
+ : original.verificationProjection ? {verificationProjection:original.verificationProjection} : {}),
8
+ ...(executionCapabilities === undefined ? {} : { executionCapabilities: structuredClone(executionCapabilities) }),
9
+ environment: { ...structuredClone(environment),
10
+ date: original?.environment.date ?? new Date().toISOString().slice(0, 10),
11
+ supportsImages: description.input.includes('image') } };
12
+ validateModelSurface(modelSurface);
13
+ return modelSurface;
14
+ }
15
+
16
+ /** Preserve content-part indices when exposing the last actual user input to children. */
17
+ export function nativeChildInputMediaSources(rows, admission) {
18
+ let sources = new Map();
19
+ rows.forEach(({ message }, index) => {
20
+ if (admission.messages[index].kind !== 'user_input') return;
21
+ sources = new Map(); let image = 0;
22
+ message.content.forEach((part, partIndex) => {
23
+ if (part.type === 'image' && part.offloaded !== true) sources.set(partIndex, admission.messages[index].media?.[image++]);
24
+ });
25
+ });
26
+ return sources;
27
+ }
@@ -0,0 +1,16 @@
1
+ import { invariant } from '@kingsoftcloud/agentkit-harness-core';
2
+
3
+ // Exact supported model IDs from the provider's tool-search compatibility table.
4
+ // Custom Anthropic endpoints can explicitly declare support in their profile.
5
+ const supported = new Set(['claude-fable-5-1', 'claude-mythos-5-1', 'claude-fable-5', 'claude-mythos-5',
6
+ 'claude-opus-5-5', 'claude-opus-5', 'claude-sonnet-5-5', 'claude-sonnet-5', 'claude-haiku-5-5',
7
+ 'claude-opus-4-8', 'claude-opus-4-7', 'claude-opus-4-6', 'claude-sonnet-4-6',
8
+ 'claude-opus-4-5-20251101', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001']);
9
+
10
+ export function admitProviderToolSearchPolicy({ protocol, model, requested, discovery }) {
11
+ invariant(requested === undefined || typeof requested === 'boolean', 'native_provider_tool_search_invalid');
12
+ invariant(requested !== true || protocol === 'anthropic-messages', 'native_provider_tool_search_protocol_unsupported');
13
+ return protocol === 'anthropic-messages' && discovery && requested !== false
14
+ && (requested === true || supported.has(String(model).trim().toLowerCase()))
15
+ ? 'anthropic-deferred/v1' : 'local-only/v1';
16
+ }
@@ -0,0 +1,27 @@
1
+ /** Keep observations outside the model transcript and outside execution policy. */
2
+ export function modelTransportObservation(now = () => performance.now()) {
3
+ const start = now(), ms = () => Math.max(0, Math.round(now() - start));
4
+ const facts = { contract: 'model-transport-observation/v1', phase: 'prepare', headersMs: null,
5
+ bodyChunks: 0, bodyBytes: 0, lastByteMs: null, lastFrameMs: null, lastOutputMs: null, lastToolDeltaMs: null };
6
+ return {
7
+ phase: value => { facts.phase = value; },
8
+ headers: () => { facts.headersMs = ms(); },
9
+ bytes: size => { if (size > 0) { facts.bodyChunks++;facts.bodyBytes += size;facts.lastByteMs = ms(); } },
10
+ frame: () => { facts.lastFrameMs = ms(); },
11
+ output: event => {
12
+ if (!event?.text) return;
13
+ facts.lastOutputMs = ms();if (event.kind === 'tool_delta') facts.lastToolDeltaMs = facts.lastOutputMs;
14
+ },
15
+ finish: () => ({ ...facts, elapsedMs: ms() }),
16
+ };
17
+ }
18
+
19
+ export function observeModelTransportPort(generate) {
20
+ return { async generate(request, emit) {
21
+ const observation = modelTransportObservation();
22
+ const result = await generate(request, event => {
23
+ observation.output(event);if (typeof emit === 'function') emit(event);
24
+ }, observation);
25
+ return { ...result, transportObservation: observation.finish() };
26
+ } };
27
+ }
@@ -0,0 +1,114 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { CHILD_OWNER_CONTRACT, canonical, copy, terminal } from '@kingsoftcloud/agentkit-harness-core';
3
+
4
+ const hash = value => createHash('sha256').update(canonical(value)).digest('hex');
5
+ const same = (left, right) => canonical(left) === canonical(right);
6
+ const identity = value => Object.fromEntries(['scope', 'runId', 'sessionId', 'turnId', 'buildId', 'parentRunId', 'taskChainId']
7
+ .map(key => [key, copy(value[key])]));
8
+
9
+ /** A saved decision awaiting consumption is runnable work, not a new request
10
+ * for approval. This observation grants nothing: the child Kernel still checks
11
+ * and consumes the original command against its own pending interaction. */
12
+ export function childNeedsApproval(record) {
13
+ const pending = record.snapshot.pendingInteraction;
14
+ return record.snapshot.phase === 'awaiting_interaction' && (!pending || !record.commands.some(row => !row.consumed
15
+ && row.command.kind === 'submit_interaction' && row.command.payload.interactionId === pending?.interactionId
16
+ && ['allow', 'deny'].includes(row.command.payload.decision)));
17
+ }
18
+
19
+ export function resolveChildToolIntent(parentRecord, operationKey) {
20
+ const intent = parentRecord.intents.find(item => item.operationKey === operationKey);
21
+ if (intent && Array.isArray(intent.payload.calls) && intent.payload.calls.length === 1)
22
+ return { intent, calls: intent.payload.calls, index: 0, base: operationKey };
23
+ const match = operationKey.match(/^(.+):call:([1-9][0-9]*)$/);
24
+ if (!match) return null;
25
+ const base = match[1], index = Number(match[2]) - 1;
26
+ const baseIntent = parentRecord.intents.find(item => item.operationKey === base);
27
+ return baseIntent?.kind === 'tool' && Array.isArray(baseIntent.payload.calls) && baseIntent.payload.calls.length > 1
28
+ && Number.isSafeInteger(index) && index < baseIntent.payload.calls.length
29
+ ? { intent: baseIntent, calls: baseIntent.payload.calls, index, base } : null;
30
+ }
31
+
32
+ /** Locators confer no authority: prove the original causal binding and committed start receipt. */
33
+ export function loadOwnedDirectChild({ store, childScopes, parent, childRunId, roles }) {
34
+ if (!(parent.childRunIds ?? []).includes(childRunId)) return null;
35
+ const parentRecord = store.load(parent.scope, parent.runId);
36
+ for (const childScope of childScopes) {
37
+ let record;
38
+ try { record = store.load(childScope, childRunId); }
39
+ catch (error) { if (error.code === 'run_not_found') continue; throw error; }
40
+ const binding = record.configuration.childOwnerBinding;
41
+ if (binding?.contract !== CHILD_OWNER_CONTRACT || !same(record.snapshot.scope, childScope)
42
+ || !same(binding.parent, identity(parent)) || roles && !roles.includes(binding.role)) continue;
43
+ const reservation = (parent.childBudgetReservations ?? []).find(item => item.childRunId === childRunId);
44
+ if (!reservation || !same(reservation.childScope, childScope)
45
+ || reservation.parentOperationKey !== binding.parentOperationKey || reservation.parentToolCallId !== binding.parentToolCallId
46
+ || reservation.childRequestDigest !== binding.childRequestDigest || reservation.deliveryMode !== binding.deliveryMode
47
+ || reservation.configurationDigest !== record.snapshot.configDigest || hash(record.configuration) !== reservation.configurationDigest
48
+ || reservation.maxModelCalls !== binding.maxModelCalls || reservation.maxToolCalls !== binding.maxToolCalls) continue;
49
+ const start = resolveChildToolIntent(parentRecord, binding.parentOperationKey), call = start?.calls[start.index];
50
+ const receipt = parentRecord.receipts.find(item => item.operationKey === start?.base && item.status === 'committed');
51
+ const original = receipt?.result.results?.[start?.index]?.childBudgetReservation;
52
+ if (start?.intent.kind !== 'tool' || !call || !['native_start_child', 'native_delegate'].includes(call.name)
53
+ || call.callId !== binding.parentToolCallId || call.arguments.role !== binding.role
54
+ || call.arguments.task !== record.configuration.contract.objective
55
+ || !original || !same(original, { ...reservation, settledUsageDigest: null })) continue;
56
+ return record;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ export function hasUnsafeChildReceipt(record) {
62
+ const reconciled = intent => {
63
+ const original = intent?.kind === 'tool_receipt'
64
+ ? record.intents.find(row => row.operationKey === intent.payload.originalOperationKey) : intent;
65
+ const call = original?.payload.calls?.[0];
66
+ if (original?.kind !== 'tool' || original.status !== 'running' || original.payload.calls.length !== 1
67
+ || !Number.isSafeInteger(original.claimedFence) || original.claimedFence <= 0
68
+ || record.configuration.tools.find(tool => tool.name === call.name)?.recoveryPolicy !== 'receipt-observation/v1') return false;
69
+ if (intent.kind === 'tool_receipt' && (intent.payload.contract !== 'agentkit.tool-receipt-observation/v1'
70
+ || intent.payload.callId !== call.callId || intent.payload.originalClaimedFence !== original.claimedFence)) return false;
71
+ return record.intents.some(query => {
72
+ const p = query.payload;
73
+ if (query.kind !== 'tool_receipt' || query.status !== 'running' || p.contract !== 'agentkit.tool-receipt-observation/v1'
74
+ || p.originalOperationKey !== original.operationKey || p.callId !== call.callId || p.originalClaimedFence !== original.claimedFence) return false;
75
+ const receipt = record.receipts.find(row => row.operationKey === query.operationKey);
76
+ return receipt?.status === 'committed' && receipt.result.contract === p.contract
77
+ && receipt.result.originalOperationKey === original.operationKey && receipt.result.callId === call.callId
78
+ && ['ok', 'error'].includes(receipt.result.result?.status);
79
+ });
80
+ };
81
+ return record.receipts.some(receipt => receipt.status === 'uncertain'
82
+ && !(receipt.result.retryScheduled === true && ['safe-read/v1', 'workspace-observation/v1'].includes(receipt.result.recoveryPolicy))
83
+ && record.intents.find(intent => intent.operationKey === receipt.operationKey)?.kind !== 'model'
84
+ && !reconciled(record.intents.find(intent => intent.operationKey === receipt.operationKey)));
85
+ }
86
+
87
+ /** A settled marker alone cannot authorize delivery or reuse of a Session. */
88
+ export function accountedChildDelivery(parentRecord, childRecord) {
89
+ const child = childRecord.snapshot;
90
+ if (!terminal(child.phase) || child.phase === 'interrupted' || hasUnsafeChildReceipt(childRecord)) return null;
91
+ const reservation = parentRecord.snapshot.childBudgetReservations.find(item => item.childRunId === child.runId);
92
+ if (!reservation?.settledUsageDigest) return null;
93
+ return parentRecord.receipts.filter(receipt => receipt.status === 'committed').flatMap(receipt => receipt.result.results ?? [])
94
+ .find(result => result.childBudgetSettlement?.childRunId === child.runId
95
+ && result.childBudgetSettlement.terminalSnapshotDigest === hash(child)
96
+ && hash({ settlement: result.childBudgetSettlement, usage: result.resourceUsage, childRunIds: result.childRunIds })
97
+ === reservation.settledUsageDigest) ?? null;
98
+ }
99
+
100
+ /** Only terminal ancestors in this exact parent Session may grant their original child. */
101
+ export function loadSessionChild({ store, childScopes, parent, childRunId }) {
102
+ const seen = new Set();
103
+ for (let cursor = parent; cursor && !seen.has(cursor.runId);) {
104
+ seen.add(cursor.runId);
105
+ const child = loadOwnedDirectChild({ store, childScopes, parent: cursor, childRunId });
106
+ if (child) return { child, parent: store.load(cursor.scope, cursor.runId) };
107
+ if (!cursor.parentRunId) return null;
108
+ const previous = store.load(parent.scope, cursor.parentRunId);
109
+ if (!same(previous.snapshot.scope, parent.scope) || previous.snapshot.sessionId !== parent.sessionId
110
+ || !terminal(previous.snapshot.phase)) return null;
111
+ cursor = previous.snapshot;
112
+ }
113
+ return null;
114
+ }
@@ -0,0 +1,106 @@
1
+ import { canonical, copy, backgroundCommandRef, NativeError } from '@kingsoftcloud/agentkit-harness-core';
2
+ const check = (value, code) => { if (!value) throw new NativeError(code); };
3
+ const same = (a, b) => canonical(a) === canonical(b);
4
+ export const OWNED_COMMAND_OUTPUT_CONTRACT = 'agentkit.owned-command-output/v1';
5
+
6
+ /** Authenticated Hosts may page an existing process without manufacturing a
7
+ * model tool call. Every read is bound to the current session head and the
8
+ * original committed launch receipt, including reads after a Host restart.
9
+ * Readers have no launch/cancel capability. Raw output still requires the
10
+ * Host's operator-output disclosure policy before publication.
11
+ */
12
+ export function createOwnedCommandOutputReader({ scope, store, readers = {}, isClosed = () => false }) {
13
+ const pinned = copy(scope), bindings = Object.fromEntries(Object.entries(readers));
14
+ check(typeof isClosed === 'function' && Object.entries(bindings).every(([name, reader]) =>
15
+ ['local_process', 'e2b'].includes(name) && typeof reader === 'function'), 'invalid_command_output_readers');
16
+ return Object.freeze({ contract: OWNED_COMMAND_OUTPUT_CONTRACT, async read(request) {
17
+ const signal = request?.signal;
18
+ check(signal instanceof AbortSignal, 'command_output_signal_invalid');
19
+ const { signal: _, ...body } = request, sealed = canonical(body);
20
+ check(Object.keys(body).every(key => ['contract', 'scope', 'runId', 'sessionId', 'taskId', 'stdoutCursor', 'stderrCursor', 'maxBytes'].includes(key))
21
+ && body.contract === OWNED_COMMAND_OUTPUT_CONTRACT && same(body.scope, pinned)
22
+ && ['runId', 'sessionId'].every(key => typeof body[key] === 'string' && body[key].trim())
23
+ && /^[a-f0-9]{64}$/.test(body.taskId)
24
+ && [body.stdoutCursor, body.stderrCursor].every(cursor => Number.isSafeInteger(cursor) && cursor >= 0)
25
+ && Number.isSafeInteger(body.maxBytes) && body.maxBytes >= 4 && body.maxBytes <= 65536,
26
+ 'command_output_request_invalid');
27
+ const current = () => {
28
+ signal.throwIfAborted(); check(isClosed() === false, 'command_output_closed');
29
+ const { signal: liveSignal, ...live } = request;
30
+ check(liveSignal === signal && canonical(live) === sealed, 'command_output_request_changed');
31
+ const head = store.sessionHead(pinned, body.sessionId);
32
+ check(head?.runId === body.runId, 'command_output_head_changed');
33
+ const record = store.load(pinned, body.runId);
34
+ check(record.snapshot.sessionId === body.sessionId && same(record.snapshot.scope, pinned), 'command_output_owner_mismatch');
35
+ const matches = record.snapshot.backgroundCommands.filter(row => row.ref.taskId === body.taskId);
36
+ check(matches.length === 1 && head.backgroundCommands.some(row => same(row.ref, matches[0].ref)), 'background_command_not_owned');
37
+ return ownedCommandReceipt({ scope: pinned, store, current: record, ref: matches[0].ref, includeCommand: true });
38
+ };
39
+ const owner = current(), reader = bindings[owner.backend];
40
+ check(reader, 'command_output_backend_unavailable');
41
+ const output = await reader({ scope: copy(pinned), sessionId: body.sessionId, runId: body.runId,
42
+ ref: copy(owner.ref), stdoutCursor: body.stdoutCursor, stderrCursor: body.stderrCursor,
43
+ maxBytes: body.maxBytes, waitSeconds: 0, signal });
44
+ check(same(current(), owner), 'command_output_owner_changed');
45
+ check(output && typeof output === 'object' && !Array.isArray(output)
46
+ && ['starting', 'running', 'completed', 'failed', 'canceled', 'timed_out', 'output_limit', 'unknown', 'unavailable'].includes(output.status),
47
+ 'command_output_invalid');
48
+ for (const channel of ['stdout', 'stderr']) {
49
+ check(typeof output[channel] === 'string' && Buffer.byteLength(output[channel]) <= body.maxBytes
50
+ && Number.isSafeInteger(output[channel + 'Cursor']) && output[channel + 'Cursor'] >= body[channel + 'Cursor']
51
+ && output[channel + 'Cursor'] - body[channel + 'Cursor'] === Buffer.byteLength(output[channel]), 'command_output_cursor_invalid');
52
+ }
53
+ check((output.exitCode == null || Number.isSafeInteger(output.exitCode))
54
+ && (output.observedExitCode == null || Number.isSafeInteger(output.observedExitCode))
55
+ && [output.outputMayBeTruncated, output.evidenceIncomplete].every(value => value === undefined || typeof value === 'boolean'),
56
+ 'command_output_invalid');
57
+ const settled = ['completed', 'failed', 'canceled', 'timed_out', 'output_limit'].includes(output.status);
58
+ return { contract: OWNED_COMMAND_OUTPUT_CONTRACT, sessionId: body.sessionId, runId: body.runId, taskId: body.taskId,
59
+ command: owner.command, status: output.status, stdout: output.stdout, stderr: output.stderr,
60
+ stdoutCursor: output.stdoutCursor, stderrCursor: output.stderrCursor,
61
+ exitCode: settled && output.evidenceIncomplete !== true ? output.exitCode ?? null : null,
62
+ observedExitCode: output.observedExitCode ?? null, evidenceIncomplete: output.evidenceIncomplete === true,
63
+ outputMayBeTruncated: output.outputMayBeTruncated === true };
64
+ } });
65
+ }
66
+
67
+ /** The backend is a fact in the canonical launch receipt, never a model hint. */
68
+ export function ownedCommandReceipt({ scope, store, current, ref: input, includeCommand = false }) {
69
+ const ref = backgroundCommandRef(input), source = store.load(scope, ref.sourceRunId);
70
+ const receipt = source.receipts.find(item => item.operationKey === ref.operationKey);
71
+ const launch = source.intents.find(item => item.operationKey === ref.operationKey);
72
+ const calls = launch?.payload.calls, results = receipt?.result.results;
73
+ const result = Array.isArray(results) && results.length === 1 ? results[0] : null;
74
+ const definition = Array.isArray(calls) && calls.length === 1
75
+ ? source.configuration.tools.find(tool => tool.name === calls[0].name) : null;
76
+ check(same(source.snapshot.scope, scope) && source.snapshot.sessionId === current.snapshot.sessionId
77
+ && receipt?.status === 'committed' && launch?.kind === 'tool'
78
+ && definition?.backgroundCommandPolicy === 'owned-command-launch/v1' && result?.status === 'ok'
79
+ && same(result.backgroundCommandRef ?? null, ref), 'background_command_launcher_receipt_mismatch');
80
+ const backend = typeof result.content?.backend === 'string' && result.content.backend.startsWith('sandbox/')
81
+ ? result.content.backend.slice('sandbox/'.length) : null;
82
+ return { ref, backend: ['local_process', 'e2b'].includes(backend) ? backend : null,
83
+ ...(includeCommand ? { command: typeof calls[0].arguments?.command === 'string' ? calls[0].arguments.command.slice(0, 32768) : '' } : {}) };
84
+ }
85
+
86
+ /** Resolve read/stop authority from the current claimed Native tool operation.
87
+ * Inherited session handles retain their original source run and receipt.
88
+ */
89
+ export function createOwnedCommandResolver({ scope, store }) {
90
+ const pinnedScope = copy(scope);
91
+ return (request, taskId) => {
92
+ request.signal.throwIfAborted();
93
+ check(same(request.scope, pinnedScope), 'background_command_scope_mismatch');
94
+ const current = store.load(pinnedScope, request.runId), snapshot = current.snapshot;
95
+ check(['sessionId', 'turnId', 'buildId', 'parentRunId', 'taskChainId'].every(key => request[key] === snapshot[key]),
96
+ 'background_command_owner_mismatch');
97
+ const intent = current.intents.find(item => item.operationKey === snapshot.pendingIntent), calls = intent?.payload.calls;
98
+ const index = Array.isArray(calls) ? calls.findIndex(call => same(call, request.call)) : -1;
99
+ check(intent?.kind === 'tool' && intent.status === 'running' && index >= 0
100
+ && request.operationKey === (calls.length === 1 ? intent.operationKey : `${intent.operationKey}:call:${index + 1}`),
101
+ 'background_command_tool_operation_mismatch');
102
+ const matches = snapshot.backgroundCommands.filter(item => item.ref.taskId === taskId);
103
+ check(matches.length === 1, 'background_command_not_owned');
104
+ return ownedCommandReceipt({ scope: pinnedScope, store, current, ref: matches[0].ref });
105
+ };
106
+ }
@@ -0,0 +1,60 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { Workspace, PositionEncoding } from '@astral-sh/ruff-wasm-nodejs';
3
+ import { NATIVE_PYTHON_SYNTAX } from './python-syntax.mjs';
4
+
5
+ if (Workspace.version() !== NATIVE_PYTHON_SYNTAX.parserVersion) throw new Error('Python parser version mismatch');
6
+ const workspace = new Workspace({ 'target-version': NATIVE_PYTHON_SYNTAX.targetVersion,
7
+ lint: { select: [] } }, PositionEncoding.Utf32);
8
+ const bytes = Buffer.from(workerData, 'utf8');
9
+ function issueAt(start, message) {
10
+ const lines = bytes.subarray(0, start).toString('utf8').replace(/\r\n?/g, '\n').split('\n');
11
+ return [{ severity: 'error', line: lines.length, column: Array.from(lines.at(-1)).length + 1, message }];
12
+ }
13
+ let issues;
14
+ try {
15
+ const nul = bytes.indexOf(0);
16
+ if (nul !== -1) issues = issueAt(nul, 'Source code cannot contain NUL bytes.');
17
+ // `check` also performs compile-time checks even with no lint rules selected:
18
+ // duplicate parameters, module nonlocal, unreachable match cases, etc.
19
+ // CPython ast.parse deliberately permits these. `comments` first calls the
20
+ // strict grammar parser, without those later scoping/compilation checks.
21
+ try { if (!issues) workspace.comments(workerData); }
22
+ catch (error) {
23
+ const match = /^(.*) at byte range (\d+)\.\.(\d+)$/s.exec(error.message);
24
+ if (!match) throw error;
25
+ const start = Number(match[2]);
26
+ if (!Number.isSafeInteger(start) || start > bytes.length) throw error;
27
+ issues = issueAt(start, match[1]);
28
+ }
29
+ if (!issues) {
30
+ // Ruff exposes token kind/byte ranges as a debug string in this pinned
31
+ // WASM API. Source lexing remains in Rust: digits in strings/comments,
32
+ // floats, imaginaries and base-prefixed integers never count as decimal
33
+ // integer tokens. Preserve CPython 3.12's default conversion/token limits.
34
+ let nesting = 0, indent = 0;
35
+ const tokens = workspace.tokens(workerData);
36
+ for (const match of tokens.matchAll(/^\s+(Int|Lpar|Rpar|Lsqb|Rsqb|Lbrace|Rbrace|Indent|Dedent) (\d+)\.\.(\d+),?$/gm)) {
37
+ const [, kind, from, to] = match, start = Number(from), end = Number(to);
38
+ if (end < start || end > bytes.length) throw new Error('Python token range invalid');
39
+ let message;
40
+ if (kind === 'Int') {
41
+ const token = bytes.subarray(start, end).toString('utf8');
42
+ if (/^[0-9][0-9_]*$/.test(token) && token.replaceAll('_', '').length > NATIVE_PYTHON_SYNTAX.decimalDigitLimit)
43
+ message = 'Decimal integer exceeds the 4300-digit limit.';
44
+ } else if (['Lpar', 'Lsqb', 'Lbrace'].includes(kind)) {
45
+ if (++nesting > NATIVE_PYTHON_SYNTAX.nestingLimit) message = 'Too many nested parentheses.';
46
+ } else if (['Rpar', 'Rsqb', 'Rbrace'].includes(kind)) nesting--;
47
+ else if (kind === 'Indent') {
48
+ if (++indent > NATIVE_PYTHON_SYNTAX.indentLimit) message = 'Too many levels of indentation.';
49
+ } else if (kind === 'Dedent') indent--;
50
+ if (message) { issues = issueAt(start, message); break; }
51
+ }
52
+ }
53
+ // The grammar-only API parses the latest grammar. Keep its version gates
54
+ // from `check`, but never promote compile-time diagnostics to ast.parse.
55
+ if (!issues) issues = workspace.check(workerData)
56
+ .filter(item => / on Python 3\.12 \(syntax was added in Python 3\.\d+\)$/.test(item.message))
57
+ .slice(0, 1).map(item => ({ severity: 'error', line: item.start_location.row,
58
+ column: item.start_location.column, message: item.message }));
59
+ parentPort.postMessage({ contract: NATIVE_PYTHON_SYNTAX.contract, issues });
60
+ } finally { workspace.free(); parentPort.close(); }
@@ -0,0 +1,50 @@
1
+ import { Worker } from 'node:worker_threads';
2
+
3
+ export const NATIVE_PYTHON_SYNTAX = Object.freeze({
4
+ contract: 'agentkit.python-ast-syntax/v1',
5
+ parser: 'ruff-wasm', parserVersion: '0.16.8', targetVersion: 'py312', mode: 'ast-only',
6
+ decimalDigitLimit: 4300, nestingLimit: 200, indentLimit: 99,
7
+ });
8
+ const failure = (code, message) => Object.assign(new Error(message), { code });
9
+
10
+ /** Parse captured source, never a path, project configuration or executable.
11
+ * A fresh worker bounds each parse's lifetime and keeps malformed source from
12
+ * blocking the Host. V8 limits do not cap WASM linear memory; the input limit
13
+ * and terminated worker are additional resource boundaries, not a sandbox.
14
+ */
15
+ export async function checkPythonSyntax(source, { signal, timeoutMs = 10000 } = {}) {
16
+ signal?.throwIfAborted();
17
+ if (typeof source !== 'string' || Buffer.byteLength(source, 'utf8') > 2 * 1024 * 1024)
18
+ throw failure('python_syntax_input_invalid', 'Python source exceeds the workspace text limit.');
19
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10000)
20
+ throw failure('python_syntax_deadline_invalid', 'Invalid Python syntax deadline.');
21
+ let worker;
22
+ try { worker = new Worker(new URL('./python-syntax-worker.mjs', import.meta.url), {
23
+ workerData: source, execArgv: [],
24
+ resourceLimits: { maxOldGenerationSizeMb: 128, maxYoungGenerationSizeMb: 16, stackSizeMb: 4 },
25
+ }); } catch { throw failure('python_syntax_unavailable', 'The Node Python syntax parser could not start.'); }
26
+ return await new Promise((resolve, reject) => {
27
+ let output, error, stopping = false;
28
+ const stop = reason => {
29
+ if (stopping) return;
30
+ stopping = true; error = reason;
31
+ // Settle only at exit, after the parser can no longer consume resources.
32
+ void worker.terminate().catch(() => {});
33
+ };
34
+ const abort = () => stop(signal.reason ?? failure('python_syntax_cancelled', 'Python syntax check cancelled.'));
35
+ const timer = setTimeout(() => stop(failure('python_syntax_timeout', 'Python syntax check timed out.')), timeoutMs);
36
+ signal?.addEventListener('abort', abort, { once: true });
37
+ worker.once('message', value => { output = value; });
38
+ worker.once('error', () => stop(failure('python_syntax_unavailable', 'The Node Python syntax parser failed.')));
39
+ worker.once('exit', code => {
40
+ clearTimeout(timer); signal?.removeEventListener('abort', abort);
41
+ if (error) return reject(error);
42
+ if (code !== 0 || output?.contract !== NATIVE_PYTHON_SYNTAX.contract || !Array.isArray(output.issues)
43
+ || output.issues.some(issue => issue?.severity !== 'error' || !Number.isSafeInteger(issue.line) || issue.line < 1
44
+ || !Number.isSafeInteger(issue.column) || issue.column < 1 || typeof issue.message !== 'string' || !issue.message))
45
+ return reject(failure('python_syntax_unavailable', 'The Node Python syntax parser returned no valid result.'));
46
+ resolve(output.issues);
47
+ });
48
+ if (signal?.aborted) abort();
49
+ });
50
+ }
@@ -0,0 +1,16 @@
1
+ import { safeReadRecoveryCalls } from '@kingsoftcloud/agentkit-harness-core';
2
+
3
+ /** Classify an original complete ledger, never a presentation read model. */
4
+ export function canCloseNativeReadInterruption(record) {
5
+ if (!record || record.snapshot.phase !== 'interrupted' || record.snapshot.reason !== 'effect_outcome_unknown'
6
+ || record.snapshot.pendingIntent || record.snapshot.toolReconciliation
7
+ || record.snapshot.children?.length || record.snapshot.backgroundCommands?.length
8
+ || record.snapshot.childBudgetReservations?.some(row => row.settledUsageDigest === null)) return false;
9
+ const unknown = record.receipts.filter(receipt => receipt.status === 'uncertain'
10
+ || receipt.result.results?.some(result => result.status === 'uncertain'));
11
+ return unknown.length > 0 && unknown.every(receipt => {
12
+ const intent = record.intents.find(item => item.operationKey === receipt.operationKey);
13
+ return intent?.kind === 'tool' && safeReadRecoveryCalls(intent.payload.calls, record.configuration);
14
+ }) && !record.intents.some(intent => ['tool', 'hook', 'child', 'input'].includes(intent.kind)
15
+ && intent.status === 'running' && !record.receipts.some(receipt => receipt.operationKey === intent.operationKey));
16
+ }