@ssheleg/agent-stack 0.20.0 → 0.22.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 CHANGED
@@ -1,3 +1,83 @@
1
+ ## v0.22.0 — the failures named from outside, and the control that is absence
2
+
3
+ **`graph-engineering.md` was argued entirely from what breaks, with no citation behind it.**
4
+ *Why Do Multi-Agent LLM Systems Fail?* (2025) measured the same ground: traces from seven
5
+ mainstream frameworks — MetaGPT, ChatDev, AG2, Magentic-One — with human annotators
6
+ independently analysing ~**150 traces** at **Cohen's kappa = 0.88**, yielding **14 failure
7
+ modes in three groups**.
8
+
9
+ The reason to carry it is that the three groups land on this file's own five-field node
10
+ contract without being bent to fit: *system design flaws* on the interface and the `owner`,
11
+ *inter-agent alignment failures* on the edge payload — §3's **Carries** column — and
12
+ *missing task verification* on `check` and the checker node. An outside measurement arriving
13
+ at the same joints is worth more than another argument from failure.
14
+
15
+ **And its load-bearing result is the negative one.** Better prompts, more explicit roles and
16
+ retries bought ChatDev **15.6%**, and the authors conclude the modes are **architectural
17
+ rather than bugs** — the same claim §1 makes about the two fields nobody draws. The section
18
+ is written as a review checklist, not a taxonomy to admire: per node, is the interface
19
+ stated, is there exactly one owner, and does the check run on something other than the
20
+ node's own claim of success.
21
+
22
+ **`governance.md` put per-tool authorisation at the moment of invocation, and there is a
23
+ strictly stronger control one layer earlier.** *The model cannot reason about capabilities
24
+ it does not know exist.* A tool absent from the schema cannot be invoked, argued for, or
25
+ probed for a bypass; a tool present and refused at call time is a negotiation, and
26
+ negotiations are won sometimes. Sub-agent isolation needs both halves — schema filtering at
27
+ construction **and** `message_history = None` at execution, because a sub-agent handed its
28
+ parent's transcript has been told about every capability you removed from its schema.
29
+
30
+ **Two numbers that point in opposite directions on purpose.** Eagerly loading every MCP tool
31
+ schema at startup consumed **40% of the context budget before the first user message**; a
32
+ metadata index with schemas fetched on selection takes it **under 5%**. Yet the same system
33
+ builds *its own* prompt and tool schemas **eagerly**, in the constructor. The rule is not
34
+ *lazy is better*: it is **eager for what you own and always need, lazy for what is foreign
35
+ and might not be used.**
36
+
37
+ **Approval fatigue is filed as a safety failure, not a UX complaint.** An approval system
38
+ with no persistence makes users re-approve the same operations every session, which produces
39
+ blanket auto-approval and defeats the safety system entirely — through the user rather than
40
+ through a bug, so nothing in the logs looks wrong. **A control that is asked too often is a
41
+ control on its way to being switched off.**
42
+
43
+ ## v0.21.0 — the risk one tool cannot show you, and the money an iteration refund does not cover
44
+
45
+ Two findings, both of them about a rule that is right on one axis and silently assumed to
46
+ cover a second.
47
+
48
+ **Tool annotations are a risk vocabulary, and their defaults are asymmetric on purpose.**
49
+ `readOnlyHint` **false**, `destructiveHint` **true**, `idempotentHint` **false**,
50
+ `openWorldHint` **true** — so a server author who omits annotations entirely has declared
51
+ the *most dangerous* shape. That is the correct fail-closed choice and the opposite of what
52
+ most authors think they are doing: annotation **narrows** an assumption, silence widens it.
53
+ And every one is a hint rather than a contract — the specification says a client must treat
54
+ them as untrusted unless the server is, so an annotation may inform a UI or a policy default
55
+ and may never be the thing that decides whether a destructive call runs.
56
+
57
+ **The lethal trifecta now has a name here.** Private data, untrusted content, and the
58
+ ability to communicate externally: any two are safe, all three in one session are an
59
+ exfiltration path that no prompt-level instruction reliably closes. It lands in
60
+ `agent-harness/references/tools.md` rather than in a permission section for a structural
61
+ reason — **it is a property of the tool set assembled in a session, so per-tool analysis
62
+ cannot see it by construction.** Every tool can pass its own review and the combination
63
+ still be unsafe; adding a `fetch(url)` beside a private-data reader closes the triangle and
64
+ will not look like a security change. `agent-interop/references/gateway.md` gains the
65
+ composer's half: a gateway's whole value is assembling many servers into one surface, which
66
+ is exactly the operation that can create a risk none of its inputs had, so **granting a role
67
+ one more server is a trifecta question, not only a least-privilege one.**
68
+
69
+ **Refund the iteration, charge the money.** Our loop refunds an iteration on a recoverable
70
+ provider error — a 502 should not consume one of the ten attempts. That is right, and it
71
+ says nothing about money: the provider still billed the call. A response that arrived and
72
+ then failed to parse was generated, metered and charged. The observed failure is precise —
73
+ a harness charged its cost on the **success path** of `query()`, so a format error meant the
74
+ cost was never added, and the code compensated inside the exception handler. Where a cost
75
+ ceiling is the *primary* bound (that harness ships `cost_limit = 3.0` with the step limit
76
+ **off**), a leak there is a leak in the only guard. Accounting belongs in a `finally`, and
77
+ **a budget that under-counts is worse than one that over-counts**: over-counting stops a run
78
+ early and visibly, under-counting is invisible until the invoice and biases toward the
79
+ failing runs, which are the expensive ones.
80
+
1
81
  ## v0.20.0 — what a trajectory cannot carry across a vendor, and where the capability goes
