@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.
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,434 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { nativeDurableStream } from './assistant-stream.mjs';
3
+ import { canonical, invariant, terminal, RESULT_DELIVERY_POLICY, FINAL_SUBMISSION_TOOL } from '@kingsoftcloud/agentkit-harness-core';
4
+ import { NATIVE_DSH_CONTENT_FORMAT } from './dsh-turn-contract.mjs';
5
+ import { readNativeDshBackgroundOwnership } from './dsh-background-ownership.mjs';
6
+ import { readNativeDshManagedOwnership } from './dsh-managed-ownership.mjs';
7
+ import { nativeDeliveryId, nativeDeliveryTimeline } from './delivery-timeline.mjs';
8
+ import { projectNativeSessionDelivery } from './session-delivery-journal.mjs';
9
+ import { nativeContextMarkerFacts } from './context-markers.mjs';
10
+
11
+ export const NATIVE_DELIVERY_PROJECTION = 'agentkitNativeDelivery';
12
+ export const NATIVE_CONTEXT_PRESENTATION_EVENT = 'agentkit-native/context-marker';
13
+ export const NATIVE_CONTEXT_PRESENTATION_CONTRACT = 'agentkit.native-context-presentation/v1';
14
+ export const NATIVE_TOOL_OUTCOME_CONTRACT = 'agentkit.native-tool-outcome/v1';
15
+ export const NATIVE_TOOL_OUTCOME_PRESENTATION_EVENT = 'agentkit-native/tool-outcome';
16
+
17
+ /** Presentation fact from a scoped committed journal, never model prose. */
18
+ export function nativeToolOutcomeFact(identity, callId, status, presentationTurn = Number(identity.turnId)) {
19
+ // Native identities may use opaque turn IDs. The external owner separately
20
+ // validates the SDK's numeric presentation position; never rewrite a Run's
21
+ // execution identity to manufacture that position.
22
+ const turn = presentationTurn;
23
+ invariant(Number.isSafeInteger(turn) && turn > 0 && typeof identity.sessionId === 'string' && identity.sessionId
24
+ && typeof identity.runId === 'string' && identity.runId && typeof callId === 'string' && callId
25
+ && ['ok', 'error', 'uncertain', 'not_dispatched'].includes(status), 'native_tool_outcome_identity_invalid');
26
+ return { contract: NATIVE_TOOL_OUTCOME_CONTRACT, sessionId: identity.sessionId, runId: identity.runId, turn,
27
+ callId: nativeDeliveryId(identity.scope, identity.runId, `call:${callId}`), status };
28
+ }
29
+ const hash = value => createHash('sha256').update(canonical(value)).digest('hex');
30
+ const ownObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
31
+
32
+ /** Public display deltas only. Opaque replay/signatures and tool previews are never content. */
33
+ function displayText(stream, type) {
34
+ return stream.filter(record => record.chunk?.type === type && typeof record.chunk.text === 'string')
35
+ .map(record => record.chunk.text).join('');
36
+ }
37
+
38
+ /** Native totals include cache input; DSH's input/cache buckets are disjoint. */
39
+ function sessionUsage(usage) {
40
+ const read = usage.cachedInputTokens ?? 0, write = usage.cacheWriteInputTokens ?? 0;
41
+ const splitKnown = Number.isSafeInteger(read) && read >= 0 && Number.isSafeInteger(write) && write >= 0
42
+ && read <= usage.inputTokens && write <= usage.inputTokens - read;
43
+ const total = usage.inputTokens + usage.outputTokens;
44
+ return {
45
+ inputTokens: splitKnown ? usage.inputTokens - read - write : usage.inputTokens,
46
+ outputTokens: usage.outputTokens,
47
+ ...(Number.isSafeInteger(total) ? { totalTokens: total } : {}),
48
+ ...(splitKnown && usage.cachedInputTokens !== undefined ? { cacheReadTokens: read } : {}),
49
+ ...(splitKnown && usage.cacheWriteInputTokens !== undefined ? { cacheWriteTokens: write } : {}),
50
+ ...(usage.reasoningTokens === undefined ? {} : { reasoningTokens: usage.reasoningTokens }),
51
+ };
52
+ }
53
+
54
+ function atomKey(event) {
55
+ if (event.type === NATIVE_TOOL_OUTCOME_PRESENTATION_EVENT) {
56
+ const fact = event.data;
57
+ invariant(event.ignorable === true && fact?.contract === NATIVE_TOOL_OUTCOME_CONTRACT
58
+ && fact.status === 'not_dispatched' && [fact.runId, fact.sessionId].every(value => typeof value === 'string' && value.trim())
59
+ && Number.isSafeInteger(fact.turn) && fact.turn > 0 && /^ak-native-[a-f0-9]{64}$/.test(fact.callId ?? ''),
60
+ 'native_delivery_tool_outcome_invalid');
61
+ return `tool-outcome:${fact.runId}:${fact.callId}:${fact.status}`;
62
+ }
63
+ if (event.type === NATIVE_CONTEXT_PRESENTATION_EVENT) {
64
+ const { contract, runId, sessionId, turn, marker } = event.data ?? {};
65
+ invariant(event.ignorable === true && contract === NATIVE_CONTEXT_PRESENTATION_CONTRACT
66
+ && [runId, sessionId].every(value => typeof value === 'string' && value.trim() && value.length <= 256)
67
+ && Number.isSafeInteger(turn) && turn > 0 && /^ak-native-[a-f0-9]{64}$/.test(marker?.id ?? '')
68
+ && ['completed', 'failed', 'interrupted'].includes(marker.status) && marker.origin === 'auto'
69
+ && ['start', 'tool', 'message'].includes(marker.after?.kind)
70
+ && (marker.after.kind === 'start' || /^ak-native-[a-f0-9]{64}$/.test(marker.after.id ?? '')),
71
+ 'native_delivery_context_marker_invalid');
72
+ return `context:${runId}:${marker.id}:${marker.status}`;
73
+ }
74
+ if (event.type === 'plan/mode' && /^ak-native-[a-f0-9]{64}$/.test(event.data.nativeId ?? '')) return `plan:${event.data.nativeId}`;
75
+ if (event.type === 'todo/write' && /^ak-native-[a-f0-9]{64}$/.test(event.data.nativeId ?? '')) return `todo:${event.data.nativeId}`;
76
+ if (['system/message', 'user/message', 'assistant/message', 'tool/result'].includes(event.type)) {
77
+ const message = event.type === 'user/message' ? event.data : event.data.message;
78
+ return `message:${message.id}`;
79
+ }
80
+ if (event.type === 'tool/call') return `call:${event.data.callId}`;
81
+ // Standard DSH may produce several failed attempts in the same step. Their
82
+ // streams must coexist in this host-wide fold without claiming a Native ID.
83
+ if (event.type === 'assistant/attempt') return `attempt:${hash(event.data)}`;
84
+ if (event.type === 'step/start' || event.type === 'step/end') return `${event.type}:${event.data.turn}:${event.data.step}`;
85
+ return null;
86
+ }
87
+ function atomDigest(event) {
88
+ return hash({ type: event.type, data: event.data,
89
+ ...(event.ignorable ? { ignorable: true } : {}),
90
+ ...(event.surfaceOp ? { surfaceOp: event.surfaceOp } : {}),
91
+ ...(event.sourceEventSeqs ? { sourceEventSeqs: event.sourceEventSeqs } : {}),
92
+ });
93
+ }
94
+
95
+ /** Host-only fold of actual Session events; it grants no Native execution authority. */
96
+ export function nativeDeliveryProjection() {
97
+ return {
98
+ key: NATIVE_DELIVERY_PROJECTION, stateVersion: 3,
99
+ stateSchema: { parse(value) {
100
+ invariant(ownObject(value) && ownObject(value.atoms) && ownObject(value.seqs) && Number.isSafeInteger(value.turn) && value.turn >= 0
101
+ && typeof value.openTurn === 'boolean' && typeof value.hasSurface === 'boolean' && Object.values(value.atoms).every(item => /^[a-f0-9]{64}$/.test(item)), 'native_delivery_state_invalid');
102
+ invariant(Object.keys(value.atoms).length === Object.keys(value.seqs).length
103
+ && Object.keys(value.atoms).every(key => Number.isSafeInteger(value.seqs[key]) && value.seqs[key] > 0), 'native_delivery_state_invalid');
104
+ if (value.head !== null) invariant(ownObject(value.head) && Number.isSafeInteger(value.head.seq)
105
+ && typeof value.head.id === 'string' && Array.isArray(value.head.content), 'native_delivery_state_invalid');
106
+ canonical(value);
107
+ return value;
108
+ } },
109
+ init: () => ({ atoms: {}, seqs: {}, turn: 0, openTurn: false, hasSurface: false, head: null }),
110
+ apply(state, event) {
111
+ if (event.type === 'turn/start') return { ...state, turn: event.data.turn, openTurn: true };
112
+ if (event.type === 'turn/end') return { ...state, openTurn: false };
113
+ const key = atomKey(event);
114
+ if (key === null) return state;
115
+ const fingerprint = atomDigest(event);
116
+ invariant(!Object.hasOwn(state.atoms, key) || state.atoms[key] === fingerprint, 'native_delivery_atom_conflict');
117
+ const head = event.type === 'system/message' && event.surfaceOp
118
+ ? { id: event.data.message.id, seq: event.seq, content: event.data.message.content } : state.head;
119
+ return { ...state, atoms: { ...state.atoms, [key]: fingerprint }, seqs: { ...state.seqs, [key]: event.seq }, head,
120
+ hasSurface: state.hasSurface || Boolean(event.surfaceOp) };
121
+ },
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Deliver committed Native transcript and usage into a live DSH turn. Register
127
+ * nativeDeliveryProjection first. `flush` must call the Host's sessions.flush.
128
+ * The Native cursor is acknowledged only after a durable flush. Replaying the
129
+ * complete plan deduplicates an append that succeeded before acknowledgment.
130
+ * Without an explicit external ownership contract, Native turn ID must equal
131
+ * the numeric DSH position. Managed ownership pins the sealed Run and input;
132
+ * background ownership pins its admission/notice with empty inputMessages.
133
+ */
134
+ export function createNativeSessionDelivery({ store, scope, runId, session, turn, projections,
135
+ flush, systemText, inputMessages, source, onStreamSettled, isStreamPending, flushTimeoutMs = 30_000,
136
+ backgroundOwnership = null, managedOwnership = null } = {}) {
137
+ invariant(typeof flush === 'function' && typeof systemText === 'string' && Array.isArray(inputMessages)
138
+ && Number.isSafeInteger(turn) && turn > 0 && typeof source?.provider === 'string' && source.provider
139
+ && typeof source?.model === 'string' && source.model, 'native_delivery_binding_invalid');
140
+ invariant(Number.isFinite(flushTimeoutMs) && flushTimeoutMs > 0, 'native_delivery_flush_timeout_invalid');
141
+ const input = structuredClone(inputMessages);
142
+ canonical(input);
143
+ invariant(input.every(message => message.role === 'user' && typeof message.id === 'string'), 'native_delivery_input_invalid');
144
+ // Explicit external-owner contracts separate Native identity from the SDK's
145
+ // numeric delivery position. Ordinary turns retain their original contract.
146
+ invariant(backgroundOwnership === null || managedOwnership === null, 'native_delivery_binding_invalid');
147
+ const background = backgroundOwnership === null ? null
148
+ : readNativeDshBackgroundOwnership(backgroundOwnership, { sessionId: String(session.id), turn });
149
+ const managed = managedOwnership === null ? null
150
+ : readNativeDshManagedOwnership(managedOwnership, { sessionId: String(session.id), turn });
151
+ if (managed) invariant(managed.identity.runId === runId && canonical(managed.identity.scope) === canonical(scope),
152
+ 'native_delivery_managed_admission_invalid');
153
+ if (background) invariant(input.length === 0 && background.identity.runId === runId
154
+ && canonical(background.identity.scope) === canonical(scope) && background.configuration.instructions === systemText,
155
+ 'native_delivery_background_admission_invalid');
156
+ source = { provider: source.provider, model: source.model };
157
+ const id = key => nativeDeliveryId(scope, runId, key);
158
+ const projectedCallId = callId => id(`call:${callId}`);
159
+ const projectionPrefix = `dsh:${session.id}:${turn}:v1:`;
160
+ const projectionName = `${projectionPrefix}${hash({ systemText, input, source, ...(background ? { background } : {}),
161
+ ...(managed ? { managed } : {}) })}`;
162
+ let chain = Promise.resolve();
163
+ const state = () => {
164
+ const value = projections.stateOf(session, NATIVE_DELIVERY_PROJECTION);
165
+ invariant(value !== undefined, 'native_delivery_projection_missing');
166
+ invariant(value.turn === turn, 'native_delivery_turn_mismatch');
167
+ invariant(value.openTurn, 'native_delivery_turn_closed');
168
+ return value;
169
+ };
170
+ const append = (type, data, options = {}) => {
171
+ const event = { type, data, ...options }, key = atomKey(event);
172
+ invariant(key !== null, 'native_delivery_event_invalid');
173
+ const fingerprint = atomDigest(event), previous = state().atoms[key];
174
+ if (previous !== undefined) { invariant(previous === fingerprint, 'native_delivery_atom_conflict'); return state().seqs[key]; }
175
+ session.append(type, data, options);
176
+ invariant(state().atoms[key] === fingerprint, 'native_delivery_projection_not_advanced');
177
+ return state().seqs[key];
178
+ };
179
+
180
+ async function flushDurably() {
181
+ let timer;
182
+ try {
183
+ const persisted = await Promise.race([Promise.resolve().then(() => flush(session)), new Promise((_, reject) => {
184
+ timer = setTimeout(() => reject(Object.assign(new Error('native_delivery_flush_timeout'), { code: 'native_delivery_flush_timeout' })), flushTimeoutMs);
185
+ })]);
186
+ invariant(persisted === true, 'native_delivery_not_durable');
187
+ } finally { clearTimeout(timer); }
188
+ }
189
+ async function settleStream(operationKey, eventType, seq) {
190
+ if (!operationKey || !onStreamSettled || isStreamPending && !isStreamPending(operationKey)) return;
191
+ // The SDK's committed end frame names a DURABLE Session event. Keep later
192
+ // tool events behind this barrier so a follower can first release its
193
+ // staged assistant message. A failed flush leaves the live attempt open
194
+ // for abandonment; Native execution evidence is already committed.
195
+ await flushDurably();
196
+ await onStreamSettled(operationKey, eventType, seq);
197
+ }
198
+
199
+ async function deliver(snapshot) {
200
+ const record = typeof store.sessionDeliveryState === 'function'
201
+ ? store.sessionDeliveryState(scope, runId) : projectNativeSessionDelivery(store.load(scope, runId));
202
+ invariant(record.snapshot.revision === snapshot.revision && record.snapshot.eventSeq === snapshot.eventSeq,
203
+ 'native_delivery_checkpoint_stale');
204
+ invariant(canonical(snapshot.scope) === canonical(scope) && snapshot.runId === runId
205
+ && snapshot.sessionId === String(session.id) && snapshot.turnId === (background?.identity.turnId ?? managed?.identity.turnId ?? String(turn)),
206
+ 'native_delivery_identity_mismatch');
207
+ if (managed) {
208
+ const capsule = record.configuration.hostRecoveryBinding;
209
+ invariant(snapshot.configDigest === managed.configDigest && hash(record.configuration) === managed.configDigest
210
+ && Object.entries(managed.identity).every(([key, value]) => canonical(snapshot[key]) === canonical(value))
211
+ && capsule && canonical(capsule.messages) === canonical(input) && canonical(capsule.source) === canonical(source)
212
+ && capsule.systemPrompt === systemText && record.configuration.instructions === systemText,
213
+ 'native_delivery_managed_admission_invalid');
214
+ }
215
+ if (background) invariant(record.snapshot.configDigest === background.configDigest
216
+ && canonical(record.configuration) === canonical(background.configuration)
217
+ && canonical(record.snapshot.backgroundNotification ?? null) === canonical(background.notification)
218
+ && ['buildId', 'parentRunId', 'taskChainId'].every(key => snapshot[key] === background.identity[key]),
219
+ 'native_delivery_background_admission_invalid');
220
+ const head = store.sessionHeadView ? store.sessionHeadView(scope, snapshot.sessionId) : store.sessionHead(scope, snapshot.sessionId);
221
+ invariant(head?.runId === runId, 'native_delivery_run_superseded');
222
+ invariant(Object.keys(record.projections).filter(key => key.startsWith(projectionPrefix))
223
+ .every(key => key === projectionName), 'native_delivery_admission_changed');
224
+ // Pin the consumer identity before any Session append; a retry must use
225
+ // the same admitted input even if no Native event has been delivered yet.
226
+ state();
227
+ if (!Object.hasOwn(record.projections, projectionName)) store.acknowledge(scope, runId, projectionName, 0);
228
+ append('step/start', { turn, step: 1 });
229
+ const desired = systemText ? [{ type: 'text', text: systemText }] : [];
230
+ const current = state();
231
+ if (!current.head || canonical(current.head.content) !== canonical(desired)) {
232
+ invariant(current.head || !current.hasSurface, 'native_delivery_system_head_missing');
233
+ append('system/message', { turn, step: 1, message: { id: id('system'), role: 'system', content: desired,
234
+ source: { kind: 'system-prompt' } } }, current.head
235
+ ? { surfaceOp: 'replace', sourceEventSeqs: [current.head.seq] } : { surfaceOp: 'append' });
236
+ }
237
+ for (const message of input) {
238
+ append('user/message', message, { surfaceOp: 'append' });
239
+ }
240
+ if (background) {
241
+ const notices = snapshot.messages.filter(message => message.role === 'user'
242
+ && message.kind === 'background_command_observation' && !message.history);
243
+ invariant(notices.length === 1, 'native_delivery_background_notice_missing');
244
+ append('user/message', { id: id('background-notification'), role: 'user',
245
+ source: { kind: 'agentkit-native-background-notification', form: 'notice', summary: 'Background command result',
246
+ sourceRunId: background.notification.sourceRunId, taskId: background.notification.taskId },
247
+ content: [{ type: 'text', text: notices[0].content }] }, { surfaceOp: 'append' });
248
+ }
249
+
250
+ const events = record.events.filter(event => event.seq <= snapshot.eventSeq);
251
+ const inputBoundaries = new Map(record.inputBoundaries);
252
+ // Output retirement preserves immutable model events with their ORIGINAL
253
+ // transcript positions. Reconstruct that timeline, then replay each exact
254
+ // retirement at its original point. Never index old events into new text.
255
+ const { messages, retirements } = nativeDeliveryTimeline(record);
256
+ // Admission can contain multiple user/context messages and attachments.
257
+ // They have already been published above, with their original host source
258
+ // and content blocks. Never publish a flattened second copy from Native.
259
+ const admittedUsers = new Set(messages.flatMap((message, index) => message.role === 'user' && !message.history && message.kind !== 'session_injection' ? [index] : [])
260
+ .slice(0, record.configuration.inputMessages?.length ?? 1));
261
+ const completed = new Map(record.toolOutcomes), displays = new Map(record.displays), closures = new Map(record.closures);
262
+ const privateResultCalls = new Set(record.configuration.deliveryScopePolicy === RESULT_DELIVERY_POLICY
263
+ ? messages.flatMap(message => (message.toolCalls ?? []).filter(call => call.name === FINAL_SUBMISSION_TOOL).map(call => call.callId)) : []);
264
+ const pending = new Map();
265
+ let cursor = 0, step = 1, modelCount = 0, lastAssistant = null;
266
+ const visibleReasoning = record.configuration.profile.providerThinkingDisplayPolicy === 'provider-visible/v1';
267
+ const assistant = async (key, text, calls, usage, stream = [], interrupted = false, operationKey) => {
268
+ calls = calls.filter(call => !privateResultCalls.has(call.callId));
269
+ const reasoning = visibleReasoning ? displayText(stream, 'reasoning-delta') : '';
270
+ const content = [...(reasoning ? [{ type: 'reasoning', text: reasoning }] : []), ...(text ? [{ type: 'text', text }] : []),
271
+ ...calls.map(call => ({ type: 'tool-call', id: projectedCallId(call.callId), name: call.name, arguments: canonical(call.arguments) }))];
272
+ const seq = append('assistant/message', { turn, step, message: { id: id(key), role: 'assistant', content, source: { kind: 'model', ...source } },
273
+ stream, ...(interrupted ? { interrupted: true } : {}), ...(usage ? { usage } : {}) }, { surfaceOp: 'append' });
274
+ await settleStream(operationKey, 'assistant/message', seq);
275
+ lastAssistant = interrupted ? null : { text, calls };
276
+ for (const call of calls) {
277
+ pending.set(call.callId, call);
278
+ append('tool/call', { turn, step, callId: projectedCallId(call.callId), name: call.name, arguments: canonical(call.arguments) });
279
+ }
280
+ return seq;
281
+ };
282
+ const blocks = message => message.presentation?.format === NATIVE_DSH_CONTENT_FORMAT
283
+ ? structuredClone(message.presentation.content) : [{ type: 'text', text: message.content }];
284
+ const result = (key, callId, message, isError) => {
285
+ const projected = projectedCallId(callId);
286
+ const outcome = completed.get(callId);
287
+ // A separate additive fact preserves the exact old tool/result atom on
288
+ // replay. Publish it first for new results so the Native follower never
289
+ // paints an undispatched request as a failed execution.
290
+ if (outcome === 'not_dispatched') append(NATIVE_TOOL_OUTCOME_PRESENTATION_EVENT,
291
+ nativeToolOutcomeFact(snapshot, callId, outcome, turn), { ignorable: true });
292
+ const nativeOutcome = outcome === 'uncertain' ? nativeToolOutcomeFact(snapshot, callId, outcome, turn) : null;
293
+ const display = !isError && displays.has(callId) ? structuredClone(displays.get(callId)) : null;
294
+ append('tool/result', { turn, step, message: { id: id(key), role: 'tool', source: { kind: 'tool', callId: projected },
295
+ toolCallId: projected, content: blocks(message), isError },
296
+ ...(display || nativeOutcome ? { meta: { ...display, ...(nativeOutcome ? { nativeOutcome } : {}) } } : {}) }, { surfaceOp: 'append' });
297
+ pending.delete(callId);
298
+ };
299
+ const drain = through => {
300
+ invariant(Number.isSafeInteger(through) && through >= cursor && through <= messages.length, 'native_delivery_transcript_cursor_invalid');
301
+ while (cursor < through) {
302
+ const index = cursor++, message = messages[index];
303
+ if (message.history || message.role === 'system' || admittedUsers.has(index)) continue;
304
+ if (message.role === 'user') {
305
+ // Human steering and Host-admitted tool context keep distinct sources.
306
+ // Model, verification and Hook feedback remain private execution context.
307
+ if (['tool_context', 'host_input'].includes(message.kind) && message.presentation?.format === NATIVE_DSH_CONTENT_FORMAT) {
308
+ const original = message.presentation.source;
309
+ const contextSource = message.kind === 'tool_context' && original?.kind === 'tool' ? { ...original, callId: projectedCallId(original.callId) }
310
+ : original || (message.kind === 'host_input' ? { kind: 'user' }
311
+ : { kind: 'agentkit-native-tool-context', form: 'notice', summary: 'Tool-provided context' });
312
+ append('user/message', { id: id(`transcript:${index}`), role: 'user', source: contextSource,
313
+ content: blocks(message) }, { surfaceOp: 'append' });
314
+ } else if (['guidance', 'steering', 'host_input'].includes(message.kind)) append('user/message', {
315
+ id: id(`transcript:${index}`), role: 'user', source: { kind: 'user' },
316
+ content: [{ type: 'text', text: message.content }] }, { surfaceOp: 'append' });
317
+ }
318
+ else if (message.role === 'tool') {
319
+ if (privateResultCalls.has(message.callId)) continue;
320
+ let status = completed.get(message.callId);
321
+ if (status === undefined) {
322
+ try { status = JSON.parse(message.content)?.status; } catch { status = undefined; }
323
+ }
324
+ result(`transcript:${index}`, message.callId, message, ['error', 'uncertain', 'not_dispatched'].includes(status));
325
+ } else if (message.role !== 'assistant' || message.kind !== 'candidate_draft') invariant(false, 'native_delivery_assistant_binding_missing');
326
+ }
327
+ };
328
+ const contextFacts = nativeContextMarkerFacts(record, { driving: !terminal(snapshot.phase) });
329
+ for (const event of events) {
330
+ // The adoption/terminal event fixes the durable position across warm
331
+ // checkpoints, append-before-flush retries and cold replay.
332
+ for (const { marker, settledSeq } of contextFacts) if (settledSeq === event.seq) {
333
+ append(NATIVE_CONTEXT_PRESENTATION_EVENT, { contract: NATIVE_CONTEXT_PRESENTATION_CONTRACT,
334
+ runId, sessionId: String(session.id), turn, marker }, { ignorable: true });
335
+ }
336
+ if (['plan.initialized', 'plan.mode_changed'].includes(event.kind)) {
337
+ drain(event.payload.messageCursor);
338
+ append('plan/mode', { active: event.payload.active, nativeId: id(`plan:${event.eventId}`) });
339
+ continue;
340
+ }
341
+ if (['todo.initialized', 'todo.updated'].includes(event.kind)) {
342
+ drain(event.payload.messageCursor);
343
+ append('todo/write', { todos: structuredClone(event.payload.todos), nativeId: id(`todo:${event.eventId}`) });
344
+ continue;
345
+ }
346
+ if (event.kind === 'effect.prepared' && event.payload.kind === 'input'
347
+ && inputBoundaries.has(event.payload.operationKey) && modelCount > 0) {
348
+ // Host pre-step observers consume durable file touches at step/end.
349
+ // Publish that boundary before preparing the next input, including on
350
+ // cold replay; the intent cursor fixes its order before new context.
351
+ drain(inputBoundaries.get(event.payload.operationKey));
352
+ invariant(pending.size === 0, 'native_delivery_input_before_tool_settlement');
353
+ append('step/end', { turn, step });
354
+ continue;
355
+ }
356
+ if (event.kind === 'output.restarted') {
357
+ const { retained, retired, notice } = retirements.get(event.seq);
358
+ invariant(cursor === retained + retired.length, 'native_delivery_output_restart_cursor_invalid');
359
+ messages.splice(retained, retired.length, notice); cursor = retained;
360
+ continue;
361
+ }
362
+ if (!['model.completed', 'context.summary_completed', 'completion.delivery_review_completed', 'model.stream_interrupted'].includes(event.kind)) continue;
363
+ const payload = event.payload, index = payload.messageIndex;
364
+ const summary = event.kind === 'context.summary_completed' || event.kind === 'completion.delivery_review_completed' && index === null;
365
+ const interrupted = event.kind === 'model.stream_interrupted';
366
+ if (interrupted) {
367
+ drain(payload.messageCursor);
368
+ if (modelCount++ > 0) { append('step/end', { turn, step }); step++; append('step/start', { turn, step }); }
369
+ const stream = nativeDurableStream(payload.operationKey, payload.displayStream, record.configuration);
370
+ const readable = visibleReasoning && (displayText(stream, 'text-delta') || displayText(stream, 'reasoning-delta'));
371
+ if (readable) await assistant(`interrupted:${event.eventId}`, displayText(stream, 'text-delta'), [], null, stream, true, payload.operationKey);
372
+ else {
373
+ const seq = append('assistant/attempt', { turn, step, stream });
374
+ await settleStream(payload.operationKey, 'assistant/attempt', seq);
375
+ }
376
+ continue;
377
+ }
378
+ invariant(summary || index === null || Number.isSafeInteger(index) && index >= cursor && messages[index]?.role === 'assistant',
379
+ 'native_delivery_model_binding_missing');
380
+ drain(summary || index === null ? payload.messageCursor : index);
381
+ if (modelCount++ > 0) { append('step/end', { turn, step }); step++; append('step/start', { turn, step }); }
382
+ const message = summary || index === null ? null : messages[index];
383
+ const usage = payload.usageReported === false ? null : sessionUsage(payload.usage);
384
+ const stream = nativeDurableStream(payload.operationKey, payload.displayStream, record.configuration);
385
+ // An uncommitted provider prefix is an attempt, never a tool batch or
386
+ // final candidate. New display admissions retain readable public content;
387
+ // legacy admissions keep their attempt and empty usage message unchanged.
388
+ if (!summary && index === null && stream.length) {
389
+ if (visibleReasoning && (displayText(stream, 'text-delta') || displayText(stream, 'reasoning-delta'))) {
390
+ await assistant(`response:${event.eventId}`, displayText(stream, 'text-delta'), [], usage, stream, true, payload.operationKey);
391
+ } else {
392
+ const seq = append('assistant/attempt', { turn, step, stream });
393
+ await settleStream(payload.operationKey, 'assistant/attempt', seq);
394
+ await assistant(`response:${event.eventId}`, '', [], usage);
395
+ }
396
+ } else {
397
+ await assistant(`response:${event.eventId}`, summary || payload.candidateStatus === 'rejected' ? '' : message?.content ?? payload.text,
398
+ message?.toolCalls ?? [], usage, stream, false, payload.operationKey);
399
+ }
400
+ if (!summary && index !== null) cursor = index + 1;
401
+ drain(payload.messageCursor);
402
+ }
403
+ drain(messages.length);
404
+ if (terminal(snapshot.phase)) {
405
+ for (const [callId] of pending) {
406
+ const closure = closures.get(callId);
407
+ invariant(closure, 'native_delivery_closure_missing');
408
+ const { context, isError } = closure;
409
+ result(`closure:${callId}`, callId, context, isError);
410
+ }
411
+ if (snapshot.finalText && (lastAssistant?.text !== snapshot.finalText || lastAssistant.calls.length)) {
412
+ await assistant('final-delivery', snapshot.finalText, [], null);
413
+ }
414
+ append('step/end', { turn, step });
415
+ }
416
+ await flushDurably();
417
+ state();
418
+ store.acknowledge(scope, runId, projectionName, snapshot.eventSeq);
419
+ }
420
+ const delivery = snapshot => {
421
+ const next = chain.then(() => deliver(snapshot));
422
+ chain = next.catch(() => {});
423
+ return next;
424
+ };
425
+ delivery.beginStream = () => {
426
+ const events = typeof store.sessionDeliveryState === 'function'
427
+ ? store.sessionDeliveryState(scope, runId).events : store.events(scope, runId);
428
+ const step = 1 + events.filter(event => ['model.completed', 'context.summary_completed', 'completion.delivery_review_completed', 'model.stream_interrupted'].includes(event.kind)).length;
429
+ if (step > 1) append('step/end', { turn, step: step - 1 });
430
+ append('step/start', { turn, step });
431
+ return step;
432
+ };
433
+ return delivery;
434
+ }
@@ -0,0 +1,50 @@
1
+ import { parseBashSyntax } from '../../dist/native-command-plan.mjs';
2
+
3
+ /** Syntax facts only: no expansion, evaluation, filesystem reads or permission
4
+ * decision. Called inside the bounded policy worker. Comments and quoted
5
+ * heredoc bodies are data; executable substitutions have their own AST nodes.
6
+ * Plan mode uses this same parser through the bundled ZCode classifier.
7
+ */
8
+ export function analyzeShellSource(source) {
9
+ const script = parseBashSyntax(source);
10
+ const commands = [], backgrounds = [], scripts = [], lineStarts = [0];
11
+ for (let i = 0; i < source.length; i++) if (source[i] === '\n') lineStarts.push(i + 1);
12
+ const lineAt = position => {
13
+ let lo = 0, hi = lineStarts.length;
14
+ while (lo + 1 < hi) { const mid = (lo + hi) >>> 1; if (lineStarts[mid] <= position) lo = mid; else hi = mid; }
15
+ return lo + 1;
16
+ };
17
+ const pending = [{ node: script, depth: 0 }];
18
+ let hasParseErrors = false, hasBashCombinedRedirects = false;
19
+ while (pending.length) {
20
+ const { node, depth } = pending.pop();
21
+ if (!node || typeof node !== 'object') continue;
22
+ if (depth > 128) throw Error('command_policy_nesting_limit');
23
+ if (node.type === 'Script') scripts.push(node);
24
+ if (['&>', '&>>'].includes(node.operator)) hasBashCombinedRedirects = true;
25
+ if (['AndOr', 'Pipeline'].includes(node.type) && node.operators.length !== node.commands.length - 1) hasParseErrors = true;
26
+ if ((node.type === 'Statement' && node.background) || node.type === 'Coproc') backgrounds.push(lineAt(node.pos));
27
+ if (node.type === 'Command' && node.name) {
28
+ const input = node.redirects.filter(redirect => (redirect.fileDescriptor ?? 0) === 0
29
+ && ['<', '<&', '<<', '<<-', '<<<'].includes(redirect.operator)).at(-1);
30
+ commands.push({ argv: [node.name, ...node.suffix].map(word => word.value), line: lineAt(node.pos),
31
+ stdin: input && ['<<', '<<-', '<<<'].includes(input.operator)
32
+ ? input.operator === '<<<' ? input.target.value : input.content : null });
33
+ }
34
+ // Traverse structural children, including assignments, redirects, compound
35
+ // bodies and command/process/arithmetic substitutions. Never parse raw text
36
+ // payloads a second time: the parser already distinguishes them from code.
37
+ for (const value of Object.values(node).reverse()) {
38
+ if (Array.isArray(value)) {
39
+ for (let i = value.length - 1; i >= 0; i--) pending.push({ node: value[i], depth: depth + 1 });
40
+ }
41
+ else if (value && typeof value === 'object') pending.push({ node: value, depth: depth + 1 });
42
+ }
43
+ // unbash resolves Word.parts lazily through a non-enumerable getter.
44
+ // Materialize it explicitly, including words outside Command.argv.
45
+ if (!Object.hasOwn(node, 'parts') && Array.isArray(node.parts))
46
+ pending.push({ node: node.parts, depth: depth + 1 });
47
+ }
48
+ if (scripts.some(script => script.errors?.length)) hasParseErrors = true;
49
+ return { commands, backgrounds, hasParseErrors, hasBashCombinedRedirects };
50
+ }