@relayflows/sdk 2.0.55 → 2.0.56
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/authored-broker.d.ts +13 -0
- package/dist/authored-broker.d.ts.map +1 -0
- package/dist/authored-broker.js +75 -0
- package/dist/authored-broker.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 +8 -2
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +6 -0
- 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 +11 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +104 -20
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +42 -3
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +1 -1
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-hooks.d.ts +2 -1
- package/dist/authored-hooks.d.ts.map +1 -1
- package/dist/authored-hooks.js +2 -2
- package/dist/authored-hooks.js.map +1 -1
- package/dist/authored-human.d.ts +18 -0
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +32 -4
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +3 -2
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.js +4 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +1 -0
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +3 -2
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-preflight.d.ts.map +1 -1
- package/dist/authored-preflight.js +2 -1
- package/dist/authored-preflight.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +13 -3
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-serial-ordinals.d.ts +41 -0
- package/dist/authored-serial-ordinals.d.ts.map +1 -0
- package/dist/authored-serial-ordinals.js +45 -0
- package/dist/authored-serial-ordinals.js.map +1 -0
- package/dist/authored-worker-step.d.ts +5 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +19 -2
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +11 -2
- package/dist/budget-preflight.js.map +1 -1
- package/dist/bundle-transport.d.ts +2 -0
- package/dist/bundle-transport.d.ts.map +1 -1
- package/dist/bundle-transport.js +4 -0
- package/dist/bundle-transport.js.map +1 -1
- package/dist/cached-flow-runtime.d.ts +3 -0
- package/dist/cached-flow-runtime.d.ts.map +1 -0
- package/dist/cached-flow-runtime.js +37 -0
- package/dist/cached-flow-runtime.js.map +1 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +13 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-deploy.js +6 -5
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-mirror-session.d.ts +8 -2
- package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
- package/dist/cli/cloud-mirror-session.js +20 -27
- package/dist/cli/cloud-mirror-session.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +2 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +19 -4
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +39 -18
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +3 -3
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +15 -9
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-flow-source.d.ts +13 -0
- package/dist/cloud-flow-source.d.ts.map +1 -0
- package/dist/cloud-flow-source.js +55 -0
- package/dist/cloud-flow-source.js.map +1 -0
- package/dist/cloud-mirror-transport.d.ts +2 -0
- package/dist/cloud-mirror-transport.d.ts.map +1 -1
- package/dist/cloud-mirror-transport.js +2 -0
- package/dist/cloud-mirror-transport.js.map +1 -1
- package/dist/cloud-mirror.d.ts.map +1 -1
- package/dist/cloud-mirror.js +10 -1
- package/dist/cloud-mirror.js.map +1 -1
- package/dist/communication/broker-preflight.d.ts +3 -0
- package/dist/communication/broker-preflight.d.ts.map +1 -0
- package/dist/communication/broker-preflight.js +22 -0
- package/dist/communication/broker-preflight.js.map +1 -0
- package/dist/communication/broker-spec.d.ts +20 -0
- package/dist/communication/broker-spec.d.ts.map +1 -0
- package/dist/communication/broker-spec.js +46 -0
- package/dist/communication/broker-spec.js.map +1 -0
- package/dist/communication/broker-worker.d.ts +17 -0
- package/dist/communication/broker-worker.d.ts.map +1 -0
- package/dist/communication/broker-worker.js +214 -0
- package/dist/communication/broker-worker.js.map +1 -0
- package/dist/communication/pty.d.ts +29 -0
- package/dist/communication/pty.d.ts.map +1 -0
- package/dist/communication/pty.js +40 -0
- package/dist/communication/pty.js.map +1 -0
- package/dist/communication/relay.d.ts +18 -0
- package/dist/communication/relay.d.ts.map +1 -1
- package/dist/communication/relay.js +4 -0
- package/dist/communication/relay.js.map +1 -1
- package/dist/communication/skills.d.ts +3 -0
- package/dist/communication/skills.d.ts.map +1 -0
- package/dist/communication/skills.js +17 -0
- package/dist/communication/skills.js.map +1 -0
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +9 -26
- package/dist/communication/worker.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +2 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/plugin-manifest.js +1 -1
- package/dist/plugin-manifest.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +2 -1
- package/dist/preflight.js.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +12 -0
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-broker.ts +73 -0
- package/src/authored-budget.ts +6 -2
- package/src/authored-flow-error.ts +6 -0
- package/src/authored-flow-executor.ts +115 -20
- package/src/authored-flow-loader.ts +43 -3
- package/src/authored-helper-effect.ts +2 -1
- package/src/authored-hooks.ts +4 -3
- package/src/authored-human.ts +35 -4
- package/src/authored-mcp.ts +7 -2
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +1 -0
- package/src/authored-plugin-effect.ts +7 -2
- package/src/authored-preflight.ts +2 -1
- package/src/authored-root.ts +13 -3
- package/src/authored-serial-ordinals.ts +76 -0
- package/src/authored-worker-step.ts +17 -2
- package/src/budget-preflight.ts +9 -1
- package/src/bundle-transport.ts +4 -0
- package/src/cached-flow-runtime.ts +32 -0
- package/src/cli/check.ts +11 -1
- package/src/cli/cloud-deploy.ts +5 -5
- package/src/cli/cloud-mirror-session.ts +23 -29
- package/src/cli/direct-run.ts +2 -1
- package/src/cli/run.ts +18 -3
- package/src/cli.ts +31 -13
- package/src/cloud-deploy.ts +19 -14
- package/src/cloud-flow-source.ts +58 -0
- package/src/cloud-mirror-transport.ts +4 -0
- package/src/cloud-mirror.ts +9 -1
- package/src/communication/broker-preflight.ts +22 -0
- package/src/communication/broker-spec.ts +48 -0
- package/src/communication/broker-worker.ts +198 -0
- package/src/communication/pty.ts +47 -0
- package/src/communication/relay.ts +12 -1
- package/src/communication/skills.ts +16 -0
- package/src/communication/worker.ts +9 -26
- package/src/failure-kinds.ts +2 -0
- package/src/plugin-manifest.ts +1 -1
- package/src/preflight.ts +3 -1
- package/src/worker.ts +11 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { AgentOptions, AgentResult, NamedGate } from '@relayflows/surface';
|
|
3
|
+
import { parseAgentStepTimeout } from './compile.js';
|
|
4
|
+
import type { StepSpec } from './spec.js';
|
|
5
|
+
import { BROKER_TYPE, brokerAgentName, brokerOptionsError, type BrokerInstruction } from './communication/broker-spec.js';
|
|
6
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
7
|
+
|
|
8
|
+
type Declaration = { instruction: BrokerInstruction; cli?: string; model?: string; secrets?: string[] };
|
|
9
|
+
|
|
10
|
+
/** Broker options stay in a journaled instruction; the kernel only sees agent steps. */
|
|
11
|
+
export function authoredBroker(definition: { name: string }, rootRunId: string | undefined,
|
|
12
|
+
localStream: string | undefined, run: (step: StepSpec) => Promise<unknown>) {
|
|
13
|
+
const owner = rootRunId ?? randomUUID();
|
|
14
|
+
const declarations = new Map<string, Declaration>();
|
|
15
|
+
const invalid = (message: string): never => { throw new AuthoredFlowExecutionError('agent_cli_unresolved', message); };
|
|
16
|
+
const execute = async (id: string, declaration: Declaration, verification?: NamedGate): Promise<AgentResult> => {
|
|
17
|
+
const { instruction, cli, model, secrets } = declaration;
|
|
18
|
+
const output = await run({ id, type: 'agent', instruction: JSON.stringify(instruction), cli, model,
|
|
19
|
+
timeoutMs: instruction.timeoutMs,
|
|
20
|
+
// Retained sessions must return to the same worker, including after a child completes.
|
|
21
|
+
surfaces: localStream === undefined ? undefined : { streams: [{ stream: localStream }] },
|
|
22
|
+
maxIterations: 1, transportRetries: 0,
|
|
23
|
+
...(secrets?.length ? { secrets } : {}),
|
|
24
|
+
...(verification === undefined ? {} : { verification }) });
|
|
25
|
+
if (!output || typeof output !== 'object' || !('completionReason' in output)) {
|
|
26
|
+
throw new AuthoredFlowExecutionError('journal_protocol_violation', 'Broker step produced an invalid result');
|
|
27
|
+
}
|
|
28
|
+
return output as AgentResult;
|
|
29
|
+
};
|
|
30
|
+
const lookup = (name: string): Declaration => declarations.get(name) ?? invalid(`No retained broker agent named ${name} in this flow`);
|
|
31
|
+
return {
|
|
32
|
+
async agent(id: string, name: string, options: AgentOptions, verification?: NamedGate,
|
|
33
|
+
secrets?: string[]): Promise<AgentResult> {
|
|
34
|
+
const problem = brokerOptionsError(options);
|
|
35
|
+
if (problem) invalid(`f.agent options: ${problem}`);
|
|
36
|
+
if (options.keepAlive && localStream === undefined) invalid('Retained broker agents require a local agent worker stream');
|
|
37
|
+
if (options.keepAlive && verification !== undefined) invalid('Retained broker agents do not support gates; validate their result in a later step');
|
|
38
|
+
if (typeof options.task !== 'string' || !options.task.trim()) invalid('f.agent task must be nonempty');
|
|
39
|
+
if (options.workspace !== undefined || options.permissions !== undefined) invalid('Broker agents use cwd; workspace and permissions are not supported');
|
|
40
|
+
if ((options.maxIterations ?? 1) !== 1 || (options.transportRetries ?? 0) !== 0
|
|
41
|
+
|| options.recoveryMode !== undefined) invalid('Broker sessions require maxIterations 1, transportRetries 0, and no recoveryMode');
|
|
42
|
+
if (typeof name !== 'string' || !name.trim()) invalid('Broker agent name must be nonempty');
|
|
43
|
+
const instruction: BrokerInstruction = { type: BROKER_TYPE, operation: 'spawn', owner,
|
|
44
|
+
flow: definition.name, step: name, agentName: brokerAgentName(definition.name, name, owner, id),
|
|
45
|
+
task: options.task, cwd: options.cwd, skills: options.skills ?? [],
|
|
46
|
+
channel: options.channel ?? `flow-${owner}`, keepAlive: options.keepAlive ?? false,
|
|
47
|
+
timeoutMs: options.timeout === undefined ? 3_600_000 : parseAgentStepTimeout(options.timeout) };
|
|
48
|
+
const declaration = { instruction, cli: options.cli, model: options.model,
|
|
49
|
+
...(secrets?.length ? { secrets } : {}) };
|
|
50
|
+
const result = await execute(id, declaration, verification);
|
|
51
|
+
if (instruction.keepAlive && result.completionReason === 'success') declarations.set(instruction.agentName, declaration);
|
|
52
|
+
return result;
|
|
53
|
+
},
|
|
54
|
+
async message(id: string, name: string, text: string, options?: { timeout?: string | number },
|
|
55
|
+
verification?: NamedGate): Promise<AgentResult> {
|
|
56
|
+
const declaration = lookup(name);
|
|
57
|
+
if (typeof text !== 'string' || !text.trim()) invalid('f.message text must be nonempty');
|
|
58
|
+
const result = await execute(id, { ...declaration, instruction: { ...declaration.instruction,
|
|
59
|
+
operation: 'message', task: text, timeoutMs: options?.timeout === undefined
|
|
60
|
+
? declaration.instruction.timeoutMs : parseAgentStepTimeout(options.timeout) } }, verification);
|
|
61
|
+
return result;
|
|
62
|
+
},
|
|
63
|
+
async release(id: string, name: string): Promise<void> {
|
|
64
|
+
const declaration = lookup(name);
|
|
65
|
+
await run({ id, type: 'agent', cli: declaration.cli, model: declaration.model,
|
|
66
|
+
instruction: JSON.stringify({ ...declaration.instruction, operation: 'release' }),
|
|
67
|
+
surfaces: localStream === undefined ? undefined : { streams: [{ stream: localStream }] },
|
|
68
|
+
...(declaration.secrets?.length ? { secrets: declaration.secrets } : {}),
|
|
69
|
+
maxIterations: 1, transportRetries: 0 });
|
|
70
|
+
// Keep the declaration so an explicit release remains idempotent.
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/authored-budget.ts
CHANGED
|
@@ -109,13 +109,17 @@ export class AuthoredBudget {
|
|
|
109
109
|
|
|
110
110
|
async execute<T>(journal: JournalClient, spec: KernelRunSpec,
|
|
111
111
|
consume: (outcome: RunOutcome) => Promise<T>, admissionKey?: string,
|
|
112
|
-
isolatedStart = false): Promise<T> {
|
|
112
|
+
isolatedStart = false, cleanup = false): Promise<T> {
|
|
113
113
|
if (this.limit === undefined) return consume(await journal.runStart(spec, undefined, admissionKey, false, isolatedStart));
|
|
114
114
|
const previous = this.tail;
|
|
115
115
|
let release!: () => void;
|
|
116
116
|
this.tail = new Promise<void>(resolve => { release = resolve; });
|
|
117
117
|
await previous;
|
|
118
118
|
try {
|
|
119
|
+
// A broker release stays journaled after a failed gate or an exhausted
|
|
120
|
+
// header. It carries no budget, so the kernel cannot refuse it, and it
|
|
121
|
+
// does not latch another failure over the one that required cleanup.
|
|
122
|
+
if (cleanup) return await consume(await journal.runStart(spec, undefined, admissionKey, false, isolatedStart));
|
|
119
123
|
if (this.failed) throw new AuthoredFlowExecutionError('step_failed', 'A prior budgeted step did not finish successfully.');
|
|
120
124
|
// Window selection follows journal timestamps, never the SDK host clock.
|
|
121
125
|
const day = this.charges.at(-1)?.day;
|
|
@@ -162,7 +166,7 @@ export class AuthoredBudget {
|
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
} catch (error) {
|
|
165
|
-
this.failed = true;
|
|
169
|
+
if (!cleanup) this.failed = true;
|
|
166
170
|
throw error;
|
|
167
171
|
} finally { release(); }
|
|
168
172
|
}
|
|
@@ -58,6 +58,12 @@ export type AuthoredFlowExecutionErrorCode =
|
|
|
58
58
|
| 'unsupported_header'
|
|
59
59
|
| 'unsettled_derived_work'
|
|
60
60
|
| 'unsupported_promise_lifecycle'
|
|
61
|
+
/**
|
|
62
|
+
* A resumed serial body reached a journaled step id with a different call:
|
|
63
|
+
* concurrent branches resolved in another order than the journaled run.
|
|
64
|
+
* Refused before the step is admitted (authored-serial-ordinals.ts).
|
|
65
|
+
*/
|
|
66
|
+
| 'serial_replay_order_mismatch'
|
|
61
67
|
/** A pinned parallel root cannot dispatch safely in this process environment. */
|
|
62
68
|
| 'unsafe_parallel_runtime'
|
|
63
69
|
| 'unsupported_workspace_permission'
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
AuthoredHumanParked,
|
|
49
49
|
type AuthoredFlowExecutionErrorCode,
|
|
50
50
|
} from './authored-flow-error.js';
|
|
51
|
-
import {
|
|
51
|
+
import { readHumanWait } from './authored-human.js';
|
|
52
52
|
import { parseHumanTo } from './human-to.js';
|
|
53
53
|
import {
|
|
54
54
|
AuthoredFlowOperation,
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
import { AuthoredFlowLifecycle, parallelPromiseIntrinsicsIntact } from './authored-flow-lifecycle.js';
|
|
59
59
|
import { AuthoredActivities } from './authored-activity.js';
|
|
60
60
|
import { displayLabel, type AuthoredStepEdges } from './authored-step-index.js';
|
|
61
|
+
import { assertSerialOrdinal, serialOrdinals, serialReplayOrderMismatch, type SerialOrdinals } from './authored-serial-ordinals.js';
|
|
61
62
|
import { JournalClient } from './journal-client.js';
|
|
62
63
|
import { PluginError } from './plugin-manifest.js';
|
|
63
64
|
import { createHookEvaluator } from './authored-hooks.js';
|
|
@@ -209,6 +210,15 @@ export interface ExecuteAuthoredFlowOptions {
|
|
|
209
210
|
readonly flowGraph?: readonly LoadedAuthoredFlowNode[];
|
|
210
211
|
/** Internal namespace for descendant step/admission identities. */
|
|
211
212
|
readonly stepPrefix?: string;
|
|
213
|
+
/**
|
|
214
|
+
* The step ids the root's index held when this attempt began, read by the
|
|
215
|
+
* root driver on a re-dispatch and shared with dispatched children, so a
|
|
216
|
+
* serial body is checked against the journal it is replaying
|
|
217
|
+
* (authored-serial-ordinals.ts). Absent on a first attempt: nothing to replay.
|
|
218
|
+
*/
|
|
219
|
+
readonly journaledSteps?: readonly string[];
|
|
220
|
+
/** Root-owned replay refusal, shared with dispatched children so one refusal stops the whole tree. */
|
|
221
|
+
readonly replayLatch?: { violation?: AuthoredFlowExecutionError };
|
|
212
222
|
/** Current child depth. Root is zero; dispatch is capped at three. */
|
|
213
223
|
readonly dispatchDepth?: number;
|
|
214
224
|
/** Parent predecessors inherited by the first operation in a child body. */
|
|
@@ -327,10 +337,42 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
327
337
|
const assertParallelRunOnly = (kind: string): void => {
|
|
328
338
|
if (options.executionMode === 'parallel') rejectParallel(`parallel f.run body attempted ${kind} before admission`);
|
|
329
339
|
};
|
|
340
|
+
// Serial bodies only: parallel ids come from the causal frontier, not one
|
|
341
|
+
// shared counter. A refusal is latched, so an author `catch` around the
|
|
342
|
+
// call cannot let the body go on to admit anything else.
|
|
343
|
+
const journaledSteps = options.executionMode === 'parallel' || options.rootRunId === undefined
|
|
344
|
+
? undefined : options.journaledSteps;
|
|
345
|
+
const ordinals: SerialOrdinals | undefined = journaledSteps === undefined
|
|
346
|
+
? undefined : serialOrdinals(journaledSteps, options.stepPrefix ?? '');
|
|
347
|
+
const replayLatch = options.replayLatch ?? {};
|
|
348
|
+
const refuseReplay = (error: AuthoredFlowExecutionError): never => {
|
|
349
|
+
replayLatch.violation ??= error;
|
|
350
|
+
throw replayLatch.violation;
|
|
351
|
+
};
|
|
352
|
+
const assertNoReplayViolation = (): void => {
|
|
353
|
+
if (replayLatch.violation !== undefined) throw replayLatch.violation;
|
|
354
|
+
};
|
|
355
|
+
// Every operation re-checks at start: a step declared before the refusal
|
|
356
|
+
// and awaited after it is refused too, not admitted.
|
|
357
|
+
const assertStartAllowed = (verb: string): void => {
|
|
358
|
+
assertNoReplayViolation();
|
|
359
|
+
assertOperationAllowed(verb, definition.name, requestedCompletion);
|
|
360
|
+
};
|
|
361
|
+
const claimedOrdinals = new Set<number>();
|
|
362
|
+
const serialId = (kind: string, sequence: number): string => {
|
|
363
|
+
assertNoReplayViolation();
|
|
364
|
+
claimedOrdinals.add(sequence);
|
|
365
|
+
if (ordinals !== undefined) {
|
|
366
|
+
try { assertSerialOrdinal(ordinals, kind, sequence); } catch (error) {
|
|
367
|
+
refuseReplay(error as AuthoredFlowExecutionError);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return qualify(`${kind}-${sequence}`);
|
|
371
|
+
};
|
|
330
372
|
const nextId = (kind: string): string => {
|
|
331
373
|
if (parallelViolation !== undefined) throw parallelViolation;
|
|
374
|
+
if (options.executionMode !== 'parallel') return serialId(kind, nextStep++);
|
|
332
375
|
const sequence = nextStep++;
|
|
333
|
-
if (options.executionMode !== 'parallel') return qualify(`${kind}-${sequence}`);
|
|
334
376
|
if (kind !== 'run') rejectParallel(`parallel f.run body attempted ${kind} before admission`);
|
|
335
377
|
if (!lifecycle.combinatorsObserved()) rejectParallel(
|
|
336
378
|
'cannot assign a replay-stable f.run identity: a Promise intrinsic was replaced');
|
|
@@ -486,7 +528,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
486
528
|
let llmOp!: AuthoredFlowOperation<unknown>;
|
|
487
529
|
llmOp = new AuthoredFlowOperation(
|
|
488
530
|
id, 'llm',
|
|
489
|
-
() =>
|
|
531
|
+
() => assertStartAllowed('llm'),
|
|
490
532
|
() => observeStep(id, 'llm', () => {
|
|
491
533
|
if (typeof prompt === 'string') {
|
|
492
534
|
if (values.length !== 1 || values[0] === undefined) {
|
|
@@ -532,7 +574,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
532
574
|
let recordOp!: AuthoredFlowOperation<RunResult>;
|
|
533
575
|
recordOp = new AuthoredFlowOperation<RunResult>(
|
|
534
576
|
id, 'run',
|
|
535
|
-
() =>
|
|
577
|
+
() => assertStartAllowed('run'),
|
|
536
578
|
() => observeStep(id, 'deterministic',
|
|
537
579
|
() => lowerDeterministic.recording(id, command, leaseMs, recordOp.namedGate, secrets), options.onProgress),
|
|
538
580
|
lifecycle,
|
|
@@ -545,7 +587,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
545
587
|
let runOp!: AuthoredFlowOperation<string>;
|
|
546
588
|
runOp = new AuthoredFlowOperation<string>(
|
|
547
589
|
id, 'run',
|
|
548
|
-
() =>
|
|
590
|
+
() => assertStartAllowed('run'),
|
|
549
591
|
() => observeStep(id, 'deterministic', () => lowerDeterministic(id, command, false, leaseMs, runOp.namedGate, secrets), options.onProgress),
|
|
550
592
|
lifecycle,
|
|
551
593
|
// No label: a command is not a display name. It carries literal
|
|
@@ -561,10 +603,10 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
561
603
|
assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion);
|
|
562
604
|
assertHelperAllowed(call.provider);
|
|
563
605
|
const snapshot = snapshotJsonValue(call, 'f.slack call') as unknown as SlackCall;
|
|
564
|
-
const id =
|
|
606
|
+
const id = serialId(`slack-${slackRun}`, nextStep++);
|
|
565
607
|
return trackStep(authoredSteps, new AuthoredFlowOperation<T>(
|
|
566
608
|
id, `slack.${call.verb}`,
|
|
567
|
-
() =>
|
|
609
|
+
() => assertStartAllowed(`slack.${call.verb}`),
|
|
568
610
|
async () => {
|
|
569
611
|
const receipt = await runHelperEffect(journal, definition.name, id, snapshot,
|
|
570
612
|
options.dataDir ?? dirname(journal.socketPath), journalSteps, options.rootRunId);
|
|
@@ -581,7 +623,22 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
581
623
|
declared: definition.header?.hooks ?? [],
|
|
582
624
|
extensions: options.extensions ?? [],
|
|
583
625
|
peekStep: () => nextStep,
|
|
584
|
-
restoreStep: (step) => {
|
|
626
|
+
restoreStep: (step, hook) => {
|
|
627
|
+
// A replayed verdict jumps the counter to the watermark it recorded,
|
|
628
|
+
// which also counts ordinals other branches took while the hook ran.
|
|
629
|
+
// Skipping one of those would hand that branch's completed step a fresh
|
|
630
|
+
// ordinal and run it again, so a jump over a journaled ordinal this
|
|
631
|
+
// attempt has not claimed is refused. (It also refuses skipping the
|
|
632
|
+
// hook's own steps; resuming past those already fails at the kernel.)
|
|
633
|
+
for (let ordinal = nextStep; ordinals !== undefined && ordinal < step; ordinal++) {
|
|
634
|
+
const journaled = ordinals.get(ordinal)?.[0];
|
|
635
|
+
if (journaled !== undefined && !claimedOrdinals.has(ordinal)) {
|
|
636
|
+
refuseReplay(serialReplayOrderMismatch(
|
|
637
|
+
`hook step "${hook}" would skip ordinal ${ordinal}, journaled as step "${journaled.step}"`));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
nextStep = step;
|
|
641
|
+
},
|
|
585
642
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
586
643
|
});
|
|
587
644
|
|
|
@@ -592,9 +649,9 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
592
649
|
assertOperationAllowed(verb, definition.name, requestedCompletion);
|
|
593
650
|
assertHelperAllowed(call.provider);
|
|
594
651
|
const snapshot = snapshotJsonValue(call, `f.${verb} call`) as unknown as HelperCall;
|
|
595
|
-
const id =
|
|
652
|
+
const id = serialId(`${call.provider}-${slackRun}`, nextStep++);
|
|
596
653
|
return trackStep(authoredSteps, new AuthoredFlowOperation<T>(id, verb,
|
|
597
|
-
() =>
|
|
654
|
+
() => assertStartAllowed(verb),
|
|
598
655
|
async () => await runHelperEffect(journal, definition.name, id, snapshot,
|
|
599
656
|
options.dataDir ?? dirname(journal.socketPath), journalSteps, options.rootRunId) as T,
|
|
600
657
|
lifecycle));
|
|
@@ -610,7 +667,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
610
667
|
const id = nextId('mcp');
|
|
611
668
|
return trackStep(authoredSteps, new AuthoredFlowOperation(
|
|
612
669
|
id, 'mcp',
|
|
613
|
-
() =>
|
|
670
|
+
() => assertStartAllowed('mcp'),
|
|
614
671
|
() => runMcpEffect(journal, definition.name, id, server, tool, args,
|
|
615
672
|
known, checkedMcp.servers[server]!, journalSteps, options.rootRunId),
|
|
616
673
|
lifecycle,
|
|
@@ -618,7 +675,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
618
675
|
}),
|
|
619
676
|
memory: authoredMemory(
|
|
620
677
|
scriptMemoryScope(flowPath, definition.name),
|
|
621
|
-
() => { assertParallelRunOnly('memory'); assertOperationAllowed('memory', definition.name, requestedCompletion); },
|
|
678
|
+
() => { assertParallelRunOnly('memory'); assertNoReplayViolation(); assertOperationAllowed('memory', definition.name, requestedCompletion); },
|
|
622
679
|
definition.header.memory?.script !== false,
|
|
623
680
|
memoryWrites,
|
|
624
681
|
options.rootRunId === undefined ? undefined : journalLearnLedger(journal, options.rootRunId),
|
|
@@ -634,15 +691,41 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
634
691
|
agentOp = new AuthoredFlowOperation<AgentResult>(
|
|
635
692
|
id,
|
|
636
693
|
'agent',
|
|
637
|
-
() =>
|
|
638
|
-
() => observeStep(id, 'agent', () =>
|
|
694
|
+
() => assertStartAllowed('agent'),
|
|
695
|
+
() => observeStep(id, 'agent', () => {
|
|
696
|
+
if (options.transport === 'broker' && options.keepAlive
|
|
697
|
+
&& (agentOp.namedGate !== undefined || agentOp.predicateGate !== undefined)) {
|
|
698
|
+
throw new AuthoredFlowExecutionError('agent_cli_unresolved',
|
|
699
|
+
'Retained broker agents do not support gates; validate their result in a later step');
|
|
700
|
+
}
|
|
701
|
+
return worker.agent(id, options, agentOp.namedGate, name);
|
|
702
|
+
}, onProgress),
|
|
639
703
|
lifecycle,
|
|
640
704
|
displayLabel(typeof name === 'string' ? name : undefined),
|
|
641
705
|
);
|
|
642
706
|
return trackStep(authoredSteps, agentOp);
|
|
643
707
|
},
|
|
708
|
+
message(agentName, text, messageOptions) {
|
|
709
|
+
assertOperationAllowed('message', definition.name, requestedCompletion);
|
|
710
|
+
const id = nextId('agent');
|
|
711
|
+
let messageOp!: AuthoredFlowOperation<AgentResult>;
|
|
712
|
+
messageOp = new AuthoredFlowOperation<AgentResult>(id, 'message',
|
|
713
|
+
() => assertStartAllowed('message'),
|
|
714
|
+
() => observeStep(id, 'agent',
|
|
715
|
+
() => worker.message(id, agentName, text, messageOptions, messageOp.namedGate), onProgress),
|
|
716
|
+
lifecycle);
|
|
717
|
+
return trackStep(authoredSteps, messageOp);
|
|
718
|
+
},
|
|
719
|
+
release(agentName) {
|
|
720
|
+
assertOperationAllowed('release', definition.name, requestedCompletion);
|
|
721
|
+
const id = nextId('agent');
|
|
722
|
+
return trackStep(authoredSteps, new AuthoredFlowOperation<void>(id, 'release',
|
|
723
|
+
() => assertStartAllowed('release'),
|
|
724
|
+
() => observeStep(id, 'agent', () => worker.release(id, agentName), onProgress), lifecycle));
|
|
725
|
+
},
|
|
644
726
|
on(source, activityOptions) {
|
|
645
727
|
assertParallelRunOnly('on');
|
|
728
|
+
assertNoReplayViolation();
|
|
646
729
|
assertOperationAllowed('on', definition.name, requestedCompletion);
|
|
647
730
|
// Activity ids are root-scoped (`activity-N`); a child's own counter would collide.
|
|
648
731
|
if ((options.dispatchDepth ?? 0) > 0) {
|
|
@@ -697,7 +780,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
697
780
|
let humanOp!: AuthoredFlowOperation<boolean>;
|
|
698
781
|
humanOp = new AuthoredFlowOperation<boolean>(
|
|
699
782
|
id, 'human',
|
|
700
|
-
() =>
|
|
783
|
+
() => assertStartAllowed('human'),
|
|
701
784
|
() => observeStep(id, 'deterministic', async () => {
|
|
702
785
|
const rootRunId = options.rootRunId;
|
|
703
786
|
if (rootRunId === undefined) {
|
|
@@ -706,7 +789,15 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
706
789
|
`f.human needs a durable root to park in; "${id}" has no run to wait on. Run the flow with flows run.`,
|
|
707
790
|
);
|
|
708
791
|
}
|
|
709
|
-
const recorded = await
|
|
792
|
+
const { answer: recorded, asked } = await readHumanWait(journal, rootRunId, id);
|
|
793
|
+
// The answer is keyed by the wait id alone, and a reordered resume
|
|
794
|
+
// can hand that id to a different f.human: never apply an answer
|
|
795
|
+
// to a question nobody was asked.
|
|
796
|
+
if (asked.some(wait => wait.question !== question || wait.to !== to)) {
|
|
797
|
+
refuseReplay(serialReplayOrderMismatch(
|
|
798
|
+
`ordinal ${id.slice(id.lastIndexOf('-') + 1)} is journaled as human step "${id}" asking a `
|
|
799
|
+
+ 'different question or recipient, but this resume reached it with another f.human'));
|
|
800
|
+
}
|
|
710
801
|
if (recorded === undefined) throw new AuthoredHumanParked({ waitId: id, question, to }, rootRunId);
|
|
711
802
|
const record = { human: id, to, answer: recorded.answer,
|
|
712
803
|
...(recorded.note === undefined ? {} : { note: recorded.note }),
|
|
@@ -735,7 +826,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
735
826
|
dispatchOp = new AuthoredFlowOperation<DispatchResult>(
|
|
736
827
|
id,
|
|
737
828
|
'dispatch',
|
|
738
|
-
() =>
|
|
829
|
+
() => assertStartAllowed('dispatch'),
|
|
739
830
|
async () => {
|
|
740
831
|
const inheritedAfter = stepEdges(id)?.after ?? [];
|
|
741
832
|
const currentHelperAuthority = helperAuthority(definition);
|
|
@@ -743,6 +834,8 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
743
834
|
? currentHelperAuthority
|
|
744
835
|
: new Set([...currentHelperAuthority].filter(provider => options.allowedHelperProviders!.has(provider)));
|
|
745
836
|
const childResult = await executeAuthoredFlow(child.handle, journal, inputSnapshot, {
|
|
837
|
+
...(journaledSteps === undefined ? {} : { journaledSteps }),
|
|
838
|
+
replayLatch,
|
|
746
839
|
getDefinition: child.getDefinition,
|
|
747
840
|
dataDir: options.dataDir,
|
|
748
841
|
flowPath: child.path,
|
|
@@ -799,7 +892,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
799
892
|
const snapshot = snapshotJsonValue(input, 'f.hook input');
|
|
800
893
|
return trackStep(authoredSteps, new AuthoredFlowOperation<boolean>(
|
|
801
894
|
id, 'hook',
|
|
802
|
-
() =>
|
|
895
|
+
() => assertStartAllowed('hook'),
|
|
803
896
|
async () => {
|
|
804
897
|
const verdict = await evaluateHook(id, name, snapshot, context);
|
|
805
898
|
const record = { hook: name, step: id, verdict: verdict ? 'pass' : 'fail' };
|
|
@@ -862,7 +955,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
862
955
|
assertOperationAllowed(label, definition.name, requestedCompletion);
|
|
863
956
|
const id = nextId('plugin');
|
|
864
957
|
return trackStep(authoredSteps, new AuthoredFlowOperation(
|
|
865
|
-
id, label, () =>
|
|
958
|
+
id, label, () => assertStartAllowed(label),
|
|
866
959
|
() => runPluginEffect(journal, definition.name, id, plugin, verb, args,
|
|
867
960
|
journalSteps, budget, options.rootRunId),
|
|
868
961
|
lifecycle,
|
|
@@ -890,6 +983,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
890
983
|
// A source certificate is a performance admission decision, never a way
|
|
891
984
|
// for authored try/catch to turn a refused effect into successful output.
|
|
892
985
|
if (parallelViolation !== undefined) { bodyFailed = true; bodyFailure = parallelViolation; }
|
|
986
|
+
if (replayLatch.violation !== undefined) { bodyFailed = true; bodyFailure = replayLatch.violation; }
|
|
893
987
|
if (bodyFailed) {
|
|
894
988
|
// Refuse queued work immediately. A slow memory commit must not leave an
|
|
895
989
|
// admission slot open after the body has already failed.
|
|
@@ -982,7 +1076,8 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
982
1076
|
// explanation the root had already journaled to `run_admission_conflict`.
|
|
983
1077
|
// With no detail there is no environment in the command and nothing to
|
|
984
1078
|
// commit, so that path is untouched.
|
|
985
|
-
const terminalId =
|
|
1079
|
+
const terminalId = options.executionMode === 'parallel'
|
|
1080
|
+
? qualify(`complete-${nextStep}`) : serialId('complete', nextStep);
|
|
986
1081
|
const verdict = await commitAuthoredVerdict(journal, options.rootRunId, terminalId, {
|
|
987
1082
|
reason: requestedCompletion,
|
|
988
1083
|
...(requestedDetail === undefined ? {} : { detail: requestedDetail }),
|
|
@@ -311,14 +311,54 @@ export async function packageTreeSha256(root: string): Promise<string> {
|
|
|
311
311
|
}
|
|
312
312
|
await visit(root);
|
|
313
313
|
const hash = createHash('sha256');
|
|
314
|
-
|
|
315
|
-
const bytes = await readFile(path);
|
|
314
|
+
await readInOrder(files, SURFACE_READ_CONCURRENCY, (path, bytes) => {
|
|
316
315
|
const name = relative(root, path).split(sep).join('/');
|
|
317
316
|
hash.update(name).update('\0').update(String(bytes.length)).update('\0').update(bytes);
|
|
318
|
-
}
|
|
317
|
+
});
|
|
319
318
|
return hash.digest('hex');
|
|
320
319
|
}
|
|
321
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Surface is about 600 small files, and both the CLI and the authored Node
|
|
323
|
+
* child hash it on the startup path. Awaiting one read at a time paid a
|
|
324
|
+
* filesystem round trip per file; a sandbox disk made that several hundred ms.
|
|
325
|
+
*/
|
|
326
|
+
const SURFACE_READ_CONCURRENCY = 32;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Read `paths` through a window of at most `limit` files, started in order
|
|
330
|
+
* and handed to `consume` strictly in `paths` order, so the digest is the
|
|
331
|
+
* serial walk's. The window bounds buffered bytes as well as reads in flight:
|
|
332
|
+
* a slow early file stops new reads instead of letting later files pile up.
|
|
333
|
+
* After any read fails no new read starts. The lowest-index failure, the
|
|
334
|
+
* error a serial walk would meet first, is thrown once every started read
|
|
335
|
+
* has settled.
|
|
336
|
+
*/
|
|
337
|
+
async function readInOrder(
|
|
338
|
+
paths: readonly string[],
|
|
339
|
+
limit: number,
|
|
340
|
+
consume: (path: string, bytes: Buffer) => void,
|
|
341
|
+
): Promise<void> {
|
|
342
|
+
const reads: Array<Promise<Buffer>> = [];
|
|
343
|
+
let failed = false, next = 0;
|
|
344
|
+
const start = (): void => {
|
|
345
|
+
const read = readFile(paths[next++]!);
|
|
346
|
+
read.catch(() => { failed = true; });
|
|
347
|
+
reads.push(read);
|
|
348
|
+
};
|
|
349
|
+
while (next < paths.length && reads.length < limit) start();
|
|
350
|
+
for (let index = 0; reads.length !== 0; index += 1) {
|
|
351
|
+
let bytes: Buffer;
|
|
352
|
+
try { bytes = await reads[0]!; } catch (error) {
|
|
353
|
+
await Promise.allSettled(reads);
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
reads.shift();
|
|
357
|
+
consume(paths[index]!, bytes);
|
|
358
|
+
if (!failed && next < paths.length) start();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
322
362
|
// Exported for callers (internal SDK tests, and any co-located flow that is
|
|
323
363
|
// provably the same `@relayflows/surface` instance as this package) that
|
|
324
364
|
// have no need to resolve a separate copy.
|
|
@@ -51,7 +51,8 @@ export async function runHelperEffect(
|
|
|
51
51
|
const outcome = await journal.runStart(spec, undefined, admissionKey);
|
|
52
52
|
await atomicJson(join(dataDir, 'helper-runs', `${outcome.run_id}.json`), { provider: call.provider });
|
|
53
53
|
await driveHelperEffect(journal, outcome.run_id, spec.steps[0] as KernelAgentStep, call, dataDir);
|
|
54
|
-
const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps
|
|
54
|
+
const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps,
|
|
55
|
+
rootRunId === undefined ? {} : { rootRunId });
|
|
55
56
|
return (output as { receipt: unknown }).receipt;
|
|
56
57
|
}
|
|
57
58
|
|
package/src/authored-hooks.ts
CHANGED
|
@@ -32,7 +32,8 @@ export function createHookEvaluator(options: {
|
|
|
32
32
|
readonly declared: readonly string[];
|
|
33
33
|
readonly extensions: readonly LoadedFlowExtension[];
|
|
34
34
|
readonly peekStep?: () => number;
|
|
35
|
-
|
|
35
|
+
/** Restore the parent watermark a recorded verdict carries; `hook` is the replayed hook step. */
|
|
36
|
+
readonly restoreStep?: (step: number, hook: string) => void;
|
|
36
37
|
readonly signal?: AbortSignal;
|
|
37
38
|
}): (id: string, name: string, input: unknown, context: Ctx) => Promise<boolean> {
|
|
38
39
|
let recorded: Promise<Map<string, HookRecord>> | undefined;
|
|
@@ -83,7 +84,7 @@ export function createHookEvaluator(options: {
|
|
|
83
84
|
const existing = await lookup(`${id}:noop`);
|
|
84
85
|
const record = existing ?? { hook: name, step: id, plugin: null, verdict: 'noop' as const };
|
|
85
86
|
if (existing === undefined) await append(record);
|
|
86
|
-
else if (existing.afterStep !== undefined) options.restoreStep?.(existing.afterStep);
|
|
87
|
+
else if (existing.afterStep !== undefined) options.restoreStep?.(existing.afterStep, id);
|
|
87
88
|
return true;
|
|
88
89
|
}
|
|
89
90
|
for (const extension of impls) {
|
|
@@ -111,7 +112,7 @@ export function createHookEvaluator(options: {
|
|
|
111
112
|
};
|
|
112
113
|
await append(record);
|
|
113
114
|
} else if (record.afterStep !== undefined) {
|
|
114
|
-
options.restoreStep?.(record.afterStep);
|
|
115
|
+
options.restoreStep?.(record.afterStep, id);
|
|
115
116
|
}
|
|
116
117
|
if (record.verdict === 'fail') return false;
|
|
117
118
|
}
|
package/src/authored-human.ts
CHANGED
|
@@ -105,13 +105,44 @@ export async function readHumanAnswer(
|
|
|
105
105
|
runId: string,
|
|
106
106
|
waitId: string,
|
|
107
107
|
): Promise<HumanAnswer | undefined> {
|
|
108
|
+
return (await readHumanWait(journal, runId, waitId)).answer;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `readHumanAnswer`, plus every question the run has parked on under
|
|
113
|
+
* `waitId`. The answer is keyed by the wait id alone, so the executor checks
|
|
114
|
+
* that the question it is about to answer is the one that was asked.
|
|
115
|
+
*/
|
|
116
|
+
export async function readHumanWait(
|
|
117
|
+
journal: JournalClient,
|
|
118
|
+
runId: string,
|
|
119
|
+
waitId: string,
|
|
120
|
+
): Promise<{ answer?: HumanAnswer; asked: Array<{ question: unknown; to: unknown }> }> {
|
|
121
|
+
const asked: Array<{ question: unknown; to: unknown }> = [];
|
|
108
122
|
for (const entry of await readWaitEntries(journal, runId)) {
|
|
109
|
-
if (entry.
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
if (entry.payload?.wait_id !== waitId) continue;
|
|
124
|
+
if (entry.entry_type === 'wait.human') {
|
|
125
|
+
asked.push({ question: entry.payload.prompt, to: entry.payload.requested_of });
|
|
126
|
+
} else if (entry.payload.completionReason === 'human_responded') {
|
|
127
|
+
return { answer: parseHumanAnswer(entry.payload.result, waitId), asked };
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
|
-
return
|
|
130
|
+
return { asked };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Every `f.human` wait id the run has parked on, answered or not. A parked
|
|
135
|
+
* question is not in the step index until its answer is lowered, but it
|
|
136
|
+
* already holds its serial ordinal.
|
|
137
|
+
*/
|
|
138
|
+
export async function readHumanWaitIds(journal: JournalClient, runId: string): Promise<string[]> {
|
|
139
|
+
const ids = new Set<string>();
|
|
140
|
+
for (const entry of await readWaitEntries(journal, runId)) {
|
|
141
|
+
const waitId = entry.payload?.wait_id;
|
|
142
|
+
if (entry.entry_type === 'wait.human' && typeof waitId === 'string'
|
|
143
|
+
&& /(?:^|--)human-[1-9][0-9]*$/u.test(waitId)) ids.add(waitId);
|
|
144
|
+
}
|
|
145
|
+
return [...ids];
|
|
115
146
|
}
|
|
116
147
|
|
|
117
148
|
/** Every `wait.human` on the run that no `wait.completed` has closed. */
|
package/src/authored-mcp.ts
CHANGED
|
@@ -142,13 +142,18 @@ export async function runMcpEffect(
|
|
|
142
142
|
childRunId = outcome.run_id;
|
|
143
143
|
if (outcome.status !== 'completed') await completed;
|
|
144
144
|
if (diagnostic !== undefined) {
|
|
145
|
-
try {
|
|
145
|
+
try {
|
|
146
|
+
await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps,
|
|
147
|
+
rootRunId === undefined ? {} : { rootRunId }, outcome.status);
|
|
148
|
+
}
|
|
146
149
|
catch (error) {
|
|
147
150
|
if (!(error instanceof AuthoredFlowExecutionError) || error.code !== 'step_failed') throw error;
|
|
148
151
|
throw new McpStepError(diagnostic, outcome.run_id);
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
|
-
|
|
154
|
+
// Indexed on the root like every other child, so a resume can see its ordinal.
|
|
155
|
+
const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps,
|
|
156
|
+
rootRunId === undefined ? {} : { rootRunId }, outcome.status) as { output: unknown };
|
|
152
157
|
return receipt.output;
|
|
153
158
|
} finally {
|
|
154
159
|
clearTimeout(timer);
|
|
@@ -53,7 +53,7 @@ try {
|
|
|
53
53
|
executableSha256: hash(process.execPath), payloadSha256: hash(process.argv[1]!) } });
|
|
54
54
|
const request = await new Promise<{ channelKey: string; metadata: AuthoredRootMetadata; socketPath: string;
|
|
55
55
|
rootRunId: string; dataDir: string; localAgentStream?: string; workerCapacity?: number;
|
|
56
|
-
agentEnvironment?: unknown }>((resolve, reject) => {
|
|
56
|
+
journaledSteps?: unknown; agentEnvironment?: unknown }>((resolve, reject) => {
|
|
57
57
|
let buffer = '';
|
|
58
58
|
process.stdin.setEncoding('utf8');
|
|
59
59
|
const onData = (chunk: string): void => {
|
|
@@ -90,6 +90,8 @@ try {
|
|
|
90
90
|
}
|
|
91
91
|
if (request.localAgentStream !== request.metadata.localAgentStream) throw new Error('authored root local agent surface mismatch');
|
|
92
92
|
if (request.workerCapacity !== undefined && !isAgentCapacity(request.workerCapacity)) throw new Error('invalid authored worker capacity');
|
|
93
|
+
if (request.journaledSteps !== undefined && (!Array.isArray(request.journaledSteps)
|
|
94
|
+
|| !request.journaledSteps.every(step => typeof step === 'string'))) throw new Error('invalid authored journaled steps');
|
|
93
95
|
client = new JournalClient(request.socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS, budgetHandshake: true });
|
|
94
96
|
await client.connect(); await client.hello('flows-authored-node');
|
|
95
97
|
const result = await executeAuthoredFlow(loaded.handle, client,
|
|
@@ -102,6 +104,7 @@ try {
|
|
|
102
104
|
flowGraph: loaded.graph,
|
|
103
105
|
localAgentStream: request.localAgentStream, signal: controller.signal,
|
|
104
106
|
...(request.workerCapacity === undefined ? {} : { workerCapacity: request.workerCapacity }),
|
|
107
|
+
...(request.journaledSteps === undefined ? {} : { journaledSteps: request.journaledSteps as string[] }),
|
|
105
108
|
...(agentEnvironment === undefined ? {} : { agentEnvironment: { ...process.env, ...agentEnvironment } }),
|
|
106
109
|
onProgress: event => send({ type: 'progress', event }),
|
|
107
110
|
onWait: event => send({ type: 'wait', event }),
|
|
@@ -191,6 +191,7 @@ export async function runAuthoredInNode(
|
|
|
191
191
|
child.stdin!.write(JSON.stringify({ channelKey, metadata, socketPath, rootRunId,
|
|
192
192
|
dataDir: options.dataDir, localAgentStream: options.localAgentStream,
|
|
193
193
|
workerCapacity: options.workerCapacity,
|
|
194
|
+
...(options.journaledSteps === undefined ? {} : { journaledSteps: options.journaledSteps }),
|
|
194
195
|
agentEnvironment: options.agentEnvironment }) + '\n');
|
|
195
196
|
} else if (!ready || result) throw new Error('unexpected authored runtime message');
|
|
196
197
|
else if (message.type === 'progress') options.onProgress?.(message.event);
|
|
@@ -127,13 +127,18 @@ export async function runPluginEffect(
|
|
|
127
127
|
if (dispatchExpired) await cancelChildRun();
|
|
128
128
|
if (outcome.status !== 'completed') await completed;
|
|
129
129
|
if (diagnostic !== undefined) {
|
|
130
|
-
try {
|
|
130
|
+
try {
|
|
131
|
+
await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps,
|
|
132
|
+
rootRunId === undefined ? {} : { rootRunId }, outcome.status);
|
|
133
|
+
}
|
|
131
134
|
catch (error) {
|
|
132
135
|
if (!(error instanceof AuthoredFlowExecutionError) || error.code !== 'step_failed') throw error;
|
|
133
136
|
throw new PluginStepError(diagnostic, outcome.run_id);
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
|
-
|
|
139
|
+
// Indexed on the root like every other child, so a resume can see its ordinal.
|
|
140
|
+
const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps,
|
|
141
|
+
rootRunId === undefined ? {} : { rootRunId }, outcome.status) as { output: unknown };
|
|
137
142
|
return receipt.output;
|
|
138
143
|
}, admissionKey), deadline]);
|
|
139
144
|
} finally {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { brokerInstruction } from './communication/broker-spec.js';
|
|
1
2
|
import { dirname, resolve } from 'node:path';
|
|
2
3
|
import { checkAuthoredFlow, readProjectConfig, type ProjectConfig } from './cli/check.js';
|
|
3
4
|
import { probeCliAsync } from './cli/cli-probe.js';
|
|
@@ -21,7 +22,7 @@ export function authoredPreflight(path: string, environment?: NodeJS.ProcessEnv)
|
|
|
21
22
|
if (!resolved.ok) return checkAuthoredFlow(flow, path, config, { environment });
|
|
22
23
|
await Promise.all(resolved.resolutions.map(async resolution => {
|
|
23
24
|
const step = resolved.compiled!.steps.find(step => step.id === resolution.stepId)!;
|
|
24
|
-
const managed = step.type === 'agent' && communicationInstruction(step.instruction) !== undefined;
|
|
25
|
+
const managed = step.type === 'agent' && (communicationInstruction(step.instruction) !== undefined || brokerInstruction(step.instruction) !== undefined);
|
|
25
26
|
const key = cliProbeKey(resolution, managed, step.secrets);
|
|
26
27
|
if (probeCache.has(key)) return;
|
|
27
28
|
let probing = pending.get(key);
|