agentfootprint 7.9.0 → 7.11.0
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/README.md +2 -2
- package/dist/core-flow/Graph.js +607 -0
- package/dist/core-flow/Graph.js.map +1 -0
- package/dist/core-flow/Workflow.js +210 -0
- package/dist/core-flow/Workflow.js.map +1 -0
- package/dist/esm/core-flow/Graph.d.ts +272 -0
- package/dist/esm/core-flow/Graph.js +601 -0
- package/dist/esm/core-flow/Graph.js.map +1 -0
- package/dist/esm/core-flow/Workflow.d.ts +146 -0
- package/dist/esm/core-flow/Workflow.js +205 -0
- package/dist/esm/core-flow/Workflow.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/patterns/LlmRouter.d.ts +221 -0
- package/dist/esm/patterns/LlmRouter.js +400 -0
- package/dist/esm/patterns/LlmRouter.js.map +1 -0
- package/dist/esm/patterns/LlmSwarm.d.ts +100 -0
- package/dist/esm/patterns/LlmSwarm.js +109 -0
- package/dist/esm/patterns/LlmSwarm.js.map +1 -0
- package/dist/esm/patterns/index.d.ts +2 -0
- package/dist/esm/patterns/index.js +2 -0
- package/dist/esm/patterns/index.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/patterns/LlmRouter.js +406 -0
- package/dist/patterns/LlmRouter.js.map +1 -0
- package/dist/patterns/LlmSwarm.js +113 -0
- package/dist/patterns/LlmSwarm.js.map +1 -0
- package/dist/patterns/index.js +6 -1
- package/dist/patterns/index.js.map +1 -1
- package/dist/types/core-flow/Graph.d.ts +273 -0
- package/dist/types/core-flow/Graph.d.ts.map +1 -0
- package/dist/types/core-flow/Workflow.d.ts +147 -0
- package/dist/types/core-flow/Workflow.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/patterns/LlmRouter.d.ts +222 -0
- package/dist/types/patterns/LlmRouter.d.ts.map +1 -0
- package/dist/types/patterns/LlmSwarm.d.ts +101 -0
- package/dist/types/patterns/LlmSwarm.d.ts.map +1 -0
- package/dist/types/patterns/index.d.ts +2 -0
- package/dist/types/patterns/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* workflow() — sequential steps whose hand-offs are checked by the compiler.
|
|
4
|
+
*
|
|
5
|
+
* WHY this exists: `Sequence` is the workhorse for "A, then B, then C",
|
|
6
|
+
* and every step it accepts has the same shape — takes `{ message }`,
|
|
7
|
+
* returns `string`. That is exactly right for chaining LLM calls, and
|
|
8
|
+
* exactly wrong the moment a step wants to hand the next one something
|
|
9
|
+
* structured: `Sequence` coerces any non-string step output to `''`
|
|
10
|
+
* (Sequence.ts, the step `outputMapper`), so a step that returns a parsed
|
|
11
|
+
* ticket silently hands the next step nothing at all. The mistake shows up
|
|
12
|
+
* as an empty prompt three steps later, at runtime, in production.
|
|
13
|
+
*
|
|
14
|
+
* `workflow()` closes that gap from both ends:
|
|
15
|
+
*
|
|
16
|
+
* - **At compile time** — step N's OUTPUT type must be what step N+1
|
|
17
|
+
* accepts. A `Runner<{ message: string }, Ticket>` followed by a
|
|
18
|
+
* `Runner<{ orderId: string }, string>` does not compile. The chain is
|
|
19
|
+
* proven before you run it, not debugged after.
|
|
20
|
+
* - **At run time** — a step's value is handed to the next step
|
|
21
|
+
* UNCHANGED. Objects stay objects. The one convenience is the house
|
|
22
|
+
* convention: a step that returns a `string` feeds the next step's
|
|
23
|
+
* `{ message }`, because that is what every LLM runner here wants.
|
|
24
|
+
*
|
|
25
|
+
* Pattern: Adapter over footprintjs's `addSubFlowChartNext`, with the
|
|
26
|
+
* type-level handoff proof carried by overloads (1–8 steps).
|
|
27
|
+
* Role: core-flow/ layer, alongside Sequence/Parallel/Conditional/Loop.
|
|
28
|
+
* Pure control flow — no LLM dependency.
|
|
29
|
+
* Emits: agentfootprint.composition.enter / exit, reported as kind
|
|
30
|
+
* `'Sequence'` — a workflow IS a sequential composition, and
|
|
31
|
+
* widening the public `CompositionKind` union would break
|
|
32
|
+
* exhaustive switches in consumer code for no behavioural gain.
|
|
33
|
+
*
|
|
34
|
+
* THREE HONEST LIMITS, all inherited from the engine and all verified in
|
|
35
|
+
* `test/core-flow/scenario/Workflow.test.ts` — worth knowing before you
|
|
36
|
+
* put rich objects on the wire:
|
|
37
|
+
*
|
|
38
|
+
* 1. Only PLAIN DATA crosses a step boundary. A value with a prototype
|
|
39
|
+
* (Date, Map, Set, a class instance) arrives as `{}`, and `undefined`
|
|
40
|
+
* fields are dropped. Send strings, numbers, arrays and plain
|
|
41
|
+
* objects; send a timestamp as an ISO string, not a `Date`.
|
|
42
|
+
* 2. A step must RETURN its output — the value handed forward is the
|
|
43
|
+
* step chart's traversal result. A step whose last stage returns
|
|
44
|
+
* nothing hands its whole scope forward instead.
|
|
45
|
+
* 3. The workflow's own input keys stay visible to LATER steps too
|
|
46
|
+
* (footprintjs's `getArgs()` inherits the run's arguments). A key the
|
|
47
|
+
* previous step actually produced always wins; a key it did NOT
|
|
48
|
+
* produce can still be read from the original input rather than
|
|
49
|
+
* coming back `undefined`.
|
|
50
|
+
*
|
|
51
|
+
* @example a typed three-step chain
|
|
52
|
+
* ```ts
|
|
53
|
+
* interface Ticket { orderId: string; angry: boolean }
|
|
54
|
+
*
|
|
55
|
+
* const parse: Runner<{ message: string }, Ticket> = …;
|
|
56
|
+
* const lookup: Runner<Ticket, { refundUsd: number }> = …;
|
|
57
|
+
* const reply: Runner<{ refundUsd: number }, string> = …;
|
|
58
|
+
*
|
|
59
|
+
* const intake = workflow(parse, lookup, reply);
|
|
60
|
+
* const answer = await intake.run({ message: 'where is my refund?' });
|
|
61
|
+
* // ^? string — the chain's last output type
|
|
62
|
+
*
|
|
63
|
+
* workflow(parse, reply); // ✗ compile error: Ticket is not { refundUsd }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
+
exports.workflow = exports.Workflow = void 0;
|
|
68
|
+
const footprintjs_1 = require("footprintjs");
|
|
69
|
+
const RunnerBase_js_1 = require("../core/RunnerBase.js");
|
|
70
|
+
const AgentRecorder_js_1 = require("../recorders/core/AgentRecorder.js");
|
|
71
|
+
const CompositionRecorder_js_1 = require("../recorders/core/CompositionRecorder.js");
|
|
72
|
+
const ContextRecorder_js_1 = require("../recorders/core/ContextRecorder.js");
|
|
73
|
+
const StreamRecorder_js_1 = require("../recorders/core/StreamRecorder.js");
|
|
74
|
+
const typedEmit_js_1 = require("../recorders/core/typedEmit.js");
|
|
75
|
+
/**
|
|
76
|
+
* Hand the previous step's value to the next step as its input args.
|
|
77
|
+
*
|
|
78
|
+
* `string` → `{ message }` (the house convention). Plain object → itself.
|
|
79
|
+
* Anything else is a broken hand-off and says so loudly: the alternative
|
|
80
|
+
* is an empty input three steps downstream with nothing pointing back
|
|
81
|
+
* here.
|
|
82
|
+
*/
|
|
83
|
+
function toStepArgs(value, stepNumber) {
|
|
84
|
+
if (typeof value === 'string')
|
|
85
|
+
return { message: value };
|
|
86
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
87
|
+
return { ...value };
|
|
88
|
+
}
|
|
89
|
+
const got = value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value;
|
|
90
|
+
throw new Error(`workflow: step ${stepNumber - 1} handed forward ${got}, but step ${stepNumber} needs an ` +
|
|
91
|
+
'object (or a string, which arrives as { message }). Make each step return its output.');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A sequential composition that passes values through untouched. Build one
|
|
95
|
+
* with {@link workflow} — that factory carries the type-level chain proof.
|
|
96
|
+
*/
|
|
97
|
+
class Workflow extends RunnerBase_js_1.RunnerBase {
|
|
98
|
+
name;
|
|
99
|
+
id;
|
|
100
|
+
steps;
|
|
101
|
+
opts;
|
|
102
|
+
currentRunContext = {
|
|
103
|
+
runStartMs: 0,
|
|
104
|
+
runId: 'pending',
|
|
105
|
+
compositionPath: [],
|
|
106
|
+
};
|
|
107
|
+
constructor(steps, opts = {}) {
|
|
108
|
+
super();
|
|
109
|
+
if (steps.length === 0) {
|
|
110
|
+
throw new Error('Workflow: must have at least one step');
|
|
111
|
+
}
|
|
112
|
+
this.opts = opts;
|
|
113
|
+
this.name = opts.name ?? 'Workflow';
|
|
114
|
+
this.id = opts.id ?? 'workflow';
|
|
115
|
+
this.steps = steps;
|
|
116
|
+
// Eager chart construction — see `RunnerBase.initChart` JSDoc.
|
|
117
|
+
this.initChart(() => this.buildChart());
|
|
118
|
+
}
|
|
119
|
+
async run(input, options) {
|
|
120
|
+
const executor = this.createExecutor();
|
|
121
|
+
this.lastExecutor = executor;
|
|
122
|
+
const result = await executor.run({ input: { ...input }, ...(options ?? {}) });
|
|
123
|
+
return this.finalizeResult(executor, result);
|
|
124
|
+
}
|
|
125
|
+
async resume(checkpoint, input, options) {
|
|
126
|
+
this.emitPauseResume(checkpoint, input);
|
|
127
|
+
const executor = this.createExecutor();
|
|
128
|
+
this.lastExecutor = executor;
|
|
129
|
+
const result = await executor.resume(checkpoint, input, options);
|
|
130
|
+
return this.finalizeResult(executor, result);
|
|
131
|
+
}
|
|
132
|
+
createExecutor() {
|
|
133
|
+
this.currentRunContext = {
|
|
134
|
+
runStartMs: Date.now(),
|
|
135
|
+
runId: (0, RunnerBase_js_1.makeRunId)(),
|
|
136
|
+
compositionPath: [`Workflow:${this.id}`],
|
|
137
|
+
};
|
|
138
|
+
const executor = new footprintjs_1.FlowChartExecutor(this.getSpec());
|
|
139
|
+
const dispatcher = this.getDispatcher();
|
|
140
|
+
const getRunCtx = () => this.currentRunContext;
|
|
141
|
+
executor.attachCombinedRecorder(new ContextRecorder_js_1.ContextRecorder({ dispatcher, getRunContext: getRunCtx }));
|
|
142
|
+
executor.attachCombinedRecorder((0, StreamRecorder_js_1.streamRecorder)({ dispatcher, getRunContext: getRunCtx }));
|
|
143
|
+
executor.attachCombinedRecorder((0, AgentRecorder_js_1.agentRecorder)({ dispatcher, getRunContext: getRunCtx }));
|
|
144
|
+
executor.attachCombinedRecorder((0, CompositionRecorder_js_1.compositionRecorder)({ dispatcher, getRunContext: getRunCtx }));
|
|
145
|
+
for (const r of this.attachedRecorders)
|
|
146
|
+
executor.attachCombinedRecorder(r);
|
|
147
|
+
return executor;
|
|
148
|
+
}
|
|
149
|
+
finalizeResult(executor, result) {
|
|
150
|
+
const paused = this.detectPause(executor, result);
|
|
151
|
+
if (paused)
|
|
152
|
+
return paused;
|
|
153
|
+
if (result instanceof Error)
|
|
154
|
+
throw result;
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
buildChart() {
|
|
158
|
+
const steps = this.steps;
|
|
159
|
+
const compositionId = this.id;
|
|
160
|
+
const compositionName = this.name;
|
|
161
|
+
const seed = (scope) => {
|
|
162
|
+
// The workflow's own input IS step 1's input — no unwrapping, no
|
|
163
|
+
// re-wrapping; that is the whole point of the typed chain.
|
|
164
|
+
scope.current = scope.$getArgs();
|
|
165
|
+
(0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.composition.enter', {
|
|
166
|
+
kind: 'Sequence',
|
|
167
|
+
id: compositionId,
|
|
168
|
+
name: compositionName,
|
|
169
|
+
childCount: steps.length,
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
// Root description prefix `Sequence:` is the taxonomy marker every
|
|
173
|
+
// consumer (Lens, FlowchartRecorder.mapTopologyToSteps) already reads.
|
|
174
|
+
let builder = (0, footprintjs_1.flowChart)('Seed', seed, 'seed', {
|
|
175
|
+
...(this.opts.structureRecorders !== undefined && {
|
|
176
|
+
structureRecorders: [...this.opts.structureRecorders],
|
|
177
|
+
}),
|
|
178
|
+
description: `Sequence: ${steps.length}-step typed workflow`,
|
|
179
|
+
});
|
|
180
|
+
steps.forEach((step, index) => {
|
|
181
|
+
const stepNumber = index + 1;
|
|
182
|
+
builder = builder.addSubFlowChartNext(`step-${stepNumber}`, step.getSpec(), `Step ${stepNumber}`, {
|
|
183
|
+
inputMapper: (parent) => toStepArgs(parent.current, stepNumber),
|
|
184
|
+
// Untouched: whatever the step's chart returned is what the next
|
|
185
|
+
// step (or the caller) receives. No string coercion.
|
|
186
|
+
outputMapper: (sfOutput) => ({ current: sfOutput }),
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
builder = builder.addFunction('Finalize', (scope) => {
|
|
190
|
+
(0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.composition.exit', {
|
|
191
|
+
kind: 'Sequence',
|
|
192
|
+
id: compositionId,
|
|
193
|
+
name: compositionName,
|
|
194
|
+
status: 'ok',
|
|
195
|
+
durationMs: Date.now() - this.currentRunContext.runStartMs,
|
|
196
|
+
});
|
|
197
|
+
return scope.current;
|
|
198
|
+
}, 'finalize', 'Workflow finalize');
|
|
199
|
+
return builder.build();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exports.Workflow = Workflow;
|
|
203
|
+
function workflow(...steps) {
|
|
204
|
+
if (steps.length === 0) {
|
|
205
|
+
throw new Error('workflow(): needs at least one step');
|
|
206
|
+
}
|
|
207
|
+
return new Workflow(steps);
|
|
208
|
+
}
|
|
209
|
+
exports.workflow = workflow;
|
|
210
|
+
//# sourceMappingURL=Workflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Workflow.js","sourceRoot":"","sources":["../../src/core-flow/Workflow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graph() — a FIXED DAG of runners, levelized at build time.
|
|
3
|
+
*
|
|
4
|
+
* WHY this exists: `Sequence` and `workflow()` run steps in a line, and
|
|
5
|
+
* `Parallel` fans out ONCE and merges. Real pipelines are neither: an
|
|
6
|
+
* intake step feeds two independent lookups, and a writer waits for both.
|
|
7
|
+
* Expressing that with the existing compositions means nesting a Parallel
|
|
8
|
+
* inside a Sequence and threading values through by hand — and on this
|
|
9
|
+
* codebase that hand-off silently loses structured data (see "The trap"
|
|
10
|
+
* below). `graph()` states the shape once, as nodes and edges, and lets
|
|
11
|
+
* the engine work out what can run at the same time.
|
|
12
|
+
*
|
|
13
|
+
* What it gives you:
|
|
14
|
+
* - **Concurrency you did not have to schedule.** Kahn levelization at
|
|
15
|
+
* BUILD time groups nodes with no dependency between them; every node
|
|
16
|
+
* in a level runs at the same time.
|
|
17
|
+
* - **A shape checked before it runs.** A cycle, an edge pointing at a
|
|
18
|
+
* node that does not exist, or a duplicate id is refused at BUILD
|
|
19
|
+
* time, naming the offender. You cannot construct a broken graph.
|
|
20
|
+
* - **No silent merges.** A node with two or more parents MUST declare
|
|
21
|
+
* a `join` — a silent merge is a wrong merge, so the build refuses
|
|
22
|
+
* and names the node.
|
|
23
|
+
* - **Values, not text.** An edge's payload is the producer's OUTPUT
|
|
24
|
+
* handed to the consumer, unchanged. There is no shared mutable scope
|
|
25
|
+
* between nodes: a node reads exactly what its parents produced.
|
|
26
|
+
*
|
|
27
|
+
* Pattern: Adapter over footprintjs's subflow mounts — a level with
|
|
28
|
+
* several nodes becomes stacked `addSubFlowChart` calls (a fork,
|
|
29
|
+
* run concurrently); a level with one node is mounted
|
|
30
|
+
* sequentially (`addSubFlowChartNext`, which resumes cleanly
|
|
31
|
+
* across a pause). One join stage between levels.
|
|
32
|
+
* Role: core-flow/ layer, alongside Sequence/Parallel/Conditional/
|
|
33
|
+
* Loop/Workflow. Pure control flow — no LLM dependency.
|
|
34
|
+
* Emits: agentfootprint.composition.enter / exit, reported as kind
|
|
35
|
+
* `'Sequence'`. See "Why kind 'Sequence'" below.
|
|
36
|
+
*
|
|
37
|
+
* ## The trap this was built around (verified against footprintjs, not docs)
|
|
38
|
+
*
|
|
39
|
+
* The obvious sketch — `graph = Sequence(Parallel(level0), Parallel(level1), …)`
|
|
40
|
+
* — does NOT work on this codebase, for two independent reasons:
|
|
41
|
+
*
|
|
42
|
+
* 1. `Sequence`'s step contract is `{ message: string } -> string`, and
|
|
43
|
+
* its step `outputMapper` coerces a non-string step output to `''`.
|
|
44
|
+
* `workflow()` (v7.10.0) exists precisely because of this.
|
|
45
|
+
* 2. `Parallel` has the SAME limit one layer down: its branch type is
|
|
46
|
+
* `Runner<{ message: string }, string>` and its branch `outputMapper`
|
|
47
|
+
* coerces a non-string branch output to `''` (`Parallel.ts`, the
|
|
48
|
+
* `typeof sfOutput === 'string' ? sfOutput : ''` mapper). So a
|
|
49
|
+
* Parallel level cannot carry a structured value either.
|
|
50
|
+
*
|
|
51
|
+
* So `graph()` is built on the pass-through model `workflow()` established
|
|
52
|
+
* — its own composition, its own mappers, the same recorder wiring and the
|
|
53
|
+
* same `composition.enter` / `exit` events — rather than on top of
|
|
54
|
+
* Sequence/Parallel.
|
|
55
|
+
*
|
|
56
|
+
* ## Why kind 'Sequence'
|
|
57
|
+
*
|
|
58
|
+
* `CompositionKind` is a CLOSED public union (`'Sequence' | 'Parallel' |
|
|
59
|
+
* 'Conditional' | 'Loop'`). Widening it would break exhaustive switches in
|
|
60
|
+
* consumer code for no behavioural gain — the same call v7.10.0 made for
|
|
61
|
+
* `workflow()`. A graph's LEVELS are a sequence (level 0, then level 1, …),
|
|
62
|
+
* so `'Sequence'` is the honest member of that union: this composition runs
|
|
63
|
+
* its levels in order. The fan-out WITHIN a level is visible in the chart
|
|
64
|
+
* itself (a fork node per level), which is where a renderer reads it from.
|
|
65
|
+
*
|
|
66
|
+
* ## Honest limits (all verified against the engine, pinned in tests)
|
|
67
|
+
*
|
|
68
|
+
* 1. Only PLAIN DATA crosses a node boundary — the same limit
|
|
69
|
+
* `workflow()` documents. A value with a prototype (Date, Map, class
|
|
70
|
+
* instance) arrives as `{}`; `undefined` fields are dropped.
|
|
71
|
+
* 2. A node must RETURN its output: the value handed to its children is
|
|
72
|
+
* the node chart's traversal result.
|
|
73
|
+
* 3. A node that THROWS is always reported as
|
|
74
|
+
* `graph '<id>': node '<node>' failed: <reason>`, but it reaches that
|
|
75
|
+
* sentence by two different routes. In a CONCURRENT level footprintjs
|
|
76
|
+
* runs children under `Promise.allSettled`, so a failed child is
|
|
77
|
+
* simply ABSENT from the results and the level join turns that
|
|
78
|
+
* absence into the error. In a SEQUENTIAL (single-node) level the
|
|
79
|
+
* error rejects the run raw, and `rethrowWithNodeAttribution` renames
|
|
80
|
+
* it. Consumers see one shape either way.
|
|
81
|
+
* 4. A node that PAUSES surfaces as a pause — the engine halts the
|
|
82
|
+
* traversal before the level's join runs, so `run()` returns a
|
|
83
|
+
* `RunnerPauseOutcome`. `resume()` then carries on through the REST
|
|
84
|
+
* of the graph only when the paused node was ALONE in its level (a
|
|
85
|
+
* sequential mount). Resuming into a fork child completes that child
|
|
86
|
+
* and stops: the remaining levels do not run. Give a node that asks a
|
|
87
|
+
* human a level of its own. Both halves are pinned in tests.
|
|
88
|
+
*
|
|
89
|
+
* @example a diamond: A feeds B and C, D waits for both
|
|
90
|
+
* ```ts
|
|
91
|
+
* const pipeline = graph({
|
|
92
|
+
* nodes: [
|
|
93
|
+
* { id: 'intake', runner: intake },
|
|
94
|
+
* { id: 'orders', runner: lookupOrders },
|
|
95
|
+
* { id: 'billing', runner: lookupBilling },
|
|
96
|
+
* {
|
|
97
|
+
* id: 'reply',
|
|
98
|
+
* runner: writeReply,
|
|
99
|
+
* // Two parents ⇒ a join is REQUIRED. `upstream` is keyed by node id.
|
|
100
|
+
* join: (upstream) => ({
|
|
101
|
+
* orders: upstream.orders as OrderInfo,
|
|
102
|
+
* billing: upstream.billing as BillingInfo,
|
|
103
|
+
* }),
|
|
104
|
+
* },
|
|
105
|
+
* ],
|
|
106
|
+
* edges: [
|
|
107
|
+
* { from: 'intake', to: 'orders' },
|
|
108
|
+
* { from: 'intake', to: 'billing' },
|
|
109
|
+
* { from: 'orders', to: 'reply' },
|
|
110
|
+
* { from: 'billing', to: 'reply' },
|
|
111
|
+
* ],
|
|
112
|
+
* });
|
|
113
|
+
*
|
|
114
|
+
* const out = await pipeline.run({ message: 'where is my refund?' });
|
|
115
|
+
* // out = { intake: …, orders: …, billing: …, reply: … } — keyed by node id
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
import { type FlowchartCheckpoint, type RunOptions, type StructureRecorder } from 'footprintjs';
|
|
119
|
+
import type { RunnerPauseOutcome } from '../core/pause.js';
|
|
120
|
+
import type { Runner } from '../core/runner.js';
|
|
121
|
+
import { RunnerBase } from '../core/RunnerBase.js';
|
|
122
|
+
/**
|
|
123
|
+
* One node of the graph: an id, the runner that does the work, and — when
|
|
124
|
+
* the node has more than one parent — how to merge what those parents
|
|
125
|
+
* produced into this node's input.
|
|
126
|
+
*/
|
|
127
|
+
export interface GraphNode<I = unknown, O = unknown> {
|
|
128
|
+
/** Unique within the graph. Used as the results key and the chart node id. */
|
|
129
|
+
readonly id: string;
|
|
130
|
+
/** The work. Any Runner: LLMCall, Agent, a Sequence, another graph. */
|
|
131
|
+
readonly runner: Runner<I, O>;
|
|
132
|
+
/**
|
|
133
|
+
* Merge upstream outputs into this node's input. `upstream` is keyed by
|
|
134
|
+
* PARENT NODE ID, and each value is that parent's output, unchanged.
|
|
135
|
+
*
|
|
136
|
+
* Optional for a node with 0 or 1 parents (a single parent's output is
|
|
137
|
+
* passed through). **REQUIRED when a node has 2+ parents** — a silent
|
|
138
|
+
* merge is a wrong merge, so the build refuses and names the node.
|
|
139
|
+
*/
|
|
140
|
+
readonly join?: (upstream: Readonly<Record<string, unknown>>) => I;
|
|
141
|
+
/** Human-friendly label for events + topology. Default: the node id. */
|
|
142
|
+
readonly name?: string;
|
|
143
|
+
}
|
|
144
|
+
/** A directed dependency: `from` must finish before `to` starts. */
|
|
145
|
+
export interface GraphEdge {
|
|
146
|
+
readonly from: string;
|
|
147
|
+
readonly to: string;
|
|
148
|
+
}
|
|
149
|
+
export interface GraphOptions {
|
|
150
|
+
/** The nodes. Ids must be unique; at least one is required. */
|
|
151
|
+
readonly nodes: readonly GraphNode<any, any>[];
|
|
152
|
+
/** The dependencies. Every endpoint must name a declared node. */
|
|
153
|
+
readonly edges: readonly GraphEdge[];
|
|
154
|
+
/** Human-friendly name for events + topology. Default `'Graph'`. */
|
|
155
|
+
readonly name?: string;
|
|
156
|
+
/** Stable id used for topology + events. Default `'graph'`. */
|
|
157
|
+
readonly id?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Optional build-time recorders passed through to footprintjs's
|
|
160
|
+
* `flowChart()` factory — they observe this graph's OWN nodes (Seed +
|
|
161
|
+
* one mount per graph node + one join per level + Finalize). Not
|
|
162
|
+
* propagated into the mounted node charts; attach them to each node
|
|
163
|
+
* runner for full coverage.
|
|
164
|
+
*/
|
|
165
|
+
readonly structureRecorders?: readonly StructureRecorder[];
|
|
166
|
+
}
|
|
167
|
+
/** The graph's own input — handed to every ROOT node (one with no parents). */
|
|
168
|
+
export type GraphInput = Record<string, unknown>;
|
|
169
|
+
/** Outputs keyed by node id. Every node that ran contributes one entry. */
|
|
170
|
+
export type GraphOutput = Record<string, unknown>;
|
|
171
|
+
/**
|
|
172
|
+
* Kahn levelization: group nodes so that everything in level N depends
|
|
173
|
+
* only on levels < N. Nodes within a level are independent BY
|
|
174
|
+
* CONSTRUCTION, which is exactly the licence to run them concurrently.
|
|
175
|
+
*
|
|
176
|
+
* Declaration order is preserved inside each level so a graph's chart —
|
|
177
|
+
* and therefore its trace — is deterministic.
|
|
178
|
+
*
|
|
179
|
+
* Throws (naming the offender) on: an unknown edge endpoint, a duplicate
|
|
180
|
+
* node id, a cycle, or a fan-in > 1 with no `join`.
|
|
181
|
+
*/
|
|
182
|
+
export declare function levelize(nodes: readonly GraphNode<any, any>[], edges: readonly GraphEdge[]): readonly (readonly GraphNode<any, any>[])[];
|
|
183
|
+
/**
|
|
184
|
+
* A fixed DAG of runners. Build one with {@link graph}.
|
|
185
|
+
*/
|
|
186
|
+
export declare class Graph extends RunnerBase<GraphInput, GraphOutput> {
|
|
187
|
+
readonly name: string;
|
|
188
|
+
readonly id: string;
|
|
189
|
+
private readonly nodes;
|
|
190
|
+
private readonly levels;
|
|
191
|
+
private readonly parentsOf;
|
|
192
|
+
private readonly opts;
|
|
193
|
+
private currentRunContext;
|
|
194
|
+
/**
|
|
195
|
+
* Per-node first-error records for the current run. footprintjs's
|
|
196
|
+
* `SubflowExecutor` swallows a subflow error into the parent's debug
|
|
197
|
+
* bag and skips the `outputMapper`, so the message never reaches parent
|
|
198
|
+
* scope on its own. An internal recorder captures it here; the level
|
|
199
|
+
* join reads it to name what actually went wrong. Mirrors Parallel's
|
|
200
|
+
* `branchErrors`, epoch-guarded for the same reason.
|
|
201
|
+
*/
|
|
202
|
+
private readonly nodeErrors;
|
|
203
|
+
/** Monotonic run token — see Parallel's `runEpoch`. */
|
|
204
|
+
private runEpoch;
|
|
205
|
+
constructor(opts: GraphOptions);
|
|
206
|
+
/** How the graph was levelized — level 0 first. Stable post-construction. */
|
|
207
|
+
getLevels(): readonly (readonly string[])[];
|
|
208
|
+
run(input: GraphInput, options?: RunOptions): Promise<GraphOutput | RunnerPauseOutcome>;
|
|
209
|
+
resume(checkpoint: FlowchartCheckpoint, input?: unknown, options?: RunOptions): Promise<GraphOutput | RunnerPauseOutcome>;
|
|
210
|
+
/**
|
|
211
|
+
* Give a RAW rejection the same node-naming shape the level join
|
|
212
|
+
* produces.
|
|
213
|
+
*
|
|
214
|
+
* A node in a SEQUENTIAL (single-node) level rejects the run with its
|
|
215
|
+
* own error — the level join never runs, so nothing has attributed it to
|
|
216
|
+
* a node yet. The error recorder did see it, so correlate (by identity
|
|
217
|
+
* first, then bare message) and rename. Anything that does not correlate
|
|
218
|
+
* — including the join's own already-attributed error — is rethrown
|
|
219
|
+
* untouched.
|
|
220
|
+
*/
|
|
221
|
+
private rethrowWithNodeAttribution;
|
|
222
|
+
private createExecutor;
|
|
223
|
+
/**
|
|
224
|
+
* Capture the first error per node. The node id is the first segment of
|
|
225
|
+
* the engine-prefixed `stageId` (`orders/call-llm` → node `orders`) —
|
|
226
|
+
* the same correlation Parallel uses, and the only one that survives a
|
|
227
|
+
* node mounting subflows of its own.
|
|
228
|
+
*/
|
|
229
|
+
private makeNodeErrorRecorder;
|
|
230
|
+
private finalizeResult;
|
|
231
|
+
private buildChart;
|
|
232
|
+
/**
|
|
233
|
+
* What one node receives. Roots get the graph's own input; a single
|
|
234
|
+
* parent is passed through; 2+ parents go through the node's `join`
|
|
235
|
+
* (which the build already guaranteed exists).
|
|
236
|
+
*
|
|
237
|
+
* `parent` here is the RAW parent state the engine hands an
|
|
238
|
+
* `inputMapper` — not a TypedScope — so structured upstream values read
|
|
239
|
+
* back intact.
|
|
240
|
+
*/
|
|
241
|
+
private inputForNode;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Build a fixed DAG of runners. Independent nodes run concurrently; the
|
|
245
|
+
* result is every node's output, keyed by node id.
|
|
246
|
+
*
|
|
247
|
+
* The shape is checked at BUILD time — a cycle, an edge pointing at an
|
|
248
|
+
* unknown node, a duplicate id, or a 2+-parent node with no `join` throws
|
|
249
|
+
* here, naming the offender, rather than misbehaving mid-run.
|
|
250
|
+
*
|
|
251
|
+
* @example a fan-out with a merge
|
|
252
|
+
* ```ts
|
|
253
|
+
* const pipeline = graph({
|
|
254
|
+
* nodes: [
|
|
255
|
+
* { id: 'plan', runner: planner },
|
|
256
|
+
* { id: 'search', runner: searcher },
|
|
257
|
+
* { id: 'recall', runner: memory },
|
|
258
|
+
* { id: 'answer', runner: writer, join: (u) => ({ ...u }) },
|
|
259
|
+
* ],
|
|
260
|
+
* edges: [
|
|
261
|
+
* { from: 'plan', to: 'search' },
|
|
262
|
+
* { from: 'plan', to: 'recall' },
|
|
263
|
+
* { from: 'search', to: 'answer' },
|
|
264
|
+
* { from: 'recall', to: 'answer' },
|
|
265
|
+
* ],
|
|
266
|
+
* });
|
|
267
|
+
*
|
|
268
|
+
* const out = await pipeline.run({ message: 'what changed last week?' });
|
|
269
|
+
* console.log(out.answer);
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export declare function graph(opts: GraphOptions): Graph;
|