@ssheleg/agent-stack 0.8.0 → 0.10.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/CHANGELOG.md +132 -0
- package/README.md +18 -4
- package/package.json +1 -1
- package/plugins/agent-stack/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-stack/skills/agent-evals/SKILL.md +44 -3
- package/plugins/agent-stack/skills/agent-harness/SKILL.md +37 -9
- package/plugins/agent-stack/skills/agent-harness/references/layers.md +4 -0
- package/plugins/agent-stack/skills/agent-harness/references/pi-sdk.md +318 -0
- package/plugins/agent-stack/skills/agent-harness/references/pi.md +241 -0
- package/plugins/agent-stack/skills/agent-harness/scripts/audit_agent.py +100 -32
- package/plugins/agent-stack/skills/agent-orchestrator/SKILL.md +100 -101
- package/plugins/agent-stack/skills/agent-orchestrator/references/graph-engineering.md +327 -0
- package/plugins/agent-stack/skills/agent-orchestrator/references/llm-proxy-billing.md +1 -1
- package/plugins/agent-stack/skills/agent-orchestrator/references/patterns.md +38 -14
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Graph engineering — deciding the shape of the work before doing it
|
|
2
|
+
|
|
3
|
+
**Load this when** a job has more than two steps and you are about to write them in a
|
|
4
|
+
line: choosing between a chain and a graph, finding the dependencies that are not real,
|
|
5
|
+
placing a check between a parallel layer and the node that consumes it, or deciding
|
|
6
|
+
whether the structure may be discovered while it runs.
|
|
7
|
+
|
|
8
|
+
**Spec pinned:** *Graph Engineering with Claude*, `https://x.com/Mahaximus_/status/2082442856417956173`
|
|
9
|
+
(published 2026-07-29); Claude Code `CHANGELOG.md` v2.1.154 – v2.1.229 · read 2026-08-15
|
|
10
|
+
|
|
11
|
+
`SKILL.md` §2 owns the tool-calling loop and §5 the multi-stage pipeline. Both assume
|
|
12
|
+
the shape is already decided. **This file is how it gets decided**, and it is upstream of
|
|
13
|
+
every constant in the rest of the pack: a threshold tuned inside the wrong shape is a
|
|
14
|
+
well-measured answer to the wrong question.
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- [The source, and what this file adds](#the-source-and-what-this-file-adds)
|
|
19
|
+
- [1. Node and edge](#1-node-and-edge)
|
|
20
|
+
- [2. Your loop is already a graph, and most of its edges are fake](#2-your-loop-is-already-a-graph-and-most-of-its-edges-are-fake)
|
|
21
|
+
- [3. The fake-edge test](#3-the-fake-edge-test)
|
|
22
|
+
- [4. The diamond](#4-the-diamond)
|
|
23
|
+
- [5. Two ways a diamond fails silently](#5-two-ways-a-diamond-fails-silently)
|
|
24
|
+
- [6. The checker node](#6-the-checker-node)
|
|
25
|
+
- [7. Static or dynamic](#7-static-or-dynamic)
|
|
26
|
+
- [8. When not to build a graph at all](#8-when-not-to-build-a-graph-at-all)
|
|
27
|
+
- [9. What Claude Code actually executes](#9-what-claude-code-actually-executes)
|
|
28
|
+
- [10. Barrier or no barrier](#10-barrier-or-no-barrier)
|
|
29
|
+
- [11. Project defaults, written once](#11-project-defaults-written-once)
|
|
30
|
+
- [12. The source's four diagrams, and what each one is for](#12-the-sources-four-diagrams-and-what-each-one-is-for)
|
|
31
|
+
- [Where this file disagrees with its source](#where-this-file-disagrees-with-its-source)
|
|
32
|
+
|
|
33
|
+
## The source, and what this file adds
|
|
34
|
+
|
|
35
|
+
The model below — node, edge, the fake-edge test, the diamond, the checker node, static
|
|
36
|
+
versus dynamic — is taken from the article pinned above. It is the clearest short
|
|
37
|
+
statement of the idea available, and the link is kept so the original can be re-read
|
|
38
|
+
rather than remembered through this summary.
|
|
39
|
+
|
|
40
|
+
Four things are **this pack's**, not the source's, and each is marked where it appears:
|
|
41
|
+
|
|
42
|
+
| Added here | Why the source could not carry it |
|
|
43
|
+
|---|---|
|
|
44
|
+
| §9 — what the host actually runs, with version evidence | the article's one operational claim aged out six weeks after publication (see §9) |
|
|
45
|
+
| §10 — the barrier distinction | the article's diamond has a barrier at every convergence; most convergences do not need one |
|
|
46
|
+
| §6 — what a checker costs, and when it is a rubber stamp | a check nobody measures is a node that always says yes |
|
|
47
|
+
| §7 — the auditability rule as a **hard** rule, not a preference | this pack's own doctrine is that a green nobody watched fail is not evidence |
|
|
48
|
+
|
|
49
|
+
## 1. Node and edge
|
|
50
|
+
|
|
51
|
+
**A node is one unit of work.** One input, one output, one job. Not *"research the topic,
|
|
52
|
+
summarise it, and check the sources"* — that is three nodes wearing one name. The
|
|
53
|
+
smaller and more defined the job, the more useful the node, because a node is also the
|
|
54
|
+
unit you retry, cache, review and replace.
|
|
55
|
+
|
|
56
|
+
**An edge is a dependency, and it carries data.** It exists when the second node
|
|
57
|
+
genuinely consumes what the first produced. Not when the second merely *happens after*
|
|
58
|
+
the first.
|
|
59
|
+
|
|
60
|
+
That distinction is the whole discipline. Write it on the edge and it stops being
|
|
61
|
+
abstract: `research --findings--> write --draft--> verify`. **An edge you cannot label
|
|
62
|
+
with what crosses it is not an edge.**
|
|
63
|
+
|
|
64
|
+
## 2. Your loop is already a graph, and most of its edges are fake
|
|
65
|
+
|
|
66
|
+
A prompt that says *"research this, then summarise, then draft"* is a graph — a single
|
|
67
|
+
unbranching chain in which every step waits for its predecessor. It is correct. It is
|
|
68
|
+
also the slowest possible arrangement of that work and the most brittle: one bad step
|
|
69
|
+
takes the whole chain, and nothing runs while any step is running.
|
|
70
|
+
|
|
71
|
+
The first move is therefore not to learn a new structure. It is to look at the one you
|
|
72
|
+
already have and ask which of its waits are real.
|
|
73
|
+
|
|
74
|
+
## 3. The fake-edge test
|
|
75
|
+
|
|
76
|
+
Five minutes, no tooling, and it is the highest-yield thing in this file.
|
|
77
|
+
|
|
78
|
+
1. Write every step as a box.
|
|
79
|
+
2. Draw an arrow between each pair of consecutive steps.
|
|
80
|
+
3. For each arrow ask: **does data from A actually enter B?** — not *"does B come after
|
|
81
|
+
A"*.
|
|
82
|
+
4. Yes → keep it, and **write the payload on the arrow**.
|
|
83
|
+
5. No → delete it. That wait was free to give away and you were paying for it.
|
|
84
|
+
6. Everything with no incoming arrow starts immediately.
|
|
85
|
+
7. Everything with no outgoing arrow is a final output.
|
|
86
|
+
|
|
87
|
+
The tell that the test is being done honestly is step 4: if the payload cell is empty,
|
|
88
|
+
the edge is fake, and the person drawing it now has to say so out loud rather than
|
|
89
|
+
leaving the arrow in place because it looked orderly.
|
|
90
|
+
|
|
91
|
+
**Expect two or three fake edges in any workflow you have not run this against.** The
|
|
92
|
+
classic is *"review file A, then review file B"*: it reads as a sequence, and the review
|
|
93
|
+
of B never once looks at what A returned.
|
|
94
|
+
|
|
95
|
+
## 4. The diamond
|
|
96
|
+
|
|
97
|
+
One node fans out into several independent nodes; those all feed one node that combines
|
|
98
|
+
them. Drawn out, it is a diamond, and it is the shape that makes graphs worth the setup.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
┌──────────┐
|
|
102
|
+
│ SPLIT │
|
|
103
|
+
└────┬─────┘
|
|
104
|
+
┌─────────────┼─────────────┐
|
|
105
|
+
▼ ▼ ▼ ← parallel layer
|
|
106
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
107
|
+
│ source1 │ │ source2 │ │ source3 │
|
|
108
|
+
└────┬────┘ └────┬────┘ └────┬────┘
|
|
109
|
+
└─────────────┼─────────────┘
|
|
110
|
+
▼
|
|
111
|
+
┌──────────┐
|
|
112
|
+
│SYNTHESIZE│ ← convergence
|
|
113
|
+
└──────────┘
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The convergence waits for the slowest branch, not for the sum of all of them.
|
|
117
|
+
|
|
118
|
+
**Two rules, and both have to hold:**
|
|
119
|
+
|
|
120
|
+
1. **The parallel nodes are genuinely independent** — no fake edge dressed as a real one,
|
|
121
|
+
and no shared mutable state. Two "independent" workers writing one file are one node
|
|
122
|
+
with a race in it.
|
|
123
|
+
2. **The convergence genuinely needs all of them.** If it needs only the first to answer,
|
|
124
|
+
the rest are paid-for waste and you wanted a race, not a diamond.
|
|
125
|
+
|
|
126
|
+
Once you look for it, the shape is everywhere there is a *gather-then-combine*: research,
|
|
127
|
+
multi-file review, market analysis, a fan of checks over one artifact.
|
|
128
|
+
|
|
129
|
+
## 5. Two ways a diamond fails silently
|
|
130
|
+
|
|
131
|
+
Both are failures **of the convergence**, which is why sequential chains do not have
|
|
132
|
+
them.
|
|
133
|
+
|
|
134
|
+
**A bad node goes undetected.** Three branches run, one returns a hallucination, an empty
|
|
135
|
+
result or a misread file, and that output arrives at the synthesis node beside two good
|
|
136
|
+
ones. The synthesis node does not know one of its inputs is wrong. It produces a
|
|
137
|
+
confident answer built partly on garbage. Parallelism bought the speed by deleting the
|
|
138
|
+
checkpoints where a human would have noticed.
|
|
139
|
+
|
|
140
|
+
**The error cascades and dilutes.** In a chain, a bad step produces a visibly bad output.
|
|
141
|
+
At a convergence, the bad output is *mixed* with good ones, so the damage is spread thin
|
|
142
|
+
and the trace back to its source is gone. By the time anything looks wrong, three nodes
|
|
143
|
+
have averaged it into plausibility.
|
|
144
|
+
|
|
145
|
+
Both are the same defect: **the convergence trusts its inputs because they arrived.**
|
|
146
|
+
|
|
147
|
+
## 6. The checker node
|
|
148
|
+
|
|
149
|
+
A node between the parallel layer and the convergence whose only job is to decide whether
|
|
150
|
+
each output may proceed. It synthesises nothing and writes nothing. It answers *is this
|
|
151
|
+
usable* and then passes, flags, retries or drops.
|
|
152
|
+
|
|
153
|
+
Five things it must catch — the list is the contract, and a checker that cannot say which
|
|
154
|
+
of the five it is asserting is not a checker:
|
|
155
|
+
|
|
156
|
+
1. **Empty or null** — the node returned nothing usable.
|
|
157
|
+
2. **Mutually contradictory** — two outputs that cannot both be true.
|
|
158
|
+
3. **Off-topic** — an output that answers a different question than the one asked.
|
|
159
|
+
4. **Under-confident** — a confidence signal below the bar for the downstream decision.
|
|
160
|
+
5. **Malformed** — a shape that will break the convergence node's parsing.
|
|
161
|
+
|
|
162
|
+
Three of the five are code checks (1, 4, 5) and cost nothing; only 2 and 3 need a model.
|
|
163
|
+
Run them in that order — this is `agent-evals` §5's *cheap checks first*, applied to a
|
|
164
|
+
position in the graph rather than to a test suite.
|
|
165
|
+
|
|
166
|
+
**What a checker costs, and how it turns into a rubber stamp — this pack's addition.** A
|
|
167
|
+
checker is a node, so it has the failure mode of every node: it can be wrong. A model
|
|
168
|
+
checker that has never been shown a bad input will pass everything, and a graph with a
|
|
169
|
+
checker that always says yes is strictly worse than one with no checker, because the
|
|
170
|
+
absent checkpoint has been replaced by a false one. So:
|
|
171
|
+
|
|
172
|
+
- **Give it a planted bad input at least once and watch it refuse.** Same rule as any
|
|
173
|
+
other guard in this family.
|
|
174
|
+
- **Record its verdicts as scores with a source** (`agent-evals` §7), or you can never
|
|
175
|
+
ask afterwards how often it fired.
|
|
176
|
+
- **A checker that has never rejected anything is a finding**, not a reassurance.
|
|
177
|
+
|
|
178
|
+
**Wire the convergence to the checker, not to the layer.** The synthesis node depends on
|
|
179
|
+
the checker; the checker depends on the branches. If synthesis also takes a direct edge
|
|
180
|
+
from a branch, the gate has a bypass and the shape is decoration.
|
|
181
|
+
|
|
182
|
+
## 7. Static or dynamic
|
|
183
|
+
|
|
184
|
+
A **static** graph has its nodes and edges decided before it runs. A **dynamic** graph
|
|
185
|
+
grows: a node finishes, looks at what it found, and decides what should come next.
|
|
186
|
+
|
|
187
|
+
| Reach for | When |
|
|
188
|
+
|---|---|
|
|
189
|
+
| **static** | the task repeats and the structure is the same each time |
|
|
190
|
+
| **static** | predictability and speed matter more than flexibility |
|
|
191
|
+
| **static** | **always first** — switch only after the static version hits a wall you can name |
|
|
192
|
+
| dynamic | the scope of the work depends on what is discovered along the way |
|
|
193
|
+
| dynamic | a node must choose its successors from its own output |
|
|
194
|
+
| **never dynamic** | **you will need to audit exactly what ran and why** |
|
|
195
|
+
|
|
196
|
+
The last row is a hard rule in this pack, not a preference. A dynamic graph's executed
|
|
197
|
+
shape is not the shape anybody drew, so *"here is the graph"* and *"here is what
|
|
198
|
+
happened"* stop being the same document — and every claim about the run becomes
|
|
199
|
+
unfalsifiable from the outside. That is the same failure `agent-evals` names when a
|
|
200
|
+
system has no durable trace.
|
|
201
|
+
|
|
202
|
+
**Most workflows that feel like they need a dynamic graph need a better static one.**
|
|
203
|
+
Dynamic is more powerful and much harder to control; it is the second reach, never the
|
|
204
|
+
first.
|
|
205
|
+
|
|
206
|
+
## 8. When not to build a graph at all
|
|
207
|
+
|
|
208
|
+
The honest cost table. A graph is not free, and for a one-off it usually loses:
|
|
209
|
+
|
|
210
|
+
| | Chain | Graph |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| Time to build | low | higher — the dependencies have to be worked out |
|
|
213
|
+
| Time to run | the sum of the steps | the longest path |
|
|
214
|
+
| Debugging | easy — one line to walk | harder — concurrent state, diluted errors |
|
|
215
|
+
| Mid-run failure | poor, but visible immediately | good **only if** there is a checker |
|
|
216
|
+
| A one-off task | right answer | overkill |
|
|
217
|
+
| Something you run weekly | works | better, and the setup amortises |
|
|
218
|
+
| Growth in task size | does not scale | scales |
|
|
219
|
+
|
|
220
|
+
**Build the graph when the work repeats, or when a mid-run error is expensive enough that
|
|
221
|
+
the checker pays for itself.** Otherwise write the chain and move on — this is
|
|
222
|
+
`agent-harness`'s *start at the simplest thing that works* applied to shape.
|
|
223
|
+
|
|
224
|
+
## 9. What Claude Code actually executes
|
|
225
|
+
|
|
226
|
+
**This section is the pack's, not the source's, and it exists because the source's one
|
|
227
|
+
operational claim has since changed.** The article tells the reader that Claude Code has
|
|
228
|
+
a `workflow` keyword which parses a YAML block of `nodes:` and `depends_on:` and
|
|
229
|
+
parallelises it. Two corrections, both from the vendor's own changelog:
|
|
230
|
+
|
|
231
|
+
| Version | Entry (quoted from `anthropics/claude-code` `CHANGELOG.md`) |
|
|
232
|
+
|---|---|
|
|
233
|
+
| v2.1.154 | "Introducing dynamic workflows: ask Claude to create a workflow and it orchestrates work across tens to hundreds of agents in the background" |
|
|
234
|
+
| v2.1.160 | "Renamed the dynamic-workflow trigger keyword from `workflow` to `ultracode`. The word 'workflow' no longer triggers a run; asking for one in your own words still works" |
|
|
235
|
+
| v2.1.178 | the keyword "trigger[s] only on explicit phrases like 'run a workflow' or 'workflow:', not on any mention of the word" |
|
|
236
|
+
| v2.1.219 | dynamic workflows "default to a medium size guideline (aim for fewer than 15 agents)"; settable via `workflowSizeGuideline` |
|
|
237
|
+
| v2.1.229 | fan-outs "stagger same-prefix sibling agents so subsequent agents read the cached prompt prefix instead of re-paying it" |
|
|
238
|
+
|
|
239
|
+
So the keyword named in the article stopped being the keyword in v2.1.160, and the
|
|
240
|
+
opt-in today is `ultracode` or an explicit phrase.
|
|
241
|
+
|
|
242
|
+
**And the YAML is not what runs.** The host does not parse `nodes:`/`depends_on:`. It
|
|
243
|
+
authors and executes a **script** whose primitives are the real contract:
|
|
244
|
+
|
|
245
|
+
| Primitive | Is | Note |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| `agent(prompt, opts)` | one subagent | `opts.schema` forces a validated object back, so downstream stages get data, not prose to parse |
|
|
248
|
+
| `parallel(thunks)` | concurrent, **with a barrier** | awaits all; a thrower resolves to `null` rather than rejecting the call |
|
|
249
|
+
| `pipeline(items, ...stages)` | each item through all stages, **no barrier** | item A can be in stage 3 while B is still in stage 1 |
|
|
250
|
+
| `phase(title)` | a progress grouping | display, not semantics |
|
|
251
|
+
| `isolation: "worktree"` | a private checkout per agent | the only safe way to fan out writers |
|
|
252
|
+
|
|
253
|
+
Concurrency is capped at `min(16, cores − 2)` per run, and a run's total agents at 1000.
|
|
254
|
+
Passing 100 items is fine — they queue.
|
|
255
|
+
|
|
256
|
+
**Why this matters for the model above:** the article's diamond is `parallel()`, and the
|
|
257
|
+
next section is the distinction it does not draw.
|
|
258
|
+
|
|
259
|
+
## 10. Barrier or no barrier
|
|
260
|
+
|
|
261
|
+
**This section is the pack's.** A convergence node is a barrier: nothing downstream of it
|
|
262
|
+
starts until every branch has finished. The article treats that as the definition of a
|
|
263
|
+
diamond. It is actually a *choice*, and the wrong default.
|
|
264
|
+
|
|
265
|
+
A barrier is correct only when the downstream stage needs **cross-item** context:
|
|
266
|
+
|
|
267
|
+
- deduplicating or merging across the whole result set before expensive work;
|
|
268
|
+
- an early exit that depends on the total ("zero findings → skip verification");
|
|
269
|
+
- a stage whose prompt genuinely compares one item against the others — **which is
|
|
270
|
+
exactly what a checker node does**, and is why the checker is a legitimate barrier.
|
|
271
|
+
|
|
272
|
+
A barrier is **not** justified by:
|
|
273
|
+
|
|
274
|
+
- *"I need to flatten or filter the results first"* — do that inside a stage;
|
|
275
|
+
- *"the stages are conceptually separate"* — separate is not the same as synchronised;
|
|
276
|
+
- *"it reads more cleanly"* — the cost is real. With five branches where the slowest takes
|
|
277
|
+
three times the fastest, a barrier idles the four fast ones for two thirds of the wait.
|
|
278
|
+
|
|
279
|
+
The rule: **pipeline by default; barrier when a stage names the cross-item fact it
|
|
280
|
+
needs.** If it cannot name one, it does not need one.
|
|
281
|
+
|
|
282
|
+
## 11. Project defaults, written once
|
|
283
|
+
|
|
284
|
+
Anything you run more than twice deserves its graph conventions recorded where the agent
|
|
285
|
+
reads them (`CLAUDE.md`, or the equivalent for the host), so they are not re-derived per
|
|
286
|
+
session:
|
|
287
|
+
|
|
288
|
+
```markdown
|
|
289
|
+
## Workflow defaults
|
|
290
|
+
|
|
291
|
+
- A node with no declared dependency starts immediately; do not serialise by habit.
|
|
292
|
+
- Every declared dependency names the data it carries. No payload named ⇒ delete the edge.
|
|
293
|
+
- A checker sits between any parallel layer and the node that consumes it, and the
|
|
294
|
+
consumer depends on the checker rather than on the layer.
|
|
295
|
+
- A checker flags; it never silently passes an incomplete output.
|
|
296
|
+
- A node that fails pauses the run and reports; nothing downstream consumes a flagged output.
|
|
297
|
+
- Outputs are files with the node's name; the graph passes paths, not transcripts.
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The last line is `context-engineering.md`'s *filesystem as context* stated as a graph
|
|
301
|
+
rule: an edge that carries a path costs a few tokens, and an edge that carries a
|
|
302
|
+
transcript costs the window.
|
|
303
|
+
|
|
304
|
+
## 12. The source's four diagrams, and what each one is for
|
|
305
|
+
|
|
306
|
+
The article carries four hand-drawn figures. They are not decoration — each one is doing
|
|
307
|
+
a specific job, and knowing which one saves re-reading the prose:
|
|
308
|
+
|
|
309
|
+
| Figure | What it shows | The job it does |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| **Cover — "Graph Engineering explained"** | `START` (define the task) → `SPLIT` (break into nodes) → a fan of three workers labelled *research / compare / check* → `CHECKER` (catch errors early) → `OUTPUT` (one clean answer) | The whole argument in one line, and the only figure in which the checker appears as a first-class stage rather than an afterthought |
|
|
312
|
+
| **Node / edge** | Three boxes — `Research` (in: topic, out: findings) → `Write` (in: findings, out: draft) → `Verify` (in: draft, out: final) — with `NODE` and `EDGE` labelled, and **the arrows themselves labelled with the data they carry** | Makes §1 concrete: the payload written on the arrow is what turns "comes after" into "depends on". This is the figure to copy when teaching the model |
|
|
313
|
+
| **The diamond** | One `RESEARCH` node fanning into `SOURCE 1/2/3` (bracketed *parallel layer*), all three converging on `SYNTHESIZE` | The ideal shape, drawn **before** the failure modes — deliberately without a checker, which is what §5 then attacks |
|
|
314
|
+
| **Workflow — how the code runs** | The same shape in code terms: `research_a/b/c` in a parallel layer, three arrows into `checker` annotated *waits for all three*, one arrow from `checker` into `compare` | The repaired shape. Its point is the single edge out of the checker: `compare` depends on the **gate**, not on the branches — §6's last paragraph, drawn |
|
|
315
|
+
|
|
316
|
+
## Where this file disagrees with its source
|
|
317
|
+
|
|
318
|
+
- **The keyword.** The source's `workflow` is `ultracode` since v2.1.160 (§9). Treated as
|
|
319
|
+
a version-dated fact rather than a correction of the author: it was true when written.
|
|
320
|
+
- **The YAML.** The source presents `workflow:` / `nodes:` / `depends_on:` as a syntax the
|
|
321
|
+
host parses. It is a way of *describing* a graph in a prompt, and it works for that; the
|
|
322
|
+
execution contract is the script in §9.
|
|
323
|
+
- **The barrier.** The source's diamond always synchronises. This file makes the barrier a
|
|
324
|
+
decision with a named justification (§10).
|
|
325
|
+
- **The checker's own reliability.** The source introduces the checker and stops. This
|
|
326
|
+
file requires it to have been watched refusing a planted input, and treats a checker
|
|
327
|
+
that has never rejected anything as a finding (§6).
|
|
@@ -248,7 +248,7 @@ Map your public model names to provider ids in **one** function, and give every
|
|
|
248
248
|
provider a default and a fallback:
|
|
249
249
|
|
|
250
250
|
```
|
|
251
|
-
toUpstreamModel(provider, model) // "
|
|
251
|
+
toUpstreamModel(provider, model) // "<public-name>" → "<provider>/<upstream-id>"
|
|
252
252
|
getDefaultModel(provider) // when the caller names none
|
|
253
253
|
getFallbackModels(provider) // ordered, tried on 5xx / overload
|
|
254
254
|
```
|
|
@@ -139,6 +139,10 @@ class ExecutionPlan:
|
|
|
139
139
|
@classmethod
|
|
140
140
|
def from_json(cls, raw: str) -> ExecutionPlan: ...
|
|
141
141
|
|
|
142
|
+
def layers(self) -> list[list[PlanStage]]:
|
|
143
|
+
"""Kahn's algorithm over `depends_on`. Each returned list may run concurrently."""
|
|
144
|
+
...
|
|
145
|
+
|
|
142
146
|
@dataclass
|
|
143
147
|
class StageResult:
|
|
144
148
|
stage_id: str
|
|
@@ -149,6 +153,14 @@ class StageResult:
|
|
|
149
153
|
error: str | None = None
|
|
150
154
|
```
|
|
151
155
|
|
|
156
|
+
**`depends_on` is a claim the executor has to honour.** A plan that declares dependencies
|
|
157
|
+
and is then executed in list order has serialised itself: `stages[3]` waits for
|
|
158
|
+
`stages[2]` whether or not it consumes anything it produced. Execute by **layer** —
|
|
159
|
+
everything whose dependencies are satisfied goes together — and the declaration starts
|
|
160
|
+
paying for itself. Two rules come with it: a cycle is a plan defect and fails the plan
|
|
161
|
+
rather than deadlocking the run, and a layer of more than one stage needs the convergence
|
|
162
|
+
check in `graph-engineering.md` §6 before anything downstream consumes it.
|
|
163
|
+
|
|
152
164
|
---
|
|
153
165
|
|
|
154
166
|
## Validation Loop (SQL Execution)
|
|
@@ -187,21 +199,33 @@ for attempt in range(1, max_retries + 1):
|
|
|
187
199
|
|
|
188
200
|
## Context Window Sizes
|
|
189
201
|
|
|
202
|
+
**Do not ship a table of model ids.** This file carried one until 2026-08-15 — nine
|
|
203
|
+
vendor ids with their windows, and a `DEFAULT_CONTEXT_WINDOW` of 16 000. Every number in
|
|
204
|
+
it was correct when written and none of it survived a year: generations shipped, ids were
|
|
205
|
+
renamed, long-context variants appeared under the same family name, and a system reading
|
|
206
|
+
that table would have sized its budget for a window an order of magnitude smaller than
|
|
207
|
+
the one it was actually given. A lookup table of somebody else's identifiers is a cache
|
|
208
|
+
with no invalidation.
|
|
209
|
+
|
|
210
|
+
**Resolve the window at one boundary instead**, in this order, and let every caller ask
|
|
211
|
+
that boundary rather than a constant:
|
|
212
|
+
|
|
213
|
+
1. **Configuration** — an explicit per-model entry the operator set. It outranks
|
|
214
|
+
everything, because it is the only source that can encode a limit you have chosen (a
|
|
215
|
+
budget cap below the real window, a provider tier).
|
|
216
|
+
2. **The provider** — the model list or metadata endpoint most APIs expose. Fetched once
|
|
217
|
+
per process, cached with a TTL, refreshed on a miss.
|
|
218
|
+
3. **A conservative floor** for a model nothing knows about, plus a **loud log line**
|
|
219
|
+
naming the model. A silent default is how a new model runs at a fraction of its
|
|
220
|
+
window for months with nobody noticing.
|
|
221
|
+
|
|
222
|
+
The floor is a number to be small about, not accurate about: being early to compact costs
|
|
223
|
+
one avoidable rung of the ladder, and being late costs the request.
|
|
224
|
+
|
|
190
225
|
```python
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"gpt-4-turbo": 128_000,
|
|
195
|
-
"gpt-4": 8_192,
|
|
196
|
-
"gpt-3.5-turbo": 16_385,
|
|
197
|
-
"claude-sonnet-4-20250514": 200_000,
|
|
198
|
-
"claude-3-5-sonnet-20241022": 200_000,
|
|
199
|
-
"claude-3-haiku-20240307": 200_000,
|
|
200
|
-
"claude-3-opus-20240229": 200_000,
|
|
201
|
-
}
|
|
202
|
-
DEFAULT_CONTEXT_WINDOW = 16_000
|
|
203
|
-
|
|
204
|
-
# Token estimation: tiktoken for OpenAI models, ~4 chars/token fallback
|
|
226
|
+
# Token estimation: a real tokenizer where one is available, ~4 chars/token otherwise.
|
|
227
|
+
# The fallback runs LOW on code, JSON and non-Latin text — see context-engineering.md
|
|
228
|
+
# → Estimating what you have left, and apply the padding factor described there.
|
|
205
229
|
def estimate_tokens(text):
|
|
206
230
|
try:
|
|
207
231
|
import tiktoken
|