2
82
 
3
83
  Three findings from the harvest, all landing on the same question: **which model does what,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-stack",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "scripts": {
5
5
  "test": "python3 test/validate.py && python3 test/plant_guard_test.py && node test/installer_test.js"
6
6
  },
@@ -3,7 +3,7 @@
3
3
  "name": "agent-stack",
4
4
  "displayName": "Agent Stack",
5
5
  "description": "Four skills: agent-orchestrator — tool-calling loops, pipelines with checkpoints, provider routing with fallback, memory architecture, plus the wallet side of reselling LLM access; agent-evals — run/trace/thread evals, LLM judges, and fixtures grown from production; agent-interop — MCP servers and clients, A2A agent cards, the MCP Registry, and gateways; agent-harness — system prompts, tool shaping, workflow-vs-agent, and auditing an agent system.",
6
- "version": "0.20.0",
6
+ "version": "0.22.0",
7
7
  "author": {
8
8
  "name": "ssheleg",
9
9
  "url": "https://x.com/sshlg93"
@@ -126,6 +126,48 @@ the mistake cannot be made**, rather than documenting the mistake.
126
126
  - A required `confirm: true` on a destructive action, so a partially-formed call fails
127
127
  closed.
128
128
 
129
+ ## Annotations, and the risk one tool cannot show you
130
+
131
+ MCP tools carry four hints, and their defaults are asymmetric on purpose:
132
+
133
+ | Hint | Default | So an unannotated tool is assumed to be |
134
+ |---|---|---|
135
+ | `readOnlyHint` | `false` | one that writes |
136
+ | `destructiveHint` | `true` | destructive |
137
+ | `idempotentHint` | `false` | unsafe to repeat |
138
+ | `openWorldHint` | `true` | reaching outside your system |
139
+
140
+ **A server author who omits annotations entirely has declared the most dangerous shape**,
141
+ which is the correct fail-closed choice and the opposite of what most authors assume they
142
+ are doing. Annotate to *narrow* the assumption; silence widens it.
143
+
144
+ **Every one is a hint, not a contract.** The specification is explicit that a client must
145
+ treat descriptions and annotations as untrusted unless the server itself is trusted — so an
146
+ annotation informs a UI and a policy default, and may never be the thing that decides
147
+ whether a destructive call runs.
148
+
149
+ ### The lethal trifecta — a property of the session, not of a tool
150
+
151
+ Three capabilities that are individually ordinary and jointly an exfiltration path:
152
+
153
+ 1. access to **private data**,
154
+ 2. exposure to **untrusted content**,
155
+ 3. the ability to **communicate externally**.
156
+
157
+ Any two are safe. All three in one session mean untrusted content can instruct the agent to
158
+ read private data and send it out, and no prompt-level instruction reliably prevents it.
159
+
160
+ **The reason it belongs here rather than in a permission check:** the trifecta is a property
161
+ of *the tool set assembled in a session*, so **per-tool analysis cannot see it**. Every tool
162
+ can pass its own review and the combination still be unsafe — which is why a review that
163
+ walks a server's tools one at a time answers a different question than "what can this
164
+ session do". Look at the set, and at what a gateway composes into it.
165
+
166
+ **The practical consequence for a tool author:** a tool that only reads private data is
167
+ fine; adding a `fetch(url)` beside it is what closes the triangle, and it will not look like
168
+ a security change in review.
169
+
170
+
129
171
  ## Evaluating tools
130
172
 
131
173
  Tools deserve **thorough documentation and testing**, and testing means running the agent
@@ -150,5 +192,7 @@ Fix the interface, not the prompt, when the fault is in this table.
150
192
  can do nothing with.
151
193
  - **Treating tool output as trusted.** It is attacker-controlled input if the server is; the
152
194
  specification says descriptions and annotations are untrusted unless the server is.
195
+ - **Reviewing tools one at a time.** The lethal trifecta is a property of the assembled set;
196
+ a per-tool review cannot see it by construction.
153
197
  - **Adding a tool to fix a prompt problem.** The set grows, selection degrades, and the
154
198
  original defect is still there.
@@ -147,6 +147,25 @@ authorization** — the last being the per-tool control that a generic gateway l
147
147
  That is the strongest single argument for it in a cluster that already runs Gateway API: the
148
148
  routing objects are ones your platform team already reviews.
149
149
 
150
+ ## The gateway is where the lethal trifecta gets assembled
151
+
152
+ A gateway's whole value is composing many servers into one surface, and that is exactly the
153
+ operation that can create a risk none of its inputs had. The **lethal trifecta** — private
154
+ data, untrusted content, and the ability to communicate externally — is a property of the
155
+ **tool set in a session**, so every upstream can pass its own review and the composition
156
+ still be unsafe.
157
+
158
+ Two consequences for whoever runs the gateway:
159
+
160
+ - **A role is a trifecta decision, not only a least-privilege one.** Granting a role one
161
+ more server is the moment to ask which of the three corners it just completed — not
162
+ whether that server is individually trustworthy.
163
+ - **Per-tool annotations do not answer it.** They are hints about one tool (`readOnlyHint`,
164
+ `destructiveHint`, `idempotentHint`, `openWorldHint`, and their defaults assume the
165
+ dangerous shape — `agent-harness/references/tools.md`), and no combination of per-tool
166
+ hints computes a session-level property.
167
+
168
+
150
169
  ## Traps
151
170
 
152
171
  - **Introducing a gateway and leaving the direct paths open.** The policy is then advisory.
@@ -86,7 +86,8 @@ was gathered rather than returning nothing. The full listing is in
86
86
  - **In-loop trimming**: At ~80% capacity, collapse older assistant+tool pairs into one-liner summaries
87
87
  - **Token limit recovery**: On `LLMTokenLimitError`, compress to 60% and retry once. If still fails, return partial answer
88
88
  - **Max iterations guard**: Always have a hard limit. On exhaustion, compose best-effort answer from data gathered so far
89
- - **Iteration refund**: a recoverable provider error is not charged to that guard
89
+ - **Iteration refund**: a recoverable provider error is not charged to that guard — the
90
+ money still is, and accounting on the success path alone under-counts the worst runs
90
91
  - **Budget awareness**: tell the model what is left, or 300 steps performs like 30
91
92
 
92
93
  ---
@@ -36,6 +36,45 @@ guardrails" so often means only the first.
36
36
  The last one is the one most designs miss: a sub-agent that inherits its caller's
37
37
  authority silently widens every permission the caller had.
38
38
 
39
+ ## The cheapest control is absence
40
+
41
+ The **Tool call** row above puts per-tool authorisation at the moment of invocation. There
42
+ is a control one layer earlier and it is strictly stronger:
43
+
44
+ > **The model cannot reason about capabilities it does not know exist.**
45
+
46
+ A tool absent from the schema cannot be invoked, cannot be argued for, and cannot be probed
47
+ for a bypass — there is nothing to jailbreak toward. A tool present in the schema and
48
+ refused at call time is a negotiation, and negotiations are won sometimes.
49
+
50
+ So **filter the schema at build time**, and treat runtime authorisation as the second line
51
+ rather than the first. Sub-agent isolation comes from exactly two mechanisms used together:
52
+ schema filtering when the agent is constructed, and no inherited conversation
53
+ (`message_history = None`) when it runs. The second matters as much as the first — a
54
+ sub-agent handed its parent's transcript has been told about every capability you carefully
55
+ removed from its schema.
56
+
57
+ **Two numbers, and they point in opposite directions on purpose.** Eagerly loading every
58
+ MCP tool schema at startup consumed **40% of the context budget before the first user
59
+ message**; a metadata index at startup with the full schema fetched on selection takes it
60
+ **under 5%**. But the same system builds *its own* prompt and tool schemas **eagerly**, in
61
+ the constructor. The rule underneath is not *lazy is better*: it is **eager for what you
62
+ own and always need, lazy for what is foreign and might not be used** — the first removes
63
+ latency and race conditions from the hot path, the second removes a cost you cannot predict.
64
+
65
+ ### Approval fatigue is a safety failure, not a UX complaint
66
+
67
+ > Without persistence, users must re-approve the same operations every session, causing
68
+ > approval fatigue that leads to **blanket auto-approval, defeating the safety system
69
+ > entirely**.
70
+
71
+ An approval system with no memory converts itself into no approval system, and it does so
72
+ through the user rather than through a bug — so nothing in the logs looks wrong. The
73
+ remedy is on the same axis as the section above: **decide once what does not need asking,
74
+ remove it from the question, and spend the prompts on what genuinely changes.** A control
75
+ that is asked too often is a control on its way to being switched off.
76
+
77
+
39
78
  ## Guardrails, and their honest limit
40
79
 
41
80
  The content-layer checks worth having, roughly in order of reliability:
@@ -28,6 +28,7 @@ well-measured answer to the wrong question.
28
28
  - [9. What Claude Code actually executes](#9-what-claude-code-actually-executes)
29
29
  - [10. Barrier or no barrier](#10-barrier-or-no-barrier)
30
30
  - [11. Project defaults, written once](#11-project-defaults-written-once)
31
+ - [11a. MAST — the failures, named from outside](#11a-mast--the-failures-named-from-outside)
31
32
  - [12. The source's four diagrams, and what each one is for](#12-the-sources-four-diagrams-and-what-each-one-is-for)
32
33
  - [Where this file disagrees with its source](#where-this-file-disagrees-with-its-source)
33
34
 
@@ -375,6 +376,32 @@ The last line is `context-engineering.md`'s *filesystem as context* stated as a
375
376
  rule: an edge that carries a path costs a few tokens, and an edge that carries a
376
377
  transcript costs the window.
377
378
 
379
+ ## 11a. MAST — the failures, named from outside
380
+
381
+ Everything above is argued from what breaks. *Why Do Multi-Agent LLM Systems Fail?* (2025)
382
+ measured it: execution traces from seven mainstream frameworks — MetaGPT, ChatDev, AG2 and
383
+ Magentic-One among them — with human annotators independently analysing roughly **150
384
+ traces** at **Cohen's kappa = 0.88**, producing **14 failure modes in three groups**.
385
+
386
+ The three groups land on this file's own node contract, which is the reason to carry them:
387
+
388
+ | MAST group | Modes include | The field it lands on |
389
+ |---|---|---|
390
+ | **System design flaws** | unclear interfaces between agents, overlapping roles, wrong tool configuration | `input` / `output` (the interface), and `owner` (overlapping roles) |
391
+ | **Inter-agent alignment failures** | inconsistent understanding of the objective, downstream misinterpretation, logically contradictory operations | the **edge payload** — what actually crosses, §3's *Carries* column |
392
+ | **Missing task verification** | an agent reports *completed* and the result does not meet the requirement | `check`, and §6's checker node |
393
+
394
+ **The load-bearing result is the negative one.** Straightforward fixes — better prompts,
395
+ more explicit role descriptions, retries — bought ChatDev only **15.6%**. The authors
396
+ conclude the modes are **architectural rather than bugs**, which is the same claim §1 makes
397
+ about the two fields nobody draws: an owner and a completion test are not documentation of
398
+ a graph, they are the parts that make it a graph.
399
+
400
+ Read the mapping as a review checklist rather than a taxonomy to admire: for each node, is
401
+ the interface stated, is there exactly one owner, and does the *check* run on something
402
+ other than the node's own claim of success.
403
+
404
+
378
405
  ## 12. The source's four diagrams, and what each one is for
379
406
 
380
407
  The article carries four hand-drawn figures. They are not decoration — each one is doing
@@ -24,6 +24,7 @@ the patterns hold for any upstream that issues per-tenant keys with limits.
24
24
  - [Discovering spend you do not control](#discovering-spend-you-do-not-control)
25
25
  - [Guardrails: budgets, loops, auto-pause](#guardrails-budgets-loops-auto-pause)
26
26
  - [Key lifecycle and healing](#key-lifecycle-and-healing)
27
+ - [Refund the iteration, charge the money](#refund-the-iteration-charge-the-money)
27
28
  - [The refund waterfall](#the-refund-waterfall)
28
29
  - [Model routing and fallbacks](#model-routing-and-fallbacks)
29
30
 
@@ -226,6 +227,41 @@ handled three different ways.
226
227
 
227
228
  ---
228
229
 
230
+ ## Refund the iteration, charge the money
231
+
232
+ These are two axes and it is easy to ship one rule for both.
233
+
234
+ `agent-orchestrator`'s loop **refunds the iteration** on a recoverable provider error: a
235
+ 502 should not consume one of the ten attempts the agent has to finish its work. That is
236
+ right, and it says nothing about money.
237
+
238
+ **The provider still billed the call.** A response that arrived and then failed to parse
239
+ was generated, metered and charged upstream; so was the one that arrived truncated, and the
240
+ one whose tool call was malformed. The tempting symmetry — *the attempt did not count, so
241
+ it did not cost* — is how a spend guard under-counts on exactly the runs that go worst.
242
+
243
+ The failure mode is specific and worth naming, because it hides where nobody looks:
244
+
245
+ > A harness charged `self.cost` on the **success path** of its `query()`. When parsing the
246
+ > response raised a format error, `query()` never returned, so the cost was never added —
247
+ > and the code compensated for it explicitly inside the exception handler. Where a cost
248
+ > ceiling is the *primary* bound — that harness ships `cost_limit = 3.0` with the step
249
+ > limit **off** — a leak in that accounting is a leak in the only guard there is.
250
+
251
+ The rule, in one line each:
252
+
253
+ - **Iteration:** refunded on a recoverable provider error, never on a misconfiguration.
254
+ - **Money:** charged whenever the provider generated tokens, including on every path that
255
+ raises after the response arrived.
256
+ - **Therefore:** accounting belongs in a `finally`, or in the exception handler as well as
257
+ the success path. If the only place your cost is added is the line after a successful
258
+ parse, the guard is quietly optimistic.
259
+
260
+ A budget that under-counts is worse than one that over-counts: over-counting stops a run
261
+ early and is visible immediately; under-counting is invisible until the invoice, and it
262
+ biases toward the failing runs, which are the expensive ones.
263
+
264
+
229
265
  ## The refund waterfall
230
266
 
231
267
  A payment refund has to come out of somewhere, and the money has usually moved.