@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,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
|
+
}
|