@relayflows/sdk 2.0.11 → 2.0.13
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/dist/adapters/base.d.ts +6 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +1 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/authored-admission.d.ts +5 -0
- package/dist/authored-admission.d.ts.map +1 -0
- package/dist/authored-admission.js +14 -0
- package/dist/authored-admission.js.map +1 -0
- package/dist/authored-budget.d.ts +1 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +18 -9
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +10 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +15 -7
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +20 -0
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +86 -11
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.d.ts +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +5 -3
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-mcp.d.ts +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +7 -4
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.d.ts +2 -0
- package/dist/authored-node-entry.d.ts.map +1 -0
- package/dist/authored-node-entry.js +111 -0
- package/dist/authored-node-entry.js.map +1 -0
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -0
- package/dist/authored-node-runner.js +246 -0
- package/dist/authored-node-runner.js.map +1 -0
- package/dist/authored-plugin-effect.d.ts +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +7 -4
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-root.d.ts +38 -0
- package/dist/authored-root.d.ts.map +1 -0
- package/dist/authored-root.js +329 -0
- package/dist/authored-root.js.map +1 -0
- package/dist/authored-runtime-capability.d.ts +6 -0
- package/dist/authored-runtime-capability.d.ts.map +1 -0
- package/dist/authored-runtime-capability.js +40 -0
- package/dist/authored-runtime-capability.js.map +1 -0
- package/dist/authored-source-authority.d.ts +4 -0
- package/dist/authored-source-authority.d.ts.map +1 -0
- package/dist/authored-source-authority.js +61 -0
- package/dist/authored-source-authority.js.map +1 -0
- package/dist/authored-worker-step.d.ts +2 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +11 -6
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/budget-preflight.d.ts +28 -3
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +34 -24
- package/dist/budget-preflight.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +8 -0
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check.js +3 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/deploy.d.ts.map +1 -1
- package/dist/cli/deploy.js +5 -0
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +46 -14
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +90 -12
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts +40 -0
- package/dist/cli/step-failure.d.ts.map +1 -0
- package/dist/cli/step-failure.js +150 -0
- package/dist/cli/step-failure.js.map +1 -0
- package/dist/cli-adapter.d.ts +16 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +17 -0
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -8
- package/dist/cli.js.map +1 -1
- package/dist/cloud-run.d.ts +11 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +65 -19
- package/dist/cloud-run.js.map +1 -1
- package/dist/daemon-connection.d.ts +7 -0
- package/dist/daemon-connection.d.ts.map +1 -1
- package/dist/daemon-connection.js +7 -0
- package/dist/daemon-connection.js.map +1 -1
- package/dist/failure-kinds.d.ts +30 -4
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +8 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +3 -7
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +6 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +4 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +2 -2
- package/dist/local-agent.js.map +1 -1
- package/dist/model-pricing.d.ts +7 -6
- package/dist/model-pricing.d.ts.map +1 -1
- package/dist/model-pricing.js +8 -6
- package/dist/model-pricing.js.map +1 -1
- package/dist/preflight.d.ts +9 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +36 -18
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +24 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +27 -11
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +6 -5
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-spend.d.ts +13 -8
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +27 -4
- package/dist/worker-spend.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +4 -2
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/claude.ts +1 -0
- package/src/authored-admission.ts +17 -0
- package/src/authored-budget.ts +38 -11
- package/src/authored-flow-error.ts +0 -1
- package/src/authored-flow-executor.ts +32 -7
- package/src/authored-flow-loader.ts +96 -13
- package/src/authored-helper-effect.ts +5 -3
- package/src/authored-mcp.ts +6 -4
- package/src/authored-node-entry.ts +90 -0
- package/src/authored-node-runner.ts +219 -0
- package/src/authored-plugin-effect.ts +6 -3
- package/src/authored-root.ts +396 -0
- package/src/authored-runtime-capability.ts +40 -0
- package/src/authored-source-authority.ts +62 -0
- package/src/authored-worker-step.ts +15 -4
- package/src/budget-preflight.ts +52 -25
- package/src/cli/build.ts +7 -0
- package/src/cli/check.ts +3 -1
- package/src/cli/deploy.ts +4 -0
- package/src/cli/direct-run.ts +52 -16
- package/src/cli/run.ts +101 -12
- package/src/cli/step-failure.ts +160 -0
- package/src/cli-adapter.ts +27 -0
- package/src/cli.ts +9 -9
- package/src/cloud-run.ts +74 -21
- package/src/daemon-connection.ts +8 -0
- package/src/failure-kinds.ts +28 -2
- package/src/journal-client.ts +8 -3
- package/src/llm-worker.ts +4 -2
- package/src/local-agent.ts +7 -2
- package/src/model-pricing.ts +8 -6
- package/src/preflight.ts +45 -18
- package/src/protocol.ts +17 -2
- package/src/spec.ts +28 -5
- package/src/worker-cli.ts +7 -5
- package/src/worker-spend.ts +32 -5
- package/src/worker.ts +4 -2
- package/dist/cli/deterministic-failure.d.ts +0 -5
- package/dist/cli/deterministic-failure.d.ts.map +0 -1
- package/dist/cli/deterministic-failure.js +0 -66
- package/dist/cli/deterministic-failure.js.map +0 -1
- package/src/cli/deterministic-failure.ts +0 -69
package/src/authored-budget.ts
CHANGED
|
@@ -2,14 +2,31 @@ import { BudgetSyntaxError, parseBudget, toKernelBudget } from './budget.js';
|
|
|
2
2
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
3
3
|
import type { JournalClient } from './journal-client.js';
|
|
4
4
|
import type { RunOutcome } from './protocol.js';
|
|
5
|
-
import type { KernelBudgetSpec, KernelRunSpec } from './spec.js';
|
|
5
|
+
import type { KernelBudgetSpec, KernelPriorSpend, KernelRunSpec } from './spec.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One journaled charge, in the accumulator's exact integer form.
|
|
9
|
+
*
|
|
10
|
+
* `unmetered` mirrors the journal's `budget.dollars_unmetered`: the charge
|
|
11
|
+
* spent tokens whose dollar cost is unknown, so its `micro` is a lower bound
|
|
12
|
+
* and not a measured amount. It is carried, not derived from `micro`, because
|
|
13
|
+
* an unmetered charge and a genuinely free charge both report zero dollars.
|
|
14
|
+
*/
|
|
15
|
+
interface Charge {
|
|
16
|
+
input: bigint;
|
|
17
|
+
output: bigint;
|
|
18
|
+
micro: bigint;
|
|
19
|
+
ms: bigint;
|
|
20
|
+
day: number;
|
|
21
|
+
unmetered: boolean;
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
/** Serialized admission for the internal authored runner's separate step runs. */
|
|
8
25
|
export class AuthoredBudget {
|
|
9
26
|
private readonly limit: KernelBudgetSpec | undefined;
|
|
10
27
|
private failed = false;
|
|
11
28
|
private tail: Promise<unknown> = Promise.resolve();
|
|
12
|
-
private charges:
|
|
29
|
+
private charges: Charge[] = [];
|
|
13
30
|
|
|
14
31
|
constructor(header: unknown) {
|
|
15
32
|
try {
|
|
@@ -20,8 +37,9 @@ export class AuthoredBudget {
|
|
|
20
37
|
}
|
|
21
38
|
}
|
|
22
39
|
|
|
23
|
-
async execute<T>(journal: JournalClient, spec: KernelRunSpec,
|
|
24
|
-
|
|
40
|
+
async execute<T>(journal: JournalClient, spec: KernelRunSpec,
|
|
41
|
+
consume: (outcome: RunOutcome) => Promise<T>, admissionKey?: string): Promise<T> {
|
|
42
|
+
if (this.limit === undefined) return consume(await journal.runStart(spec, undefined, admissionKey));
|
|
25
43
|
const previous = this.tail;
|
|
26
44
|
let release!: () => void;
|
|
27
45
|
this.tail = new Promise<void>(resolve => { release = resolve; });
|
|
@@ -31,14 +49,22 @@ export class AuthoredBudget {
|
|
|
31
49
|
// Window selection follows journal timestamps, never the SDK host clock.
|
|
32
50
|
const day = this.charges.at(-1)?.day;
|
|
33
51
|
const total = this.charges.filter(c => this.limit!.window !== 'day' || c.day === day)
|
|
34
|
-
.reduce((s, c) => ({ input: s.input + c.input, output: s.output + c.output, micro: s.micro + c.micro, ms: s.ms + c.ms
|
|
35
|
-
|
|
52
|
+
.reduce((s, c) => ({ input: s.input + c.input, output: s.output + c.output, micro: s.micro + c.micro, ms: s.ms + c.ms,
|
|
53
|
+
// Sticky, exactly as the kernel's running total is: once any charge
|
|
54
|
+
// in the window was unmetered, the carried dollars are a lower bound.
|
|
55
|
+
unmetered: s.unmetered || c.unmetered }),
|
|
56
|
+
{ input: 0n, output: 0n, micro: 0n, ms: 0n, unmetered: false });
|
|
36
57
|
const exactNumber = (n: bigint) => { if (n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('budget counter overflow'); return Number(n); };
|
|
37
|
-
|
|
58
|
+
// One explicit conversion to the shared carried-spend shape, so a new
|
|
59
|
+
// accounting field is added here rather than silently dropped inline.
|
|
60
|
+
const priorSpend: KernelPriorSpend = {
|
|
38
61
|
tokens_in: exactNumber(total.input), tokens_out: exactNumber(total.output),
|
|
39
62
|
dollars: `${total.micro / 1_000_000n}.${String(total.micro % 1_000_000n).padStart(6, '0')}`,
|
|
40
|
-
wallclock_ms: exactNumber(total.ms),
|
|
41
|
-
|
|
63
|
+
wallclock_ms: exactNumber(total.ms),
|
|
64
|
+
...(this.limit.window === 'day' && day !== undefined ? { day } : {}),
|
|
65
|
+
...(total.unmetered ? { dollars_unmetered: true as const } : {}),
|
|
66
|
+
};
|
|
67
|
+
const outcome = await journal.runStart({ ...spec, budget: { ...this.limit, prior_spend: priorSpend } }, undefined, admissionKey);
|
|
42
68
|
try {
|
|
43
69
|
if (outcome.completion_reason === 'budget_exceeded') throw new AuthoredFlowExecutionError('step_failed', 'Flow budget exceeded before the next step.', 'budget_exceeded', outcome.run_id);
|
|
44
70
|
return await consume(outcome);
|
|
@@ -48,7 +74,7 @@ export class AuthoredBudget {
|
|
|
48
74
|
const { entries } = await journal.journalRead(outcome.run_id, seq);
|
|
49
75
|
if (entries.length === 0) break;
|
|
50
76
|
for (const raw of entries) {
|
|
51
|
-
const e = raw as {seq: number; entry_type: string; at_ms: number; payload: {budget?: {tokens_in: number; tokens_out: number; dollars: string}; spend?: {wallclock_ms: number}}};
|
|
77
|
+
const e = raw as {seq: number; entry_type: string; at_ms: number; payload: {budget?: {tokens_in: number; tokens_out: number; dollars: string; dollars_unmetered?: boolean}; spend?: {wallclock_ms: number}}};
|
|
52
78
|
seq = e.seq + 1;
|
|
53
79
|
if (!['step.completed', 'memory.injected'].includes(e.entry_type)) continue;
|
|
54
80
|
const b = e.payload.budget;
|
|
@@ -57,7 +83,8 @@ export class AuthoredBudget {
|
|
|
57
83
|
if (fraction.length > 6 && /[1-9]/.test(fraction.slice(6))) throw new Error('budget accounting requires microdollar precision');
|
|
58
84
|
this.charges.push({input: BigInt(b.tokens_in), output: BigInt(b.tokens_out),
|
|
59
85
|
micro: BigInt(whole!) * 1_000_000n + BigInt(fraction.slice(0, 6).padEnd(6, '0')),
|
|
60
|
-
ms: BigInt(e.payload.spend?.wallclock_ms ?? 0), day: Math.floor(e.at_ms / 86_400_000)
|
|
86
|
+
ms: BigInt(e.payload.spend?.wallclock_ms ?? 0), day: Math.floor(e.at_ms / 86_400_000),
|
|
87
|
+
unmetered: b.dollars_unmetered === true});
|
|
61
88
|
}
|
|
62
89
|
}
|
|
63
90
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
|
|
1
2
|
import { pluginHelpers } from './plugin-loader.js';
|
|
2
3
|
import { runPluginEffect } from './authored-plugin-effect.js';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
@@ -71,7 +72,16 @@ export interface AuthoredFlowJournalStep {
|
|
|
71
72
|
readonly completionReason: ProtocolCompletionReason;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
export interface AuthoredExecutionRuntime {
|
|
76
|
+
readonly kind: 'node';
|
|
77
|
+
readonly version: string;
|
|
78
|
+
readonly executableSha256: string;
|
|
79
|
+
readonly payloadSha256: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
export interface AuthoredFlowExecutionResult {
|
|
83
|
+
readonly executionRuntime?: AuthoredExecutionRuntime;
|
|
84
|
+
readonly rootRunId?: string;
|
|
75
85
|
readonly name: string;
|
|
76
86
|
readonly completionReason: FlowCompletionReason;
|
|
77
87
|
readonly journalSteps: readonly AuthoredFlowJournalStep[];
|
|
@@ -122,6 +132,8 @@ export interface ExecuteAuthoredFlowOptions {
|
|
|
122
132
|
readonly onWait?: RunLifecycleOptions['onWait'];
|
|
123
133
|
readonly onProgress?: (event: ProgressEvent) => void;
|
|
124
134
|
readonly localAgentStream?: string;
|
|
135
|
+
/** Durable kernel root that owns this body's child admission identities. */
|
|
136
|
+
readonly rootRunId?: string;
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
export async function executeAuthoredFlow<Input = undefined>(
|
|
@@ -130,11 +142,17 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
130
142
|
input?: Input,
|
|
131
143
|
options: ExecuteAuthoredFlowOptions = {},
|
|
132
144
|
): Promise<AuthoredFlowExecutionResult> {
|
|
145
|
+
assertAuthoredPromiseHooks();
|
|
133
146
|
const getDefinition = options.getDefinition ?? getAuthoredFlowDefinition;
|
|
134
147
|
const localAgentStream = options.localAgentStream;
|
|
135
148
|
const onProgress = options.onProgress;
|
|
136
149
|
const flowPath = options.flowPath ?? join(process.cwd(), 'flow.ts');
|
|
137
150
|
const waitOptions: RunLifecycleOptions = {
|
|
151
|
+
// Carried so a failed `f.agent` can name the journal that holds its
|
|
152
|
+
// evidence. Each authored worker call runs as its own kernel run, and
|
|
153
|
+
// without the data dir the diagnostic can name the run id but not where
|
|
154
|
+
// on disk to read it.
|
|
155
|
+
...(options.dataDir !== undefined ? { dataDir: options.dataDir } : {}),
|
|
138
156
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
139
157
|
...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
|
|
140
158
|
};
|
|
@@ -173,9 +191,14 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
173
191
|
let nextStep = 1;
|
|
174
192
|
let requestedCompletion: FlowCompletionReason | undefined;
|
|
175
193
|
|
|
176
|
-
const lowerDeterministic = authoredDeterministicRunner(
|
|
194
|
+
const lowerDeterministic = authoredDeterministicRunner(
|
|
195
|
+
definition.name, journal, journalSteps, budget, options.rootRunId,
|
|
196
|
+
);
|
|
177
197
|
|
|
178
|
-
const worker = authoredWorkerRunner(
|
|
198
|
+
const worker = authoredWorkerRunner(
|
|
199
|
+
definition, journal, flowPath, journalSteps, waitOptions,
|
|
200
|
+
localAgentStream, budget, definition.header.budget, options.rootRunId,
|
|
201
|
+
);
|
|
179
202
|
|
|
180
203
|
function llmOperation(strings: TemplateStringsArray, ...values: unknown[]): Step<string>;
|
|
181
204
|
function llmOperation(prompt: string, options: LlmOptions): Step<unknown>;
|
|
@@ -205,7 +228,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
205
228
|
return trackStep(authoredSteps, llmOp);
|
|
206
229
|
}
|
|
207
230
|
|
|
208
|
-
const slackRun = randomUUID();
|
|
231
|
+
const slackRun = options.rootRunId ?? randomUUID();
|
|
209
232
|
function slackOperation<T>(call: SlackCall): Step<T> {
|
|
210
233
|
assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion);
|
|
211
234
|
const snapshot = snapshotJsonValue(call, 'f.slack call') as unknown as SlackCall;
|
|
@@ -215,7 +238,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
215
238
|
() => assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion),
|
|
216
239
|
async () => {
|
|
217
240
|
const receipt = await runHelperEffect(journal, definition.name, id, snapshot,
|
|
218
|
-
options.dataDir ?? dirname(journal.socketPath), journalSteps);
|
|
241
|
+
options.dataDir ?? dirname(journal.socketPath), journalSteps, options.rootRunId);
|
|
219
242
|
return (call.verb === 'react' ? undefined : receipt) as T;
|
|
220
243
|
},
|
|
221
244
|
lifecycle,
|
|
@@ -231,7 +254,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
231
254
|
return trackStep(authoredSteps, new AuthoredFlowOperation<T>(id, verb,
|
|
232
255
|
() => assertOperationAllowed(verb, definition.name, requestedCompletion),
|
|
233
256
|
async () => await runHelperEffect(journal, definition.name, id, snapshot,
|
|
234
|
-
options.dataDir ?? dirname(journal.socketPath), journalSteps) as T,
|
|
257
|
+
options.dataDir ?? dirname(journal.socketPath), journalSteps, options.rootRunId) as T,
|
|
235
258
|
lifecycle));
|
|
236
259
|
}),
|
|
237
260
|
slack: {
|
|
@@ -247,7 +270,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
247
270
|
id, 'mcp',
|
|
248
271
|
() => assertOperationAllowed('mcp', definition.name, requestedCompletion),
|
|
249
272
|
() => runMcpEffect(journal, definition.name, id, server, tool, args,
|
|
250
|
-
known, checkedMcp.servers[server]!, journalSteps),
|
|
273
|
+
known, checkedMcp.servers[server]!, journalSteps, options.rootRunId),
|
|
251
274
|
lifecycle,
|
|
252
275
|
));
|
|
253
276
|
}),
|
|
@@ -327,7 +350,8 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
327
350
|
const id = `plugin-${nextStep++}`;
|
|
328
351
|
return trackStep(authoredSteps, new AuthoredFlowOperation(
|
|
329
352
|
id, label, () => assertOperationAllowed(label, definition.name, requestedCompletion),
|
|
330
|
-
() => runPluginEffect(journal, definition.name, id, plugin, verb, args,
|
|
353
|
+
() => runPluginEffect(journal, definition.name, id, plugin, verb, args,
|
|
354
|
+
journalSteps, budget, options.rootRunId),
|
|
331
355
|
lifecycle,
|
|
332
356
|
));
|
|
333
357
|
}));
|
|
@@ -387,6 +411,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
387
411
|
await lowerDeterministic(`complete-${nextStep}`, requestedCompletion === 'needs_human'
|
|
388
412
|
? `printf '%s' '{"completionReason":"needs_human"}'` : ':', true);
|
|
389
413
|
return Object.freeze({
|
|
414
|
+
...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
|
|
390
415
|
name: definition.name,
|
|
391
416
|
completionReason: requestedCompletion,
|
|
392
417
|
journalSteps: Object.freeze([...journalSteps]),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { accessSync, constants, realpathSync } from 'node:fs';
|
|
1
|
+
import { accessSync, constants, existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
2
4
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
6
|
import { pathToFileURL } from 'node:url';
|
|
5
7
|
import {
|
|
6
8
|
getAuthoredFlowDefinition,
|
|
@@ -18,7 +20,15 @@ export class AuthoredFlowLoadError extends Error {
|
|
|
18
20
|
/** Same signature as `getAuthoredFlowDefinition`, resolved from wherever a flow was loaded. */
|
|
19
21
|
export type GetFlowDefinition = <Input = unknown>(handle: FlowHandle) => AuthoredFlowDefinition<Input>;
|
|
20
22
|
|
|
23
|
+
export interface SurfaceModuleAuthority {
|
|
24
|
+
readonly packageName: '@relayflows/surface';
|
|
25
|
+
readonly version: string;
|
|
26
|
+
readonly packageSha256: string;
|
|
27
|
+
readonly runtimeSha256: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
export interface LoadedAuthoredFlow {
|
|
31
|
+
readonly sourcePath: string;
|
|
22
32
|
readonly handle: FlowHandle;
|
|
23
33
|
/**
|
|
24
34
|
* Bound to the SAME `@relayflows/surface` module instance the flow file
|
|
@@ -28,6 +38,8 @@ export interface LoadedAuthoredFlow {
|
|
|
28
38
|
* `getAuthoredFlowDefinition` import.
|
|
29
39
|
*/
|
|
30
40
|
readonly getDefinition: GetFlowDefinition;
|
|
41
|
+
/** Exact Surface package/runtime that owns the handle's WeakMap identity. */
|
|
42
|
+
readonly surfaceAuthority: SurfaceModuleAuthority;
|
|
31
43
|
/** Dependency-first load order, each canonical absolute path appearing once. */
|
|
32
44
|
readonly graph: readonly LoadedAuthoredFlowNode[];
|
|
33
45
|
}
|
|
@@ -36,6 +48,7 @@ export interface LoadedAuthoredFlowNode {
|
|
|
36
48
|
readonly path: string;
|
|
37
49
|
readonly handle: FlowHandle;
|
|
38
50
|
readonly getDefinition: GetFlowDefinition;
|
|
51
|
+
readonly surfaceAuthority: SurfaceModuleAuthority;
|
|
39
52
|
readonly use: readonly string[];
|
|
40
53
|
}
|
|
41
54
|
|
|
@@ -58,7 +71,7 @@ export async function loadAuthoredFlow(path: string): Promise<LoadedAuthoredFlow
|
|
|
58
71
|
if (cached !== undefined) return cached;
|
|
59
72
|
visiting.add(absolutePath);
|
|
60
73
|
try {
|
|
61
|
-
const { handle, getDefinition } = await importAuthoredFlow(absolutePath);
|
|
74
|
+
const { handle, getDefinition, surfaceAuthority } = await importAuthoredFlow(absolutePath);
|
|
62
75
|
const dependencies: string[] = [];
|
|
63
76
|
for (const entry of getDefinition(handle).header.use ?? []) {
|
|
64
77
|
// Validate again at the SDK boundary: the author's surface package may
|
|
@@ -72,7 +85,7 @@ export async function loadAuthoredFlow(path: string): Promise<LoadedAuthoredFlow
|
|
|
72
85
|
}
|
|
73
86
|
dependencies.push(child.path);
|
|
74
87
|
}
|
|
75
|
-
const node = Object.freeze({ path: absolutePath, handle, getDefinition, use: Object.freeze(dependencies) });
|
|
88
|
+
const node = Object.freeze({ path: absolutePath, handle, getDefinition, surfaceAuthority, use: Object.freeze(dependencies) });
|
|
76
89
|
loaded.set(absolutePath, node);
|
|
77
90
|
return node;
|
|
78
91
|
} catch (error) {
|
|
@@ -85,10 +98,11 @@ export async function loadAuthoredFlow(path: string): Promise<LoadedAuthoredFlow
|
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
const root = await visit(resolve(path), true);
|
|
88
|
-
return Object.freeze({ handle: root.handle, getDefinition: root.getDefinition,
|
|
101
|
+
return Object.freeze({ sourcePath: root.path, handle: root.handle, getDefinition: root.getDefinition,
|
|
102
|
+
surfaceAuthority: root.surfaceAuthority, graph: Object.freeze([...loaded.values()]) });
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
async function importAuthoredFlow(path: string): Promise<Pick<LoadedAuthoredFlow, 'handle' | 'getDefinition'>> {
|
|
105
|
+
async function importAuthoredFlow(path: string): Promise<Pick<LoadedAuthoredFlow, 'handle' | 'getDefinition' | 'surfaceAuthority'>> {
|
|
92
106
|
const absolutePath = resolve(path);
|
|
93
107
|
try {
|
|
94
108
|
accessSync(absolutePath, constants.R_OK);
|
|
@@ -98,14 +112,14 @@ async function importAuthoredFlow(path: string): Promise<Pick<LoadedAuthoredFlow
|
|
|
98
112
|
|
|
99
113
|
let authoredModule: Record<string, unknown>;
|
|
100
114
|
try {
|
|
101
|
-
authoredModule = await import(pathToFileURL(absolutePath).href) as Record<string, unknown>;
|
|
115
|
+
authoredModule = await import(/* @vite-ignore */ pathToFileURL(absolutePath).href) as Record<string, unknown>;
|
|
102
116
|
} catch (error) {
|
|
103
117
|
throw new AuthoredFlowLoadError(
|
|
104
118
|
`Flow "${path}" could not be imported: ${errorMessage(error)}`,
|
|
105
119
|
);
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
const getDefinition = await
|
|
122
|
+
const { getDefinition, surfaceAuthority } = await resolveSurfaceRuntime(absolutePath, path);
|
|
109
123
|
const handle = authoredModule['default'] as FlowHandle;
|
|
110
124
|
try {
|
|
111
125
|
getDefinition(handle);
|
|
@@ -114,7 +128,7 @@ async function importAuthoredFlow(path: string): Promise<Pick<LoadedAuthoredFlow
|
|
|
114
128
|
`Flow "${path}" must default-export flow(...): ${errorMessage(error)}`,
|
|
115
129
|
);
|
|
116
130
|
}
|
|
117
|
-
return { handle, getDefinition };
|
|
131
|
+
return { handle, getDefinition, surfaceAuthority };
|
|
118
132
|
}
|
|
119
133
|
|
|
120
134
|
/**
|
|
@@ -138,21 +152,27 @@ async function importAuthoredFlow(path: string): Promise<Pick<LoadedAuthoredFlow
|
|
|
138
152
|
* compatible `@relayflows/surface` is provably resolvable from this same
|
|
139
153
|
* anchor.
|
|
140
154
|
*/
|
|
141
|
-
async function
|
|
155
|
+
async function resolveSurfaceRuntime(
|
|
142
156
|
absolutePath: string,
|
|
143
157
|
displayPath: string,
|
|
144
|
-
): Promise<GetFlowDefinition> {
|
|
158
|
+
): Promise<{ getDefinition: GetFlowDefinition; surfaceAuthority: SurfaceModuleAuthority }> {
|
|
145
159
|
const require = createRequire(pathToFileURL(absolutePath));
|
|
146
160
|
let resolvedRuntimePath: string;
|
|
147
161
|
try {
|
|
148
162
|
resolvedRuntimePath = require.resolve('@relayflows/surface/runtime');
|
|
163
|
+
// Some ESM test/load hooks return a percent-encoded absolute path without
|
|
164
|
+
// a file: scheme. Normalize it only when the literal path does not exist.
|
|
165
|
+
if (!existsSync(resolvedRuntimePath) && resolvedRuntimePath.includes('%')) {
|
|
166
|
+
const decoded = decodeURI(resolvedRuntimePath);
|
|
167
|
+
if (existsSync(decoded)) resolvedRuntimePath = decoded;
|
|
168
|
+
}
|
|
149
169
|
} catch (error) {
|
|
150
170
|
throw new AuthoredFlowLoadError(
|
|
151
171
|
`Flow "${displayPath}" imports @relayflows/surface, but @relayflows/surface/runtime `
|
|
152
172
|
+ `could not be resolved from the same location: ${errorMessage(error)}`,
|
|
153
173
|
);
|
|
154
174
|
}
|
|
155
|
-
const runtimeModule = await import(
|
|
175
|
+
const runtimeModule = await import(/* @vite-ignore */ resolvedRuntimePath) as {
|
|
156
176
|
getFlowDefinition?: unknown;
|
|
157
177
|
};
|
|
158
178
|
if (typeof runtimeModule.getFlowDefinition !== 'function') {
|
|
@@ -161,7 +181,70 @@ async function resolveGetFlowDefinition(
|
|
|
161
181
|
+ 'does not export getFlowDefinition — check its @relayflows/surface version.',
|
|
162
182
|
);
|
|
163
183
|
}
|
|
164
|
-
|
|
184
|
+
let packagePath = dirname(resolvedRuntimePath);
|
|
185
|
+
for (;;) {
|
|
186
|
+
const candidate = resolve(packagePath, 'package.json');
|
|
187
|
+
try {
|
|
188
|
+
const packageBytes = await readFile(candidate);
|
|
189
|
+
const manifest = JSON.parse(packageBytes.toString('utf8')) as { name?: unknown; version?: unknown };
|
|
190
|
+
if (manifest.name === '@relayflows/surface') {
|
|
191
|
+
if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
|
|
192
|
+
throw new AuthoredFlowLoadError(`Flow "${displayPath}": @relayflows/surface has no pinned package version.`);
|
|
193
|
+
}
|
|
194
|
+
const runtimeBytes = await readFile(resolvedRuntimePath);
|
|
195
|
+
return {
|
|
196
|
+
getDefinition: runtimeModule.getFlowDefinition as GetFlowDefinition,
|
|
197
|
+
surfaceAuthority: Object.freeze({
|
|
198
|
+
packageName: '@relayflows/surface', version: manifest.version,
|
|
199
|
+
packageSha256: await packageTreeSha256(packagePath),
|
|
200
|
+
runtimeSha256: createHash('sha256').update(runtimeBytes).digest('hex'),
|
|
201
|
+
}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!(error instanceof SyntaxError) && (error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
206
|
+
}
|
|
207
|
+
const parent = dirname(packagePath);
|
|
208
|
+
if (parent === packagePath) break;
|
|
209
|
+
packagePath = parent;
|
|
210
|
+
}
|
|
211
|
+
throw new AuthoredFlowLoadError(
|
|
212
|
+
`Flow "${displayPath}": the resolved @relayflows/surface/runtime is not inside its declared package.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Hash the exact installed Surface package payload, including path boundaries.
|
|
218
|
+
*
|
|
219
|
+
* npm may materialize a package-local node_modules tree for workspace and
|
|
220
|
+
* file: dependencies. That tree is installation state, not part of the
|
|
221
|
+
* published Surface package, and can contain platform-specific .bin symlinks.
|
|
222
|
+
* Keep it outside the authority boundary while pinning every package payload
|
|
223
|
+
* file, including the runtime module hashed separately by the caller.
|
|
224
|
+
*/
|
|
225
|
+
export async function packageTreeSha256(root: string): Promise<string> {
|
|
226
|
+
const files: string[] = [];
|
|
227
|
+
async function visit(directory: string): Promise<void> {
|
|
228
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
229
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (directory === root && entry.name === 'node_modules') continue;
|
|
232
|
+
const path = join(directory, entry.name);
|
|
233
|
+
if (entry.isDirectory()) await visit(path);
|
|
234
|
+
else if (entry.isFile()) files.push(path);
|
|
235
|
+
else throw new AuthoredFlowLoadError(
|
|
236
|
+
`@relayflows/surface package contains unsupported entry "${relative(root, path)}".`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await visit(root);
|
|
241
|
+
const hash = createHash('sha256');
|
|
242
|
+
for (const path of files) {
|
|
243
|
+
const bytes = await readFile(path);
|
|
244
|
+
const name = relative(root, path).split(sep).join('/');
|
|
245
|
+
hash.update(name).update('\0').update(String(bytes.length)).update('\0').update(bytes);
|
|
246
|
+
}
|
|
247
|
+
return hash.digest('hex');
|
|
165
248
|
}
|
|
166
249
|
|
|
167
250
|
// Exported for callers (internal SDK tests, and any co-located flow that is
|
|
@@ -13,6 +13,7 @@ import { helperProviders, type HelperCall } from '@relayflows/surface/runtime';
|
|
|
13
13
|
import { helperWriteback, HelperDeliveryError } from './helper-writeback.js';
|
|
14
14
|
import { checkSlackHelpers } from './slack-preflight.js';
|
|
15
15
|
import { atomicJson, readSlackReceipt, receiptPath, slackWriteback, type SlackCall } from './slack-writeback.js';
|
|
16
|
+
import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
16
17
|
|
|
17
18
|
type ProviderCall = HelperCall | SlackCall;
|
|
18
19
|
|
|
@@ -30,20 +31,21 @@ export function assertHelperCredentials(provider: string): void {
|
|
|
30
31
|
/** Each helper uses the existing agent lease + effect protocol; no new kernel verb. */
|
|
31
32
|
export async function runHelperEffect(
|
|
32
33
|
journal: JournalClient, name: string, stepId: string, call: ProviderCall,
|
|
33
|
-
dataDir: string, journalSteps: AuthoredFlowJournalStep[],
|
|
34
|
+
dataDir: string, journalSteps: AuthoredFlowJournalStep[], rootRunId?: string,
|
|
34
35
|
): Promise<unknown> {
|
|
35
36
|
assertHelperCredentials(call.provider);
|
|
36
37
|
if (call.provider === 'notion' && call.verb === 'appendBlock' && process.env.RELAYFLOWS_NOTION_MOCK !== '1') {
|
|
37
38
|
throw new AuthoredFlowExecutionError('helper_provider.unsupported', 'Notion appendBlock has no upstream mount writeback route');
|
|
38
39
|
}
|
|
39
|
-
const
|
|
40
|
+
const admissionKey = authoredChildAdmissionKey(rootRunId, stepId);
|
|
41
|
+
const stream = `${call.provider}-helper-${admissionKey?.slice(-32) ?? randomUUID()}`;
|
|
40
42
|
const spec = toKernelSpec(compileSpec({
|
|
41
43
|
version: SPEC_SCHEMA_VERSION, name: `${name}/${stepId}`,
|
|
42
44
|
steps: [{ id: stepId, type: 'agent', instruction: JSON.stringify(call),
|
|
43
45
|
maxIterations: 3, recoveryMode: 'reset',
|
|
44
46
|
surfaces: { streams: [{ stream }], external: [`/${call.provider}`] } }],
|
|
45
47
|
}));
|
|
46
|
-
const outcome = await journal.runStart(spec);
|
|
48
|
+
const outcome = await journal.runStart(spec, undefined, admissionKey);
|
|
47
49
|
await atomicJson(join(dataDir, 'helper-runs', `${outcome.run_id}.json`), { provider: call.provider });
|
|
48
50
|
await driveHelperEffect(journal, outcome.run_id, spec.steps[0] as KernelAgentStep, call, dataDir);
|
|
49
51
|
const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps);
|
package/src/authored-mcp.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
|
11
11
|
import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
12
12
|
import { readCompletedStepOutput } from './authored-step-output.js';
|
|
13
13
|
import { withWorkerLease } from './worker-lease.js';
|
|
14
|
+
import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
14
15
|
|
|
15
16
|
/** Own keys expose precisely the preflight inventory, including prototype-like names. */
|
|
16
17
|
export function buildMcpProxy(
|
|
@@ -49,11 +50,12 @@ export class McpStepError extends AuthoredFlowExecutionError {
|
|
|
49
50
|
export async function runMcpEffect(
|
|
50
51
|
journal: JournalClient, flowName: string, id: string, server: string, tool: string,
|
|
51
52
|
args: unknown, known: boolean, config: McpServerConfig,
|
|
52
|
-
journalSteps: AuthoredFlowJournalStep[],
|
|
53
|
+
journalSteps: AuthoredFlowJournalStep[], rootRunId?: string,
|
|
53
54
|
): Promise<unknown> {
|
|
54
55
|
const idempotencyKey = `mcp:${server}:${tool}:${createHash('sha256').update(JSON.stringify(args)).digest('hex')}`;
|
|
55
56
|
const surfacePath = `/mcp/${pathPart(server)}/${pathPart(tool)}`;
|
|
56
|
-
const
|
|
57
|
+
const admissionKey = authoredChildAdmissionKey(rootRunId, id);
|
|
58
|
+
const stream = `mcp-worker-${admissionKey?.slice(-32) ?? randomUUID()}`;
|
|
57
59
|
const instruction = JSON.stringify({ type: 'mcp', server, tool, input: args });
|
|
58
60
|
const peer = journal.createPeer();
|
|
59
61
|
let diagnostic: string | undefined;
|
|
@@ -132,11 +134,11 @@ export async function runMcpEffect(
|
|
|
132
134
|
failed(new Error('MCP worker dispatch deadline exceeded'));
|
|
133
135
|
}, 30_000);
|
|
134
136
|
// Propagate a dispatch failure even if run.start itself is still pending.
|
|
135
|
-
const starting = journal.runStart(spec);
|
|
137
|
+
const starting = journal.runStart(spec, undefined, admissionKey);
|
|
136
138
|
await Promise.race([starting, completed]);
|
|
137
139
|
const outcome = await starting;
|
|
138
140
|
childRunId = outcome.run_id;
|
|
139
|
-
await completed;
|
|
141
|
+
if (outcome.status !== 'completed') await completed;
|
|
140
142
|
if (diagnostic !== undefined) {
|
|
141
143
|
try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps); }
|
|
142
144
|
catch (error) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
3
|
+
import { readFileSync, writeSync } from 'node:fs';
|
|
4
|
+
import { JournalClient } from './journal-client.js';
|
|
5
|
+
import { executeAuthoredFlow } from './authored-flow-executor.js';
|
|
6
|
+
import { loadPinnedAuthoredSource } from './authored-source-authority.js';
|
|
7
|
+
import { assertAuthoredNodeVersion, parseAuthoredParentPid } from './authored-runtime-capability.js';
|
|
8
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
9
|
+
import type { AuthoredRootMetadata } from './authored-root.js';
|
|
10
|
+
|
|
11
|
+
let channelKey: string | undefined, sequence = 0;
|
|
12
|
+
// Capture writers before loading authored modules; credentials never enter env.
|
|
13
|
+
const writeFrame = writeSync, mac = createHmac;
|
|
14
|
+
const send = (message: unknown): void => {
|
|
15
|
+
const payload = JSON.stringify(message);
|
|
16
|
+
if (channelKey === undefined) { writeFrame(3, payload + '\n'); return; }
|
|
17
|
+
const seq = ++sequence;
|
|
18
|
+
writeFrame(3, JSON.stringify({ seq, payload,
|
|
19
|
+
mac: mac('sha256', channelKey).update(`${seq}\0${payload}`).digest('hex') }) + '\n');
|
|
20
|
+
};
|
|
21
|
+
const hash = (path: string): string => createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
let finished = false;
|
|
24
|
+
const abort = (): void => {
|
|
25
|
+
controller.abort();
|
|
26
|
+
setTimeout(() => process.exit(1), 2000).unref();
|
|
27
|
+
};
|
|
28
|
+
process.on('SIGINT', abort); process.on('SIGTERM', abort);
|
|
29
|
+
// The parent owns the root lease. Do not continue authored effects after it dies.
|
|
30
|
+
process.stdin.on('end', () => { if (!finished) process.exit(1); });
|
|
31
|
+
process.stdin.on('error', () => process.exit(1));
|
|
32
|
+
// A separate event loop enforces parent loss even while authored JS is blocked.
|
|
33
|
+
// Capture the expected PID in the parent's spawn arguments, before any child work.
|
|
34
|
+
const parentPid = parseAuthoredParentPid(process.argv[2]);
|
|
35
|
+
const watchdog = new Worker(`
|
|
36
|
+
const {parentPort,workerData}=require('node:worker_threads');
|
|
37
|
+
function check(){if(process.ppid!==workerData.parentPid)process.kill(process.pid,'SIGKILL');}
|
|
38
|
+
check();setInterval(check,50);parentPort.postMessage('ready');
|
|
39
|
+
`, { eval: true, workerData: { parentPid } });
|
|
40
|
+
// Enforcement must not silently disappear after the readiness Promise settles.
|
|
41
|
+
const watchdogLost = (): void => { if (!finished) process.kill(process.pid, 'SIGKILL'); };
|
|
42
|
+
watchdog.on('error', watchdogLost);
|
|
43
|
+
watchdog.on('exit', watchdogLost);
|
|
44
|
+
let client: JournalClient | undefined;
|
|
45
|
+
try {
|
|
46
|
+
assertAuthoredNodeVersion();
|
|
47
|
+
await new Promise<void>((resolve,reject)=>{watchdog.once('message',()=>resolve());watchdog.once('error',reject);});
|
|
48
|
+
send({ type: 'ready', runtime: { kind: 'node', version: process.versions.node,
|
|
49
|
+
executableSha256: hash(process.execPath), payloadSha256: hash(process.argv[1]!) } });
|
|
50
|
+
const request = await new Promise<{ channelKey: string; metadata: AuthoredRootMetadata; socketPath: string;
|
|
51
|
+
rootRunId: string; dataDir: string; localAgentStream?: string }>((resolve, reject) => {
|
|
52
|
+
let buffer = '';
|
|
53
|
+
process.stdin.setEncoding('utf8');
|
|
54
|
+
const onData = (chunk: string): void => {
|
|
55
|
+
buffer += chunk;
|
|
56
|
+
if (Buffer.byteLength(buffer) > 2 * 1024 * 1024) { reject(new Error('authored runtime request exceeded limit')); return; }
|
|
57
|
+
const end = buffer.indexOf('\n');
|
|
58
|
+
if (end < 0) return;
|
|
59
|
+
process.stdin.off('data', onData);
|
|
60
|
+
try { resolve(JSON.parse(buffer.slice(0, end))); } catch { reject(new Error('invalid authored runtime request')); }
|
|
61
|
+
};
|
|
62
|
+
process.stdin.on('data', onData);
|
|
63
|
+
});
|
|
64
|
+
if (typeof request.channelKey !== 'string' || !/^[a-f0-9]{64}$/.test(request.channelKey)) throw new Error('invalid authored channel key');
|
|
65
|
+
channelKey = request.channelKey;
|
|
66
|
+
controller.signal.throwIfAborted();
|
|
67
|
+
const loaded = await loadPinnedAuthoredSource(request.metadata, true);
|
|
68
|
+
if (request.localAgentStream !== request.metadata.localAgentStream) throw new Error('authored root local agent surface mismatch');
|
|
69
|
+
client = new JournalClient(request.socketPath);
|
|
70
|
+
await client.connect(); await client.hello('flows-authored-node');
|
|
71
|
+
const result = await executeAuthoredFlow(loaded.handle, client,
|
|
72
|
+
request.metadata.inputPresent ? request.metadata.input : undefined, {
|
|
73
|
+
getDefinition: loaded.getDefinition, dataDir: request.dataDir,
|
|
74
|
+
flowPath: request.metadata.flowPath, rootRunId: request.rootRunId,
|
|
75
|
+
localAgentStream: request.localAgentStream, signal: controller.signal,
|
|
76
|
+
onProgress: event => send({ type: 'progress', event }),
|
|
77
|
+
onWait: event => send({ type: 'wait', event }),
|
|
78
|
+
});
|
|
79
|
+
send({ type: 'result', result });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const message = error instanceof Error ? error.message : 'authored body failed';
|
|
82
|
+
const prefix = error instanceof AuthoredFlowExecutionError ? `${error.code}: ` : '';
|
|
83
|
+
send({ type: 'error', message: prefix && message.startsWith(prefix) ? message.slice(prefix.length) : message,
|
|
84
|
+
...(error instanceof AuthoredFlowExecutionError ? { code: error.code,
|
|
85
|
+
completionReason: error.completionReason, runId: error.runId } : {}) });
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
} finally {
|
|
88
|
+
finished = true; await watchdog.terminate(); client?.close(); process.stdin.destroy();
|
|
89
|
+
process.off('SIGINT', abort); process.off('SIGTERM', abort);
|
|
90
|
+
}
|