@dungle-scrubs/harness-cli-normalizer 0.1.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.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/execution/channel.d.ts +33 -0
- package/dist/execution/channel.d.ts.map +1 -0
- package/dist/execution/channel.js +86 -0
- package/dist/execution/channel.js.map +1 -0
- package/dist/execution/decode.d.ts +16 -0
- package/dist/execution/decode.d.ts.map +1 -0
- package/dist/execution/decode.js +60 -0
- package/dist/execution/decode.js.map +1 -0
- package/dist/execution/deps.d.ts +53 -0
- package/dist/execution/deps.d.ts.map +1 -0
- package/dist/execution/deps.js +8 -0
- package/dist/execution/deps.js.map +1 -0
- package/dist/execution/events.d.ts +54 -0
- package/dist/execution/events.d.ts.map +1 -0
- package/dist/execution/events.js +2 -0
- package/dist/execution/events.js.map +1 -0
- package/dist/execution/index.d.ts +19 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +19 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/lines.d.ts +21 -0
- package/dist/execution/lines.d.ts.map +1 -0
- package/dist/execution/lines.js +70 -0
- package/dist/execution/lines.js.map +1 -0
- package/dist/execution/node-deps.d.ts +13 -0
- package/dist/execution/node-deps.d.ts.map +1 -0
- package/dist/execution/node-deps.js +164 -0
- package/dist/execution/node-deps.js.map +1 -0
- package/dist/execution/open-session.d.ts +31 -0
- package/dist/execution/open-session.d.ts.map +1 -0
- package/dist/execution/open-session.js +360 -0
- package/dist/execution/open-session.js.map +1 -0
- package/dist/execution/stream-turn.d.ts +43 -0
- package/dist/execution/stream-turn.d.ts.map +1 -0
- package/dist/execution/stream-turn.js +283 -0
- package/dist/execution/stream-turn.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/interpretation/argv.d.ts +35 -0
- package/dist/interpretation/argv.d.ts.map +1 -0
- package/dist/interpretation/argv.js +147 -0
- package/dist/interpretation/argv.js.map +1 -0
- package/dist/interpretation/capabilities.d.ts +19 -0
- package/dist/interpretation/capabilities.d.ts.map +1 -0
- package/dist/interpretation/capabilities.js +25 -0
- package/dist/interpretation/capabilities.js.map +1 -0
- package/dist/interpretation/content.d.ts +33 -0
- package/dist/interpretation/content.d.ts.map +1 -0
- package/dist/interpretation/content.js +169 -0
- package/dist/interpretation/content.js.map +1 -0
- package/dist/interpretation/context.d.ts +17 -0
- package/dist/interpretation/context.d.ts.map +1 -0
- package/dist/interpretation/context.js +16 -0
- package/dist/interpretation/context.js.map +1 -0
- package/dist/interpretation/dimensions.d.ts +13 -0
- package/dist/interpretation/dimensions.d.ts.map +1 -0
- package/dist/interpretation/dimensions.js +6 -0
- package/dist/interpretation/dimensions.js.map +1 -0
- package/dist/interpretation/identity.d.ts +32 -0
- package/dist/interpretation/identity.d.ts.map +1 -0
- package/dist/interpretation/identity.js +44 -0
- package/dist/interpretation/identity.js.map +1 -0
- package/dist/interpretation/index.d.ts +23 -0
- package/dist/interpretation/index.d.ts.map +1 -0
- package/dist/interpretation/index.js +23 -0
- package/dist/interpretation/index.js.map +1 -0
- package/dist/interpretation/limits.d.ts +20 -0
- package/dist/interpretation/limits.d.ts.map +1 -0
- package/dist/interpretation/limits.js +35 -0
- package/dist/interpretation/limits.js.map +1 -0
- package/dist/interpretation/parse-resume.d.ts +28 -0
- package/dist/interpretation/parse-resume.d.ts.map +1 -0
- package/dist/interpretation/parse-resume.js +91 -0
- package/dist/interpretation/parse-resume.js.map +1 -0
- package/dist/interpretation/presence.d.ts +16 -0
- package/dist/interpretation/presence.d.ts.map +1 -0
- package/dist/interpretation/presence.js +32 -0
- package/dist/interpretation/presence.js.map +1 -0
- package/dist/interpretation/resume-last.d.ts +32 -0
- package/dist/interpretation/resume-last.d.ts.map +1 -0
- package/dist/interpretation/resume-last.js +70 -0
- package/dist/interpretation/resume-last.js.map +1 -0
- package/dist/interpretation/session-id.d.ts +22 -0
- package/dist/interpretation/session-id.d.ts.map +1 -0
- package/dist/interpretation/session-id.js +29 -0
- package/dist/interpretation/session-id.js.map +1 -0
- package/dist/interpretation/session-input.d.ts +13 -0
- package/dist/interpretation/session-input.d.ts.map +1 -0
- package/dist/interpretation/session-input.js +32 -0
- package/dist/interpretation/session-input.js.map +1 -0
- package/dist/interpretation/shape.d.ts +17 -0
- package/dist/interpretation/shape.d.ts.map +1 -0
- package/dist/interpretation/shape.js +54 -0
- package/dist/interpretation/shape.js.map +1 -0
- package/dist/interpretation/store.d.ts +16 -0
- package/dist/interpretation/store.d.ts.map +1 -0
- package/dist/interpretation/store.js +27 -0
- package/dist/interpretation/store.js.map +1 -0
- package/dist/interpretation/versions.d.ts +16 -0
- package/dist/interpretation/versions.d.ts.map +1 -0
- package/dist/interpretation/versions.js +44 -0
- package/dist/interpretation/versions.js.map +1 -0
- package/dist/interpretation/vocabulary.d.ts +29 -0
- package/dist/interpretation/vocabulary.d.ts.map +1 -0
- package/dist/interpretation/vocabulary.js +55 -0
- package/dist/interpretation/vocabulary.js.map +1 -0
- package/dist/knowledge/claude-code.d.ts +8 -0
- package/dist/knowledge/claude-code.d.ts.map +1 -0
- package/dist/knowledge/claude-code.js +132 -0
- package/dist/knowledge/claude-code.js.map +1 -0
- package/dist/knowledge/codex.d.ts +9 -0
- package/dist/knowledge/codex.d.ts.map +1 -0
- package/dist/knowledge/codex.js +106 -0
- package/dist/knowledge/codex.js.map +1 -0
- package/dist/knowledge/descriptor.d.ts +211 -0
- package/dist/knowledge/descriptor.d.ts.map +1 -0
- package/dist/knowledge/descriptor.js +20 -0
- package/dist/knowledge/descriptor.js.map +1 -0
- package/dist/knowledge/index.d.ts +19 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +19 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/matchers.d.ts +10 -0
- package/dist/knowledge/matchers.d.ts.map +1 -0
- package/dist/knowledge/matchers.js +11 -0
- package/dist/knowledge/matchers.js.map +1 -0
- package/dist/knowledge/muse.d.ts +12 -0
- package/dist/knowledge/muse.d.ts.map +1 -0
- package/dist/knowledge/muse.js +94 -0
- package/dist/knowledge/muse.js.map +1 -0
- package/dist/knowledge/overrides.d.ts +11 -0
- package/dist/knowledge/overrides.d.ts.map +1 -0
- package/dist/knowledge/overrides.js +166 -0
- package/dist/knowledge/overrides.js.map +1 -0
- package/dist/knowledge/pi.d.ts +11 -0
- package/dist/knowledge/pi.d.ts.map +1 -0
- package/dist/knowledge/pi.js +97 -0
- package/dist/knowledge/pi.js.map +1 -0
- package/package.json +69 -0
- package/src/execution/channel.ts +86 -0
- package/src/execution/decode.ts +79 -0
- package/src/execution/deps.ts +59 -0
- package/src/execution/events.ts +37 -0
- package/src/execution/index.ts +18 -0
- package/src/execution/lines.ts +71 -0
- package/src/execution/node-deps.ts +179 -0
- package/src/execution/open-session.ts +392 -0
- package/src/execution/stream-turn.ts +310 -0
- package/src/index.ts +7 -0
- package/src/interpretation/argv.ts +188 -0
- package/src/interpretation/capabilities.ts +51 -0
- package/src/interpretation/content.ts +191 -0
- package/src/interpretation/context.ts +28 -0
- package/src/interpretation/dimensions.ts +20 -0
- package/src/interpretation/identity.ts +81 -0
- package/src/interpretation/index.ts +22 -0
- package/src/interpretation/limits.ts +63 -0
- package/src/interpretation/parse-resume.ts +117 -0
- package/src/interpretation/presence.ts +49 -0
- package/src/interpretation/resume-last.ts +93 -0
- package/src/interpretation/session-id.ts +35 -0
- package/src/interpretation/session-input.ts +45 -0
- package/src/interpretation/shape.ts +55 -0
- package/src/interpretation/store.ts +43 -0
- package/src/interpretation/versions.ts +43 -0
- package/src/interpretation/vocabulary.ts +74 -0
- package/src/knowledge/claude-code.ts +132 -0
- package/src/knowledge/codex.ts +106 -0
- package/src/knowledge/descriptor.ts +217 -0
- package/src/knowledge/index.ts +23 -0
- package/src/knowledge/matchers.ts +19 -0
- package/src/knowledge/muse.ts +94 -0
- package/src/knowledge/overrides.ts +182 -0
- package/src/knowledge/pi.ts +97 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openSession: the persistent headless session runner - ONE process, many
|
|
3
|
+
* turns (A-001). `send` during idle writes a descriptor-encoded user record to
|
|
4
|
+
* stdin and starts a turn; `send` during a live turn is QUEUED to the next
|
|
5
|
+
* boundary (mid-turn stdin writes would interleave into the model's
|
|
6
|
+
* context unpredictably - the harness itself queues, so we mirror its
|
|
7
|
+
* disposition). `result` lines delimit turns; identity dedupe (D-022)
|
|
8
|
+
* spans the whole session. Lifecycle is bounded end to end: close() ends
|
|
9
|
+
* stdin, escalates SIGTERM->SIGKILL if the child ignores EOF, and pipes
|
|
10
|
+
* held open past exit close out after grace - a session can always be
|
|
11
|
+
* ended. Queued sends that die with the session are surfaced, never
|
|
12
|
+
* silently dropped. Structured lifecycle events (session open/close, turn
|
|
13
|
+
* start/end, send dispositions, drops) are always-on evidence with
|
|
14
|
+
* sessionId + turnId correlation.
|
|
15
|
+
*/
|
|
16
|
+
import { buildSessionArgv } from "../interpretation/argv.js";
|
|
17
|
+
import { detectAuthFailureInLine, detectLimitInLine } from "../interpretation/limits.js";
|
|
18
|
+
import {
|
|
19
|
+
encodeSessionInput,
|
|
20
|
+
resolveSessionInput,
|
|
21
|
+
SessionInputRefusalError,
|
|
22
|
+
} from "../interpretation/session-input.js";
|
|
23
|
+
import type { HarnessDescriptor, SessionInputContract } from "../knowledge/descriptor.js";
|
|
24
|
+
import { AsyncChannel } from "./channel.js";
|
|
25
|
+
import { decodeParsed, freshDecodeState } from "./decode.js";
|
|
26
|
+
import type { RunnerDeps, SpawnedProcess } from "./deps.js";
|
|
27
|
+
import type { ExitCause, HarnessEvent } from "./events.js";
|
|
28
|
+
import { LineBuffer } from "./lines.js";
|
|
29
|
+
import { KILL_GRACE_MS, PIPE_GRACE_MS, redactArgv, StderrTail } from "./stream-turn.js";
|
|
30
|
+
|
|
31
|
+
/** Grace after stdin EOF before concluding the child will not exit on its
|
|
32
|
+
* own and escalating signals. */
|
|
33
|
+
export const CLOSE_GRACE_MS = 5_000;
|
|
34
|
+
|
|
35
|
+
const PRETURN_MAX = 256;
|
|
36
|
+
|
|
37
|
+
export interface SessionSendResult {
|
|
38
|
+
readonly disposition: "started" | "queued";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SessionHandle {
|
|
42
|
+
/** One inner iterable per turn, each ending in a turn-scoped `done`.
|
|
43
|
+
* Breaking out of THIS iterable closes the session; breaking out of a
|
|
44
|
+
* single turn's iterable only stops reading that turn. */
|
|
45
|
+
readonly turns: AsyncIterable<AsyncIterable<HarnessEvent>>;
|
|
46
|
+
send(text: string): SessionSendResult;
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface OpenSessionOptions {
|
|
51
|
+
readonly sessionId: string;
|
|
52
|
+
readonly model?: string;
|
|
53
|
+
/** Working directory for the spawned harness. */
|
|
54
|
+
readonly cwd?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class SessionClosedError extends Error {
|
|
58
|
+
constructor() {
|
|
59
|
+
super("session is closed; sends have nowhere to go");
|
|
60
|
+
this.name = "SessionClosedError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class SessionSpawnError extends Error {
|
|
65
|
+
constructor(harness: string, cause: unknown) {
|
|
66
|
+
super(
|
|
67
|
+
`could not spawn a ${harness} session: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
68
|
+
);
|
|
69
|
+
this.name = "SessionSpawnError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const openSession = (
|
|
74
|
+
h: HarnessDescriptor,
|
|
75
|
+
opts: OpenSessionOptions,
|
|
76
|
+
deps: RunnerDeps,
|
|
77
|
+
): SessionHandle => {
|
|
78
|
+
const log = deps.log ?? (() => {});
|
|
79
|
+
const argv = buildSessionArgv(h, {
|
|
80
|
+
sessionId: opts.sessionId,
|
|
81
|
+
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
82
|
+
});
|
|
83
|
+
let sessionInput: SessionInputContract;
|
|
84
|
+
try {
|
|
85
|
+
sessionInput = resolveSessionInput(h);
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
if (!(cause instanceof SessionInputRefusalError)) throw cause;
|
|
88
|
+
log({
|
|
89
|
+
event: "session_input_refused",
|
|
90
|
+
harness: h.name,
|
|
91
|
+
issue: cause.issue,
|
|
92
|
+
sessionId: opts.sessionId,
|
|
93
|
+
});
|
|
94
|
+
throw cause;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let proc: SpawnedProcess;
|
|
98
|
+
try {
|
|
99
|
+
// A session needs a writable stdin regardless of the descriptor's
|
|
100
|
+
// one-shot stdin policy - that policy governs turns, not sessions.
|
|
101
|
+
proc = deps.spawn(argv, {
|
|
102
|
+
stdin: "pipe",
|
|
103
|
+
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
104
|
+
});
|
|
105
|
+
} catch (cause) {
|
|
106
|
+
log({ event: "session_spawn_failed", sessionId: opts.sessionId, harness: h.name });
|
|
107
|
+
throw new SessionSpawnError(h.name, cause);
|
|
108
|
+
}
|
|
109
|
+
const stdin = proc.stdin;
|
|
110
|
+
if (stdin === undefined) {
|
|
111
|
+
// The child exists but there is no way to ever send to it - end it
|
|
112
|
+
// before throwing, or it runs orphaned forever.
|
|
113
|
+
deps.signal(proc, "SIGTERM");
|
|
114
|
+
log({ event: "session_spawn_failed", sessionId: opts.sessionId, harness: h.name });
|
|
115
|
+
throw new SessionSpawnError(h.name, new Error("spawner opened no stdin pipe"));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
log({
|
|
119
|
+
event: "session_open",
|
|
120
|
+
sessionId: opts.sessionId,
|
|
121
|
+
harness: h.name,
|
|
122
|
+
argv: redactArgv(argv),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const turnsChannel = new AsyncChannel<AsyncIterable<HarnessEvent>>();
|
|
126
|
+
const state = freshDecodeState(opts.sessionId);
|
|
127
|
+
const stderrTail = new StderrTail();
|
|
128
|
+
let turnCounter = 0;
|
|
129
|
+
let activeTurn: AsyncChannel<HarnessEvent> | null = null;
|
|
130
|
+
let activeTurnId = "";
|
|
131
|
+
const pendingSends: string[] = [];
|
|
132
|
+
const preTurnEvents: HarnessEvent[] = [];
|
|
133
|
+
let dead = false;
|
|
134
|
+
let closing = false;
|
|
135
|
+
let finalized = false;
|
|
136
|
+
let exitCode: number | null = null;
|
|
137
|
+
let resultError = false;
|
|
138
|
+
let turnLimitSeen = false;
|
|
139
|
+
let pumpError: unknown = null;
|
|
140
|
+
|
|
141
|
+
const safeSignal = (sig: "SIGTERM" | "SIGKILL"): void => {
|
|
142
|
+
if (!dead) deps.signal(proc, sig);
|
|
143
|
+
};
|
|
144
|
+
const escalate = (): void => {
|
|
145
|
+
safeSignal("SIGTERM");
|
|
146
|
+
deps.clock.setTimeout(() => safeSignal("SIGKILL"), KILL_GRACE_MS);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const writeUser = (text: string): boolean => {
|
|
150
|
+
try {
|
|
151
|
+
stdin.write(encodeSessionInput(sessionInput, text));
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
activeTurn?.push({ kind: "error", message: "send failed: session stdin is gone" });
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const startTurn = (): void => {
|
|
160
|
+
turnLimitSeen = false;
|
|
161
|
+
activeTurn = new AsyncChannel<HarnessEvent>();
|
|
162
|
+
activeTurnId = `${opts.sessionId}:turn-${++turnCounter}`;
|
|
163
|
+
log({ event: "turn_start", sessionId: opts.sessionId, turnId: activeTurnId });
|
|
164
|
+
for (const held of preTurnEvents.splice(0)) activeTurn.push(held);
|
|
165
|
+
turnsChannel.push(activeTurn);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const endTurn = (done: HarnessEvent & { kind: "done" }): void => {
|
|
169
|
+
if (activeTurn === null) return;
|
|
170
|
+
activeTurn.push(done);
|
|
171
|
+
activeTurn.close();
|
|
172
|
+
log({
|
|
173
|
+
event: "turn_end",
|
|
174
|
+
sessionId: opts.sessionId,
|
|
175
|
+
turnId: activeTurnId,
|
|
176
|
+
cause: done.cause,
|
|
177
|
+
});
|
|
178
|
+
activeTurn = null;
|
|
179
|
+
resultError = false;
|
|
180
|
+
// The boundary is the only legal delivery point for queued input.
|
|
181
|
+
if (dead || closing) return;
|
|
182
|
+
const next = pendingSends.shift();
|
|
183
|
+
if (next !== undefined && writeUser(next)) startTurn();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const routeEvent = (event: HarnessEvent): Promise<void> => {
|
|
187
|
+
if (event.kind === "limit") {
|
|
188
|
+
state.limitSeen = true;
|
|
189
|
+
turnLimitSeen = true;
|
|
190
|
+
}
|
|
191
|
+
if (activeTurn !== null) {
|
|
192
|
+
// Awaited by the pumps: past the channel's high water mark this
|
|
193
|
+
// blocks the pump, so OS pipe backpressure reaches the child.
|
|
194
|
+
return activeTurn.push(event);
|
|
195
|
+
}
|
|
196
|
+
// Between-turn events wait for the next turn, bounded: past the cap
|
|
197
|
+
// droppable events go first, then oldest.
|
|
198
|
+
preTurnEvents.push(event);
|
|
199
|
+
if (preTurnEvents.length > PRETURN_MAX) {
|
|
200
|
+
const droppableAt = preTurnEvents.findIndex(
|
|
201
|
+
(e) => e.kind === "token" || e.kind === "progress" || e.kind === "context",
|
|
202
|
+
);
|
|
203
|
+
preTurnEvents.splice(droppableAt === -1 ? 0 : droppableAt, 1);
|
|
204
|
+
}
|
|
205
|
+
return Promise.resolve();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const pumpStdout = async (): Promise<void> => {
|
|
209
|
+
const lines = new LineBuffer();
|
|
210
|
+
const handleLine = async (line: string): Promise<void> => {
|
|
211
|
+
let parsed: Record<string, unknown> | null = null;
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(line) as Record<string, unknown>;
|
|
214
|
+
} catch {
|
|
215
|
+
const code = detectLimitInLine(h, line);
|
|
216
|
+
if (code !== null) {
|
|
217
|
+
await routeEvent({ kind: "limit", code, message: `limit wall detected (${code})` });
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// The result line still feeds identity dedupe (claude includes
|
|
222
|
+
// session_id on it - a rotation announced there must not be missed).
|
|
223
|
+
const events = decodeParsed(h, parsed, state, opts.model ?? "");
|
|
224
|
+
if (parsed.type === "result") {
|
|
225
|
+
// decodeParsed already surfaces the is_error case as an error event
|
|
226
|
+
// (content.ts claude reader); routing the events is enough - we only
|
|
227
|
+
// still track resultError here to classify the done cause.
|
|
228
|
+
for (const event of events) await routeEvent(event);
|
|
229
|
+
if (parsed.is_error === true) resultError = true;
|
|
230
|
+
endTurn({
|
|
231
|
+
kind: "done",
|
|
232
|
+
exitCode: null,
|
|
233
|
+
cause: turnLimitSeen ? "limit" : resultError ? "crash" : "clean",
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
for (const event of events) await routeEvent(event);
|
|
238
|
+
};
|
|
239
|
+
for await (const chunk of proc.stdout) {
|
|
240
|
+
for (const line of lines.push(chunk)) await handleLine(line);
|
|
241
|
+
}
|
|
242
|
+
const rest = lines.flush();
|
|
243
|
+
if (rest !== null) await handleLine(rest);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const pumpStderr = async (): Promise<void> => {
|
|
247
|
+
const lines = new LineBuffer();
|
|
248
|
+
for await (const chunk of proc.stderr) {
|
|
249
|
+
for (const line of lines.push(chunk)) {
|
|
250
|
+
const limit = detectLimitInLine(h, line);
|
|
251
|
+
if (limit !== null) {
|
|
252
|
+
await routeEvent({
|
|
253
|
+
kind: "limit",
|
|
254
|
+
code: limit,
|
|
255
|
+
message: `limit wall detected (${limit})`,
|
|
256
|
+
});
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const auth = detectAuthFailureInLine(h, line);
|
|
260
|
+
if (auth !== null) {
|
|
261
|
+
await routeEvent({ kind: "error", message: `auth wall: ${auth}` });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
stderrTail.push(line);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const pumping = Promise.allSettled([pumpStdout(), pumpStderr()]).then((settlements) => {
|
|
270
|
+
pumpError = settlements.find((settlement) => settlement.status === "rejected")?.reason ?? null;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
let shutdownComplete: () => void = () => {};
|
|
274
|
+
const shutdown = new Promise<void>((resolve) => {
|
|
275
|
+
shutdownComplete = resolve;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
let pipesOpenAtExit = false;
|
|
279
|
+
const finalize = (): void => {
|
|
280
|
+
if (finalized) return;
|
|
281
|
+
finalized = true;
|
|
282
|
+
const cause: ExitCause = state.limitSeen
|
|
283
|
+
? "limit"
|
|
284
|
+
: exitCode === 0
|
|
285
|
+
? "clean"
|
|
286
|
+
: exitCode === null
|
|
287
|
+
? "killed"
|
|
288
|
+
: "crash";
|
|
289
|
+
if (pumpError !== null) {
|
|
290
|
+
void routeEvent({ kind: "error", message: `session pump failed: ${String(pumpError)}` });
|
|
291
|
+
}
|
|
292
|
+
if (pendingSends.length > 0) {
|
|
293
|
+
// "queued" was an accepted disposition - the loss must be visible to
|
|
294
|
+
// both the log and the consumer, never silent.
|
|
295
|
+
void routeEvent({
|
|
296
|
+
kind: "error",
|
|
297
|
+
message: `${pendingSends.length} queued send(s) died with the session`,
|
|
298
|
+
});
|
|
299
|
+
log({
|
|
300
|
+
event: "sends_dropped",
|
|
301
|
+
sessionId: opts.sessionId,
|
|
302
|
+
count: pendingSends.length,
|
|
303
|
+
lengths: pendingSends.map((s) => s.length),
|
|
304
|
+
});
|
|
305
|
+
pendingSends.length = 0;
|
|
306
|
+
}
|
|
307
|
+
endTurn({ kind: "done", exitCode, cause });
|
|
308
|
+
if (preTurnEvents.some((e) => e.kind !== "token" && e.kind !== "progress")) {
|
|
309
|
+
log({
|
|
310
|
+
event: "preturn_events_dropped",
|
|
311
|
+
sessionId: opts.sessionId,
|
|
312
|
+
kinds: preTurnEvents.map((e) => e.kind),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
turnsChannel.close();
|
|
316
|
+
log({
|
|
317
|
+
event: "session_close",
|
|
318
|
+
sessionId: opts.sessionId,
|
|
319
|
+
exitCode,
|
|
320
|
+
cause,
|
|
321
|
+
...(pipesOpenAtExit ? { pipesOpenAtExit } : {}),
|
|
322
|
+
...(cause === "crash" || cause === "killed" ? { stderrTail: stderrTail.snapshot() } : {}),
|
|
323
|
+
});
|
|
324
|
+
shutdownComplete();
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
void proc.exited.then((code) => {
|
|
328
|
+
dead = true;
|
|
329
|
+
exitCode = code;
|
|
330
|
+
// Pipes held open past exit (a grandchild) must not hang the session.
|
|
331
|
+
const pipeGrace = deps.clock.setTimeout(() => {
|
|
332
|
+
pipesOpenAtExit = true;
|
|
333
|
+
activeTurn?.releaseBackpressure();
|
|
334
|
+
proc.disposeOutput();
|
|
335
|
+
void pumping.then(() => finalize());
|
|
336
|
+
}, PIPE_GRACE_MS);
|
|
337
|
+
void pumping.then(() => {
|
|
338
|
+
deps.clock.clearTimeout(pipeGrace);
|
|
339
|
+
finalize();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const close = async (): Promise<void> => {
|
|
344
|
+
if (closing) return shutdown;
|
|
345
|
+
closing = true;
|
|
346
|
+
try {
|
|
347
|
+
stdin.end();
|
|
348
|
+
} catch {
|
|
349
|
+
// stdin may already be gone - escalation below still bounds close.
|
|
350
|
+
}
|
|
351
|
+
// A child that does not exit on stdin EOF gets signalled after grace.
|
|
352
|
+
const closeGrace = deps.clock.setTimeout(() => escalate(), CLOSE_GRACE_MS);
|
|
353
|
+
await shutdown;
|
|
354
|
+
deps.clock.clearTimeout(closeGrace);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
// Breaking out of the turns iterable is abandonment - treat it as
|
|
359
|
+
// close() so the child is never left running undrained (the streamTurn
|
|
360
|
+
// C1 scar, adapted to a handle-shaped API).
|
|
361
|
+
turns: (async function* () {
|
|
362
|
+
try {
|
|
363
|
+
for await (const turn of turnsChannel) yield turn;
|
|
364
|
+
} finally {
|
|
365
|
+
if (!closing && !dead) void close();
|
|
366
|
+
}
|
|
367
|
+
})(),
|
|
368
|
+
send(text: string): SessionSendResult {
|
|
369
|
+
if (dead || closing) throw new SessionClosedError();
|
|
370
|
+
if (activeTurn !== null) {
|
|
371
|
+
pendingSends.push(text);
|
|
372
|
+
log({
|
|
373
|
+
event: "send",
|
|
374
|
+
sessionId: opts.sessionId,
|
|
375
|
+
turnId: activeTurnId,
|
|
376
|
+
disposition: "queued",
|
|
377
|
+
});
|
|
378
|
+
return { disposition: "queued" };
|
|
379
|
+
}
|
|
380
|
+
if (!writeUser(text)) throw new SessionClosedError();
|
|
381
|
+
startTurn();
|
|
382
|
+
log({
|
|
383
|
+
event: "send",
|
|
384
|
+
sessionId: opts.sessionId,
|
|
385
|
+
turnId: activeTurnId,
|
|
386
|
+
disposition: "started",
|
|
387
|
+
});
|
|
388
|
+
return { disposition: "started" };
|
|
389
|
+
},
|
|
390
|
+
close,
|
|
391
|
+
};
|
|
392
|
+
};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* streamTurn: the spawn-per-turn headless runner. Spawn, drain both stdio
|
|
3
|
+
* streams concurrently, decode typed HarnessEvents, watchdog stalls (only
|
|
4
|
+
* for none-granularity invocations - a structured stream proves its own
|
|
5
|
+
* liveness), classify the exit, always end with `done` (spawn failures
|
|
6
|
+
* included). Runtime primitives are injected (D-005); structured boundary
|
|
7
|
+
* events (spawn/exit/stall, argv redacted by POSITION) are always-on
|
|
8
|
+
* evidence, not diagnostics. An abandoned turn (consumer breaks early)
|
|
9
|
+
* closes backpressure, disposes output, stops the child, and awaits both
|
|
10
|
+
* pumps before it returns.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
buildLaunchArgv,
|
|
14
|
+
buildResumeArgv,
|
|
15
|
+
type LaunchOptions,
|
|
16
|
+
streamingGranularityOf,
|
|
17
|
+
} from "../interpretation/argv.js";
|
|
18
|
+
import { stdinPolicyOf } from "../interpretation/dimensions.js";
|
|
19
|
+
import { detectAuthFailureInLine, detectLimitInLine } from "../interpretation/limits.js";
|
|
20
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
21
|
+
import { AsyncChannel } from "./channel.js";
|
|
22
|
+
import { decodeLine, freshDecodeState } from "./decode.js";
|
|
23
|
+
import type { RunnerDeps, SpawnedProcess } from "./deps.js";
|
|
24
|
+
import type { ExitCause, HarnessEvent } from "./events.js";
|
|
25
|
+
import { LineBuffer } from "./lines.js";
|
|
26
|
+
|
|
27
|
+
/** Fallback correlation when the host does not mint turn ids: monotonic per
|
|
28
|
+
* process. Hosts that need cross-process uniqueness pass deps.turnId. */
|
|
29
|
+
let turnCounter = 0;
|
|
30
|
+
|
|
31
|
+
/** SIGTERM -> SIGKILL escalation budget for a child that ignores the first
|
|
32
|
+
* signal, and the grace allowed for pipes still held open (by a grandchild)
|
|
33
|
+
* after the process itself exited. */
|
|
34
|
+
export const KILL_GRACE_MS = 5_000;
|
|
35
|
+
export const PIPE_GRACE_MS = 2_000;
|
|
36
|
+
|
|
37
|
+
const OUTPUT_STREAMS = ["stdout", "stderr"] as const;
|
|
38
|
+
|
|
39
|
+
const pumpFailureMessage = (stream: (typeof OUTPUT_STREAMS)[number], cause: unknown): string =>
|
|
40
|
+
`${stream} pump failed: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
41
|
+
|
|
42
|
+
/** Stray secret-shaped tokens are masked; identifiers (session UUIDs, model
|
|
43
|
+
* ids, paths) log verbatim - they are what the log exists to correlate. */
|
|
44
|
+
const SECRETISH = /(sk-[A-Za-z0-9_-]{8,}|(?:token|key|secret|password)=\S+)/i;
|
|
45
|
+
|
|
46
|
+
/** Redact by POSITION, not shape: the prompt is a known argv slot and is
|
|
47
|
+
* masked wholesale (content never reaches a log line - v1 D-005); every
|
|
48
|
+
* other token is kept unless it is secret-shaped. */
|
|
49
|
+
export const redactArgv = (argv: readonly string[], prompt?: string): string[] =>
|
|
50
|
+
argv.map((token) => {
|
|
51
|
+
if (prompt !== undefined && token === prompt) return `[prompt:${prompt.length}ch]`;
|
|
52
|
+
if (SECRETISH.test(token)) return "[redacted]";
|
|
53
|
+
return token;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Bounded tail of unmatched stderr - the crash context a nonzero exit is
|
|
57
|
+
* explained by (v1 kept the turn's output slice for exactly this). Shared
|
|
58
|
+
* with the session runner. */
|
|
59
|
+
export class StderrTail {
|
|
60
|
+
private readonly lines: string[] = [];
|
|
61
|
+
private bytes = 0;
|
|
62
|
+
push(line: string): void {
|
|
63
|
+
this.lines.push(line);
|
|
64
|
+
this.bytes += line.length;
|
|
65
|
+
while (this.lines.length > 20 || (this.bytes > 4096 && this.lines.length > 1)) {
|
|
66
|
+
const dropped = this.lines.shift();
|
|
67
|
+
this.bytes -= dropped?.length ?? 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
snapshot(): readonly string[] {
|
|
71
|
+
return [...this.lines];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TurnRunOptions extends LaunchOptions {
|
|
76
|
+
/** Resume this session id instead of launching fresh - the turn spawns
|
|
77
|
+
* with the descriptor's resume grammar, and identity decoding treats a
|
|
78
|
+
* DIFFERENT announced id as a rotation anomaly. */
|
|
79
|
+
readonly resume?: string;
|
|
80
|
+
/** Working directory for the spawned harness. */
|
|
81
|
+
readonly cwd?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function* streamTurn(
|
|
85
|
+
h: HarnessDescriptor,
|
|
86
|
+
opts: TurnRunOptions,
|
|
87
|
+
deps: RunnerDeps,
|
|
88
|
+
): AsyncIterable<HarnessEvent> {
|
|
89
|
+
const turnId = deps.turnId ?? `turn-${++turnCounter}`;
|
|
90
|
+
const log = deps.log ?? (() => {});
|
|
91
|
+
const argv =
|
|
92
|
+
opts.resume === undefined
|
|
93
|
+
? buildLaunchArgv(h, opts)
|
|
94
|
+
: buildResumeArgv(h, { ...opts, sessionId: opts.resume });
|
|
95
|
+
const granularity = streamingGranularityOf(h, argv);
|
|
96
|
+
|
|
97
|
+
log({
|
|
98
|
+
event: "spawn",
|
|
99
|
+
turnId,
|
|
100
|
+
harness: h.name,
|
|
101
|
+
argv: redactArgv(argv, opts.prompt),
|
|
102
|
+
granularity,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let proc: SpawnedProcess;
|
|
106
|
+
try {
|
|
107
|
+
proc = deps.spawn(argv, {
|
|
108
|
+
stdin: stdinPolicyOf(h) === "close-required" ? "close" : "inherit",
|
|
109
|
+
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
110
|
+
});
|
|
111
|
+
} catch (cause) {
|
|
112
|
+
// A spawn that cannot start still honors the contract: error, done,
|
|
113
|
+
// and a closed exit log - never a throw out of the first next().
|
|
114
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
115
|
+
log({
|
|
116
|
+
event: "exit",
|
|
117
|
+
turnId,
|
|
118
|
+
harness: h.name,
|
|
119
|
+
exitCode: 127,
|
|
120
|
+
cause: "crash",
|
|
121
|
+
spawnError: message,
|
|
122
|
+
});
|
|
123
|
+
yield { kind: "error", message: `spawn failed: ${message}` };
|
|
124
|
+
yield { kind: "done", exitCode: 127, cause: "crash" };
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const queue = new AsyncChannel<HarnessEvent>();
|
|
129
|
+
const state = freshDecodeState(opts.resume ?? null);
|
|
130
|
+
const stderrTail = new StderrTail();
|
|
131
|
+
let killedByWatchdog = false;
|
|
132
|
+
let exited = false;
|
|
133
|
+
let exitCode: number | null = null;
|
|
134
|
+
let pipesOpenAtExit = false;
|
|
135
|
+
let cancelled = false;
|
|
136
|
+
let terminalEventReached = false;
|
|
137
|
+
|
|
138
|
+
const safeSignal = (sig: "SIGTERM" | "SIGKILL"): void => {
|
|
139
|
+
if (!exited) deps.signal(proc, sig);
|
|
140
|
+
};
|
|
141
|
+
let escalationTimer: number | null = null;
|
|
142
|
+
const escalate = (): void => {
|
|
143
|
+
if (exited || escalationTimer !== null) return;
|
|
144
|
+
safeSignal("SIGTERM");
|
|
145
|
+
escalationTimer = deps.clock.setTimeout(() => {
|
|
146
|
+
escalationTimer = null;
|
|
147
|
+
safeSignal("SIGKILL");
|
|
148
|
+
}, KILL_GRACE_MS);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let watchdog: number | null = null;
|
|
152
|
+
const disarm = (): void => {
|
|
153
|
+
if (watchdog !== null) deps.clock.clearTimeout(watchdog);
|
|
154
|
+
watchdog = null;
|
|
155
|
+
};
|
|
156
|
+
const rearm = (): void => {
|
|
157
|
+
if (granularity !== "none" || deps.stallMs === undefined) return;
|
|
158
|
+
disarm();
|
|
159
|
+
watchdog = deps.clock.setTimeout(() => {
|
|
160
|
+
killedByWatchdog = true;
|
|
161
|
+
log({ event: "stall", turnId, harness: h.name, stallMs: deps.stallMs });
|
|
162
|
+
escalate();
|
|
163
|
+
}, deps.stallMs);
|
|
164
|
+
};
|
|
165
|
+
rearm();
|
|
166
|
+
|
|
167
|
+
let pipeGrace: number | null = null;
|
|
168
|
+
void proc.exited.then((code) => {
|
|
169
|
+
exited = true;
|
|
170
|
+
exitCode = code;
|
|
171
|
+
if (escalationTimer !== null) deps.clock.clearTimeout(escalationTimer);
|
|
172
|
+
escalationTimer = null;
|
|
173
|
+
// The process is gone: the watchdog has nothing left to kill, and a
|
|
174
|
+
// fire after this point would flip a completed turn to "stall".
|
|
175
|
+
disarm();
|
|
176
|
+
// Pipes held open past exit (a grandchild inherited the fd) must not
|
|
177
|
+
// hang the turn forever - close out with the exit code in hand.
|
|
178
|
+
if (cancelled) return;
|
|
179
|
+
pipeGrace = deps.clock.setTimeout(() => {
|
|
180
|
+
pipesOpenAtExit = true;
|
|
181
|
+
proc.disposeOutput();
|
|
182
|
+
}, PIPE_GRACE_MS);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const pumpStdout = async (): Promise<void> => {
|
|
186
|
+
const lines = new LineBuffer();
|
|
187
|
+
for await (const chunk of proc.stdout) {
|
|
188
|
+
if (cancelled) break;
|
|
189
|
+
rearm();
|
|
190
|
+
for (const line of lines.push(chunk)) {
|
|
191
|
+
for (const event of decodeLine(h, line, state, opts.model ?? "")) {
|
|
192
|
+
await queue.push(event);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const rest = lines.flush();
|
|
197
|
+
if (rest !== null && !cancelled) {
|
|
198
|
+
for (const event of decodeLine(h, rest, state, opts.model ?? "")) await queue.push(event);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const pumpStderr = async (): Promise<void> => {
|
|
203
|
+
const lines = new LineBuffer();
|
|
204
|
+
for await (const chunk of proc.stderr) {
|
|
205
|
+
if (cancelled) break;
|
|
206
|
+
rearm();
|
|
207
|
+
for (const line of lines.push(chunk)) {
|
|
208
|
+
const limit = detectLimitInLine(h, line);
|
|
209
|
+
if (limit !== null) {
|
|
210
|
+
state.limitSeen = true;
|
|
211
|
+
await queue.push({
|
|
212
|
+
kind: "limit",
|
|
213
|
+
code: limit,
|
|
214
|
+
message: `limit wall detected (${limit})`,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const auth = detectAuthFailureInLine(h, line);
|
|
219
|
+
if (auth !== null) {
|
|
220
|
+
await queue.push({ kind: "error", message: `auth wall: ${auth}` });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
stderrTail.push(line);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const observePump = async (
|
|
229
|
+
stream: (typeof OUTPUT_STREAMS)[number],
|
|
230
|
+
pump: Promise<void>,
|
|
231
|
+
): Promise<void> => {
|
|
232
|
+
try {
|
|
233
|
+
await pump;
|
|
234
|
+
} catch (cause) {
|
|
235
|
+
log({
|
|
236
|
+
event: "output_pump_failed",
|
|
237
|
+
turnId,
|
|
238
|
+
harness: h.name,
|
|
239
|
+
stream,
|
|
240
|
+
issue: "read-failed",
|
|
241
|
+
});
|
|
242
|
+
await queue.push({ kind: "error", message: pumpFailureMessage(stream, cause) });
|
|
243
|
+
escalate();
|
|
244
|
+
await proc.exited;
|
|
245
|
+
proc.disposeOutput();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const pumpSettlements = Promise.all([
|
|
249
|
+
observePump("stdout", pumpStdout()),
|
|
250
|
+
observePump("stderr", pumpStderr()),
|
|
251
|
+
]);
|
|
252
|
+
void Promise.all([proc.exited, pumpSettlements]).then(() => queue.close());
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
for await (const event of queue) yield event;
|
|
256
|
+
|
|
257
|
+
const cause: ExitCause = state.limitSeen
|
|
258
|
+
? "limit"
|
|
259
|
+
: killedByWatchdog && exitCode !== 0
|
|
260
|
+
? "stall"
|
|
261
|
+
: exitCode === 0
|
|
262
|
+
? "clean"
|
|
263
|
+
: exitCode === null
|
|
264
|
+
? "killed"
|
|
265
|
+
: "crash";
|
|
266
|
+
const tail = stderrTail.snapshot();
|
|
267
|
+
log({
|
|
268
|
+
event: "exit",
|
|
269
|
+
turnId,
|
|
270
|
+
harness: h.name,
|
|
271
|
+
exitCode,
|
|
272
|
+
cause,
|
|
273
|
+
...(pipesOpenAtExit ? { pipesOpenAtExit } : {}),
|
|
274
|
+
...(cause === "crash" || cause === "stall" || cause === "killed" ? { stderrTail: tail } : {}),
|
|
275
|
+
});
|
|
276
|
+
// A failure with captured stderr surfaces as a stream-level error, not
|
|
277
|
+
// only in the exit log - so a crash from the real adapter's async spawn
|
|
278
|
+
// failure carries the same error-event signal as the sync-throw path.
|
|
279
|
+
if ((cause === "crash" || cause === "killed") && tail.length > 0) {
|
|
280
|
+
yield { kind: "error", message: tail.join("\n").slice(0, 4096) };
|
|
281
|
+
}
|
|
282
|
+
terminalEventReached = true;
|
|
283
|
+
yield { kind: "done", exitCode, cause };
|
|
284
|
+
} finally {
|
|
285
|
+
const abandoned = !terminalEventReached;
|
|
286
|
+
cancelled = true;
|
|
287
|
+
queue.close();
|
|
288
|
+
disarm();
|
|
289
|
+
if (abandoned) {
|
|
290
|
+
log({ event: "abandoned", turnId, harness: h.name });
|
|
291
|
+
if (!exited) escalate();
|
|
292
|
+
}
|
|
293
|
+
proc.disposeOutput();
|
|
294
|
+
const [settledExit] = await Promise.all([proc.exited, pumpSettlements]);
|
|
295
|
+
exitCode = settledExit;
|
|
296
|
+
if (pipeGrace !== null) deps.clock.clearTimeout(pipeGrace);
|
|
297
|
+
pipeGrace = null;
|
|
298
|
+
if (escalationTimer !== null) deps.clock.clearTimeout(escalationTimer);
|
|
299
|
+
escalationTimer = null;
|
|
300
|
+
if (abandoned) {
|
|
301
|
+
log({
|
|
302
|
+
event: "abandonment_settled",
|
|
303
|
+
turnId,
|
|
304
|
+
harness: h.name,
|
|
305
|
+
exitCode,
|
|
306
|
+
outputDisposed: true,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public package surface. The three architectural layers remain available as
|
|
3
|
+
* explicit subpath exports for consumers that want the narrowest dependency.
|
|
4
|
+
*/
|
|
5
|
+
export * from "./execution/index.js";
|
|
6
|
+
export * from "./interpretation/index.js";
|
|
7
|
+
export * from "./knowledge/index.js";
|