@relayflows/sdk 2.0.34 → 2.0.36
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 +16 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +15 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +4 -0
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/usage-limit.d.ts +2 -0
- package/dist/adapters/usage-limit.d.ts.map +1 -0
- package/dist/adapters/usage-limit.js +17 -0
- package/dist/adapters/usage-limit.js.map +1 -0
- package/dist/authored-activity.d.ts +17 -0
- package/dist/authored-activity.d.ts.map +1 -0
- package/dist/authored-activity.js +218 -0
- package/dist/authored-activity.js.map +1 -0
- package/dist/authored-child-run.d.ts +34 -0
- package/dist/authored-child-run.d.ts.map +1 -0
- package/dist/authored-child-run.js +128 -0
- package/dist/authored-child-run.js.map +1 -0
- package/dist/authored-dispatch.d.ts +24 -0
- package/dist/authored-dispatch.d.ts.map +1 -0
- package/dist/authored-dispatch.js +81 -0
- package/dist/authored-dispatch.js.map +1 -0
- package/dist/authored-flow-error.d.ts +32 -3
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +3 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +26 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +122 -16
- 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 +9 -0
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-mcp.js +2 -2
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.js +4 -1
- 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 +40 -8
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-plugin-effect.js +2 -2
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-root.d.ts +10 -7
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +20 -0
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +24 -8
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +81 -15
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-subscription-park.d.ts +5 -0
- package/dist/authored-subscription-park.d.ts.map +1 -0
- package/dist/authored-subscription-park.js +47 -0
- package/dist/authored-subscription-park.js.map +1 -0
- package/dist/authored-worker-step.d.ts +3 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +25 -9
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-activities.d.ts +8 -0
- package/dist/cli/check-activities.d.ts.map +1 -0
- package/dist/cli/check-activities.js +155 -0
- package/dist/cli/check-activities.js.map +1 -0
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +14 -2
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +10 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +23 -5
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +32 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/subscription-report.d.ts +5 -0
- package/dist/cli/subscription-report.d.ts.map +1 -0
- package/dist/cli/subscription-report.js +48 -0
- package/dist/cli/subscription-report.js.map +1 -0
- package/dist/cli-adapter.d.ts +4 -2
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +4 -0
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -5
- package/dist/cli.js.map +1 -1
- package/dist/cloud-mirror-step.d.ts +2 -0
- package/dist/cloud-mirror-step.d.ts.map +1 -1
- package/dist/cloud-mirror-step.js +9 -1
- package/dist/cloud-mirror-step.js.map +1 -1
- package/dist/cloud-mirror.d.ts.map +1 -1
- package/dist/cloud-mirror.js +17 -2
- package/dist/cloud-mirror.js.map +1 -1
- package/dist/failure-kinds.d.ts +2 -2
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +17 -2
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +33 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-reader.d.ts.map +1 -1
- package/dist/journal-reader.js +2 -0
- package/dist/journal-reader.js.map +1 -1
- package/dist/local-agent.d.ts +4 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +14 -4
- package/dist/local-agent.js.map +1 -1
- package/dist/preflight.d.ts +6 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +36 -10
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +166 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/step-settling.d.ts +11 -0
- package/dist/step-settling.d.ts.map +1 -0
- package/dist/step-settling.js +22 -0
- package/dist/step-settling.js.map +1 -0
- package/dist/worker-slots.d.ts +15 -0
- package/dist/worker-slots.d.ts.map +1 -1
- package/dist/worker-slots.js +40 -3
- package/dist/worker-slots.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/base.ts +19 -0
- package/src/adapters/claude.ts +15 -0
- package/src/adapters/codex.ts +6 -0
- package/src/adapters/usage-limit.ts +17 -0
- package/src/authored-activity.ts +251 -0
- package/src/authored-child-run.ts +138 -0
- package/src/authored-dispatch.ts +113 -0
- package/src/authored-flow-error.ts +36 -0
- package/src/authored-flow-executor.ts +166 -15
- package/src/authored-flow-loader.ts +12 -0
- package/src/authored-mcp.ts +2 -2
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +41 -8
- package/src/authored-plugin-effect.ts +2 -2
- package/src/authored-root.ts +28 -4
- package/src/authored-step-output.ts +88 -15
- package/src/authored-subscription-park.ts +46 -0
- package/src/authored-worker-step.ts +24 -8
- package/src/cli/check-activities.ts +144 -0
- package/src/cli/cli-probe.ts +15 -3
- package/src/cli/direct-run.ts +9 -0
- package/src/cli/run.ts +56 -9
- package/src/cli/subscription-report.ts +51 -0
- package/src/cli-adapter.ts +8 -2
- package/src/cli.ts +16 -6
- package/src/cloud-mirror-step.ts +16 -2
- package/src/cloud-mirror.ts +15 -2
- package/src/failure-kinds.ts +2 -0
- package/src/index.ts +6 -0
- package/src/journal-client.ts +47 -3
- package/src/journal-reader.ts +2 -0
- package/src/local-agent.ts +17 -3
- package/src/preflight.ts +46 -10
- package/src/protocol.ts +129 -0
- package/src/step-settling.ts +19 -0
- package/src/worker-slots.ts +63 -4
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { commitAuthoredVerdict } from './authored-completion-record.js';
|
|
23
23
|
import {
|
|
24
24
|
type AgentResult,
|
|
25
|
+
type DispatchResult,
|
|
25
26
|
type LlmOptions,
|
|
26
27
|
type CloudHelper,
|
|
27
28
|
type CompletionReason as SurfaceCompletionReason,
|
|
@@ -38,9 +39,11 @@ import { observeStep, type ProgressEvent } from './progress.js';
|
|
|
38
39
|
import { parseStepTimeout } from './compile.js';
|
|
39
40
|
import { getAuthoredFlowDefinition } from './authored-flow.js';
|
|
40
41
|
import type { GetFlowDefinition } from './authored-flow-loader.js';
|
|
42
|
+
import type { LoadedAuthoredFlowNode } from './authored-flow-loader.js';
|
|
41
43
|
import type { RunLifecycleOptions } from './cli/run.js';
|
|
42
44
|
import {
|
|
43
45
|
AuthoredFlowExecutionError,
|
|
46
|
+
type AuthoredFlowSuspension,
|
|
44
47
|
AuthoredHumanParked,
|
|
45
48
|
type AuthoredFlowExecutionErrorCode,
|
|
46
49
|
} from './authored-flow-error.js';
|
|
@@ -52,6 +55,7 @@ import {
|
|
|
52
55
|
verifyAuthoredOperations,
|
|
53
56
|
} from './authored-flow-operation.js';
|
|
54
57
|
import { AuthoredFlowLifecycle } from './authored-flow-lifecycle.js';
|
|
58
|
+
import { AuthoredActivities } from './authored-activity.js';
|
|
55
59
|
import { displayLabel, type AuthoredStepEdges } from './authored-step-index.js';
|
|
56
60
|
import { JournalClient } from './journal-client.js';
|
|
57
61
|
import { PluginError } from './plugin-manifest.js';
|
|
@@ -66,6 +70,8 @@ import type {
|
|
|
66
70
|
CompletionReason as ProtocolCompletionReason,
|
|
67
71
|
RunCompletionReason as ProtocolRunCompletionReason,
|
|
68
72
|
} from './protocol.js';
|
|
73
|
+
import type { AuthoredWorkerSlots } from './worker-slots.js';
|
|
74
|
+
import { childLeaves, helperAuthority, parseDispatchReceipt, resolveDispatchChild } from './authored-dispatch.js';
|
|
69
75
|
|
|
70
76
|
type Assert<T extends true> = T;
|
|
71
77
|
type Equal<A, B> = [A] extends [B]
|
|
@@ -101,6 +107,7 @@ export interface AuthoredExecutionRuntime {
|
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
export interface AuthoredFlowExecutionResult {
|
|
110
|
+
readonly state?: undefined;
|
|
104
111
|
readonly executionRuntime?: AuthoredExecutionRuntime;
|
|
105
112
|
readonly rootRunId?: string;
|
|
106
113
|
readonly name: string;
|
|
@@ -114,6 +121,14 @@ export interface AuthoredFlowExecutionResult {
|
|
|
114
121
|
readonly journalSteps: readonly AuthoredFlowJournalStep[];
|
|
115
122
|
}
|
|
116
123
|
|
|
124
|
+
/** A body reached a durable event boundary and released its worker lease. */
|
|
125
|
+
export interface AuthoredFlowSuspendedResult {
|
|
126
|
+
readonly state: 'suspended';
|
|
127
|
+
readonly name: string;
|
|
128
|
+
readonly suspension: AuthoredFlowSuspension;
|
|
129
|
+
readonly journalSteps: readonly AuthoredFlowJournalStep[];
|
|
130
|
+
}
|
|
131
|
+
|
|
117
132
|
// The authoring SURFACE stays wide: `done()` accepts every
|
|
118
133
|
// `FlowCompletionReason` and refuses the kernel-owned ones at runtime with a
|
|
119
134
|
// diagnostic that names the alternative. The RESULT is narrow, because `done()`
|
|
@@ -185,6 +200,20 @@ export interface ExecuteAuthoredFlowOptions {
|
|
|
185
200
|
* from authored input: direct `/workflows/run` callers control that JSON.
|
|
186
201
|
*/
|
|
187
202
|
readonly extensionDispatch?: HostedExtensionDispatch;
|
|
203
|
+
/** Pinned static `use:` graph available to `f.dispatch`. */
|
|
204
|
+
readonly flowGraph?: readonly LoadedAuthoredFlowNode[];
|
|
205
|
+
/** Internal namespace for descendant step/admission identities. */
|
|
206
|
+
readonly stepPrefix?: string;
|
|
207
|
+
/** Current child depth. Root is zero; dispatch is capped at three. */
|
|
208
|
+
readonly dispatchDepth?: number;
|
|
209
|
+
/** Parent predecessors inherited by the first operation in a child body. */
|
|
210
|
+
readonly entryAfter?: readonly string[];
|
|
211
|
+
/** Root-owned budget accumulator shared by every descendant. */
|
|
212
|
+
readonly sharedBudget?: AuthoredBudget;
|
|
213
|
+
/** Root-owned worker capacity shared by every descendant. */
|
|
214
|
+
readonly workerSlots?: AuthoredWorkerSlots;
|
|
215
|
+
/** Parent helper authority inherited by this dispatched child. */
|
|
216
|
+
readonly allowedHelperProviders?: ReadonlySet<string>;
|
|
188
217
|
}
|
|
189
218
|
|
|
190
219
|
export async function executeAuthoredFlow<Input = undefined>(
|
|
@@ -209,7 +238,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
209
238
|
};
|
|
210
239
|
const definition = getDefinition<Input>(handle);
|
|
211
240
|
const hostedHandler = extensionHandlerForHostedDispatch(options.extensionDispatch, options.extensions ?? []);
|
|
212
|
-
const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory' && key !== 'version' && key !== 'hooks');
|
|
241
|
+
const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory' && key !== 'version' && key !== 'hooks' && key !== 'use');
|
|
213
242
|
if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['mcp', ...helperProviders.map(p => p.namespace)].includes(key))) headerFields.push('tools');
|
|
214
243
|
if (definition.header.tools?.relayfile !== undefined) headerFields.push('tools.relayfile');
|
|
215
244
|
const helperPreflight = checkSlackHelpers(definition);
|
|
@@ -242,7 +271,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
242
271
|
);
|
|
243
272
|
}
|
|
244
273
|
|
|
245
|
-
const budget = new AuthoredBudget(definition.header.budget);
|
|
274
|
+
const budget = options.sharedBudget ?? new AuthoredBudget(definition.header.budget);
|
|
246
275
|
if (definition.header.memory?.agent === true) {
|
|
247
276
|
throw new AuthoredFlowExecutionError('unsupported_header', 'memory.agent requires the follow-up identity-scoped agent memory adapter');
|
|
248
277
|
}
|
|
@@ -255,15 +284,35 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
255
284
|
const journalSteps: AuthoredFlowJournalStep[] = [];
|
|
256
285
|
const authoredSteps: AuthoredFlowOperation<unknown>[] = [];
|
|
257
286
|
const lifecycle = new AuthoredFlowLifecycle();
|
|
258
|
-
const
|
|
287
|
+
const activities = new AuthoredActivities(journal, options.rootRunId);
|
|
288
|
+
const edgeOverrides = new Map<string, AuthoredStepEdges>();
|
|
289
|
+
const stepEdges = (step: string): AuthoredStepEdges | undefined => {
|
|
290
|
+
const overridden = edgeOverrides.get(step);
|
|
291
|
+
if (overridden !== undefined) return overridden;
|
|
292
|
+
const local = lifecycle.stepEdges(step);
|
|
293
|
+
if (local === undefined || (local.after !== undefined && local.after.length > 0) || options.entryAfter === undefined
|
|
294
|
+
|| options.entryAfter.length === 0) return local;
|
|
295
|
+
return { ...local, after: [...options.entryAfter] };
|
|
296
|
+
};
|
|
259
297
|
let nextStep = 1;
|
|
260
298
|
let requestedCompletion: LoweredCompletionReason | undefined;
|
|
261
299
|
let requestedDetail: string | undefined;
|
|
300
|
+
const qualify = (id: string): string => `${options.stepPrefix ?? ''}${id}`;
|
|
301
|
+
const nextId = (kind: string): string => qualify(`${kind}-${nextStep++}`);
|
|
302
|
+
const assertHelperAllowed = (provider: string): void => {
|
|
303
|
+
if (options.allowedHelperProviders !== undefined && !options.allowedHelperProviders.has(provider)) {
|
|
304
|
+
throw new AuthoredFlowExecutionError(
|
|
305
|
+
'dispatch_invalid',
|
|
306
|
+
`child flow "${definition.name}" attempted helper provider "${provider}" outside its parent's granted integrations`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
262
310
|
|
|
263
311
|
const lowerDeterministic = authoredDeterministicRunner(
|
|
264
312
|
definition.name, journal, journalSteps, budget, {
|
|
265
313
|
...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
|
|
266
314
|
...(options.dataDir === undefined ? {} : { dataDir: options.dataDir }),
|
|
315
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
267
316
|
stepEdges,
|
|
268
317
|
},
|
|
269
318
|
);
|
|
@@ -271,6 +320,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
271
320
|
const worker = authoredWorkerRunner(
|
|
272
321
|
definition, journal, flowPath, journalSteps, waitOptions,
|
|
273
322
|
localAgentStream, budget, definition.header.budget, options.rootRunId, options.workerCapacity, stepEdges,
|
|
323
|
+
options.workerSlots,
|
|
274
324
|
);
|
|
275
325
|
|
|
276
326
|
/**
|
|
@@ -373,7 +423,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
373
423
|
function llmOperation(prompt: string, options: LlmOptions): Step<unknown>;
|
|
374
424
|
function llmOperation(prompt: string | TemplateStringsArray, ...values: unknown[]): Step<unknown> {
|
|
375
425
|
assertOperationAllowed('llm', definition.name, requestedCompletion);
|
|
376
|
-
const id =
|
|
426
|
+
const id = nextId('llm');
|
|
377
427
|
// Hoist the operation reference so the start closure can read the
|
|
378
428
|
// caller's `.gate(config)` at spec-build time. AuthoredFlowOperation
|
|
379
429
|
// begins after construction returns, so `llmOp` is defined by then.
|
|
@@ -420,7 +470,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
420
470
|
`f.run options.onNonZero must be 'fail' or 'record' (got ${JSON.stringify(policy)}).`,
|
|
421
471
|
);
|
|
422
472
|
}
|
|
423
|
-
const id =
|
|
473
|
+
const id = nextId('run');
|
|
424
474
|
if (policy === 'record') {
|
|
425
475
|
let recordOp!: AuthoredFlowOperation<RunResult>;
|
|
426
476
|
recordOp = new AuthoredFlowOperation<RunResult>(
|
|
@@ -451,8 +501,9 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
451
501
|
const slackRun = options.rootRunId ?? randomUUID();
|
|
452
502
|
function slackOperation<T>(call: SlackCall): Step<T> {
|
|
453
503
|
assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion);
|
|
504
|
+
assertHelperAllowed(call.provider);
|
|
454
505
|
const snapshot = snapshotJsonValue(call, 'f.slack call') as unknown as SlackCall;
|
|
455
|
-
const id = `slack-${slackRun}-${nextStep++}
|
|
506
|
+
const id = qualify(`slack-${slackRun}-${nextStep++}`);
|
|
456
507
|
return trackStep(authoredSteps, new AuthoredFlowOperation<T>(
|
|
457
508
|
id, `slack.${call.verb}`,
|
|
458
509
|
() => assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion),
|
|
@@ -480,8 +531,9 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
480
531
|
...createHelpers(<T>(call: HelperCall): Step<T> => {
|
|
481
532
|
const verb = `${call.provider}.${call.verb}`;
|
|
482
533
|
assertOperationAllowed(verb, definition.name, requestedCompletion);
|
|
534
|
+
assertHelperAllowed(call.provider);
|
|
483
535
|
const snapshot = snapshotJsonValue(call, `f.${verb} call`) as unknown as HelperCall;
|
|
484
|
-
const id = `${call.provider}-${slackRun}-${nextStep++}
|
|
536
|
+
const id = qualify(`${call.provider}-${slackRun}-${nextStep++}`);
|
|
485
537
|
return trackStep(authoredSteps, new AuthoredFlowOperation<T>(id, verb,
|
|
486
538
|
() => assertOperationAllowed(verb, definition.name, requestedCompletion),
|
|
487
539
|
async () => await runHelperEffect(journal, definition.name, id, snapshot,
|
|
@@ -496,7 +548,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
496
548
|
},
|
|
497
549
|
mcp: buildMcpProxy(checkedMcp.inventory, (server, tool, args, known) => {
|
|
498
550
|
assertOperationAllowed('mcp', definition.name, requestedCompletion);
|
|
499
|
-
const id =
|
|
551
|
+
const id = nextId('mcp');
|
|
500
552
|
return trackStep(authoredSteps, new AuthoredFlowOperation(
|
|
501
553
|
id, 'mcp',
|
|
502
554
|
() => assertOperationAllowed('mcp', definition.name, requestedCompletion),
|
|
@@ -516,7 +568,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
516
568
|
assertOperationAllowed('agent', definition.name, requestedCompletion);
|
|
517
569
|
// Authored headers do not yet declare reusable named agents, so the name
|
|
518
570
|
// identifies nothing to the kernel; it is the step's label in the DAG.
|
|
519
|
-
const id =
|
|
571
|
+
const id = nextId('agent');
|
|
520
572
|
let agentOp!: AuthoredFlowOperation<AgentResult>;
|
|
521
573
|
agentOp = new AuthoredFlowOperation<AgentResult>(
|
|
522
574
|
id,
|
|
@@ -528,6 +580,17 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
528
580
|
);
|
|
529
581
|
return trackStep(authoredSteps, agentOp);
|
|
530
582
|
},
|
|
583
|
+
on(source, activityOptions) {
|
|
584
|
+
assertOperationAllowed('on', definition.name, requestedCompletion);
|
|
585
|
+
// Activity ids are root-scoped (`activity-N`); a child's own counter would collide.
|
|
586
|
+
if ((options.dispatchDepth ?? 0) > 0) {
|
|
587
|
+
throw new AuthoredFlowExecutionError(
|
|
588
|
+
'dispatch_invalid',
|
|
589
|
+
`child flow "${definition.name}" cannot call f.on in this release; activities are root-scoped`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return activities.open(source, activityOptions);
|
|
593
|
+
},
|
|
531
594
|
/**
|
|
532
595
|
* `f.human` (docs/SURFACE.md §1, §7). The question is not a child run: it
|
|
533
596
|
* is the ROOT attempt parking on the kernel's `wait.human`. The body
|
|
@@ -545,6 +608,12 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
545
608
|
*/
|
|
546
609
|
human(question, humanOptions) {
|
|
547
610
|
assertOperationAllowed('human', definition.name, requestedCompletion);
|
|
611
|
+
if ((options.dispatchDepth ?? 0) > 0) {
|
|
612
|
+
throw new AuthoredFlowExecutionError(
|
|
613
|
+
'dispatch_invalid',
|
|
614
|
+
`child flow "${definition.name}" cannot call f.human in this release; the parent owns approval authority`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
548
617
|
if (typeof question !== 'string' || question.trim() === '') {
|
|
549
618
|
throw new AuthoredFlowExecutionError('human_answer_invalid', 'f.human requires a non-empty question');
|
|
550
619
|
}
|
|
@@ -558,7 +627,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
558
627
|
if (!parsedTo.ok) {
|
|
559
628
|
throw new AuthoredFlowExecutionError('human_to_invalid', `f.human to ${parsedTo.reason}`);
|
|
560
629
|
}
|
|
561
|
-
const id =
|
|
630
|
+
const id = nextId('human');
|
|
562
631
|
const to = humanOptions.to;
|
|
563
632
|
// Hoisted like `llmOp`/`runOp`: the start closure reads the caller's
|
|
564
633
|
// `.gate(config)` at spec-build time, so a named gate on the answer is
|
|
@@ -590,13 +659,79 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
590
659
|
);
|
|
591
660
|
return trackStep(authoredSteps, humanOp);
|
|
592
661
|
},
|
|
593
|
-
dispatch
|
|
662
|
+
dispatch(flowName, dispatchInput) {
|
|
594
663
|
assertOperationAllowed('dispatch', definition.name, requestedCompletion);
|
|
595
|
-
|
|
664
|
+
const depth = options.dispatchDepth ?? 0;
|
|
665
|
+
const child = resolveDispatchChild({
|
|
666
|
+
handle, definition, graph: options.flowGraph,
|
|
667
|
+
flowName, depth,
|
|
668
|
+
});
|
|
669
|
+
const childDefinition = child.getDefinition(child.handle);
|
|
670
|
+
const inputSnapshot = snapshotJsonValue(dispatchInput, `f.dispatch(${JSON.stringify(flowName)}) input`);
|
|
671
|
+
const id = nextId('dispatch');
|
|
672
|
+
let dispatchOp!: AuthoredFlowOperation<DispatchResult>;
|
|
673
|
+
dispatchOp = new AuthoredFlowOperation<DispatchResult>(
|
|
674
|
+
id,
|
|
675
|
+
'dispatch',
|
|
676
|
+
() => assertOperationAllowed('dispatch', definition.name, requestedCompletion),
|
|
677
|
+
async () => {
|
|
678
|
+
const inheritedAfter = stepEdges(id)?.after ?? [];
|
|
679
|
+
const currentHelperAuthority = helperAuthority(definition);
|
|
680
|
+
const allowedHelperProviders = options.allowedHelperProviders === undefined
|
|
681
|
+
? currentHelperAuthority
|
|
682
|
+
: new Set([...currentHelperAuthority].filter(provider => options.allowedHelperProviders!.has(provider)));
|
|
683
|
+
const childResult = await executeAuthoredFlow(child.handle, journal, inputSnapshot, {
|
|
684
|
+
getDefinition: child.getDefinition,
|
|
685
|
+
dataDir: options.dataDir,
|
|
686
|
+
flowPath: child.path,
|
|
687
|
+
...(localAgentStream === undefined ? {} : { localAgentStream }),
|
|
688
|
+
...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
|
|
689
|
+
...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
|
|
690
|
+
flowGraph: options.flowGraph,
|
|
691
|
+
stepPrefix: `${id}--`,
|
|
692
|
+
dispatchDepth: depth + 1,
|
|
693
|
+
entryAfter: inheritedAfter,
|
|
694
|
+
sharedBudget: budget,
|
|
695
|
+
...(worker.slots === undefined ? {} : { workerSlots: worker.slots }),
|
|
696
|
+
allowedHelperProviders,
|
|
697
|
+
...(options.extensions === undefined ? {} : { extensions: options.extensions }),
|
|
698
|
+
onProgress,
|
|
699
|
+
...(options.onWait === undefined ? {} : { onWait: options.onWait }),
|
|
700
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
701
|
+
});
|
|
702
|
+
if (childResult.completionReason !== 'success') {
|
|
703
|
+
throw new AuthoredFlowExecutionError(
|
|
704
|
+
'step_failed',
|
|
705
|
+
`child flow "${childResult.name}" completed with ${childResult.completionReason}`
|
|
706
|
+
+ (childResult.completionDetail === undefined ? '' : `: ${childResult.completionDetail}`),
|
|
707
|
+
childResult.completionReason === 'step_failed' ? 'step_failed' : undefined,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
const leaves = childLeaves(childResult.journalSteps);
|
|
711
|
+
edgeOverrides.set(id, {
|
|
712
|
+
...displayLabel(childResult.name),
|
|
713
|
+
...(leaves.length > 0 ? { after: leaves } : inheritedAfter.length === 0
|
|
714
|
+
? {} : { after: inheritedAfter }),
|
|
715
|
+
});
|
|
716
|
+
const receipt: DispatchResult = {
|
|
717
|
+
name: childResult.name,
|
|
718
|
+
completionReason: 'success',
|
|
719
|
+
...(childResult.completionDetail === undefined ? {} : { completionDetail: childResult.completionDetail }),
|
|
720
|
+
};
|
|
721
|
+
const literal = `'${JSON.stringify(receipt).replaceAll("'", "'\\''")}'`;
|
|
722
|
+
const recorded = await lowerDeterministic(
|
|
723
|
+
id, `printf '%s' ${literal}`, false, undefined, dispatchOp.namedGate,
|
|
724
|
+
);
|
|
725
|
+
return parseDispatchReceipt(recorded, id);
|
|
726
|
+
},
|
|
727
|
+
lifecycle,
|
|
728
|
+
displayLabel(flowName),
|
|
729
|
+
);
|
|
730
|
+
return trackStep(authoredSteps, dispatchOp);
|
|
596
731
|
},
|
|
597
732
|
hook(name, input) {
|
|
598
733
|
assertOperationAllowed('hook', definition.name, requestedCompletion);
|
|
599
|
-
const id =
|
|
734
|
+
const id = nextId('hook');
|
|
600
735
|
const snapshot = snapshotJsonValue(input, 'f.hook input');
|
|
601
736
|
return trackStep(authoredSteps, new AuthoredFlowOperation<boolean>(
|
|
602
737
|
id, 'hook',
|
|
@@ -661,7 +796,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
661
796
|
Object.assign(context, pluginHelpers(checkedMcp.plugins ?? [], (plugin, verb, args) => {
|
|
662
797
|
const label = `${verb.namespace}.${verb.method}`;
|
|
663
798
|
assertOperationAllowed(label, definition.name, requestedCompletion);
|
|
664
|
-
const id =
|
|
799
|
+
const id = nextId('plugin');
|
|
665
800
|
return trackStep(authoredSteps, new AuthoredFlowOperation(
|
|
666
801
|
id, label, () => assertOperationAllowed(label, definition.name, requestedCompletion),
|
|
667
802
|
() => runPluginEffect(journal, definition.name, id, plugin, verb, args,
|
|
@@ -683,6 +818,11 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
683
818
|
try {
|
|
684
819
|
worker.stop(bodyFailure);
|
|
685
820
|
await stopAuthoredOperations(authoredSteps, bodyFailure);
|
|
821
|
+
// A durable wait hands execution back to the control plane. Its
|
|
822
|
+
// subscriptions must keep receiving events while no body is running.
|
|
823
|
+
const parked = bodyFailure instanceof AuthoredFlowExecutionError
|
|
824
|
+
&& (bodyFailure.code === 'subscription_suspended' || bodyFailure.code === 'human_parked');
|
|
825
|
+
if (!parked) await activities.closeAll('canceled');
|
|
686
826
|
} finally {
|
|
687
827
|
lifecycle.close();
|
|
688
828
|
}
|
|
@@ -710,6 +850,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
710
850
|
try {
|
|
711
851
|
worker.stop(missingCompletion);
|
|
712
852
|
await stopAuthoredOperations(authoredSteps, missingCompletion);
|
|
853
|
+
await activities.closeAll('canceled');
|
|
713
854
|
} finally {
|
|
714
855
|
lifecycle.close();
|
|
715
856
|
}
|
|
@@ -717,6 +858,16 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
717
858
|
}
|
|
718
859
|
try {
|
|
719
860
|
await verifyAuthoredOperations(definition.name, authoredSteps, lifecycle);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
try {
|
|
863
|
+
await activities.closeAll('canceled');
|
|
864
|
+
} finally {
|
|
865
|
+
lifecycle.close();
|
|
866
|
+
}
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
await activities.closeAll('run_completed');
|
|
720
871
|
} finally {
|
|
721
872
|
lifecycle.close();
|
|
722
873
|
}
|
|
@@ -735,7 +886,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
735
886
|
// explanation the root had already journaled to `run_admission_conflict`.
|
|
736
887
|
// With no detail there is no environment in the command and nothing to
|
|
737
888
|
// commit, so that path is untouched.
|
|
738
|
-
const terminalId = `complete-${nextStep}
|
|
889
|
+
const terminalId = qualify(`complete-${nextStep}`);
|
|
739
890
|
const verdict = await commitAuthoredVerdict(journal, options.rootRunId, terminalId, {
|
|
740
891
|
reason: requestedCompletion,
|
|
741
892
|
...(requestedDetail === undefined ? {} : { detail: requestedDetail }),
|
|
@@ -100,6 +100,18 @@ export async function loadAuthoredFlow(path: string, options: LoadAuthoredFlowOp
|
|
|
100
100
|
}
|
|
101
101
|
dependencies.push(child.path);
|
|
102
102
|
}
|
|
103
|
+
const childNames = new Set<string>();
|
|
104
|
+
for (const path of dependencies) {
|
|
105
|
+
const child = loaded.get(path)!;
|
|
106
|
+
const name = child.getDefinition(child.handle).name;
|
|
107
|
+
if (childNames.has(name)) {
|
|
108
|
+
throw new AuthoredFlowLoadError(
|
|
109
|
+
`Flow "${absolutePath}" declares two direct use children named "${name}".`,
|
|
110
|
+
'use_invalid',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
childNames.add(name);
|
|
114
|
+
}
|
|
103
115
|
const node = Object.freeze({ path: absolutePath, handle, getDefinition, surfaceAuthority, use: Object.freeze(dependencies) });
|
|
104
116
|
loaded.set(absolutePath, node);
|
|
105
117
|
return node;
|
package/src/authored-mcp.ts
CHANGED
|
@@ -140,13 +140,13 @@ export async function runMcpEffect(
|
|
|
140
140
|
childRunId = outcome.run_id;
|
|
141
141
|
if (outcome.status !== 'completed') await completed;
|
|
142
142
|
if (diagnostic !== undefined) {
|
|
143
|
-
try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps); }
|
|
143
|
+
try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status); }
|
|
144
144
|
catch (error) {
|
|
145
145
|
if (!(error instanceof AuthoredFlowExecutionError) || error.code !== 'step_failed') throw error;
|
|
146
146
|
throw new McpStepError(diagnostic, outcome.run_id);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps) as { output: unknown };
|
|
149
|
+
const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status) as { output: unknown };
|
|
150
150
|
return receipt.output;
|
|
151
151
|
} finally {
|
|
152
152
|
clearTimeout(timer);
|
|
@@ -75,6 +75,7 @@ try {
|
|
|
75
75
|
getDefinition: loaded.getDefinition, dataDir: request.dataDir,
|
|
76
76
|
flowPath: request.metadata.flowPath, rootRunId: request.rootRunId,
|
|
77
77
|
extensions: loaded.extensions,
|
|
78
|
+
flowGraph: loaded.graph,
|
|
78
79
|
localAgentStream: request.localAgentStream, signal: controller.signal,
|
|
79
80
|
...(request.workerCapacity === undefined ? {} : { workerCapacity: request.workerCapacity }),
|
|
80
81
|
onProgress: event => send({ type: 'progress', event }),
|
|
@@ -93,7 +94,9 @@ try {
|
|
|
93
94
|
// the only one that saw the child's classification.
|
|
94
95
|
...(error instanceof AuthoredFlowExecutionError ? { code: error.code,
|
|
95
96
|
completionReason: error.completionReason, runId: error.runId,
|
|
96
|
-
details: error.details,
|
|
97
|
+
details: error.details,
|
|
98
|
+
parkCause: error.parkCause,
|
|
99
|
+
...(error.suspension === undefined ? {} : { suspension: error.suspension }) } : {}),
|
|
97
100
|
...(error instanceof AuthoredHumanParked ? { wait: error.wait } : {}) });
|
|
98
101
|
process.exitCode = 1;
|
|
99
102
|
} finally {
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
} from './failure-kinds.js';
|
|
19
19
|
import { HUMAN_WAIT_ID } from './authored-human.js';
|
|
20
20
|
import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
|
|
21
|
+
import { settlingCompletion } from './step-settling.js';
|
|
21
22
|
|
|
22
23
|
let embeddedSource: string | undefined;
|
|
23
24
|
/** Installed by the standalone build; never fetched or resolved from a workspace. */
|
|
@@ -144,7 +145,8 @@ export async function runAuthoredInNode(
|
|
|
144
145
|
} else if (typeof message.code === 'string') {
|
|
145
146
|
const authored = new AuthoredFlowExecutionError(message.code as AuthoredFlowExecutionErrorCode,
|
|
146
147
|
message.message, message.completionReason, message.runId,
|
|
147
|
-
stepFailedFrame(message.details)
|
|
148
|
+
stepFailedFrame(message.details),
|
|
149
|
+
isSuspension(message.suspension) ? message.suspension : undefined);
|
|
148
150
|
// Validated, not trusted, like `details`: an unrecognised cause
|
|
149
151
|
// is dropped so the boundary says nothing about workers rather
|
|
150
152
|
// than acting on a value this frame could have invented.
|
|
@@ -172,6 +174,35 @@ export async function runAuthoredInNode(
|
|
|
172
174
|
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
function isSuspension(value: unknown): value is import('./authored-flow-error.js').AuthoredFlowSuspension {
|
|
178
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
179
|
+
const record = value as Record<string, unknown>;
|
|
180
|
+
const common = typeof record['subscriptionId'] === 'string' && record['subscriptionId'].length > 0
|
|
181
|
+
&& typeof record['stream'] === 'string' && record['stream'].length > 0
|
|
182
|
+
&& Number.isSafeInteger(record['deadlineAtMs']) && (record['deadlineAtMs'] as number) >= 0;
|
|
183
|
+
if (!common) return false;
|
|
184
|
+
if (record['kind'] === 'event_wait') return true;
|
|
185
|
+
return record['kind'] === 'activation'
|
|
186
|
+
&& Array.isArray(record['eventTypes']) && record['eventTypes'].length > 0
|
|
187
|
+
&& record['eventTypes'].every(type => typeof type === 'string' && type.length > 0)
|
|
188
|
+
&& (record['pattern'] === undefined || isJsonRecord(record['pattern']))
|
|
189
|
+
&& Number.isSafeInteger(record['settleMs']) && (record['settleMs'] as number) >= 0
|
|
190
|
+
&& Number.isSafeInteger(record['idleMs']) && (record['idleMs'] as number) > 0
|
|
191
|
+
&& typeof record['includeSelf'] === 'boolean';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isJsonRecord(value: unknown): value is Record<string, unknown> {
|
|
195
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
196
|
+
&& Object.values(value).every(isJsonValue);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isJsonValue(value: unknown): boolean {
|
|
200
|
+
return value === null || typeof value === 'boolean' || typeof value === 'string'
|
|
201
|
+
|| (typeof value === 'number' && Number.isFinite(value))
|
|
202
|
+
|| (Array.isArray(value) && value.every(isJsonValue))
|
|
203
|
+
|| isJsonRecord(value);
|
|
204
|
+
}
|
|
205
|
+
|
|
175
206
|
/** A park signal from the child names the question the parent must journal. */
|
|
176
207
|
function isHumanWaitFrame(value: unknown): value is AuthoredHumanWait {
|
|
177
208
|
const wait = value as Partial<AuthoredHumanWait> | null;
|
|
@@ -342,15 +373,17 @@ export async function verifyAuthoredNodeResult(
|
|
|
342
373
|
// step a child spec may carry, and it must have completed too.
|
|
343
374
|
const lowered = spec?.steps ?? [];
|
|
344
375
|
const specShape = lowered.length === 1 || (lowered.length === 2 && lowered[1]?.id === `${claimed.id}.gate`);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
376
|
+
// Judged against the step's SETTLING completion, the same one the
|
|
377
|
+
// authored reader returned: a retried attempt (`disposition: retry`,
|
|
378
|
+
// e.g. crash recovery's `crashed`) journals a completion of its own
|
|
379
|
+
// ahead of it, so neither a count nor the first entry is the verdict.
|
|
380
|
+
const completed = settlingCompletion(entries, claimed.id);
|
|
381
|
+
const gateCompleted = lowered.length === 2 ? settlingCompletion(entries, `${claimed.id}.gate`) : undefined;
|
|
348
382
|
const terminalFacts = entries.filter(entry => entry.entry_type === 'run.completed');
|
|
349
383
|
if (spec?.name !== `${metadata.flowName}/${claimed.id}` || !specShape
|
|
350
|
-
|| step?.id !== claimed.id || completed
|
|
351
|
-
||
|
|
352
|
-
||
|
|
353
|
-
|| terminalFacts.length !== 1 || terminalFacts[0]?.payload?.completionReason !== 'success') invalid(`evidence ${claimed.id} spec=${spec?.name} step=${step?.id} completed=${completed.length}`);
|
|
384
|
+
|| step?.id !== claimed.id || completed?.payload?.completionReason !== 'success'
|
|
385
|
+
|| (lowered.length === 2 && gateCompleted?.payload?.completionReason !== 'success')
|
|
386
|
+
|| terminalFacts.length !== 1 || terminalFacts[0]?.payload?.completionReason !== 'success') invalid(`evidence ${claimed.id} spec=${spec?.name} step=${step?.id} completed=${completed?.payload?.completionReason}`);
|
|
354
387
|
if (claimed === terminal) {
|
|
355
388
|
// The claimed verdict must match the marker the journal actually
|
|
356
389
|
// recorded, so an IPC frame cannot claim `success` over a run whose
|
|
@@ -125,13 +125,13 @@ export async function runPluginEffect(
|
|
|
125
125
|
if (dispatchExpired) await cancelChildRun();
|
|
126
126
|
if (outcome.status !== 'completed') await completed;
|
|
127
127
|
if (diagnostic !== undefined) {
|
|
128
|
-
try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps); }
|
|
128
|
+
try { await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status); }
|
|
129
129
|
catch (error) {
|
|
130
130
|
if (!(error instanceof AuthoredFlowExecutionError) || error.code !== 'step_failed') throw error;
|
|
131
131
|
throw new PluginStepError(diagnostic, outcome.run_id);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps) as { output: unknown };
|
|
134
|
+
const receipt = await readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, {}, outcome.status) as { output: unknown };
|
|
135
135
|
return receipt.output;
|
|
136
136
|
}, admissionKey), deadline]);
|
|
137
137
|
} finally {
|
package/src/authored-root.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { canonicalize } from './canonical.js';
|
|
6
6
|
import { compileSpec, toKernelSpec } from './compile.js';
|
|
7
|
-
import { executeAuthoredFlow, type AuthoredFlowExecutionResult } from './authored-flow-executor.js';
|
|
7
|
+
import { executeAuthoredFlow, type AuthoredFlowExecutionResult, type AuthoredFlowSuspendedResult } from './authored-flow-executor.js';
|
|
8
8
|
import { isDurableCompletionDetail, isLoweredCompletion } from './authored-completion.js';
|
|
9
9
|
import { AUTHORED_ROOT_KIND } from './authored-verdict.js';
|
|
10
10
|
import {
|
|
@@ -20,6 +20,11 @@ import { withWorkerLease } from './worker-lease.js';
|
|
|
20
20
|
import { AuthoredFlowExecutionError, AuthoredHumanParked } from './authored-flow-error.js';
|
|
21
21
|
import { readOpenHumanWaits } from './authored-human.js';
|
|
22
22
|
import { isSurfaceCompletionReason } from './authored-step-output.js';
|
|
23
|
+
import { readSubscriptionPark } from './authored-subscription-park.js';
|
|
24
|
+
|
|
25
|
+
export type DurableAuthoredFlowResult =
|
|
26
|
+
| (AuthoredFlowExecutionResult & { readonly rootRunId: string })
|
|
27
|
+
| (AuthoredFlowSuspendedResult & { readonly rootRunId: string });
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Taken from the projection module rather than spelled twice: `flows status`
|
|
@@ -55,6 +60,8 @@ export interface AuthoredRootSourceAuthority {
|
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
export interface DurableAuthoredOptions {
|
|
63
|
+
/** Preserve root authority even if a child later fails or parks. */
|
|
64
|
+
readonly onAdmitted?: (runId: string) => void;
|
|
58
65
|
readonly dataDir: string;
|
|
59
66
|
readonly admissionKey?: string;
|
|
60
67
|
readonly localAgentStream?: string;
|
|
@@ -69,7 +76,7 @@ export async function executeDurableAuthoredFlow(
|
|
|
69
76
|
journal: JournalClient,
|
|
70
77
|
input: unknown,
|
|
71
78
|
options: DurableAuthoredOptions,
|
|
72
|
-
): Promise<
|
|
79
|
+
): Promise<DurableAuthoredFlowResult> {
|
|
73
80
|
assertAuthoredRuntimeAvailable();
|
|
74
81
|
const source = await readFile(loaded.sourcePath);
|
|
75
82
|
const sources = await Promise.all(loaded.graph.map(async node => Object.freeze({
|
|
@@ -106,6 +113,7 @@ export async function executeDurableAuthoredFlow(
|
|
|
106
113
|
workspace: [], streams: [{ stream, read_offset: 0 }],
|
|
107
114
|
});
|
|
108
115
|
const outcome = await journal.runStart(spec, undefined, admissionKey);
|
|
116
|
+
options.onAdmitted?.(outcome.run_id);
|
|
109
117
|
if (outcome.status === 'completed') {
|
|
110
118
|
dispatchWait.cancel();
|
|
111
119
|
return await completedRootResult(journal, outcome.run_id);
|
|
@@ -119,6 +127,9 @@ export async function executeDurableAuthoredFlow(
|
|
|
119
127
|
// redelivers only when the former worker connection is gone.
|
|
120
128
|
const resumed = await journal.runResume(outcome.run_id);
|
|
121
129
|
await assertNoOpenHumanWait(journal, resumed);
|
|
130
|
+
const parked = await readSubscriptionPark(journal, outcome.run_id);
|
|
131
|
+
if (parked !== undefined) return { state: 'suspended', name: metadata.flowName,
|
|
132
|
+
suspension: parked, journalSteps: [], rootRunId: outcome.run_id };
|
|
122
133
|
const dispatch = await dispatchWait.promise;
|
|
123
134
|
return await driveRoot(loaded, metadata, journal, peer, dispatch, options);
|
|
124
135
|
} finally {
|
|
@@ -132,7 +143,7 @@ export async function resumeDurableAuthoredFlow(
|
|
|
132
143
|
rootRunId: string,
|
|
133
144
|
journal: JournalClient,
|
|
134
145
|
options: Omit<DurableAuthoredOptions, 'admissionKey'>,
|
|
135
|
-
): Promise<
|
|
146
|
+
): Promise<DurableAuthoredFlowResult | undefined> {
|
|
136
147
|
const metadata = await readAuthoredRootMetadata(journal, rootRunId);
|
|
137
148
|
if (metadata === undefined) return undefined;
|
|
138
149
|
assertAuthoredRuntimeAvailable();
|
|
@@ -159,6 +170,9 @@ export async function resumeDurableAuthoredFlow(
|
|
|
159
170
|
assertRootCanDispatch(outcome);
|
|
160
171
|
await assertNoOpenHumanWait(journal, outcome);
|
|
161
172
|
options.lifecycle?.onRunStarted?.({ runId: rootRunId, flow: metadata.flowName, resumed: true });
|
|
173
|
+
const parked = await readSubscriptionPark(journal, rootRunId);
|
|
174
|
+
if (parked !== undefined) return { state: 'suspended', name: metadata.flowName,
|
|
175
|
+
suspension: parked, journalSteps: [], rootRunId };
|
|
162
176
|
const dispatch = await dispatchWait.promise;
|
|
163
177
|
return await driveRoot(loaded, metadata, journal, peer, dispatch, options);
|
|
164
178
|
} finally {
|
|
@@ -204,7 +218,7 @@ async function driveRoot(
|
|
|
204
218
|
peer: JournalClient,
|
|
205
219
|
dispatch: StepDispatchEvent,
|
|
206
220
|
options: Omit<DurableAuthoredOptions, 'admissionKey'>,
|
|
207
|
-
): Promise<
|
|
221
|
+
): Promise<DurableAuthoredFlowResult> {
|
|
208
222
|
try {
|
|
209
223
|
const result = await withWorkerLease(peer, dispatch, async rootSignal => {
|
|
210
224
|
const callerSignal = options.lifecycle?.signal;
|
|
@@ -228,6 +242,7 @@ async function driveRoot(
|
|
|
228
242
|
...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
|
|
229
243
|
rootRunId: dispatch.run_id,
|
|
230
244
|
extensions: loaded.extensions,
|
|
245
|
+
flowGraph: loaded.graph,
|
|
231
246
|
...options.lifecycle,
|
|
232
247
|
signal: callerSignal === undefined
|
|
233
248
|
? rootSignal
|
|
@@ -247,6 +262,15 @@ async function driveRoot(
|
|
|
247
262
|
);
|
|
248
263
|
return Object.freeze({ ...result, rootRunId: dispatch.run_id });
|
|
249
264
|
} catch (error) {
|
|
265
|
+
if (error instanceof AuthoredFlowExecutionError
|
|
266
|
+
&& error.code === 'subscription_suspended'
|
|
267
|
+
&& error.suspension !== undefined) {
|
|
268
|
+
await peer.subscriptionPark({ run_id: dispatch.run_id, step_id: dispatch.step_id,
|
|
269
|
+
attempt: dispatch.attempt, idempotency_key: dispatch.idempotency_key,
|
|
270
|
+
subscription_id: error.suspension.subscriptionId, phase: error.suspension.kind });
|
|
271
|
+
return Object.freeze({ state: 'suspended' as const, name: metadata.flowName,
|
|
272
|
+
suspension: error.suspension, journalSteps: Object.freeze([]), rootRunId: dispatch.run_id });
|
|
273
|
+
}
|
|
250
274
|
if (error instanceof AuthoredHumanParked) {
|
|
251
275
|
// Not a failure: the body reached a question nobody has answered. Park
|
|
252
276
|
// THIS attempt on the kernel's `wait.human` under the body's own wait
|