@spendgraph/graph 0.8.1 → 0.8.3
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/package.json +3 -5
- package/docs/nodes.mdx +0 -59
- package/docs/overview.mdx +0 -98
- package/docs/pausing.mdx +0 -109
- package/docs/running.mdx +0 -71
- package/docs/streaming.mdx +0 -62
- package/docs/wiring.mdx +0 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spendgraph/graph",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Wire nodes into a graph, run it, and get back a rollout with every step.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -27,19 +27,17 @@
|
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"import": "./dist/index.js"
|
|
29
29
|
},
|
|
30
|
-
"./docs/*": "./docs/*",
|
|
31
30
|
"./package.json": "./package.json"
|
|
32
31
|
},
|
|
33
32
|
"files": [
|
|
34
33
|
"dist",
|
|
35
|
-
"docs",
|
|
36
34
|
"README.md"
|
|
37
35
|
],
|
|
38
36
|
"dependencies": {
|
|
39
|
-
"@spendgraph/sdk": "^0.8.
|
|
37
|
+
"@spendgraph/sdk": "^0.8.3"
|
|
40
38
|
},
|
|
41
39
|
"devDependencies": {
|
|
42
|
-
"@spendgraph/config": "0.8.
|
|
40
|
+
"@spendgraph/config": "0.8.3",
|
|
43
41
|
"typescript": "^5"
|
|
44
42
|
},
|
|
45
43
|
"engines": {
|
package/docs/nodes.mdx
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Nodes: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"One unit of work. Declare its arguments with `as const` and the handler types itself; say where they come from with `input`, and the node stays reusable.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Nodes
|
|
8
|
-
|
|
9
|
-
A node is one unit of work: a name, the arguments it needs, where they come from, and what to do with them.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
const classify = node({
|
|
13
|
-
name: "classify",
|
|
14
|
-
args: [
|
|
15
|
-
{ name: "text", type: "string", required: true },
|
|
16
|
-
{ name: "top_k", type: "number", required: false },
|
|
17
|
-
] as const,
|
|
18
|
-
input: (ctx) => ({ text: ctx.values.question }),
|
|
19
|
-
run: ({ text, top_k }) => model.classify(text, top_k ?? 3),
|
|
20
|
-
});
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## `input` is what makes it reusable
|
|
24
|
-
|
|
25
|
-
The node says what it **needs**; the graph says where it **comes from**. Without that split, every node has to know about the whole run, and a node that reads `ctx.values.question` directly is a node you cannot drop into a second graph where the question is called something else.
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
input: (ctx) => ({ text: ctx.values.question }); // from the run's values
|
|
29
|
-
input: (ctx) => ({ text: String(ctx.outputs.draft) }); // from an earlier node
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Leave `input` off and the node is handed the run's values as they are.
|
|
33
|
-
|
|
34
|
-
## Write `as const`
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
args: [{ name: "text", type: "string", required: true }] as const,
|
|
38
|
-
run: ({ text }) => text.toUpperCase(), // `text` is a string, here and now
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
The handler types itself from the args: `text` a string, a `number` arg a number, an optional one optional. Rename an argument and the handler stops compiling.
|
|
42
|
-
|
|
43
|
-
**Without `as const` inference falls back to `Record<string, unknown>` silently.** Nothing errors; the types simply stop meaning anything, and a typo that used to be a compile error becomes `undefined` halfway through a graph, the most expensive place to find one.
|
|
44
|
-
|
|
45
|
-
<Callout tone="trap" title="A node cannot rewrite what an earlier node did">
|
|
46
|
-
The steps a node is handed are a frozen copy: writing to one changes nothing, and it will not throw to tell you so. Pass what the next node needs through the return value, which is what `input` reads.
|
|
47
|
-
</Callout>
|
|
48
|
-
|
|
49
|
-
## What is checked, and when
|
|
50
|
-
|
|
51
|
-
Arguments are validated **before `run` does any work**, against the same `FieldSpec` types the SDK, tools and prompts all use. A node handed something that does not validate fails the run rather than being let through to fail further in.
|
|
52
|
-
|
|
53
|
-
A node name must be letters, digits and underscores starting with a letter, and that is checked **at import** rather than on the first run.
|
|
54
|
-
|
|
55
|
-
## Returning something
|
|
56
|
-
|
|
57
|
-
Whatever `run` returns becomes the node's output: a string is itself, anything else is JSON.
|
|
58
|
-
|
|
59
|
-
A value that cannot be written down, a circular object, a `BigInt`, something whose own `toJSON` throws, is recorded as `[not recordable: …]` rather than `""`. An empty output reads as a node that produced nothing, and this is a node that produced something the record could not hold. The two are different facts and the step keeps them apart.
|
package/docs/overview.mdx
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Graph: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"Wire nodes into a graph, run it, and get back a rollout with every step priced. Four exports, compilation that catches the wiring mistakes, and a run that never throws.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Graph
|
|
8
|
-
|
|
9
|
-
Multi-step work hand-rolled in `if`/`await` runs fine and leaves nothing behind: when step four fails at 2am, what ran, in what order, and what it cost are all gone.
|
|
10
|
-
|
|
11
|
-
A graph is nodes and the edges between them. You describe the units of work and where control goes next; running it hands back a rollout: every step, in order, with what each one cost.
|
|
12
|
-
|
|
13
|
-
```sh
|
|
14
|
-
npm install @spendgraph/graph
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
<GraphToRollout />
|
|
18
|
-
|
|
19
|
-
Four things are exported. Everything else hangs off what they return.
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { graph, node, edge, end } from "@spendgraph/graph";
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## The whole thing
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
const classify = node({
|
|
29
|
-
name: "classify",
|
|
30
|
-
args: [{ name: "text", type: "string", required: true }] as const,
|
|
31
|
-
input: (ctx) => ({ text: ctx.values.question }),
|
|
32
|
-
run: ({ text }) => model.classify(text),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const lookup = node({
|
|
36
|
-
name: "lookup",
|
|
37
|
-
args: [{ name: "ref", type: "string", required: true }] as const,
|
|
38
|
-
input: (ctx) => ({ ref: ctx.values.question }),
|
|
39
|
-
run: ({ ref }) => contracts.find(ref),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const answer = node({
|
|
43
|
-
name: "answer",
|
|
44
|
-
run: (values) => llm.call([{ role: "user", content: String(values.question) }]),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const flow = graph({
|
|
48
|
-
entry: "classify",
|
|
49
|
-
nodes: [classify, lookup, answer],
|
|
50
|
-
edges: [
|
|
51
|
-
edge("classify", "lookup", (ctx) => ctx.outputs.classify === "billing"),
|
|
52
|
-
edge("classify", "answer"),
|
|
53
|
-
edge("lookup", "answer"),
|
|
54
|
-
end("answer"),
|
|
55
|
-
],
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const result = await flow.execute({ question: "Why was I charged twice?" });
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## What you get back
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
result.status; // "completed" | "failed"
|
|
65
|
-
result.output; // the last node's return, stringified; "" on a failure
|
|
66
|
-
result.steps; // every node that ran, in order
|
|
67
|
-
result.outputs; // each node's return, by name
|
|
68
|
-
result.latencyMs;
|
|
69
|
-
result.inputTokens; // summed across every step, nested ones included
|
|
70
|
-
result.outputTokens;
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
A graph run **is** a rollout with several steps, so it hands straight to `@spendgraph/prompt` without being translated first:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
await prompt.call(values, () => flow.execute(values));
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Three things worth knowing up front
|
|
80
|
-
|
|
81
|
-
**Compilation is the one place this package throws.** An edge to a node that does not exist, an entry that is not a node, two nodes with the same name, a node nothing reaches, a node whose edges are all conditional, all of it is checked before anything runs. See [wiring](/spendgraph/graph/wiring).
|
|
82
|
-
|
|
83
|
-
**A run never throws.** A node that fails halts the run and comes back as a result with the steps that did run and the failed one last. A thrown exception cannot tell you which three nodes ran; a result can. See [running a graph](/spendgraph/graph/running).
|
|
84
|
-
|
|
85
|
-
**`outputs` is the only channel between nodes.** No shared mutable bag, so node four cannot come to depend on a key node two happens to set: a dependency the graph does not declare and compilation cannot check.
|
|
86
|
-
|
|
87
|
-
<Callout tone="trap" title="Compilation is the only place mistakes are cheap">
|
|
88
|
-
An edge pointing at a node that does not exist, an entry that is not a node, a node nothing reaches, all refused by `graph()`, before a single model call. The same mistakes found at run time are found on the branch that reaches them, which may be the one that only runs at month end.
|
|
89
|
-
</Callout>
|
|
90
|
-
|
|
91
|
-
## Where to go next
|
|
92
|
-
|
|
93
|
-
| | |
|
|
94
|
-
| --- | --- |
|
|
95
|
-
| [Nodes](/spendgraph/graph/nodes) | declaring one, and the types you get for free |
|
|
96
|
-
| [Wiring](/spendgraph/graph/wiring) | edges, branching, and what compilation refuses |
|
|
97
|
-
| [Running a graph](/spendgraph/graph/running) | failure, the step ceiling, and what comes back |
|
|
98
|
-
| [Streaming](/spendgraph/graph/streaming) | the same run, narrated |
|
package/docs/pausing.mdx
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Pausing a graph: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"A node that runs something needing a person stops the graph rather than walking past the question. The pause arrives as an event, and `entry` picks the run back up where it stopped.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Stopping to ask
|
|
8
|
-
|
|
9
|
-
Some work needs a person: an approval, a choice between plans, a number nobody
|
|
10
|
-
can infer. A node that runs something like that comes back with a **question
|
|
11
|
-
rather than an answer**, and the graph stops there.
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
const stopped = await flow.execute({ expression: "12 / 0" });
|
|
15
|
-
|
|
16
|
-
stopped.waitingOn; // { question: "12 / 0 — what should this return?", detail }
|
|
17
|
-
stopped.waitingFor; // the nested run to resume, where it named one
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Nothing downstream runs. Walking on would put every later node to work against a
|
|
21
|
-
value nobody has supplied, and report the run as done while it is parked.
|
|
22
|
-
|
|
23
|
-
## Recognised by shape, not by a marker
|
|
24
|
-
|
|
25
|
-
A node returning a value that carries `waitingOn` **is** a pause. There is no
|
|
26
|
-
wrapper to remember.
|
|
27
|
-
|
|
28
|
-
That is the same argument as reading a node's outcome structurally: a node may
|
|
29
|
-
return anything, and a rule to wrap a paused run is one you learn by having the
|
|
30
|
-
graph walk past the question and call it success. Every node's return goes
|
|
31
|
-
through it, so a `waitingOn` that is data rather than a question stops the run
|
|
32
|
-
too, nothing else in a reply has that shape, and the alternative fails the
|
|
33
|
-
other way, silently.
|
|
34
|
-
|
|
35
|
-
The check itself is exported, so your own code can ask the same question the
|
|
36
|
-
graph asks:
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
import { pausedInside } from "@spendgraph/graph";
|
|
40
|
-
|
|
41
|
-
const paused = pausedInside(result); // Paused, or null
|
|
42
|
-
paused?.waitingOn.question;
|
|
43
|
-
paused?.runId; // the inner run to resume, where there is one
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Reach for it inside a node that runs a nested graph, or in a harness of your
|
|
47
|
-
own. `Wait` is declared here rather than imported for the same reason: harness
|
|
48
|
-
owns durability and depends on this package, so the shape lives on this side and
|
|
49
|
-
harness's own `Wait` satisfies it.
|
|
50
|
-
|
|
51
|
-
## Streaming, it is an event
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
for await (const event of flow.stream(values)) {
|
|
55
|
-
if (event.type === "paused") ask(event.waitingOn, event.node);
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
The event carries the `Wait` whole, the question, and whatever answering it
|
|
60
|
-
needs. A thrown error would carry only its message, which is why this is an
|
|
61
|
-
event and not an exception.
|
|
62
|
-
|
|
63
|
-
<Callout tone="trap" title="Whatever a node returned has to survive JSON">
|
|
64
|
-
A paused run is stored and handed back to running code. A `Date` comes back a string, a `Map` comes back `{}`, and neither throws, so the refusal happens at the pause, naming the node whose return would not survive, rather than three days later in whatever read it.
|
|
65
|
-
</Callout>
|
|
66
|
-
|
|
67
|
-
## The graph owns no durability
|
|
68
|
-
|
|
69
|
-
It reports the pause and stops. Storing the run, holding it while somebody
|
|
70
|
-
thinks, and handing the answer back is `@spendgraph/harness`'s job: it has the
|
|
71
|
-
store, the expiry and the resume.
|
|
72
|
-
|
|
73
|
-
## Picking it back up
|
|
74
|
-
|
|
75
|
-
`entry` starts the walk somewhere other than the compiled entry, and `outputs`
|
|
76
|
-
seeds what already ran. The `paused` event says which node stopped; this says
|
|
77
|
-
where to start again.
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
await flow.execute(values, {
|
|
81
|
-
entry: "divide", // the node that asked
|
|
82
|
-
outputs: stopped.outputs, // what already ran, so nothing runs twice
|
|
83
|
-
steps: stopped.steps, // the run so far, so it comes back as one rollout
|
|
84
|
-
});
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
`steps` is the history rather than the values. Leave it out and the resumed run
|
|
88
|
-
numbers its steps from 0 and shows a node an empty `ctx.steps`, so an edge like
|
|
89
|
-
`(ctx) => ctx.steps.length < 3` is true all over again and two halves of one run
|
|
90
|
-
cannot be stitched without renumbering them.
|
|
91
|
-
|
|
92
|
-
Only existence is checked. Reachability was settled from the compiled entry at
|
|
93
|
-
build time, and whatever a resume entry leaves unreachable has already run.
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
result.status; // "failed"
|
|
97
|
-
result.error; // 'No node is called "divid", so there is nowhere to start. Known: parse, divide, format.'
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**`maxSteps` counts one run**, so a resumed one starts its budget over. A caller
|
|
101
|
-
resuming a cyclic graph again and again is the one holding the ceiling.
|
|
102
|
-
|
|
103
|
-
## A run that paused still ran
|
|
104
|
-
|
|
105
|
-
`status` says whether the graph itself ran, and a paused run ran correctly as
|
|
106
|
-
far as it got, so it comes back `completed`, carrying the question.
|
|
107
|
-
|
|
108
|
-
Every workflow in this repo signals a pause the same way: not with a status, but
|
|
109
|
-
by carrying what it stopped to ask.
|
package/docs/running.mdx
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Running a graph: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"A run never throws: a failed node halts it and comes back as a result with the steps that did run. The step ceiling, the context between nodes, and what a rollout carries.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Running a graph
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
const result = await flow.execute({ question: "Why was I charged twice?" });
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## A run never throws
|
|
14
|
-
|
|
15
|
-
A node that throws, or is handed an argument that does not validate, **halts the run and comes back as a result**:
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
result.status; // "failed"
|
|
19
|
-
result.error; // '"lookup" failed: contract MSA 2.4 not found'
|
|
20
|
-
result.steps; // the steps that did run, with the failed one last
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Continuing would leave every later node reading an output that was never written. Stopping with the steps intact is what lets a failed run say which three nodes ran and where it stopped, which a thrown exception cannot.
|
|
24
|
-
|
|
25
|
-
## The step ceiling
|
|
26
|
-
|
|
27
|
-
`maxSteps` bounds the run and defaults to **25**. It counts the nodes a run visits: a node that returns a nested run folds that run's steps into the rollout, and none of those count against the ceiling.
|
|
28
|
-
|
|
29
|
-
A backwards edge is a feature and also how a graph hangs, and nothing tells the two apart statically. So the guard is a count and a clear failure rather than a hung process.
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
graph({ entry: "draft", nodes, edges, maxSteps: 50 });
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
A ceiling that is not finite is **refused at compile time**. `NaN` would make the count never fire, and `Number(process.env.MAX_STEPS)` on an unset variable is `NaN`, which is how an unbounded run arrives without anyone typing one.
|
|
36
|
-
|
|
37
|
-
<Callout tone="trap" title="The ceiling counts nodes, and defaults to 25">
|
|
38
|
-
`maxSteps` bounds how many nodes a run may visit, not how many tokens it may spend, a cycle between two cheap nodes hits it, an expensive straight line never does. It defaults to 25, and a run that reaches it comes back as a *failed* result rather than a truncated success.
|
|
39
|
-
</Callout>
|
|
40
|
-
|
|
41
|
-
## What comes back
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
result.status; // "completed" | "failed"
|
|
45
|
-
result.output; // the last node's return, stringified; "" on a failure
|
|
46
|
-
result.steps; // every node that ran, in order
|
|
47
|
-
result.outputs; // each node's return, by name
|
|
48
|
-
result.latencyMs;
|
|
49
|
-
result.inputTokens; // summed across every step, nested ones included
|
|
50
|
-
result.outputTokens;
|
|
51
|
-
result.waitingOn; // the question a node stopped to ask, where one did
|
|
52
|
-
result.waitingFor; // the nested run to resume, where it named one
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
A graph run is a rollout with several steps, so `GraphResult` hands straight to `@spendgraph/prompt`'s `report` and `call` without being translated:
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
await prompt.call(values, () => flow.execute(values));
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Context
|
|
62
|
-
|
|
63
|
-
`outputs` is the **only** channel between nodes. A shared mutable bag would let node four depend on a key node two happens to set: a dependency the graph does not declare and compilation cannot check.
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
ctx.values; // what execute() was called with
|
|
67
|
-
ctx.outputs; // what each finished node returned, by name
|
|
68
|
-
ctx.steps; // the steps so far
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Both halves of `steps` are sealed: the array is a copy, so pushing to it changes nothing, and each step is frozen, so a node cannot rewrite what an earlier one produced or what it cost.
|
package/docs/streaming.mdx
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Streaming a graph: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"The same run, narrated: each node's start and end, whatever its nodes emit while running, and the finished rollout last.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Streaming
|
|
8
|
-
|
|
9
|
-
`stream` is the same run as `execute`, narrated, a node's start and end, whatever its nodes emit while running, and the finished result last.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
const running = flow.stream({ question });
|
|
13
|
-
|
|
14
|
-
for await (const event of running) {
|
|
15
|
-
if (event.type === "token") process.stdout.write(event.text);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const result = await running.result;
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## A node emits without knowing what a token is
|
|
22
|
-
|
|
23
|
-
A node is handed an `emit`, and writes into the stream through it. A node wrapping a streaming model call forwards deltas that way, and the graph never has to know what it is forwarding.
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
const answer = node({
|
|
27
|
-
name: "answer",
|
|
28
|
-
run: (values, ctx) => llm.stream(messages, { onText: ctx.emit }),
|
|
29
|
-
});
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
`emit` takes the text and nothing else, the graph tags it with the node it came
|
|
33
|
-
from, because a stream carrying tokens from four nodes is unreadable without it.
|
|
34
|
-
It is always present and does nothing when nobody is listening, so a node never
|
|
35
|
-
has to ask whether anyone is.
|
|
36
|
-
|
|
37
|
-
## The run starts on the call, not the first read
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
const running = flow.stream(values);
|
|
41
|
-
const result = await running.result; // no iteration, and it still ran
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
A caller who wants the rollout and not the commentary gets it. Nothing is deferred until somebody reads the stream, so a run cannot sit there un-started because a `for await` was never reached.
|
|
45
|
-
|
|
46
|
-
<Callout tone="trap" title="One stream has one reader">
|
|
47
|
-
Two `for await` loops over the same `graph.stream()` take from the same queue, so each sees roughly half the run, which reads as a graph dropping tokens rather than as the mistake it is. A second reader is refused. More than one watcher wants one reader writing into something they can both read.
|
|
48
|
-
</Callout>
|
|
49
|
-
|
|
50
|
-
## What the events are
|
|
51
|
-
|
|
52
|
-
| | |
|
|
53
|
-
| --- | --- |
|
|
54
|
-
| a node starting | its name, and the step it is |
|
|
55
|
-
| a node finishing | what it returned, what it cost, how long it took |
|
|
56
|
-
| whatever a node emitted | passed through untouched: tokens, progress, anything |
|
|
57
|
-
| a node stopping to ask | the question, and the nested run to resume. See [pausing](/spendgraph/graph/pausing) |
|
|
58
|
-
| the result | last, and the same object `execute` would have returned |
|
|
59
|
-
|
|
60
|
-
One reader. Events are handed out and dropped, so a second `for await` over the
|
|
61
|
-
same stream takes half of them rather than seeing the run twice: fan out
|
|
62
|
-
downstream of it, not by iterating it twice.
|
package/docs/wiring.mdx
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
export const meta = {
|
|
2
|
-
title: "Wiring: spendgraph docs",
|
|
3
|
-
description:
|
|
4
|
-
"Edges say what runs next. Branching is several edges tried in order, not one clever edge, and compilation refuses the wiring mistakes before anything runs.",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
# Wiring
|
|
8
|
-
|
|
9
|
-
An edge says what runs **next**. A node's `input` says on **what**. Keeping those separate is what lets the same node sit in two graphs.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
const flow = graph({
|
|
13
|
-
entry: "classify",
|
|
14
|
-
nodes: [classify, lookup, answer],
|
|
15
|
-
edges: [
|
|
16
|
-
edge("classify", "lookup", (ctx) => ctx.outputs.classify === "billing"),
|
|
17
|
-
edge("classify", "answer"),
|
|
18
|
-
edge("lookup", "answer"),
|
|
19
|
-
end("answer"),
|
|
20
|
-
],
|
|
21
|
-
});
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Branching is several edges, not one clever edge
|
|
25
|
-
|
|
26
|
-
`edge(from, to, when)` takes a predicate. The edges leaving a node are tried **in declaration order**, and the first match wins, so an unconditional edge is the default branch and belongs last.
|
|
27
|
-
|
|
28
|
-
```ts
|
|
29
|
-
edge("triage", "refund", (ctx) => ctx.outputs.triage === "billing"),
|
|
30
|
-
edge("triage", "escalate", (ctx) => ctx.outputs.triage === "abuse"),
|
|
31
|
-
edge("triage", "answer"), // the default, and it goes last
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
`end(from)` is `edge(from, null)` said out loud: the run finishes here.
|
|
35
|
-
|
|
36
|
-
## What compilation refuses
|
|
37
|
-
|
|
38
|
-
Compilation happens once, when you call `graph()`, and it is the one place this package throws. Everything it catches is a wiring mistake that would otherwise show up as a run that quietly did the wrong thing.
|
|
39
|
-
|
|
40
|
-
| | |
|
|
41
|
-
| --- | --- |
|
|
42
|
-
| an edge to a node that does not exist | usually a typo, so the message lists the names that do |
|
|
43
|
-
| an entry that is not a node | the run would have nowhere to start |
|
|
44
|
-
| two nodes with the same name | one of them silently shadows the other |
|
|
45
|
-
| a node nothing reaches | a branch that never runs, and nothing would say so |
|
|
46
|
-
| a node whose edges are **all conditional** | the half-specified case, below |
|
|
47
|
-
|
|
48
|
-
<Callout tone="trap" title="A second default edge is refused, not ignored">
|
|
49
|
-
Edges are tried in order and a default matches everything, so a second one after it can never be taken. That reads as a branch you wired and never see fire: compilation refuses it instead, naming the node.
|
|
50
|
-
</Callout>
|
|
51
|
-
|
|
52
|
-
## The half-specified node
|
|
53
|
-
|
|
54
|
-
A node with **no** edges is the end of the run: the author wrote none and meant it.
|
|
55
|
-
|
|
56
|
-
A node whose edges are *all* conditional is different. A run arriving there when no condition matches has nowhere to go, so it would stop, and report **success**, having skipped every node after it. Saying half of it is the mistake, and compilation refuses it:
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
edges: [
|
|
60
|
-
edge("triage", "refund", (ctx) => ctx.outputs.triage === "billing"),
|
|
61
|
-
// and nothing else leaving "triage" — refused
|
|
62
|
-
];
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Add the default edge, or add `end("triage")` if stopping there is what you meant. Either is a decision; the missing one was not.
|
|
66
|
-
|
|
67
|
-
## Inspecting a compiled graph
|
|
68
|
-
|
|
69
|
-
A graph answers questions about itself, which is what the workflows in `@spendgraph/harness` are built on.
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
flow.entry; // where a run starts
|
|
73
|
-
flow.maxSteps;
|
|
74
|
-
flow.nodes(); // every node, in declaration order
|
|
75
|
-
flow.edgesFrom("classify"); // the edges leaving it, in the order they are tried
|
|
76
|
-
```
|