@relayflows/sdk 2.0.34 → 2.0.35
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-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 +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +17 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +92 -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-node-entry.js +1 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-root.js +1 -0
- package/dist/authored-root.js.map +1 -1
- 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 +18 -7
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +8 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- 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/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/authored-dispatch.ts +113 -0
- package/src/authored-flow-error.ts +3 -0
- package/src/authored-flow-executor.ts +126 -15
- package/src/authored-flow-loader.ts +12 -0
- package/src/authored-node-entry.ts +1 -0
- package/src/authored-root.ts +1 -0
- package/src/authored-worker-step.ts +17 -6
- package/src/cli/run.ts +10 -0
- package/src/cli.ts +3 -0
- package/src/cloud-mirror-step.ts +16 -2
- package/src/cloud-mirror.ts +15 -2
- package/src/worker-slots.ts +63 -4
package/dist/worker-slots.js
CHANGED
|
@@ -26,20 +26,38 @@ export class WorkerSlots {
|
|
|
26
26
|
throw new RangeError(`worker capacity must be an integer from 1 to ${MAX_LOCAL_AGENT_CAPACITY} (got ${capacity})`);
|
|
27
27
|
}
|
|
28
28
|
async run(work) {
|
|
29
|
+
return this.runInScope(work);
|
|
30
|
+
}
|
|
31
|
+
/** One independently closable admission scope over this shared capacity. */
|
|
32
|
+
scope() {
|
|
33
|
+
const scope = {};
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
run: (work) => this.runInScope(work, scope),
|
|
36
|
+
close: (reason) => this.closeScope(scope, reason),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async runInScope(work, scope) {
|
|
29
40
|
if (this.closed !== undefined)
|
|
30
41
|
throw this.closed.reason;
|
|
42
|
+
if (scope?.closed !== undefined)
|
|
43
|
+
throw scope.closed.reason;
|
|
31
44
|
if (this.held < this.capacity)
|
|
32
45
|
this.held++;
|
|
33
46
|
// A released slot is handed straight to the next waiter, so `held` never
|
|
34
47
|
// dips below capacity while anyone is queued and no later caller can jump it.
|
|
35
48
|
else {
|
|
36
|
-
await new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
49
|
+
await new Promise((resolve, reject) => this.waiting.push({ resolve, reject, scope }));
|
|
37
50
|
// Woken with the slot, but the body may have failed in between (see release).
|
|
38
51
|
const closed = this.closedReason();
|
|
39
52
|
if (closed !== undefined) {
|
|
40
53
|
this.release();
|
|
41
54
|
throw closed.reason;
|
|
42
55
|
}
|
|
56
|
+
const scopeClosed = scopeClosedReason(scope);
|
|
57
|
+
if (scopeClosed !== undefined) {
|
|
58
|
+
this.release();
|
|
59
|
+
throw scopeClosed.reason;
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
try {
|
|
45
63
|
return await work();
|
|
@@ -66,16 +84,28 @@ export class WorkerSlots {
|
|
|
66
84
|
return;
|
|
67
85
|
}
|
|
68
86
|
setImmediate(() => {
|
|
69
|
-
if (this.closed === undefined) {
|
|
87
|
+
if (this.closed === undefined && next.scope?.closed === undefined) {
|
|
70
88
|
next.resolve();
|
|
71
89
|
return;
|
|
72
90
|
}
|
|
73
91
|
// Closed meanwhile. `close` could not see this waiter (already taken off
|
|
74
92
|
// the queue), so refuse it here, and pass the slot on instead of leaking it.
|
|
75
|
-
next.reject(this.closed.reason);
|
|
93
|
+
next.reject(this.closed?.reason ?? next.scope.closed.reason);
|
|
76
94
|
this.release();
|
|
77
95
|
});
|
|
78
96
|
}
|
|
97
|
+
closeScope(scope, reason) {
|
|
98
|
+
if (scope.closed !== undefined)
|
|
99
|
+
return;
|
|
100
|
+
scope.closed = { reason };
|
|
101
|
+
for (let index = this.waiting.length - 1; index >= 0; index--) {
|
|
102
|
+
const waiter = this.waiting[index];
|
|
103
|
+
if (waiter.scope !== scope)
|
|
104
|
+
continue;
|
|
105
|
+
this.waiting.splice(index, 1);
|
|
106
|
+
waiter.reject(reason);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
79
109
|
/**
|
|
80
110
|
* Refuse every queued and future admission with `reason`; work already
|
|
81
111
|
* holding a slot is not interrupted. The authored body calls this when it
|
|
@@ -91,4 +121,11 @@ export class WorkerSlots {
|
|
|
91
121
|
waiter.reject(reason);
|
|
92
122
|
}
|
|
93
123
|
}
|
|
124
|
+
/** Read through a call so a check after an `await` is not narrowed away. */
|
|
125
|
+
function scopeClosedReason(scope) {
|
|
126
|
+
return scope?.closed;
|
|
127
|
+
}
|
|
128
|
+
export function authoredWorkerSlots(capacity) {
|
|
129
|
+
return Object.freeze({ agent: new WorkerSlots(capacity), llm: new WorkerSlots(capacity) });
|
|
130
|
+
}
|
|
94
131
|
//# sourceMappingURL=worker-slots.js.map
|
package/dist/worker-slots.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-slots.js","sourceRoot":"","sources":["../src/worker-slots.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAC9C,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAE3C,sFAAsF;AACtF,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAK,KAAgB,IAAI,CAAC,IAAK,KAAgB,IAAI,wBAAwB,CAAC;AAC5G,CAAC;
|
|
1
|
+
{"version":3,"file":"worker-slots.js","sourceRoot":"","sources":["../src/worker-slots.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayflows/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.35",
|
|
4
4
|
"description": "TypeScript-first authoring SDK for Relayflows. Compiles specs; speaks the journal protocol.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
50
50
|
"@relayfile/adapter-core": "0.6.2",
|
|
51
51
|
"@relayfile/relay-helpers": "0.4.12",
|
|
52
|
-
"@relayflows/surface": "2.0.
|
|
52
|
+
"@relayflows/surface": "2.0.35",
|
|
53
53
|
"@types/js-yaml": "^4.0.9",
|
|
54
54
|
"acorn": "^8.18.0",
|
|
55
55
|
"ai-hist": "0.4.1",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { DispatchResult } from '@relayflows/surface';
|
|
2
|
+
import type { FlowHandle } from './authored-flow.js';
|
|
3
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
4
|
+
import type { LoadedAuthoredFlowNode } from './authored-flow-loader.js';
|
|
5
|
+
import { flowRequirements, type RequirementsFlowDefinition } from './flow-requirements.js';
|
|
6
|
+
|
|
7
|
+
export const MAX_DISPATCH_DEPTH = 3;
|
|
8
|
+
|
|
9
|
+
/** Resolve one attenuated direct child from the statically loaded `use` graph. */
|
|
10
|
+
export function resolveDispatchChild(options: {
|
|
11
|
+
readonly handle: FlowHandle;
|
|
12
|
+
readonly definition: RequirementsFlowDefinition & { readonly name: string };
|
|
13
|
+
readonly graph?: readonly LoadedAuthoredFlowNode[];
|
|
14
|
+
readonly flowName: string;
|
|
15
|
+
readonly depth: number;
|
|
16
|
+
}): LoadedAuthoredFlowNode {
|
|
17
|
+
if (typeof options.flowName !== 'string' || options.flowName.trim() === '') {
|
|
18
|
+
throw new AuthoredFlowExecutionError('dispatch_invalid', 'f.dispatch requires a non-empty declared child flow name');
|
|
19
|
+
}
|
|
20
|
+
if (options.depth >= MAX_DISPATCH_DEPTH) {
|
|
21
|
+
throw new AuthoredFlowExecutionError(
|
|
22
|
+
'dispatch_depth_exceeded',
|
|
23
|
+
`f.dispatch depth exceeds ${MAX_DISPATCH_DEPTH}; flatten the flow tree or move work into the current child`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const current = options.graph?.find(node => node.handle === options.handle);
|
|
27
|
+
const direct = current?.use.map(path => options.graph!.find(node => node.path === path)) ?? [];
|
|
28
|
+
const matches = direct.filter((node): node is LoadedAuthoredFlowNode =>
|
|
29
|
+
node !== undefined && node.getDefinition(node.handle).name === options.flowName);
|
|
30
|
+
if (matches.length !== 1) {
|
|
31
|
+
const declared = direct.filter((node): node is LoadedAuthoredFlowNode => node !== undefined)
|
|
32
|
+
.map(node => node.getDefinition(node.handle).name).sort();
|
|
33
|
+
throw new AuthoredFlowExecutionError(
|
|
34
|
+
matches.length > 1 ? 'dispatch_invalid' : 'dispatch_unknown',
|
|
35
|
+
`flow "${options.definition.name}" cannot dispatch ${JSON.stringify(options.flowName)}; direct use children: ${declared.length === 0 ? '(none)' : declared.join(', ')}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const child = matches[0]!;
|
|
39
|
+
const childDefinition = child.getDefinition(child.handle);
|
|
40
|
+
assertChildCapabilities(options.definition, childDefinition);
|
|
41
|
+
if (childDefinition.header.budget !== undefined) {
|
|
42
|
+
throw new AuthoredFlowExecutionError(
|
|
43
|
+
'dispatch_invalid',
|
|
44
|
+
`child flow "${childDefinition.name}" declares its own budget; put the run-tree ceiling on "${options.definition.name}" until nested budget intersection ships`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return child;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseDispatchReceipt(value: string, step: string): DispatchResult {
|
|
51
|
+
let parsed: unknown;
|
|
52
|
+
try { parsed = JSON.parse(value); } catch {
|
|
53
|
+
throw new AuthoredFlowExecutionError('journal_protocol_violation', `dispatch step "${step}" recorded invalid JSON`);
|
|
54
|
+
}
|
|
55
|
+
const receipt = parsed as Partial<DispatchResult> | null;
|
|
56
|
+
if (typeof receipt !== 'object' || receipt === null || typeof receipt.name !== 'string'
|
|
57
|
+
|| receipt.completionReason !== 'success'
|
|
58
|
+
|| (receipt.completionDetail !== undefined && typeof receipt.completionDetail !== 'string')) {
|
|
59
|
+
throw new AuthoredFlowExecutionError('journal_protocol_violation', `dispatch step "${step}" recorded an invalid child receipt`);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
name: receipt.name,
|
|
63
|
+
completionReason: 'success',
|
|
64
|
+
...(receipt.completionDetail === undefined ? {} : { completionDetail: receipt.completionDetail }),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Last visible child operations; the dispatch receipt joins all of them. */
|
|
69
|
+
export function childLeaves(steps: readonly { readonly id: string; readonly after?: readonly string[] }[]): string[] {
|
|
70
|
+
const visible = steps.filter(step => !/(?:^|--)complete-[1-9][0-9]*$/.test(step.id));
|
|
71
|
+
const parents = new Set(visible.flatMap(step => step.after ?? []));
|
|
72
|
+
return visible.map(step => step.id).filter(id => !parents.has(id));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A child may use only provider/MCP capabilities its parent already declared. */
|
|
76
|
+
function assertChildCapabilities(
|
|
77
|
+
parent: RequirementsFlowDefinition & { readonly name: string },
|
|
78
|
+
child: RequirementsFlowDefinition & { readonly name: string },
|
|
79
|
+
): void {
|
|
80
|
+
const parentTools = parent.header?.tools ?? {};
|
|
81
|
+
const childTools = child.header?.tools ?? {};
|
|
82
|
+
for (const [tool, declared] of Object.entries(childTools)) {
|
|
83
|
+
if (tool === 'mcp') {
|
|
84
|
+
const allowed = new Set(Array.isArray(parentTools.mcp) ? parentTools.mcp as readonly unknown[] : []);
|
|
85
|
+
const requested = Array.isArray(declared) ? declared : [];
|
|
86
|
+
if (requested.some(server => !allowed.has(server))) {
|
|
87
|
+
throw new AuthoredFlowExecutionError(
|
|
88
|
+
'dispatch_invalid',
|
|
89
|
+
`child flow "${child.name}" widens tools.mcp beyond parent "${parent.name}"`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const parentHelpers = helperAuthority(parent);
|
|
95
|
+
for (const requirement of flowRequirements(child).integrations) {
|
|
96
|
+
if ((requirement.from === 'tools' || requirement.from === 'helper')
|
|
97
|
+
&& !parentHelpers.has(requirement.provider)) {
|
|
98
|
+
throw new AuthoredFlowExecutionError(
|
|
99
|
+
'dispatch_invalid',
|
|
100
|
+
`child flow "${child.name}" requires ${requirement.detail}, which parent "${parent.name}" did not grant`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Helper providers this flow can delegate, from explicit or statically visible use. */
|
|
107
|
+
export function helperAuthority(
|
|
108
|
+
definition: RequirementsFlowDefinition,
|
|
109
|
+
): ReadonlySet<string> {
|
|
110
|
+
return new Set(flowRequirements(definition).integrations
|
|
111
|
+
.filter(requirement => requirement.from === 'tools' || requirement.from === 'helper')
|
|
112
|
+
.map(requirement => requirement.provider));
|
|
113
|
+
}
|
|
@@ -16,6 +16,9 @@ export type AuthoredFlowExecutionErrorCode =
|
|
|
16
16
|
| 'llm_cli_unresolved'
|
|
17
17
|
| 'llm_parked'
|
|
18
18
|
| 'duplicate_completion'
|
|
19
|
+
| 'dispatch_depth_exceeded'
|
|
20
|
+
| 'dispatch_invalid'
|
|
21
|
+
| 'dispatch_unknown'
|
|
19
22
|
| 'journal_protocol_violation'
|
|
20
23
|
| 'missing_completion'
|
|
21
24
|
| 'memory_unreachable'
|
|
@@ -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,6 +39,7 @@ 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,
|
|
@@ -66,6 +68,8 @@ import type {
|
|
|
66
68
|
CompletionReason as ProtocolCompletionReason,
|
|
67
69
|
RunCompletionReason as ProtocolRunCompletionReason,
|
|
68
70
|
} from './protocol.js';
|
|
71
|
+
import type { AuthoredWorkerSlots } from './worker-slots.js';
|
|
72
|
+
import { childLeaves, helperAuthority, parseDispatchReceipt, resolveDispatchChild } from './authored-dispatch.js';
|
|
69
73
|
|
|
70
74
|
type Assert<T extends true> = T;
|
|
71
75
|
type Equal<A, B> = [A] extends [B]
|
|
@@ -185,6 +189,20 @@ export interface ExecuteAuthoredFlowOptions {
|
|
|
185
189
|
* from authored input: direct `/workflows/run` callers control that JSON.
|
|
186
190
|
*/
|
|
187
191
|
readonly extensionDispatch?: HostedExtensionDispatch;
|
|
192
|
+
/** Pinned static `use:` graph available to `f.dispatch`. */
|
|
193
|
+
readonly flowGraph?: readonly LoadedAuthoredFlowNode[];
|
|
194
|
+
/** Internal namespace for descendant step/admission identities. */
|
|
195
|
+
readonly stepPrefix?: string;
|
|
196
|
+
/** Current child depth. Root is zero; dispatch is capped at three. */
|
|
197
|
+
readonly dispatchDepth?: number;
|
|
198
|
+
/** Parent predecessors inherited by the first operation in a child body. */
|
|
199
|
+
readonly entryAfter?: readonly string[];
|
|
200
|
+
/** Root-owned budget accumulator shared by every descendant. */
|
|
201
|
+
readonly sharedBudget?: AuthoredBudget;
|
|
202
|
+
/** Root-owned worker capacity shared by every descendant. */
|
|
203
|
+
readonly workerSlots?: AuthoredWorkerSlots;
|
|
204
|
+
/** Parent helper authority inherited by this dispatched child. */
|
|
205
|
+
readonly allowedHelperProviders?: ReadonlySet<string>;
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
export async function executeAuthoredFlow<Input = undefined>(
|
|
@@ -209,7 +227,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
209
227
|
};
|
|
210
228
|
const definition = getDefinition<Input>(handle);
|
|
211
229
|
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');
|
|
230
|
+
const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory' && key !== 'version' && key !== 'hooks' && key !== 'use');
|
|
213
231
|
if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['mcp', ...helperProviders.map(p => p.namespace)].includes(key))) headerFields.push('tools');
|
|
214
232
|
if (definition.header.tools?.relayfile !== undefined) headerFields.push('tools.relayfile');
|
|
215
233
|
const helperPreflight = checkSlackHelpers(definition);
|
|
@@ -242,7 +260,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
242
260
|
);
|
|
243
261
|
}
|
|
244
262
|
|
|
245
|
-
const budget = new AuthoredBudget(definition.header.budget);
|
|
263
|
+
const budget = options.sharedBudget ?? new AuthoredBudget(definition.header.budget);
|
|
246
264
|
if (definition.header.memory?.agent === true) {
|
|
247
265
|
throw new AuthoredFlowExecutionError('unsupported_header', 'memory.agent requires the follow-up identity-scoped agent memory adapter');
|
|
248
266
|
}
|
|
@@ -255,10 +273,28 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
255
273
|
const journalSteps: AuthoredFlowJournalStep[] = [];
|
|
256
274
|
const authoredSteps: AuthoredFlowOperation<unknown>[] = [];
|
|
257
275
|
const lifecycle = new AuthoredFlowLifecycle();
|
|
258
|
-
const
|
|
276
|
+
const edgeOverrides = new Map<string, AuthoredStepEdges>();
|
|
277
|
+
const stepEdges = (step: string): AuthoredStepEdges | undefined => {
|
|
278
|
+
const overridden = edgeOverrides.get(step);
|
|
279
|
+
if (overridden !== undefined) return overridden;
|
|
280
|
+
const local = lifecycle.stepEdges(step);
|
|
281
|
+
if (local === undefined || (local.after !== undefined && local.after.length > 0) || options.entryAfter === undefined
|
|
282
|
+
|| options.entryAfter.length === 0) return local;
|
|
283
|
+
return { ...local, after: [...options.entryAfter] };
|
|
284
|
+
};
|
|
259
285
|
let nextStep = 1;
|
|
260
286
|
let requestedCompletion: LoweredCompletionReason | undefined;
|
|
261
287
|
let requestedDetail: string | undefined;
|
|
288
|
+
const qualify = (id: string): string => `${options.stepPrefix ?? ''}${id}`;
|
|
289
|
+
const nextId = (kind: string): string => qualify(`${kind}-${nextStep++}`);
|
|
290
|
+
const assertHelperAllowed = (provider: string): void => {
|
|
291
|
+
if (options.allowedHelperProviders !== undefined && !options.allowedHelperProviders.has(provider)) {
|
|
292
|
+
throw new AuthoredFlowExecutionError(
|
|
293
|
+
'dispatch_invalid',
|
|
294
|
+
`child flow "${definition.name}" attempted helper provider "${provider}" outside its parent's granted integrations`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
262
298
|
|
|
263
299
|
const lowerDeterministic = authoredDeterministicRunner(
|
|
264
300
|
definition.name, journal, journalSteps, budget, {
|
|
@@ -271,6 +307,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
271
307
|
const worker = authoredWorkerRunner(
|
|
272
308
|
definition, journal, flowPath, journalSteps, waitOptions,
|
|
273
309
|
localAgentStream, budget, definition.header.budget, options.rootRunId, options.workerCapacity, stepEdges,
|
|
310
|
+
options.workerSlots,
|
|
274
311
|
);
|
|
275
312
|
|
|
276
313
|
/**
|
|
@@ -373,7 +410,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
373
410
|
function llmOperation(prompt: string, options: LlmOptions): Step<unknown>;
|
|
374
411
|
function llmOperation(prompt: string | TemplateStringsArray, ...values: unknown[]): Step<unknown> {
|
|
375
412
|
assertOperationAllowed('llm', definition.name, requestedCompletion);
|
|
376
|
-
const id =
|
|
413
|
+
const id = nextId('llm');
|
|
377
414
|
// Hoist the operation reference so the start closure can read the
|
|
378
415
|
// caller's `.gate(config)` at spec-build time. AuthoredFlowOperation
|
|
379
416
|
// begins after construction returns, so `llmOp` is defined by then.
|
|
@@ -420,7 +457,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
420
457
|
`f.run options.onNonZero must be 'fail' or 'record' (got ${JSON.stringify(policy)}).`,
|
|
421
458
|
);
|
|
422
459
|
}
|
|
423
|
-
const id =
|
|
460
|
+
const id = nextId('run');
|
|
424
461
|
if (policy === 'record') {
|
|
425
462
|
let recordOp!: AuthoredFlowOperation<RunResult>;
|
|
426
463
|
recordOp = new AuthoredFlowOperation<RunResult>(
|
|
@@ -451,8 +488,9 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
451
488
|
const slackRun = options.rootRunId ?? randomUUID();
|
|
452
489
|
function slackOperation<T>(call: SlackCall): Step<T> {
|
|
453
490
|
assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion);
|
|
491
|
+
assertHelperAllowed(call.provider);
|
|
454
492
|
const snapshot = snapshotJsonValue(call, 'f.slack call') as unknown as SlackCall;
|
|
455
|
-
const id = `slack-${slackRun}-${nextStep++}
|
|
493
|
+
const id = qualify(`slack-${slackRun}-${nextStep++}`);
|
|
456
494
|
return trackStep(authoredSteps, new AuthoredFlowOperation<T>(
|
|
457
495
|
id, `slack.${call.verb}`,
|
|
458
496
|
() => assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion),
|
|
@@ -480,8 +518,9 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
480
518
|
...createHelpers(<T>(call: HelperCall): Step<T> => {
|
|
481
519
|
const verb = `${call.provider}.${call.verb}`;
|
|
482
520
|
assertOperationAllowed(verb, definition.name, requestedCompletion);
|
|
521
|
+
assertHelperAllowed(call.provider);
|
|
483
522
|
const snapshot = snapshotJsonValue(call, `f.${verb} call`) as unknown as HelperCall;
|
|
484
|
-
const id = `${call.provider}-${slackRun}-${nextStep++}
|
|
523
|
+
const id = qualify(`${call.provider}-${slackRun}-${nextStep++}`);
|
|
485
524
|
return trackStep(authoredSteps, new AuthoredFlowOperation<T>(id, verb,
|
|
486
525
|
() => assertOperationAllowed(verb, definition.name, requestedCompletion),
|
|
487
526
|
async () => await runHelperEffect(journal, definition.name, id, snapshot,
|
|
@@ -496,7 +535,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
496
535
|
},
|
|
497
536
|
mcp: buildMcpProxy(checkedMcp.inventory, (server, tool, args, known) => {
|
|
498
537
|
assertOperationAllowed('mcp', definition.name, requestedCompletion);
|
|
499
|
-
const id =
|
|
538
|
+
const id = nextId('mcp');
|
|
500
539
|
return trackStep(authoredSteps, new AuthoredFlowOperation(
|
|
501
540
|
id, 'mcp',
|
|
502
541
|
() => assertOperationAllowed('mcp', definition.name, requestedCompletion),
|
|
@@ -516,7 +555,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
516
555
|
assertOperationAllowed('agent', definition.name, requestedCompletion);
|
|
517
556
|
// Authored headers do not yet declare reusable named agents, so the name
|
|
518
557
|
// identifies nothing to the kernel; it is the step's label in the DAG.
|
|
519
|
-
const id =
|
|
558
|
+
const id = nextId('agent');
|
|
520
559
|
let agentOp!: AuthoredFlowOperation<AgentResult>;
|
|
521
560
|
agentOp = new AuthoredFlowOperation<AgentResult>(
|
|
522
561
|
id,
|
|
@@ -545,6 +584,12 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
545
584
|
*/
|
|
546
585
|
human(question, humanOptions) {
|
|
547
586
|
assertOperationAllowed('human', definition.name, requestedCompletion);
|
|
587
|
+
if ((options.dispatchDepth ?? 0) > 0) {
|
|
588
|
+
throw new AuthoredFlowExecutionError(
|
|
589
|
+
'dispatch_invalid',
|
|
590
|
+
`child flow "${definition.name}" cannot call f.human in this release; the parent owns approval authority`,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
548
593
|
if (typeof question !== 'string' || question.trim() === '') {
|
|
549
594
|
throw new AuthoredFlowExecutionError('human_answer_invalid', 'f.human requires a non-empty question');
|
|
550
595
|
}
|
|
@@ -558,7 +603,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
558
603
|
if (!parsedTo.ok) {
|
|
559
604
|
throw new AuthoredFlowExecutionError('human_to_invalid', `f.human to ${parsedTo.reason}`);
|
|
560
605
|
}
|
|
561
|
-
const id =
|
|
606
|
+
const id = nextId('human');
|
|
562
607
|
const to = humanOptions.to;
|
|
563
608
|
// Hoisted like `llmOp`/`runOp`: the start closure reads the caller's
|
|
564
609
|
// `.gate(config)` at spec-build time, so a named gate on the answer is
|
|
@@ -590,13 +635,79 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
590
635
|
);
|
|
591
636
|
return trackStep(authoredSteps, humanOp);
|
|
592
637
|
},
|
|
593
|
-
dispatch
|
|
638
|
+
dispatch(flowName, dispatchInput) {
|
|
594
639
|
assertOperationAllowed('dispatch', definition.name, requestedCompletion);
|
|
595
|
-
|
|
640
|
+
const depth = options.dispatchDepth ?? 0;
|
|
641
|
+
const child = resolveDispatchChild({
|
|
642
|
+
handle, definition, graph: options.flowGraph,
|
|
643
|
+
flowName, depth,
|
|
644
|
+
});
|
|
645
|
+
const childDefinition = child.getDefinition(child.handle);
|
|
646
|
+
const inputSnapshot = snapshotJsonValue(dispatchInput, `f.dispatch(${JSON.stringify(flowName)}) input`);
|
|
647
|
+
const id = nextId('dispatch');
|
|
648
|
+
let dispatchOp!: AuthoredFlowOperation<DispatchResult>;
|
|
649
|
+
dispatchOp = new AuthoredFlowOperation<DispatchResult>(
|
|
650
|
+
id,
|
|
651
|
+
'dispatch',
|
|
652
|
+
() => assertOperationAllowed('dispatch', definition.name, requestedCompletion),
|
|
653
|
+
async () => {
|
|
654
|
+
const inheritedAfter = stepEdges(id)?.after ?? [];
|
|
655
|
+
const currentHelperAuthority = helperAuthority(definition);
|
|
656
|
+
const allowedHelperProviders = options.allowedHelperProviders === undefined
|
|
657
|
+
? currentHelperAuthority
|
|
658
|
+
: new Set([...currentHelperAuthority].filter(provider => options.allowedHelperProviders!.has(provider)));
|
|
659
|
+
const childResult = await executeAuthoredFlow(child.handle, journal, inputSnapshot, {
|
|
660
|
+
getDefinition: child.getDefinition,
|
|
661
|
+
dataDir: options.dataDir,
|
|
662
|
+
flowPath: child.path,
|
|
663
|
+
...(localAgentStream === undefined ? {} : { localAgentStream }),
|
|
664
|
+
...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
|
|
665
|
+
...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
|
|
666
|
+
flowGraph: options.flowGraph,
|
|
667
|
+
stepPrefix: `${id}--`,
|
|
668
|
+
dispatchDepth: depth + 1,
|
|
669
|
+
entryAfter: inheritedAfter,
|
|
670
|
+
sharedBudget: budget,
|
|
671
|
+
...(worker.slots === undefined ? {} : { workerSlots: worker.slots }),
|
|
672
|
+
allowedHelperProviders,
|
|
673
|
+
...(options.extensions === undefined ? {} : { extensions: options.extensions }),
|
|
674
|
+
onProgress,
|
|
675
|
+
...(options.onWait === undefined ? {} : { onWait: options.onWait }),
|
|
676
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
677
|
+
});
|
|
678
|
+
if (childResult.completionReason !== 'success') {
|
|
679
|
+
throw new AuthoredFlowExecutionError(
|
|
680
|
+
'step_failed',
|
|
681
|
+
`child flow "${childResult.name}" completed with ${childResult.completionReason}`
|
|
682
|
+
+ (childResult.completionDetail === undefined ? '' : `: ${childResult.completionDetail}`),
|
|
683
|
+
childResult.completionReason === 'step_failed' ? 'step_failed' : undefined,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const leaves = childLeaves(childResult.journalSteps);
|
|
687
|
+
edgeOverrides.set(id, {
|
|
688
|
+
...displayLabel(childResult.name),
|
|
689
|
+
...(leaves.length > 0 ? { after: leaves } : inheritedAfter.length === 0
|
|
690
|
+
? {} : { after: inheritedAfter }),
|
|
691
|
+
});
|
|
692
|
+
const receipt: DispatchResult = {
|
|
693
|
+
name: childResult.name,
|
|
694
|
+
completionReason: 'success',
|
|
695
|
+
...(childResult.completionDetail === undefined ? {} : { completionDetail: childResult.completionDetail }),
|
|
696
|
+
};
|
|
697
|
+
const literal = `'${JSON.stringify(receipt).replaceAll("'", "'\\''")}'`;
|
|
698
|
+
const recorded = await lowerDeterministic(
|
|
699
|
+
id, `printf '%s' ${literal}`, false, undefined, dispatchOp.namedGate,
|
|
700
|
+
);
|
|
701
|
+
return parseDispatchReceipt(recorded, id);
|
|
702
|
+
},
|
|
703
|
+
lifecycle,
|
|
704
|
+
displayLabel(flowName),
|
|
705
|
+
);
|
|
706
|
+
return trackStep(authoredSteps, dispatchOp);
|
|
596
707
|
},
|
|
597
708
|
hook(name, input) {
|
|
598
709
|
assertOperationAllowed('hook', definition.name, requestedCompletion);
|
|
599
|
-
const id =
|
|
710
|
+
const id = nextId('hook');
|
|
600
711
|
const snapshot = snapshotJsonValue(input, 'f.hook input');
|
|
601
712
|
return trackStep(authoredSteps, new AuthoredFlowOperation<boolean>(
|
|
602
713
|
id, 'hook',
|
|
@@ -661,7 +772,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
661
772
|
Object.assign(context, pluginHelpers(checkedMcp.plugins ?? [], (plugin, verb, args) => {
|
|
662
773
|
const label = `${verb.namespace}.${verb.method}`;
|
|
663
774
|
assertOperationAllowed(label, definition.name, requestedCompletion);
|
|
664
|
-
const id =
|
|
775
|
+
const id = nextId('plugin');
|
|
665
776
|
return trackStep(authoredSteps, new AuthoredFlowOperation(
|
|
666
777
|
id, label, () => assertOperationAllowed(label, definition.name, requestedCompletion),
|
|
667
778
|
() => runPluginEffect(journal, definition.name, id, plugin, verb, args,
|
|
@@ -735,7 +846,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
735
846
|
// explanation the root had already journaled to `run_admission_conflict`.
|
|
736
847
|
// With no detail there is no environment in the command and nothing to
|
|
737
848
|
// commit, so that path is untouched.
|
|
738
|
-
const terminalId = `complete-${nextStep}
|
|
849
|
+
const terminalId = qualify(`complete-${nextStep}`);
|
|
739
850
|
const verdict = await commitAuthoredVerdict(journal, options.rootRunId, terminalId, {
|
|
740
851
|
reason: requestedCompletion,
|
|
741
852
|
...(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;
|
|
@@ -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 }),
|
package/src/authored-root.ts
CHANGED
|
@@ -228,6 +228,7 @@ async function driveRoot(
|
|
|
228
228
|
...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
|
|
229
229
|
rootRunId: dispatch.run_id,
|
|
230
230
|
extensions: loaded.extensions,
|
|
231
|
+
flowGraph: loaded.graph,
|
|
231
232
|
...options.lifecycle,
|
|
232
233
|
signal: callerSignal === undefined
|
|
233
234
|
? rootSignal
|
|
@@ -16,7 +16,7 @@ import { snapshotJsonValue } from './json-value.js';
|
|
|
16
16
|
import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
17
17
|
import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
|
|
18
18
|
import type { StepFailedDetails } from './failure-kinds.js';
|
|
19
|
-
import {
|
|
19
|
+
import { authoredWorkerSlots, type AuthoredWorkerSlots } from './worker-slots.js';
|
|
20
20
|
|
|
21
21
|
const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
|
|
22
22
|
|
|
@@ -26,11 +26,16 @@ export function authoredWorkerRunner(
|
|
|
26
26
|
journalSteps: AuthoredFlowJournalStep[], waitOptions: RunLifecycleOptions,
|
|
27
27
|
localAgentStream?: string, budget?: AuthoredBudget, headerBudget?: unknown,
|
|
28
28
|
rootRunId?: string, workerCapacity?: number, stepEdges?: AuthoredStepContext['stepEdges'],
|
|
29
|
+
sharedSlots?: AuthoredWorkerSlots,
|
|
29
30
|
) {
|
|
30
31
|
// Sized to the attached local workers, so concurrent calls wait here for a
|
|
31
32
|
// slot instead of being admitted and parked for want of a free worker.
|
|
32
|
-
const
|
|
33
|
-
|
|
33
|
+
const ownsSlots = sharedSlots === undefined;
|
|
34
|
+
const slots = sharedSlots ?? (workerCapacity === undefined ? undefined : authoredWorkerSlots(workerCapacity));
|
|
35
|
+
const slotScopes = slots === undefined ? undefined : {
|
|
36
|
+
agent: slots.agent.scope(),
|
|
37
|
+
llm: slots.llm.scope(),
|
|
38
|
+
};
|
|
34
39
|
const check = authoredPreflight(flowPath);
|
|
35
40
|
const context: AuthoredStepContext = {
|
|
36
41
|
...(rootRunId === undefined ? {} : { rootRunId }),
|
|
@@ -136,14 +141,20 @@ export function authoredWorkerRunner(
|
|
|
136
141
|
const admit = async () => budget === undefined
|
|
137
142
|
? consume(await journal.runStart(spec, undefined, admissionKey))
|
|
138
143
|
: budget.execute(journal, spec, consume, admissionKey);
|
|
139
|
-
return
|
|
144
|
+
return slotScopes === undefined ? admit() : slotScopes[step.type === 'llm' ? 'llm' : 'agent'].run(admit);
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
return {
|
|
148
|
+
slots,
|
|
143
149
|
/** Refuse every agent/LLM call still waiting for a worker slot (body teardown). */
|
|
144
150
|
stop(reason: unknown): void {
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
if (ownsSlots) {
|
|
152
|
+
slots?.agent.close(reason);
|
|
153
|
+
slots?.llm.close(reason);
|
|
154
|
+
} else {
|
|
155
|
+
slotScopes?.agent.close(reason);
|
|
156
|
+
slotScopes?.llm.close(reason);
|
|
157
|
+
}
|
|
147
158
|
},
|
|
148
159
|
async agent(id: string, options: AgentOptions, verification?: NamedGate): Promise<AgentResult> {
|
|
149
160
|
if (options.workspace !== undefined && localAgentStream !== undefined) {
|
package/src/cli/run.ts
CHANGED
|
@@ -122,6 +122,8 @@ export interface RunLifecycleOptions {
|
|
|
122
122
|
onJournalEntry?: (entry: JournalEvent) => void;
|
|
123
123
|
/** An authored root was admitted: its id is known before its body runs. */
|
|
124
124
|
onRunStarted?: (run: { runId: string; flow: string; resumed?: boolean }) => void;
|
|
125
|
+
/** A declarative root returned its admission receipt, even if no watched entry arrived. */
|
|
126
|
+
onRunReceipt?: (run: { runId: string; flow: string }) => void;
|
|
125
127
|
localAgent?: boolean;
|
|
126
128
|
/** `--agent-capacity`: the local workers' concurrency; the default is `DEFAULT_LOCAL_AGENT_CAPACITY`. */
|
|
127
129
|
agentCapacity?: number;
|
|
@@ -191,6 +193,14 @@ async function executeCheckedFlow(
|
|
|
191
193
|
}
|
|
192
194
|
if (options.onJournalEntry !== undefined) client.on('entry', options.onJournalEntry);
|
|
193
195
|
const outcome = await startWatched(client, spec, options);
|
|
196
|
+
// `run.start { watch: true }` is an event stream, not the source of the
|
|
197
|
+
// root identity. A short deterministic run can finish before its first
|
|
198
|
+
// watched entry is delivered (and an older daemon may refuse `watch`), so
|
|
199
|
+
// receipt consumers such as the Cloud mirror need the admitted id too.
|
|
200
|
+
// This stays separate from `onRunStarted`: opening the observer without a
|
|
201
|
+
// streamed journal would manufacture an empty projection and suppress its
|
|
202
|
+
// truthful "not projected" diagnostic.
|
|
203
|
+
options.onRunReceipt?.({ runId: outcome.run_id, flow: spec.name ?? 'flow' });
|
|
194
204
|
const execution = await classifyOutcome(client, 'run', outcome, base, socketPath, { ...options, dataDir });
|
|
195
205
|
if (options.reuseFromRunId !== undefined) {
|
|
196
206
|
execution.report.reuse = await reuseSummary(client, outcome.run_id, options.reuseFromRunId);
|
package/src/cli.ts
CHANGED
|
@@ -380,6 +380,9 @@ export async function runCli(
|
|
|
380
380
|
observer?.onRunStarted(run);
|
|
381
381
|
mirror?.onRunStarted(run);
|
|
382
382
|
},
|
|
383
|
+
...(mirror === undefined ? {} : {
|
|
384
|
+
onRunReceipt: (run: { runId: string }) => { mirror.onRunStarted(run); },
|
|
385
|
+
}),
|
|
383
386
|
}),
|
|
384
387
|
onWait: (progress: RunProgress) => {
|
|
385
388
|
emitWait(progress, logged);
|