@spendgraph/graph 0.7.0 → 0.8.1
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/types/result.d.ts +9 -0
- package/docs/nodes.mdx +4 -4
- package/docs/overview.mdx +12 -10
- package/docs/pausing.mdx +7 -7
- package/docs/running.mdx +5 -5
- package/docs/streaming.mdx +7 -7
- package/docs/wiring.mdx +5 -5
- package/package.json +3 -2
package/dist/types/result.d.ts
CHANGED
|
@@ -13,6 +13,15 @@ export interface GraphResult {
|
|
|
13
13
|
/** Summed across every step, nested ones included. */
|
|
14
14
|
inputTokens: number;
|
|
15
15
|
outputTokens: number;
|
|
16
|
+
/**
|
|
17
|
+
* The counts a provider bills apart from input and output, summed the same
|
|
18
|
+
* way — each present only where some node actually reported it, so absence
|
|
19
|
+
* is the claim that nothing was billed under it rather than a zero.
|
|
20
|
+
*/
|
|
21
|
+
cacheReadTokens?: number;
|
|
22
|
+
cacheWriteTokens?: number;
|
|
23
|
+
citationTokens?: number;
|
|
24
|
+
reasoningTokens?: number;
|
|
16
25
|
/** The question a node stopped to ask, where one did; `status` still says the graph itself ran. */
|
|
17
26
|
waitingOn?: Wait;
|
|
18
27
|
/** The nested run to resume, where it named one. */
|
package/docs/nodes.mdx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
-
title: "Nodes
|
|
2
|
+
title: "Nodes: spendgraph docs",
|
|
3
3
|
description:
|
|
4
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
5
|
};
|
|
@@ -22,7 +22,7 @@ const classify = node({
|
|
|
22
22
|
|
|
23
23
|
## `input` is what makes it reusable
|
|
24
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
|
|
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
26
|
|
|
27
27
|
```ts
|
|
28
28
|
input: (ctx) => ({ text: ctx.values.question }); // from the run's values
|
|
@@ -40,7 +40,7 @@ run: ({ text }) => text.toUpperCase(), // `text` is a string, here and now
|
|
|
40
40
|
|
|
41
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
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
|
|
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
44
|
|
|
45
45
|
<Callout tone="trap" title="A node cannot rewrite what an earlier node did">
|
|
46
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.
|
|
@@ -56,4 +56,4 @@ A node name must be letters, digits and underscores starting with a letter, and
|
|
|
56
56
|
|
|
57
57
|
Whatever `run` returns becomes the node's output: a string is itself, anything else is JSON.
|
|
58
58
|
|
|
59
|
-
A value that cannot be written down
|
|
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
-
title: "Graph
|
|
2
|
+
title: "Graph: spendgraph docs",
|
|
3
3
|
description:
|
|
4
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
5
|
};
|
|
@@ -8,12 +8,14 @@ export const meta = {
|
|
|
8
8
|
|
|
9
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
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
|
|
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
12
|
|
|
13
13
|
```sh
|
|
14
14
|
npm install @spendgraph/graph
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
<GraphToRollout />
|
|
18
|
+
|
|
17
19
|
Four things are exported. Everything else hangs off what they return.
|
|
18
20
|
|
|
19
21
|
```ts
|
|
@@ -76,21 +78,21 @@ await prompt.call(values, () => flow.execute(values));
|
|
|
76
78
|
|
|
77
79
|
## Three things worth knowing up front
|
|
78
80
|
|
|
79
|
-
**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
|
|
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).
|
|
80
82
|
|
|
81
|
-
**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](/
|
|
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).
|
|
82
84
|
|
|
83
|
-
**`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
|
|
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.
|
|
84
86
|
|
|
85
87
|
<Callout tone="trap" title="Compilation is the only place mistakes are cheap">
|
|
86
|
-
An edge pointing at a node that does not exist, an entry that is not a node, a node nothing reaches
|
|
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.
|
|
87
89
|
</Callout>
|
|
88
90
|
|
|
89
91
|
## Where to go next
|
|
90
92
|
|
|
91
93
|
| | |
|
|
92
94
|
| --- | --- |
|
|
93
|
-
| [Nodes](/
|
|
94
|
-
| [Wiring](/
|
|
95
|
-
| [Running a graph](/
|
|
96
|
-
| [Streaming](/
|
|
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
-
title: "Pausing a graph
|
|
2
|
+
title: "Pausing a graph: spendgraph docs",
|
|
3
3
|
description:
|
|
4
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
5
|
};
|
|
@@ -29,7 +29,7 @@ That is the same argument as reading a node's outcome structurally: a node may
|
|
|
29
29
|
return anything, and a rule to wrap a paused run is one you learn by having the
|
|
30
30
|
graph walk past the question and call it success. Every node's return goes
|
|
31
31
|
through it, so a `waitingOn` that is data rather than a question stops the run
|
|
32
|
-
too
|
|
32
|
+
too, nothing else in a reply has that shape, and the alternative fails the
|
|
33
33
|
other way, silently.
|
|
34
34
|
|
|
35
35
|
The check itself is exported, so your own code can ask the same question the
|
|
@@ -44,7 +44,7 @@ paused?.runId; // the inner run to resume, where there i
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
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
|
|
47
|
+
own. `Wait` is declared here rather than imported for the same reason: harness
|
|
48
48
|
owns durability and depends on this package, so the shape lives on this side and
|
|
49
49
|
harness's own `Wait` satisfies it.
|
|
50
50
|
|
|
@@ -56,18 +56,18 @@ for await (const event of flow.stream(values)) {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
The event carries the `Wait` whole
|
|
59
|
+
The event carries the `Wait` whole, the question, and whatever answering it
|
|
60
60
|
needs. A thrown error would carry only its message, which is why this is an
|
|
61
61
|
event and not an exception.
|
|
62
62
|
|
|
63
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
|
|
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
65
|
</Callout>
|
|
66
66
|
|
|
67
67
|
## The graph owns no durability
|
|
68
68
|
|
|
69
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
|
|
70
|
+
thinks, and handing the answer back is `@spendgraph/harness`'s job: it has the
|
|
71
71
|
store, the expiry and the resume.
|
|
72
72
|
|
|
73
73
|
## Picking it back up
|
|
@@ -103,7 +103,7 @@ resuming a cyclic graph again and again is the one holding the ceiling.
|
|
|
103
103
|
## A run that paused still ran
|
|
104
104
|
|
|
105
105
|
`status` says whether the graph itself ran, and a paused run ran correctly as
|
|
106
|
-
far as it got
|
|
106
|
+
far as it got, so it comes back `completed`, carrying the question.
|
|
107
107
|
|
|
108
108
|
Every workflow in this repo signals a pause the same way: not with a status, but
|
|
109
109
|
by carrying what it stopped to ask.
|
package/docs/running.mdx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
-
title: "Running a graph
|
|
2
|
+
title: "Running a graph: spendgraph docs",
|
|
3
3
|
description:
|
|
4
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
5
|
};
|
|
@@ -20,7 +20,7 @@ result.error; // '"lookup" failed: contract MSA 2.4 not found'
|
|
|
20
20
|
result.steps; // the steps that did run, with the failed one last
|
|
21
21
|
```
|
|
22
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
|
|
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
24
|
|
|
25
25
|
## The step ceiling
|
|
26
26
|
|
|
@@ -32,10 +32,10 @@ A backwards edge is a feature and also how a graph hangs, and nothing tells the
|
|
|
32
32
|
graph({ entry: "draft", nodes, edges, maxSteps: 50 });
|
|
33
33
|
```
|
|
34
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
|
|
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
36
|
|
|
37
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
|
|
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
39
|
</Callout>
|
|
40
40
|
|
|
41
41
|
## What comes back
|
|
@@ -60,7 +60,7 @@ await prompt.call(values, () => flow.execute(values));
|
|
|
60
60
|
|
|
61
61
|
## Context
|
|
62
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
|
|
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
64
|
|
|
65
65
|
```ts
|
|
66
66
|
ctx.values; // what execute() was called with
|
package/docs/streaming.mdx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
-
title: "Streaming a graph
|
|
2
|
+
title: "Streaming a graph: spendgraph docs",
|
|
3
3
|
description:
|
|
4
4
|
"The same run, narrated: each node's start and end, whatever its nodes emit while running, and the finished rollout last.",
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
# Streaming
|
|
8
8
|
|
|
9
|
-
`stream` is the same run as `execute`, narrated
|
|
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
10
|
|
|
11
11
|
```ts
|
|
12
12
|
const running = flow.stream({ question });
|
|
@@ -29,7 +29,7 @@ const answer = node({
|
|
|
29
29
|
});
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`emit` takes the text and nothing else
|
|
32
|
+
`emit` takes the text and nothing else, the graph tags it with the node it came
|
|
33
33
|
from, because a stream carrying tokens from four nodes is unreadable without it.
|
|
34
34
|
It is always present and does nothing when nobody is listening, so a node never
|
|
35
35
|
has to ask whether anyone is.
|
|
@@ -44,7 +44,7 @@ const result = await running.result; // no iteration, and it still ran
|
|
|
44
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
45
|
|
|
46
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
|
|
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
48
|
</Callout>
|
|
49
49
|
|
|
50
50
|
## What the events are
|
|
@@ -53,10 +53,10 @@ A caller who wants the rollout and not the commentary gets it. Nothing is deferr
|
|
|
53
53
|
| --- | --- |
|
|
54
54
|
| a node starting | its name, and the step it is |
|
|
55
55
|
| a node finishing | what it returned, what it cost, how long it took |
|
|
56
|
-
| whatever a node emitted | passed through untouched
|
|
57
|
-
| a node stopping to ask | the question, and the nested run to resume
|
|
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
58
|
| the result | last, and the same object `execute` would have returned |
|
|
59
59
|
|
|
60
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
|
|
61
|
+
same stream takes half of them rather than seeing the run twice: fan out
|
|
62
62
|
downstream of it, not by iterating it twice.
|
package/docs/wiring.mdx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
-
title: "Wiring
|
|
2
|
+
title: "Wiring: spendgraph docs",
|
|
3
3
|
description:
|
|
4
|
-
"Edges say what runs next. Branching is several edges tried in order, not one clever edge
|
|
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
5
|
};
|
|
6
6
|
|
|
7
7
|
# Wiring
|
|
@@ -23,7 +23,7 @@ const flow = graph({
|
|
|
23
23
|
|
|
24
24
|
## Branching is several edges, not one clever edge
|
|
25
25
|
|
|
26
|
-
`edge(from, to, when)` takes a predicate. The edges leaving a node are tried **in declaration order**, and the first match wins
|
|
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
27
|
|
|
28
28
|
```ts
|
|
29
29
|
edge("triage", "refund", (ctx) => ctx.outputs.triage === "billing"),
|
|
@@ -46,14 +46,14 @@ Compilation happens once, when you call `graph()`, and it is the one place this
|
|
|
46
46
|
| a node whose edges are **all conditional** | the half-specified case, below |
|
|
47
47
|
|
|
48
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
|
|
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
50
|
</Callout>
|
|
51
51
|
|
|
52
52
|
## The half-specified node
|
|
53
53
|
|
|
54
54
|
A node with **no** edges is the end of the run: the author wrote none and meant it.
|
|
55
55
|
|
|
56
|
-
A node whose edges are *all* conditional is different. A run arriving there when no condition matches has nowhere to go
|
|
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
57
|
|
|
58
58
|
```ts
|
|
59
59
|
edges: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spendgraph/graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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": {
|
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
"README.md"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@spendgraph/sdk": "^0.
|
|
39
|
+
"@spendgraph/sdk": "^0.8.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
+
"@spendgraph/config": "0.8.1",
|
|
42
43
|
"typescript": "^5"
|
|
43
44
|
},
|
|
44
45
|
"engines": {
|