@ssheleg/agent-stack 0.23.1 → 0.24.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 +42 -0
- package/package.json +4 -3
- package/plugins/agent-stack/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-stack/skills/agent-evals/SKILL.md +31 -14
- package/plugins/agent-stack/skills/agent-evals/references/otel-genai.md +60 -19
- package/plugins/agent-stack/skills/agent-evals/references/statistics.md +78 -9
- package/plugins/agent-stack/skills/agent-harness/SKILL.md +25 -14
- package/plugins/agent-stack/skills/agent-harness/references/audit.md +25 -3
- package/plugins/agent-stack/skills/agent-harness/references/tools.md +12 -3
- package/plugins/agent-stack/skills/agent-interop/SKILL.md +25 -6
- package/plugins/agent-stack/skills/agent-interop/references/mcp-scale.md +2 -2
- package/plugins/agent-stack/skills/agent-interop/references/mcp-ship.md +36 -0
- package/plugins/agent-stack/skills/agent-interop/references/mcp.md +5 -1
- package/plugins/agent-stack/skills/agent-orchestrator/SKILL.md +13 -8
- package/plugins/agent-stack/skills/agent-orchestrator/references/graph-engineering.md +51 -20
- package/plugins/agent-stack/skills/agent-orchestrator/references/llm-proxy-billing.md +87 -25
- package/plugins/agent-stack/skills/agent-orchestrator/references/memory-architecture.md +6 -4
- package/plugins/agent-stack/skills/agent-orchestrator/references/memory-lifecycle.md +36 -4
- package/plugins/agent-stack/skills/agent-orchestrator/references/patterns.md +81 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
## 0.24.0 — the agent stack closes its audit findings
|
|
2
|
+
|
|
3
|
+
Sherlock external-v3 (24 findings), each carrying its own executable regression
|
|
4
|
+
under `test/audit_regressions/`.
|
|
5
|
+
|
|
6
|
+
CI now MEASURES the token budget with a real tokenizer instead of estimating it
|
|
7
|
+
from character count — the pinned auditor had been issuing a token verdict from a
|
|
8
|
+
chars/3.9 estimate, which is the defect that script's own doctrine names.
|
|
9
|
+
|
|
10
|
+
## v0.23.2 — a path that resolves only from the neighbour's directory, and the description with no room left
|
|
11
|
+
|
|
12
|
+
Family audit 2026-09-06 (wave AUDIT-WAVE-0906) brought three findings for this member.
|
|
13
|
+
Two changed files; the third changed a board instead.
|
|
14
|
+
|
|
15
|
+
**Two references pointed at `references/kv-cache.md` from directories where it does not
|
|
16
|
+
exist.** `agent-evals/references/otel-genai.md:137` and
|
|
17
|
+
`agent-interop/references/mcp-scale.md:92` both named the bare path; the file lives only
|
|
18
|
+
in sibling `agent-orchestrator`. The prose around each named the sibling, so a reader
|
|
19
|
+
could recover the target — but the path as written resolves to nothing from either
|
|
20
|
+
skill's own root, and a bare `references/x.md` means *this skill's file* by the
|
|
21
|
+
validator's own rule (`test/validate.py:263-267`). The check never saw them because it
|
|
22
|
+
reads SKILL.md alone; these sat in reference files nothing scans. Both now read
|
|
23
|
+
`../agent-orchestrator/references/kv-cache.md`, which resolves from each skill's root —
|
|
24
|
+
and the leading `../` keeps the validator's lookbehind from reading it as a claim on a
|
|
25
|
+
local file.
|
|
26
|
+
|
|
27
|
+
**`agent-interop`'s description sat at exactly 970/970 — the working limit with zero
|
|
28
|
+
headroom** — so the next trigger, the next boundary clause, any edit at all would have
|
|
29
|
+
fought the gate before doing its work. Eleven characters of connective prose trimmed
|
|
30
|
+
("another agent" → "an agent"; two list-internal "and"s → commas): now **959/970**, with
|
|
31
|
+
every quoted trigger phrase untouched — **14 before, 14 after**, English and Russian,
|
|
32
|
+
counted by re-folding the YAML scalar rather than by eyeballing the diff.
|
|
33
|
+
|
|
34
|
+
**The audit also asked for a debulk of `agent-orchestrator` and `agent-evals`, and that
|
|
35
|
+
was declined rather than done blind.** Re-measured at this commit: **4655/4750** (98.0%)
|
|
36
|
+
and **4631/4750** (97.5%), both `0 GAP`. The family precedent (B-141 class) is that
|
|
37
|
+
manufacturing headroom with no pending edit is negative-value work: room bought today is
|
|
38
|
+
spent by nobody, and a trim chosen without the edit it serves cuts on the wrong line.
|
|
39
|
+
Filed as board row **B-135** — the debulk belongs to the release that next edits these
|
|
40
|
+
bodies, beside B-129's standing record that the durable remedy for `agent-orchestrator`
|
|
41
|
+
is a split, not a trim.
|
|
42
|
+
|
|
1
43
|
## v0.23.1 — the registry card stopped being ten releases stale
|
|
2
44
|
|
|
3
45
|
`SKILL-CARD.md` carries the fields Anthropic's Skills-for-enterprise guidance asks every
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/agent-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"scripts": {
|
|
5
|
-
"test": "python3 test/validate.py && python3 test/plant_guard_test.py && node test/installer_test.js"
|
|
5
|
+
"test": "python3 test/validate.py && python3 test/plant_guard_test.py && node test/installer_test.js && npm run test:audit",
|
|
6
|
+
"test:audit": "for t in test/audit_regressions/*.py; do python3 \"$t\" || exit 1; done"
|
|
6
7
|
},
|
|
7
|
-
"description": "Production patterns for AI agent orchestrators
|
|
8
|
+
"description": "Production patterns for AI agent orchestrators \u2014 tool-calling loops, multi-stage pipelines with checkpoints, LLM provider routing with fallback, four-layer memory with confidence decay \u2014 plus the wallet side of reselling LLM access. This package is the installer CLI.",
|
|
8
9
|
"bin": {
|
|
9
10
|
"agent-stack": "bin/agent-stack.js"
|
|
10
11
|
},
|
|
@@ -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.
|
|
6
|
+
"version": "0.24.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ssheleg",
|
|
9
9
|
"url": "https://x.com/sshlg93"
|
|
@@ -76,7 +76,7 @@ Assert on three axes at once, with three different mechanisms:
|
|
|
76
76
|
|
|
77
77
|
| Axis | Assert | With |
|
|
78
78
|
|---|---|---|
|
|
79
|
-
| Trajectory | what the run **must not** do,
|
|
79
|
+
| Trajectory | what the run **must not** do, what it must have touched, and the mandatory **happens-before** edges — never the full exact order | set/subset matchers, forbidden-call lists, partial-order (a before b) |
|
|
80
80
|
| Final response | quality, tone, policy compliance | rubric or judge |
|
|
81
81
|
| **State change** | the memory row exists, the file was written, the artifact is there | direct inspection of the side effect |
|
|
82
82
|
|
|
@@ -152,19 +152,26 @@ assertions, tool-call correctness — all deterministic, all faster and cheaper
|
|
|
152
152
|
call. Send to a judge only what cannot be decided by code.
|
|
153
153
|
|
|
154
154
|
**Read the trajectory; do not match it.** An agent that reaches a correct answer through
|
|
155
|
-
three wrong tool calls is a latent outage — and asserting the *sequence* to catch
|
|
156
|
-
measurably the wrong instrument. Anthropic
|
|
157
|
-
|
|
158
|
-
regularly find valid approaches that eval designers didn't anticipate"
|
|
155
|
+
three wrong tool calls is a latent outage — and asserting the *exact sequence* to catch
|
|
156
|
+
that is measurably the wrong instrument. Anthropic rejects its strict form: exact
|
|
157
|
+
tool-order assertions are *"too rigid and results in overly brittle tests, as agents
|
|
158
|
+
regularly find valid approaches that eval designers didn't anticipate"* — the worked
|
|
159
159
|
case is an agent that solved a τ²-bench booking task through a policy loophole, failing
|
|
160
160
|
the eval as written while serving the user better. Grade **what was produced and what
|
|
161
|
-
changed**, and let the path vary.
|
|
161
|
+
changed**, and let the incidental path vary.
|
|
162
162
|
|
|
163
|
-
The opposite edge is measured too
|
|
164
|
-
to the trajectory misses **44% of safety violations and 13% of robustness failures**,
|
|
163
|
+
The opposite edge is measured too: a grader blind to the trajectory misses **44% of safety violations and 13% of robustness failures**,
|
|
165
164
|
because a policy breach on the way to a correct result leaves no trace in the outcome. Use
|
|
166
165
|
the trajectory for the claims the outcome cannot carry — a forbidden call, a missing
|
|
167
|
-
confirmation, a secret read — as a **set and a forbidden list
|
|
166
|
+
confirmation, a secret read — as a **set and a forbidden list**.
|
|
167
|
+
|
|
168
|
+
What is forbidden is the redundant **exact global sequence**, not order as such.
|
|
169
|
+
A few **happens-before** edges are semantically mandatory: authorization
|
|
170
|
+
precedes its effect, a fresh read precedes the write depending on it, a
|
|
171
|
+
transaction commits before what publishes it. Assert those as a **partial
|
|
172
|
+
order** (a before b), never a total one — reordering two independent reads must
|
|
173
|
+
pass, reordering confirm/charge or acquire/write must fail — and keep the
|
|
174
|
+
negative example (a confirm-after-charge trace) beside the rubric.
|
|
168
175
|
|
|
169
176
|
**Calibrate the judge before trusting it.** Collect human labels on the same traces,
|
|
170
177
|
measure agreement, iterate the judge prompt until agreement is high — *then* let it score
|
|
@@ -253,10 +260,16 @@ with no production in it is imagination.** The requirement itself gets its id an
|
|
|
253
260
|
definition of done from `task-pipeline`'s REQ spine — what this pack owns is the
|
|
254
261
|
observable's *form*, not the register it hangs on.
|
|
255
262
|
|
|
256
|
-
**The first release has no production
|
|
257
|
-
is not
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
**The first release has no production — so it runs against a SEED corpus, and
|
|
264
|
+
observable-only is not release-ready.** A criterion with no input proves
|
|
265
|
+
no capability, so a greenfield feature seeds a curated/synthetic/manual corpus with at least a **happy**, an **adversarial** and a
|
|
266
|
+
**failure/retry** trial. Each seed input carries its **provenance**
|
|
267
|
+
(`curated`/`synthetic`/`manual`) and is SUPPLEMENTED by production traces,
|
|
268
|
+
never declared full coverage. The release gate requires EXECUTED trials;
|
|
269
|
+
observables with nothing run against them are `specification-ready`, not
|
|
270
|
+
`release-ready`. A corrupted fixture, input or runner is a `TEST_ERROR`,
|
|
271
|
+
never a behaviour pass/fail; cases are isolated, so B's result never depends
|
|
272
|
+
on whether A ran.
|
|
260
273
|
|
|
261
274
|
**Never author the suite up front** — the *corpus*, that is: the inputs. Every production
|
|
262
275
|
failure and every thumbs-down becomes a fixture:
|
|
@@ -300,7 +313,11 @@ None of the above runs without these, and they are the part people skip:
|
|
|
300
313
|
of `human` | `llm_judge` | `code_check`. A score with no source cannot be calibrated,
|
|
301
314
|
audited, or trusted differently from its neighbours.
|
|
302
315
|
- **Whole prompts, not just messages** — instructions, tool schemas and context as they
|
|
303
|
-
were sent. A fixture cannot be replayed from a summary.
|
|
316
|
+
were sent. A fixture cannot be replayed from a summary. And a CANDIDATE's
|
|
317
|
+
version, output and score are their own records beside the old trace — a
|
|
318
|
+
regrade of the stored output is labelled regrade, never "the candidate
|
|
319
|
+
passed": only executing the candidate against the frozen fixture (a real,
|
|
320
|
+
stochastic call, costed in the receipt) can say the decision changed.
|
|
304
321
|
- **State snapshots at turn boundaries**, so a thread test can assert what carried.
|
|
305
322
|
|
|
306
323
|
---
|
|
@@ -10,7 +10,7 @@ three places it will not carry what this skill requires.
|
|
|
10
10
|
## Contents
|
|
11
11
|
|
|
12
12
|
- [Read this before encoding any of it](#read-this-before-encoding-any-of-it)
|
|
13
|
-
- [Span names are formulas, and the operation
|
|
13
|
+
- [Span names are formulas, and the operation set is well-known but extensible](#span-names-are-formulas-and-the-operation-set-is-well-known-but-extensible)
|
|
14
14
|
- [The evaluation event, missing the field §7 requires](#the-evaluation-event-missing-the-field-7-requires)
|
|
15
15
|
- [Content: three tiers, and a hook that runs when nothing else does](#content-three-tiers-and-a-hook-that-runs-when-nothing-else-does)
|
|
16
16
|
- [Tokens are eleven numbers and money is none of them](#tokens-are-eleven-numbers-and-money-is-none-of-them)
|
|
@@ -31,11 +31,14 @@ inference span the only attributes marked `Stable` are the ones borrowed from co
|
|
|
31
31
|
attribute is `Development`.**
|
|
32
32
|
|
|
33
33
|
So: adopt it, because a moving standard beats a private vocabulary that will never be read
|
|
34
|
-
by anyone else's tooling — and **pin the version you adopted and expect to migrate**.
|
|
34
|
+
by anyone else's tooling — and **pin the version you adopted and expect to migrate**. When
|
|
35
|
+
you encode any field from this file, record the **semconv schema revision and the commit SHA
|
|
36
|
+
you read it at, plus the observation date** beside your instrumentation — a `gen_ai.*` field
|
|
37
|
+
quoted with no revision is a field with no expiry, and this whole spec is `Development`. Treat
|
|
35
38
|
any code that branches on a `gen_ai.*` attribute as code with an expiry date, and re-read
|
|
36
39
|
the spec before quoting a field name from this file.
|
|
37
40
|
|
|
38
|
-
## Span names are formulas, and the operation
|
|
41
|
+
## Span names are formulas, and the operation set is well-known but extensible
|
|
39
42
|
|
|
40
43
|
Span names are computed, not free text:
|
|
41
44
|
|
|
@@ -48,10 +51,15 @@ Span names are computed, not free text:
|
|
|
48
51
|
| agent creation | `create_agent {gen_ai.agent.name}` | — |
|
|
49
52
|
| MCP | `{mcp.method.name} {target}`, target being the tool or prompt name | — |
|
|
50
53
|
|
|
51
|
-
`gen_ai.operation.name` is a **closed
|
|
52
|
-
`
|
|
53
|
-
`create_agent`, `invoke_agent`, `plan` and the
|
|
54
|
-
|
|
54
|
+
`gen_ai.operation.name` is a **well-known SET, not a closed enum** — the semconv
|
|
55
|
+
lists `chat`, `text_completion`, `generate_content`, `embeddings`, `retrieval`,
|
|
56
|
+
`fetch_response`, `execute_tool`, `create_agent`, `invoke_agent`, `plan` and the
|
|
57
|
+
rest, and a well-known value is preferred WHERE ONE FITS. But a provider
|
|
58
|
+
operation with no matching well-known value is allowed to carry a custom value:
|
|
59
|
+
it is not silently dropped, and an unknown value is stored RAW so a later
|
|
60
|
+
schema revision can recognise it. A backend groups the well-known values and
|
|
61
|
+
keeps the raw ones addressable — losing them is the failure, not carrying
|
|
62
|
+
them.
|
|
55
63
|
|
|
56
64
|
**Only two attributes are Required on an inference span.** Everything else that matters —
|
|
57
65
|
the model that actually answered, token counts, finish reasons — is Recommended or
|
|
@@ -127,17 +135,43 @@ leaks through the one field it never inspected. Decide naming and redaction toge
|
|
|
127
135
|
standardises tokens and never money, so cost is always a join against a price table living
|
|
128
136
|
outside the trace — and that join is where the number goes wrong.
|
|
129
137
|
|
|
130
|
-
Because usage is not one number
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
Because usage is not one number, and the counters are of TWO kinds — TOTALS and
|
|
139
|
+
DISJOINT BILLING BUCKETS, and confusing them double-counts. `input_tokens` and
|
|
140
|
+
`output_tokens` are the totals; `reasoning.output_tokens` is a SUBSET of
|
|
141
|
+
`output_tokens` (not an addition to it), and `cache_read.input_tokens` /
|
|
142
|
+
`cache_write.input_tokens` are subsets of `input_tokens`. The per-modality
|
|
143
|
+
`text.*` / `image.*` / `audio.*` splits (including `image.cache_read.input_tokens`)
|
|
144
|
+
partition those same totals by modality — they are not extra tokens either.
|
|
145
|
+
|
|
146
|
+
**A cost computed from `input_tokens + output_tokens` alone is wrong** because it
|
|
147
|
+
prices the cached portion of the input at the full input rate — cache reads are
|
|
148
|
+
the cheap ones. The fix is NOT to add reasoning or modality counters back onto
|
|
149
|
+
the totals (that bills them twice): it is to **SUBTRACT the cached portion from
|
|
150
|
+
the total and apply the provider's cache-read (and cache-write) rate to it**,
|
|
151
|
+
pricing the remaining full-rate input and the output at their own rates. `../agent-orchestrator/references/kv-cache.md` is the other half of
|
|
138
152
|
this: the cache read is the case worth getting right, because at scale it is most of the
|
|
139
153
|
traffic.
|
|
140
154
|
|
|
155
|
+
**The worked receipt — a disjoint partition that reconciles.** Say `input_tokens
|
|
156
|
+
= 1000` with `cache_read.input_tokens = 800`, and `output_tokens = 200` with
|
|
157
|
+
`reasoning.output_tokens = 120`, at rates `$3 / $0.30 / $15` per 1k for
|
|
158
|
+
full-input / cache-read / output:
|
|
159
|
+
|
|
160
|
+
| Bucket | Tokens | Rate /1k | Cost |
|
|
161
|
+
|---|---|---|---|
|
|
162
|
+
| input, full-rate = `input − cache_read` | 200 | $3.00 | $0.60 |
|
|
163
|
+
| `cache_read` (a subset of input) | 800 | $0.30 | $0.24 |
|
|
164
|
+
| output (reasoning is a SUBSET, not added) | 200 | $15.00 | $3.00 |
|
|
165
|
+
| **total** | | | **$3.84** |
|
|
166
|
+
|
|
167
|
+
Two invariants a receipt MUST satisfy, and an independent example checks: the
|
|
168
|
+
priced token buckets **sum back to the totals** — 200 + 800 = 1000 input, and
|
|
169
|
+
output is 200 (reasoning's 120 is inside it, never a fourth line) — so no token
|
|
170
|
+
is charged twice; and the naive `input + output` at the input/output rates
|
|
171
|
+
($3.00 + $3.00 = $6.00) OVER-charges by pricing the 800 cached tokens at $3
|
|
172
|
+
instead of $0.30. The receipt reconciles with the totals; the naive number does
|
|
173
|
+
not.
|
|
174
|
+
|
|
141
175
|
And `gen_ai.client.token.usage` carries a hard **MUST NOT report** when the counts are not
|
|
142
176
|
obtainable. A zero is a claim; absence is the honest value. That is the same rule
|
|
143
177
|
`agent-harness/references/audit.md` states for cost attribution — *missing attribution beats
|
|
@@ -171,7 +205,14 @@ which:
|
|
|
171
205
|
|---|---|---|---|
|
|
172
206
|
| **Durable execution** (Temporal) | nothing — recorded results are replayed and only the failed step retries | free, deterministic | can I resume without redoing 20 web searches |
|
|
173
207
|
| **Trace playground** (Phoenix) | the model call, against the live provider, with an edited prompt | a real call | would a different prompt have done better |
|
|
174
|
-
| **
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
208
|
+
| **Regrade of a stored output** (this skill, §2 single-step) | nothing — a new assertion over the OLD output | free, deterministic | does the OLD output satisfy the changed rubric — **explicitly NOT a check of the candidate** |
|
|
209
|
+
| **Candidate execution over a frozen fixture** | the CANDIDATE (new prompt / model / tool schema) against frozen inputs | a real model call, stochastic — say so in the receipt | did the candidate's decision change |
|
|
210
|
+
|
|
211
|
+
They are not interchangeable, and a runbook that says *"replay the run"* has not said what
|
|
212
|
+
it means. Name the sense. The one this table exists to un-blur: **an old output does not
|
|
213
|
+
change when the candidate changes** — a regrade re-marks yesterday's homework under a new
|
|
214
|
+
rubric, and calling it "did the decision change" claims a candidate check that never ran.
|
|
215
|
+
The test with teeth is the fourth row: mutate the candidate to a knowingly wrong tool, and
|
|
216
|
+
the gate's result MUST change; a gate a candidate mutation cannot move is a regrade wearing
|
|
217
|
+
the wrong label. The candidate's version, its output and its score are stored as their own
|
|
218
|
+
records, never overwriting the old trace they are compared against.
|
|
@@ -38,9 +38,32 @@ The 95% band is roughly `±1.96 · SE`. Computed, not quoted:
|
|
|
38
38
|
|
|
39
39
|
```python
|
|
40
40
|
import math
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
def band(p, n):
|
|
43
|
+
"""Wald approximation, percentage points. Valid only for moderate n with p
|
|
44
|
+
away from the boundary (rule of thumb: n*p >= 10 and n*(1-p) >= 10). At
|
|
45
|
+
p=0 or p=1 it returns ZERO width — which is exactly wrong: a run that has
|
|
46
|
+
never failed is not a run with no uncertainty."""
|
|
47
|
+
return 1.96 * math.sqrt(p * (1 - p) / n) * 100
|
|
48
|
+
|
|
49
|
+
def wilson(p, n, z=1.96):
|
|
50
|
+
"""Wilson score interval — the DEFAULT for a proportion. Nonzero width at
|
|
51
|
+
the boundary, honest at small n; use exact (Clopper–Pearson) when n is
|
|
52
|
+
tiny and the decision is expensive. n == 0 is total uncertainty (0, 1),
|
|
53
|
+
never a zero-width claim."""
|
|
54
|
+
if n == 0:
|
|
55
|
+
return (0.0, 1.0)
|
|
56
|
+
denom = 1 + z * z / n
|
|
57
|
+
centre = (p + z * z / (2 * n)) / denom
|
|
58
|
+
half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
|
|
59
|
+
return (max(0.0, centre - half), min(1.0, centre + half))
|
|
42
60
|
```
|
|
43
61
|
|
|
62
|
+
The table above is Wald and inherits its limits; **a zero or tiny sample is not
|
|
63
|
+
a universal bound.** `wilson(0.0, 5)` spans up to ≈43% — five clean runs still
|
|
64
|
+
leave nearly half the range open — where Wald would print ±0.0 and read as
|
|
65
|
+
certainty.
|
|
66
|
+
|
|
44
67
|
**So "the new one gets 73% where the old one got 70%, on a hundred cases" is not a
|
|
45
68
|
result.** It is a number inside its own noise. The error shrinks as `1/√n`, which is the
|
|
46
69
|
whole practical consequence: **the fix for a 2–3 pp expected gain is more tasks, not more
|
|
@@ -49,9 +72,12 @@ argument.** Quadrupling the set halves the band.
|
|
|
49
72
|
A corollary worth stating because leaderboards invite the opposite: **differences below
|
|
50
73
|
about 3 pp deserve scepticism until both configurations are documented and matched.**
|
|
51
74
|
|
|
52
|
-
> The formula assumes independent cases. A benchmark whose tasks share a
|
|
53
|
-
> environment or a generator violates that, and the
|
|
54
|
-
>
|
|
75
|
+
> The formula assumes independent (iid) cases. A benchmark whose tasks share a
|
|
76
|
+
> fixture, an environment or a generator violates that, and under the usual
|
|
77
|
+
> assumption — POSITIVE intra-cluster correlation, which is what shared
|
|
78
|
+
> fixtures produce — the true band is wider than this, so treat the table as a
|
|
79
|
+
> floor. That is an assumption, not a theorem: engineered negative dependence
|
|
80
|
+
> can narrow a band, it just never happens by accident in a shared fixture.
|
|
55
81
|
|
|
56
82
|
## pass@k and pass^k are different questions
|
|
57
83
|
|
|
@@ -78,6 +104,13 @@ If a failed attempt leaves a charge, a message or a mutated row behind, `pass@k`
|
|
|
78
104
|
available to you as a metric — you cannot pick the best of five refunds. Sample in a
|
|
79
105
|
sandbox or a rollback-capable environment, and count **every** failure.
|
|
80
106
|
|
|
107
|
+
**Both are computed over TASK-LEVEL trials.** k repeated trials of one task
|
|
108
|
+
estimate that task's own p_i; the benchmark number is the mean over TASKS of
|
|
109
|
+
the per-task pass@k (or pass^k). Pooling repeated trials of one task into the
|
|
110
|
+
denominator as if each were a new task inflates n with copies of the same
|
|
111
|
+
difficulty — the trials are not independent tasks, and counting them as tasks
|
|
112
|
+
is how a small suite pretends to be a large one.
|
|
113
|
+
|
|
81
114
|
**A report that gives k without saying which k it means is unreadable.** *k independent
|
|
82
115
|
samples of one task* and *k consecutive tasks on one live pipeline* are different claims.
|
|
83
116
|
|
|
@@ -103,9 +136,12 @@ Two consequences, and they cut in opposite directions:
|
|
|
103
136
|
|
|
104
137
|
- **You cannot compute `pass^k` from `pass^1`.** Exponentiating a headline rate gives a
|
|
105
138
|
number far below the truth. Measure `pass^k` directly, at the k you care about.
|
|
106
|
-
- **Anthropic's `0.75³ ≈ 42%` is
|
|
107
|
-
|
|
108
|
-
|
|
139
|
+
- **Anthropic's `0.75³ ≈ 42%` is the INDEPENDENCE BASELINE, not a bound.** Real
|
|
140
|
+
curves usually sit above it because successes cluster by task (positive
|
|
141
|
+
dependence), but that is an empirical pattern, not a guarantee — engineered
|
|
142
|
+
negative dependence can fall below it. The right shape for an argument —
|
|
143
|
+
*consistency is a much harder bar* — and the wrong number to put in a release
|
|
144
|
+
gate either way.
|
|
109
145
|
|
|
110
146
|
The other half of independence is the harness, not the task: Anthropic requires each trial
|
|
111
147
|
start from a clean environment, because *"unnecessary shared state between runs (leftover
|
|
@@ -115,8 +151,13 @@ metric mean anything.
|
|
|
115
151
|
|
|
116
152
|
## Pairing: same tasks, same seeds, per-task deltas
|
|
117
153
|
|
|
118
|
-
**
|
|
119
|
-
list with the *same* fixed seeds,
|
|
154
|
+
**Prefer pairing — and never subtract two averages WITHOUT an interval.** Run
|
|
155
|
+
both configurations over the *same* task list with the *same* fixed seeds,
|
|
156
|
+
record a per-task win/loss/tie, and test the deltas: pairing cancels the
|
|
157
|
+
per-task difficulty variance and needs far fewer runs. An UNPAIRED comparison
|
|
158
|
+
of two independent averages is still legitimate when pairing is impossible —
|
|
159
|
+
it just pays for it with the wider two-sample band, and the sin is quoting the
|
|
160
|
+
subtraction bare, as if the band were zero.
|
|
120
161
|
|
|
121
162
|
```
|
|
122
163
|
for task in tasks: # identical list
|
|
@@ -138,6 +179,34 @@ for task in tasks: # identical list
|
|
|
138
179
|
**Ship on three conditions, not one:** the difference exceeds the noise band, it survives
|
|
139
180
|
the paired analysis, and it reproduces on a rerun.
|
|
140
181
|
|
|
182
|
+
## The design decides the method — and the receipt names both
|
|
183
|
+
|
|
184
|
+
Paired, clustered and unpaired are three DIFFERENT corpus structures, and a
|
|
185
|
+
test method borrowed from the wrong one produces confident nonsense. The
|
|
186
|
+
result's receipt names the design AND the method, and they must match:
|
|
187
|
+
|
|
188
|
+
| Corpus structure | Matching method | Mismatch that looks fine and is not |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| **paired** — same tasks, same seeds, per-task deltas | McNemar (binary) or a paired bootstrap over the DELTAS | running McNemar on two independent runs pairs rows that share nothing |
|
|
191
|
+
| **clustered** — k dependent repeats per task | a cluster bootstrap that resamples TASKS (each task carries its repeats along) | bootstrapping TRIALS treats dependent repeats as iid and shrinks the band by ~√k for free |
|
|
192
|
+
| **unpaired** — two independent samples | the two-sample (Welch) SE, wider band | quoting the paired-sized band for an unpaired design |
|
|
193
|
+
|
|
194
|
+
**Dependent repeats are never claimed iid**, and a small sample never buys
|
|
195
|
+
imaginary certainty — the Wilson bounds above are the floor either way.
|
|
196
|
+
|
|
197
|
+
## Splits are spent once — case ids and groupings are FIXED
|
|
198
|
+
|
|
199
|
+
The corpus's case IDs and their groupings (which task belongs to which cluster,
|
|
200
|
+
which split) are frozen before any run and never regrouped to taste.
|
|
201
|
+
|
|
202
|
+
- **Validation** MAY be used for tuning — that is what it is for.
|
|
203
|
+
- **The final holdout is spent ONCE**, on the version already chosen. It is
|
|
204
|
+
never used to pick between versions; a holdout consulted per candidate is a
|
|
205
|
+
second validation set wearing a blindfold.
|
|
206
|
+
- **A reused validation example can never be relabelled an "unseen final
|
|
207
|
+
test"** — the receipt says which split every number came from, and "unseen"
|
|
208
|
+
is a property of the RUN HISTORY, not of the label somebody wrote today.
|
|
209
|
+
|
|
141
210
|
## The harness is a variable, so pin it
|
|
142
211
|
|
|
143
212
|
The container spec is part of the measurement. On Terminal-Bench 2.0 the gap between the
|
|
@@ -33,13 +33,16 @@ forwards and backwards, which is why they live together here.
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
-
## Rule zero —
|
|
36
|
+
## Rule zero — check the prompt first (a diagnostic heuristic, with exceptions)
|
|
37
37
|
|
|
38
|
-
The instinct when an agent misbehaves is to change the code. The
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
The instinct when an agent misbehaves is to change the code. The sources this skill was
|
|
39
|
+
built from pull the other way: **"the biggest performance improvements often come from
|
|
40
|
+
clearly explaining tool usage in the system prompt"**, and **"even small refinements to
|
|
41
|
+
tool descriptions can yield dramatic improvements."** That is vendor guidance about where
|
|
42
|
+
leverage OFTEN lives, not a measured share of defects — so it orders the DIAGNOSIS, never
|
|
43
|
+
the verdict. **The exceptions are the findings a prompt cannot touch:** a deterministic
|
|
44
|
+
race, a hardcoded secret, a timeout wired to the wrong operation — source-level invariant
|
|
45
|
+
violations are code bugs, provable by reading, and no rewording treats them.
|
|
43
46
|
|
|
44
47
|
Before adding a retry, a router, or a sub-agent, check in this order:
|
|
45
48
|
|
|
@@ -97,11 +100,15 @@ A **static** graph has every node and edge decided up front; a **dynamic** one g
|
|
|
97
100
|
nodes read their own output and decide what comes next.
|
|
98
101
|
|
|
99
102
|
**Static first, always** — go dynamic only after the static version hits a wall you can
|
|
100
|
-
name, because dynamic is more powerful and much harder to control.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
name, because dynamic is more powerful and much harder to control. But **auditability is
|
|
104
|
+
NOT the same axis as static structure** — that conflates the plan drawn beforehand with
|
|
105
|
+
the execution graph saved afterward. A run is auditable when its EXECUTION RECORD is
|
|
106
|
+
complete: every node, edge and event that actually ran, the policy version in force, and
|
|
107
|
+
deterministic bounds (budget / depth / node caps) with provenance. A static graph is the
|
|
108
|
+
PREFERENCE because its executed shape usually matches the drawn one; a dynamic graph is
|
|
109
|
+
auditable too when it keeps that record within those caps. What is never evidence is a
|
|
110
|
+
design DIAGRAM on its own — *"here is what I planned"* is not *"here is what happened"*,
|
|
111
|
+
in either mode.
|
|
105
112
|
|
|
106
113
|
The six-row table, the rest of the model — the fake-edge test, the diamond, the checker
|
|
107
114
|
node before a convergence — and what a host actually executes when it fans out are one
|
|
@@ -145,8 +152,12 @@ The long version is `references/audit.md`. The shape:
|
|
|
145
152
|
Monday. This is the same rule `agent-evals` applies to eval rubrics and
|
|
146
153
|
`seo-aeo-audit` to sites.
|
|
147
154
|
|
|
148
|
-
**The finding
|
|
149
|
-
|
|
155
|
+
**The finding most audits surface first:** the system has no evals. That is a finding
|
|
156
|
+
about UNKNOWN RELIABILITY — every *behavioural estimate* downstream is unfalsifiable,
|
|
157
|
+
including this audit's. It does NOT dissolve what is provable at the source: a
|
|
158
|
+
demonstrable double charge, a hardcoded secret, a deterministic race keep their own
|
|
159
|
+
findings and their own priority, set by the concrete harm — a general "no evals" never
|
|
160
|
+
masks a specific proven harm.
|
|
150
161
|
|
|
151
162
|
---
|
|
152
163
|
|
|
@@ -189,7 +200,7 @@ prompt.
|
|
|
189
200
|
## Checklist — a harness worth shipping
|
|
190
201
|
|
|
191
202
|
- [ ] Workflow-versus-agent decided deliberately, and the simpler option was actually tried
|
|
192
|
-
- [ ] Static-versus-dynamic decided too
|
|
203
|
+
- [ ] Static-versus-dynamic decided too — static preferred for predictability; a run that must be auditable keeps a complete execution record (not merely a static shape)
|
|
193
204
|
- [ ] System prompt at the **right altitude** — heuristics, not hardcoded branches, not vague hope
|
|
194
205
|
- [ ] Every status, category and enum the agent must produce is **enumerated in the prompt**
|
|
195
206
|
- [ ] Today's date, and any other volatile context, injected rather than assumed
|
|
@@ -88,10 +88,21 @@ Walk them in order. Later tracks assume earlier ones.
|
|
|
88
88
|
- Is tool output treated as **untrusted input**?
|
|
89
89
|
- Can an audit row prove a control was applied — does it carry the **policy version**?
|
|
90
90
|
- Is there a deterministic limit anywhere consequential, or only probabilistic content checks?
|
|
91
|
+
- **The lethal trifecta is a specific EXFILTRATION pattern, not a full threat
|
|
92
|
+
model.** Private data + untrusted content + external comms in one session is
|
|
93
|
+
the exfiltration triangle — but a session MISSING one leg is not thereby
|
|
94
|
+
"safe". Audit **capabilities and effects SEPARATELY**: untrusted content +
|
|
95
|
+
a write capability, with no private data at all, is an unrelated destructive
|
|
96
|
+
effect (injected content corrupts state or takes a damaging action) and is
|
|
97
|
+
its OWN finding. "Only two of the three, therefore a PASS" is the mistake —
|
|
98
|
+
removing a trifecta leg removes THAT exfiltration path, not every risk.
|
|
91
99
|
|
|
92
100
|
### 7 — Evidence
|
|
93
101
|
|
|
94
|
-
- **Are there evals?** If not,
|
|
102
|
+
- **Are there evals?** If not, that is finding number one — about unknown reliability: every
|
|
103
|
+
*behavioural estimate* in this audit is then unfalsifiable. Findings proven at the source
|
|
104
|
+
(an invariant read off the code, a deterministic reproduction) stand on their own and are
|
|
105
|
+
prioritized by their concrete harm, not discounted under the general finding.
|
|
95
106
|
- Do they judge the **trajectory**, or only the final answer?
|
|
96
107
|
- Has any production failure become a permanent fixture?
|
|
97
108
|
- Is a judge calibrated against human labels, or trusted because it is a judge?
|
|
@@ -110,6 +121,13 @@ Every finding carries one, and the tier is part of the finding:
|
|
|
110
121
|
**Never present judgement as measured.** A finding whose tier is honest survives the meeting
|
|
111
122
|
where it is challenged; one that is inflated loses the whole report.
|
|
112
123
|
|
|
124
|
+
Orthogonal to the tier, name the PROOF CLASS, because it decides what "no evals" does to
|
|
125
|
+
the finding: a **source-level invariant proof** (the race, the hardcoded secret, the
|
|
126
|
+
miswired timeout — read off the code) and a **deterministic reproduction** (a script that
|
|
127
|
+
shows the double charge every run) survive a system with no evals untouched; only a
|
|
128
|
+
**behavioural estimate** ("the agent usually recovers") needs an eval suite to be
|
|
129
|
+
falsifiable — and inherits the no-evals finding until one exists.
|
|
130
|
+
|
|
113
131
|
## Priority — four axes, and no scalar
|
|
114
132
|
|
|
115
133
|
`P = blast × confidence / effort` used to sit here, and it contradicted the two sections
|
|
@@ -172,6 +190,10 @@ a finding whose tier is `judgement` says so there rather than being quietly disc
|
|
|
172
190
|
- **Grading instead of planning.** A score ends the conversation the audit was meant to
|
|
173
191
|
start — including a score assembled from honest axes. Publish the axes; do not multiply
|
|
174
192
|
them.
|
|
175
|
-
- **Confusing "no evals" with "not measured yet."** It is the root finding
|
|
176
|
-
|
|
193
|
+
- **Confusing "no evals" with "not measured yet."** It is the root finding for every
|
|
194
|
+
behavioural estimate, which inherits it. A source-level proof or deterministic
|
|
195
|
+
reproduction does NOT inherit it — burying a demonstrable double charge under a general
|
|
196
|
+
"everything is unfalsifiable" is how the one finding with a victim gets deprioritized.
|
|
197
|
+
- **Treating a broken unit invariant with a prompt change.** Rule zero orders the
|
|
198
|
+
diagnosis; it does not convert a code bug into a wording bug.
|
|
177
199
|
- **Reading a silent scanner as a clean system.** It is silent about what it can see.
|
|
@@ -124,7 +124,12 @@ the mistake cannot be made**, rather than documenting the mistake.
|
|
|
124
124
|
- An `enum` instead of a free-text field with a list of valid values in the description.
|
|
125
125
|
- One tool that does the two-step correctly instead of two tools that must be ordered.
|
|
126
126
|
- A required `confirm: true` on a destructive action, so a partially-formed call fails
|
|
127
|
-
closed.
|
|
127
|
+
closed. **But `confirm: true` is a SYNTAX GUARD, not user approval** — the MODEL can
|
|
128
|
+
set the boolean itself, so it proves only that the call is complete, never that a human
|
|
129
|
+
agreed. Real user approval is a **verifiable grant from a trusted control plane, bound to
|
|
130
|
+
the principal, action, exact arguments and an expiry** (or an already-existing user
|
|
131
|
+
authorization); a stale grant does NOT authorize changed arguments, and a client-supplied
|
|
132
|
+
boolean creates no authorization at all.
|
|
128
133
|
|
|
129
134
|
## Annotations, and the risk one tool cannot show you
|
|
130
135
|
|
|
@@ -154,8 +159,12 @@ Three capabilities that are individually ordinary and jointly an exfiltration pa
|
|
|
154
159
|
2. exposure to **untrusted content**,
|
|
155
160
|
3. the ability to **communicate externally**.
|
|
156
161
|
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
All three in one session mean untrusted content can instruct the agent to read private data
|
|
163
|
+
and send it out, and no prompt-level instruction reliably prevents it. But the trifecta
|
|
164
|
+
names a SUFFICIENT configuration for one SPECIFIC risk — private-data EXFILTRATION — not a
|
|
165
|
+
complete security model: **"any two are safe" over-claims.** Untrusted content plus a write
|
|
166
|
+
capability, with no access to private data at all, still lets injected content corrupt state
|
|
167
|
+
or take a damaging action; drop any leg and you have removed THAT triangle, not every risk.
|
|
159
168
|
|
|
160
169
|
**The reason it belongs here rather than in a permission check:** the trifecta is a property
|
|
161
170
|
of *the tool set assembled in a session*, so **per-tool analysis cannot see it**. Every tool
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
name: agent-interop
|
|
3
3
|
description: >-
|
|
4
4
|
Use when an agent must talk to something outside its own process — building or consuming an
|
|
5
|
-
MCP server, exposing or calling
|
|
5
|
+
MCP server, exposing or calling an agent over A2A, publishing to the MCP Registry, or
|
|
6
6
|
putting a gateway in front of agent traffic. Carries the MCP 2026-07-28 wire surface and what it
|
|
7
7
|
deprecated (server/discover, stateless per-request _meta, elicitation in form and URL mode,
|
|
8
8
|
subscriptions/listen; sampling, roots, logging and dynamic client registration going),
|
|
9
|
-
A2A 1.0 agent cards, task states
|
|
9
|
+
A2A 1.0 agent cards, task states, three bindings, registry namespaces, server.json, tool
|
|
10
10
|
federation, and what a gateway must do that an API gateway does not. Triggers - "MCP server",
|
|
11
11
|
"MCP client", "A2A", "agent card", "agent interoperability", "MCP registry", "server.json",
|
|
12
12
|
"agentgateway", "tool federation", "MCP-сервер", "карточка агента", "интероперабельность
|
|
@@ -71,10 +71,29 @@ tasks, while MCP is more about agents using capabilities."* Real systems run bot
|
|
|
71
71
|
server whose internals speak MCP — and that is the recommended architecture, not a
|
|
72
72
|
compromise.
|
|
73
73
|
|
|
74
|
-
**The
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
**The dispatch criterion is WHAT the other side is, not how long it runs.** MCP =
|
|
75
|
+
a CAPABILITY / tool you control the shape of; A2A = an AUTONOMOUS PEER whose
|
|
76
|
+
outcome you delegate and whose insides you cannot see. **Duration is a SECOND
|
|
77
|
+
question, and it is about a Tasks CAPABILITY, not a protocol.** A long-running
|
|
78
|
+
FIXED job you own — a ten-minute export, a batch transform — is MCP with the
|
|
79
|
+
**Tasks** extension (a durable handle: poll, supply input mid-flight, retrieve
|
|
80
|
+
later; see `references/mcp.md`), NOT A2A. So decide by the routing set, and
|
|
81
|
+
never by the word *long-running*:
|
|
82
|
+
|
|
83
|
+
- long-running FIXED export → **MCP Tasks, when the client/SDK negotiates that
|
|
84
|
+
extension**;
|
|
85
|
+
- autonomous outsourced negotiation → **A2A** (you delegate the outcome, not
|
|
86
|
+
the steps);
|
|
87
|
+
- **Tasks unsupported** by the reached client/SDK → an explicit fallback
|
|
88
|
+
(chunk the job, a job id the caller polls with a plain `tools/call`, or a
|
|
89
|
+
webhook) — reaching for A2A because Tasks is absent is picking a protocol to
|
|
90
|
+
dodge a missing extension.
|
|
91
|
+
|
|
92
|
+
Check the client's ACTUALLY-negotiated extensions before building on Tasks;
|
|
93
|
+
inventing a task lifecycle on top of `tools/call` when Tasks IS available is
|
|
94
|
+
re-implementing the extension, and reaching for A2A when Tasks is merely
|
|
95
|
+
unsupported is the mis-route this audit closes. Publishing an agent card for
|
|
96
|
+
something that is one HTTP call with a JSON schema is still the MCP direction.
|
|
78
97
|
|
|
79
98
|
---
|
|
80
99
|
|
|
@@ -88,8 +88,8 @@ tokens than the definitions you so carefully removed.
|
|
|
88
88
|
mitigations below were approximating: `defer_loading` keeps deferred definitions out of the
|
|
89
89
|
system-prompt prefix and appends discovered ones inline as `tool_reference` blocks, leaving the
|
|
90
90
|
cached prefix untouched (at least one tool must stay non-deferred — all-deferred is a 400). The
|
|
91
|
-
economics, the arithmetic and the other invalidators are in
|
|
92
|
-
|
|
91
|
+
economics, the arithmetic and the other invalidators are in
|
|
92
|
+
`../agent-orchestrator/references/kv-cache.md`.
|
|
93
93
|
|
|
94
94
|
Three mitigations, in the order they are usually right:
|
|
95
95
|
|
|
@@ -13,11 +13,47 @@ repeat it.
|
|
|
13
13
|
|
|
14
14
|
## Contents
|
|
15
15
|
|
|
16
|
+
- Every example names its SDK — distribution, import, tested version, lifecycle
|
|
16
17
|
- Mounting into an existing web app
|
|
17
18
|
- Auth middleware and a health endpoint
|
|
18
19
|
- Client configuration
|
|
19
20
|
- Debugging a client that will not connect
|
|
20
21
|
|
|
22
|
+
## Every example names its SDK — distribution, import, tested version, lifecycle
|
|
23
|
+
|
|
24
|
+
`FastMCP` exists in TWO distributions, and they are not the same package: the
|
|
25
|
+
official `mcp` SDK (`from mcp.server.fastmcp import FastMCP`) and the
|
|
26
|
+
standalone `fastmcp` package, whose 2.x renamed the server class and moved the
|
|
27
|
+
transport args out of the constructor. A snippet that names only the class
|
|
28
|
+
names neither. Every executable example in this file is written against **the
|
|
29
|
+
official `mcp` SDK, pinned**:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
# requirements.txt — the pin IS the example's identity
|
|
33
|
+
mcp==1.12.3 # the version these snippets were verified against (2026-08-13);
|
|
34
|
+
starlette==0.47.* # re-pin only together with a re-run of the acceptance below
|
|
35
|
+
uvicorn==0.35.*
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from mcp.server.fastmcp import FastMCP # official SDK — NOT `from fastmcp import FastMCP`
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Current path (v2/current spec, pinned SDK above):** the snippets below,
|
|
43
|
+
as written.
|
|
44
|
+
- **v1 / standalone-`fastmcp` migration:** explicitly **out of scope** here.
|
|
45
|
+
Supporting it means its OWN fixture verified against its own pin — renaming
|
|
46
|
+
imports and hoping is how the constructor-args move ships a 404.
|
|
47
|
+
- **Lifecycle, in the verified order:** create the server → register tools →
|
|
48
|
+
build the ASGI app → register `/health` on the OUTER app → wrap with the
|
|
49
|
+
auth middleware → mount → serve. The health route registers BEFORE the auth
|
|
50
|
+
wrap, or the probe that says "up" needs a credential to say it.
|
|
51
|
+
- **Proof is a localhost protocol call, never a string in markdown.** The
|
|
52
|
+
acceptance boots the pinned example in a clean env and asserts:
|
|
53
|
+
`GET /health` → 200 · unauthenticated `/mcp` → 401 · then `initialize`,
|
|
54
|
+
`tools/list` and one `tools/call` succeed against `http://127.0.0.1`. A
|
|
55
|
+
snippet nobody booted is a hope with syntax highlighting.
|
|
56
|
+
|
|
21
57
|
## Mounting into an existing web app
|
|
22
58
|
|
|
23
59
|
The common production shape: you already run a FastAPI/Starlette app, and the MCP server
|
|
@@ -219,7 +219,11 @@ Opt-in, negotiated, and worth checking before inventing an equivalent:
|
|
|
219
219
|
|
|
220
220
|
- **Tasks** — a durable handle for long-running requests: poll for status, supply input
|
|
221
221
|
mid-flight, retrieve the result later. This is the answer to "my tool takes ten minutes",
|
|
222
|
-
and it exists so you do not hold a connection open or invent a job table.
|
|
222
|
+
and it exists so you do not hold a connection open or invent a job table. **Duration is a
|
|
223
|
+
TASKS-capability question, never a reason to switch to A2A**: a long-running FIXED job you
|
|
224
|
+
own is MCP-with-Tasks. But Tasks is an OPT-IN, NEGOTIATED extension — check the client/SDK
|
|
225
|
+
actually supports it before building on it; where it is unsupported, the fallback is a
|
|
226
|
+
chunked job or a caller-polled job id over plain `tools/call`, not a protocol change.
|
|
223
227
|
- **MCP Apps** — interactive UI rendered inline in the conversation.
|
|
224
228
|
- **Skills over MCP** — structured instruction sets discovered and consumed through MCP,
|
|
225
229
|
which is how a server ships Agent Skills rather than only tools.
|
|
@@ -6,7 +6,7 @@ description: >-
|
|
|
6
6
|
loops, pipelines with human checkpoints, provider routing with fallback/retry, memory
|
|
7
7
|
architecture, retrieval and decay, context budgets, sub-agent coordination, error hierarchies; the
|
|
8
8
|
work as a graph — parallel layers, fake edges, a checker before convergence; for resale:
|
|
9
|
-
tiered wallets, one markup boundary,
|
|
9
|
+
tiered wallets, one markup boundary, the saga across database and provider API,
|
|
10
10
|
spend-delta polling, budget and loop guards, per-tenant keys. Triggers - "agent system",
|
|
11
11
|
"orchestrator", "tool calling", "sub-agent", "LLM router", "fallback chain", "human in the
|
|
12
12
|
loop", "memory layer", "LLM billing", "token wallet", "агентная система", "оркестратор",
|
|
@@ -282,10 +282,13 @@ comes from, and what this host actually executes are in
|
|
|
282
282
|
|
|
283
283
|
Four rules, and these are the ones that change code:
|
|
284
284
|
|
|
285
|
-
- **
|
|
286
|
-
over any chain you inherited
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
- **Type every edge: data, control, authorization or resource.** Run the fake-edge
|
|
286
|
+
test over any chain you inherited — and delete an arrow only when it carries NONE
|
|
287
|
+
of the four: no payload, no causal constraint, no permission, no shared resource.
|
|
288
|
+
backup→migration, approval→charge and lease→edit carry no bytes and are real;
|
|
289
|
+
the arrows that only encode the order somebody typed are the two or three per
|
|
290
|
+
workflow the test normally yields. Before a fan-out, compare read/write sets —
|
|
291
|
+
read-only branches parallelise, two writers of one thing were a resource edge.
|
|
289
292
|
- **`depends_on` is a claim, so execute by layer.** §5's executor walked `plan.stages` in
|
|
290
293
|
list order beside a model that declared its dependencies — which serialises a plan that
|
|
291
294
|
went to the trouble of saying it need not be. Kahn the graph; a cycle fails the plan
|
|
@@ -294,9 +297,11 @@ Four rules, and these are the ones that change code:
|
|
|
294
297
|
returns a hallucination, and the synthesis node cannot tell: it combines all three and
|
|
295
298
|
answers confidently. The checker decides *usable / not usable* and nothing else, and
|
|
296
299
|
the convergence depends on **the checker**, never directly on a branch.
|
|
297
|
-
- **Static unless you can name what forces dynamic
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
- **Static unless you can name what forces dynamic** — for predictability, not
|
|
301
|
+
auditability. A run is auditable when it SAVES its execution record (nodes/edges/events
|
|
302
|
+
that ran, the policy version, deterministic budget/depth/node caps, provenance); a
|
|
303
|
+
dynamic graph that keeps that record is auditable too. A design diagram alone is never
|
|
304
|
+
evidence: the shape drawn is not the shape that ran.
|
|
300
305
|
|
|
301
306
|
---
|
|
302
307
|
|
|
@@ -120,24 +120,43 @@ already have and ask which of its waits are real.
|
|
|
120
120
|
|
|
121
121
|
## 3. The fake-edge test
|
|
122
122
|
|
|
123
|
-
Five minutes, no tooling, and it is the highest-yield thing in this file
|
|
123
|
+
Five minutes, no tooling, and it is the highest-yield thing in this file —
|
|
124
|
+
and its question is **typed**, because "does data cross?" alone deletes real
|
|
125
|
+
constraints (AS-04). An edge is one of four kinds:
|
|
126
|
+
|
|
127
|
+
| Kind | What crosses | Example that MUST survive |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| **data** | A's output enters B | findings → draft |
|
|
130
|
+
| **control** | ordering only — a causal constraint with no bytes | backup → migration |
|
|
131
|
+
| **authorization** | a decision that permits B | approval → charge |
|
|
132
|
+
| **resource** | A and B touch one thing that tolerates one writer | two writes to one ledger |
|
|
124
133
|
|
|
125
134
|
1. Write every step as a box.
|
|
126
135
|
2. Draw an arrow between each pair of consecutive steps.
|
|
127
|
-
3. For each arrow ask:
|
|
128
|
-
A
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
3. For each arrow ask, in order: does data from A enter B? does B's SAFETY
|
|
137
|
+
depend on A having finished (control)? does A PERMIT B (authorization)?
|
|
138
|
+
do A and B contend for one resource?
|
|
139
|
+
4. Any yes → keep it, **type it, and write the rationale on the arrow** —
|
|
140
|
+
the payload for a data edge, the constraint for the other three.
|
|
141
|
+
5. No to all four → delete it. That wait encoded the order somebody typed.
|
|
142
|
+
6. Everything with no incoming arrow starts immediately — and **before any
|
|
143
|
+
fan-out, compare the branches' side-effect footprints and read/write
|
|
144
|
+
sets**: two read-only reviews genuinely parallelise; two writers of one
|
|
145
|
+
file were a resource edge nobody drew.
|
|
132
146
|
7. Everything with no outgoing arrow is a final output.
|
|
133
147
|
|
|
134
|
-
The tell that the test is being done honestly is step 4:
|
|
135
|
-
|
|
136
|
-
leaving
|
|
148
|
+
The tell that the test is being done honestly is step 4: an arrow with no
|
|
149
|
+
type and no rationale is fake, and the person drawing it now has to say so
|
|
150
|
+
out loud rather than leaving it in place because it looked orderly. The
|
|
151
|
+
inverse tell is step 5 done lazily: backup→migration, approval→charge and
|
|
152
|
+
lease→edit all carry NO payload, and deleting them for that is how a
|
|
153
|
+
migration runs against nothing — an empty payload cell justifies deletion
|
|
154
|
+
only when there is also no causal, permissive or resource constraint.
|
|
137
155
|
|
|
138
|
-
**Expect two or three fake edges in any workflow you have not run this
|
|
139
|
-
classic is *"review file A, then review file B"*: it reads as
|
|
140
|
-
of B never
|
|
156
|
+
**Expect two or three fake edges in any workflow you have not run this
|
|
157
|
+
against.** The classic is *"review file A, then review file B"*: it reads as
|
|
158
|
+
a sequence, the review of B never looks at what A returned, and both are
|
|
159
|
+
read-only — no data, no control, no authorization, no resource.
|
|
141
160
|
|
|
142
161
|
## 4. The diamond
|
|
143
162
|
|
|
@@ -263,13 +282,20 @@ grows: a node finishes, looks at what it found, and decides what should come nex
|
|
|
263
282
|
| **static** | **always first** — switch only after the static version hits a wall you can name |
|
|
264
283
|
| dynamic | the scope of the work depends on what is discovered along the way |
|
|
265
284
|
| dynamic | a node must choose its successors from its own output |
|
|
266
|
-
|
|
|
267
|
-
|
|
268
|
-
The last row is a hard
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
285
|
+
| prefer static | **you will need to audit exactly what ran and why** — but see below |
|
|
286
|
+
|
|
287
|
+
The last row is a PREFERENCE, not a hard ban — and the earlier draft got this wrong by
|
|
288
|
+
equating auditability with static structure, which conflates the plan drawn beforehand
|
|
289
|
+
with the execution graph saved afterward. **Auditability is a property of the RECORD, not
|
|
290
|
+
of the shape:** a run is auditable when its execution record is complete (every node,
|
|
291
|
+
edge and event that ran), the policy version is captured, and the run stayed inside
|
|
292
|
+
deterministic bounds — a **budget, a depth cap and a node cap**, each with provenance. A
|
|
293
|
+
static graph is preferred because its executed shape usually matches the drawn one and it
|
|
294
|
+
is predictable; a DYNAMIC graph under those caps, saving that record, passes the same
|
|
295
|
+
reconstruction audit — the SAME reconstruction that detects a deleted event or edge in
|
|
296
|
+
either mode. What never passes, static or dynamic, is a design DIAGRAM on its own:
|
|
297
|
+
*"here is the graph"* is not *"here is what happened"*. That is the same failure
|
|
298
|
+
`agent-evals` names when a system has no durable trace.
|
|
273
299
|
|
|
274
300
|
**Most workflows that feel like they need a dynamic graph need a better static one.**
|
|
275
301
|
Dynamic is more powerful and much harder to control; it is the second reach, never the
|
|
@@ -364,7 +390,12 @@ session:
|
|
|
364
390
|
## Workflow defaults
|
|
365
391
|
|
|
366
392
|
- A node with no declared dependency starts immediately; do not serialise by habit.
|
|
367
|
-
- Every declared dependency
|
|
393
|
+
- Every declared dependency carries a type (data/control/authorization/resource)
|
|
394
|
+
and a rationale. Delete an edge only when it has none of the four — an empty
|
|
395
|
+
payload alone never justifies deletion: backup→migration carries no bytes and
|
|
396
|
+
is real.
|
|
397
|
+
- Before a fan-out, compare the branches' side-effect footprints and read/write
|
|
398
|
+
sets; writers of one resource serialise, read-only branches run in parallel.
|
|
368
399
|
- A checker sits between any parallel layer and the node that consumes it, and the
|
|
369
400
|
consumer depends on the checker rather than on the layer.
|
|
370
401
|
- A checker flags; it never silently passes an incomplete output.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Reselling LLM access — metering, wallets and guardrails
|
|
2
2
|
|
|
3
3
|
**Load this when** the product resells LLM access: tiered wallets and the single
|
|
4
|
-
boundary where markup applies,
|
|
4
|
+
boundary where markup applies, the saga across a database and a provider API
|
|
5
5
|
with compensating transactions, advisory locking, optimistic concurrency for reclaims,
|
|
6
6
|
spend-delta polling and its three cases, budget / loop / auto-pause guardrails,
|
|
7
7
|
per-tenant key lifecycle and healing, the refund waterfall, and model-routing
|
|
@@ -18,7 +18,8 @@ the patterns hold for any upstream that issues per-tenant keys with limits.
|
|
|
18
18
|
## Contents
|
|
19
19
|
|
|
20
20
|
- [The tiered wallet](#the-tiered-wallet)
|
|
21
|
-
- [
|
|
21
|
+
- [The saga across a DB and an external API](#the-saga-across-a-db-and-an-external-api)
|
|
22
|
+
- [Reconciling an unknown](#reconciling-an-unknown)
|
|
22
23
|
- [Serializing concurrent transfers](#serializing-concurrent-transfers)
|
|
23
24
|
- [Optimistic concurrency for reclaims](#optimistic-concurrency-for-reclaims)
|
|
24
25
|
- [Discovering spend you do not control](#discovering-spend-you-do-not-control)
|
|
@@ -67,29 +68,76 @@ the constant changes.
|
|
|
67
68
|
|
|
68
69
|
---
|
|
69
70
|
|
|
70
|
-
##
|
|
71
|
+
## The saga across a DB and an external API
|
|
71
72
|
|
|
72
|
-
You have a database you can roll back and an HTTP API you cannot.
|
|
73
|
-
|
|
73
|
+
You have a database you can roll back and an HTTP API you cannot. That pair is
|
|
74
|
+
a **saga** — local transactions stitched together by compensations — and it is
|
|
75
|
+
not two-phase commit: 2PC needs a coordinator both participants obey, and the
|
|
76
|
+
provider's API never agreed to prepare/commit. Naming it 2PC is how the next
|
|
77
|
+
defect ships, because 2PC has no *unknown* outcome, and an HTTP call to a
|
|
78
|
+
system you do not control has one all the time.
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
Every operation that touches the provider carries an **`operation_id`**, minted
|
|
81
|
+
inside the DB transaction, and a state that moves
|
|
82
|
+
`pending → applied | unknown | compensated`:
|
|
76
83
|
|
|
77
84
|
1. Acquire the lock (below).
|
|
78
85
|
2. Read fresh balances **inside** the transaction — not before it.
|
|
79
86
|
3. Compute the transfer and apply the markup once.
|
|
80
|
-
4. Zero the source tier, increment the destination, write
|
|
87
|
+
4. Zero the source tier, increment the destination, write the intent row —
|
|
88
|
+
`operation_id`, state `pending` — an outbox entry, not a log line.
|
|
81
89
|
5. Commit.
|
|
82
|
-
6. Call the provider to raise the key limit
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
6. Call the provider to raise the key limit, idempotently where the API allows
|
|
91
|
+
(send the `operation_id` as the idempotency key).
|
|
92
|
+
7. **On an outcome that proves the call did not apply** — a 4xx validation
|
|
93
|
+
refusal, a "no such key" — a compensating transaction restores every DB
|
|
94
|
+
value, writes a `compensation` audit row, and marks the operation
|
|
95
|
+
`compensated`.
|
|
96
|
+
8. **On an AMBIGUOUS outcome — a timeout, a connection reset after send, a
|
|
97
|
+
5xx — the operation is marked `unknown` and is NOT compensated.** The
|
|
98
|
+
provider may have applied the change: compensating on a guess restores a
|
|
99
|
+
ledger the key no longer matches, and the money drifts in the direction you
|
|
100
|
+
cannot see. `unknown` resolves only by **reconciliation** — read the
|
|
101
|
+
provider's actual state (the key's real limit), then mark `applied` or
|
|
102
|
+
compensate on evidence. Until it resolves, the operation blocks retries of
|
|
103
|
+
itself: a retry of an `unknown` is how one top-up applies twice.
|
|
85
104
|
|
|
86
105
|
The alternative — API first, DB second — leaves money on the key that your
|
|
87
106
|
ledger does not know about, and no amount of retrying finds it again. The
|
|
88
|
-
compensating transaction
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Log
|
|
92
|
-
successes cannot answer "where did the $35 go"
|
|
107
|
+
compensating transaction makes the *known* failure recoverable; the `unknown`
|
|
108
|
+
state is what keeps the ambiguous one honest.
|
|
109
|
+
|
|
110
|
+
Log the intent, the outcome and the compensation, keyed by `operation_id`. An
|
|
111
|
+
audit trail that records only successes cannot answer "where did the $35 go"
|
|
112
|
+
six weeks later — and one that cannot say "we do not know yet" answers it
|
|
113
|
+
wrongly.
|
|
114
|
+
|
|
115
|
+
## Reconciling an unknown
|
|
116
|
+
|
|
117
|
+
Three rules, and every one exists because a late HTTP response is a message
|
|
118
|
+
from the past:
|
|
119
|
+
|
|
120
|
+
- **Ask by the operation's own idempotency key.** Reconciliation queries the
|
|
121
|
+
provider for what happened to THIS `operation_id` — never "read the limit
|
|
122
|
+
and guess whose change it reflects". Ambient state is the sum of every
|
|
123
|
+
operation that ever landed; only the key isolates yours.
|
|
124
|
+
- **The tenant's ledger carries a revision, and every resolve is a CAS.** A
|
|
125
|
+
reconcile or compensation writes only if the revision it read is still
|
|
126
|
+
current; a late or concurrent response that lost the race aborts and
|
|
127
|
+
re-reads, it never blind-writes. Without this, the response to operation A —
|
|
128
|
+
arriving after operation B moved the same tenant's ledger — "restores"
|
|
129
|
+
values B already superseded, and the compensation itself becomes the
|
|
130
|
+
corruption.
|
|
131
|
+
- **Compensate only your own confirmed operation.** A compensation names its
|
|
132
|
+
`operation_id`, reverses exactly that operation's delta, and runs only after
|
|
133
|
+
reconciliation confirmed THAT operation did not apply. A response for A is
|
|
134
|
+
never grounds to touch B's rows — however tempting the arithmetic looks.
|
|
135
|
+
|
|
136
|
+
**Repeated reconciliation is idempotent.** `unknown → applied` and
|
|
137
|
+
`unknown → compensated` are one-way edges: resolving an already-resolved
|
|
138
|
+
operation reads its state and stops — zero new writes, zero new audit rows. A
|
|
139
|
+
reconciler that runs twice (and it will: cron plus a manual "Sync now" is the
|
|
140
|
+
normal case, not the weird one) must find nothing left to do the second time.
|
|
93
141
|
|
|
94
142
|
---
|
|
95
143
|
|
|
@@ -141,16 +189,30 @@ discover spend by **polling a cumulative counter and taking the delta**:
|
|
|
141
189
|
delta = currentUsage - lastRecordedUsage
|
|
142
190
|
```
|
|
143
191
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
192
|
+
**Zero is a value, not an absence.** The baseline row carries three fields
|
|
193
|
+
BESIDE the sum — `baseline_initialized`, `observed_at`, and
|
|
194
|
+
`provider_key_generation` (the key's id or created-at, whatever the provider
|
|
195
|
+
lets you read) — because `lastRecordedUsage == 0` has two meanings that cost
|
|
196
|
+
money to conflate: "never watched" and "watched from zero". Testing the sum
|
|
197
|
+
for zero eats the first REAL spend of every key you watched from birth,
|
|
198
|
+
silently, as "seeding".
|
|
199
|
+
|
|
200
|
+
Four cases, decided by the flags, never by the sum:
|
|
201
|
+
|
|
202
|
+
- `!baseline_initialized` → **seed the baseline, record nothing**, set
|
|
203
|
+
`baseline_initialized`, stamp `observed_at` and the generation. Recording
|
|
204
|
+
here charges the tenant for everything spent before you started watching.
|
|
205
|
+
- initialized, `currentUsage > lastRecordedUsage` → record `delta` — including
|
|
206
|
+
the very first delta of a key whose baseline is a genuine 0 — then
|
|
207
|
+
immediately enforce budgets (below).
|
|
208
|
+
- initialized, `currentUsage < lastRecordedUsage`, **generation changed** →
|
|
209
|
+
the key really was recreated: resync the baseline to the new generation,
|
|
210
|
+
record nothing. The new key's next increase is recorded normally.
|
|
211
|
+
- initialized, `currentUsage < lastRecordedUsage`, **same generation** →
|
|
212
|
+
**ANOMALY.** Do not resync, do not record, do not guess "recreated" — a
|
|
213
|
+
counter that went backwards on the same key is the provider disagreeing
|
|
214
|
+
with your ledger, and reconciliation (above) owns it. A guessed resync here
|
|
215
|
+
quietly forgives the difference forever.
|
|
154
216
|
|
|
155
217
|
Sync your stored limit from the provider's authoritative value on the same pass —
|
|
156
218
|
under the lock, with a re-read, so the sync does not clobber a transfer that
|
|
@@ -235,10 +235,12 @@ The progression, and both ends are wrong:
|
|
|
235
235
|
access control**.
|
|
236
236
|
|
|
237
237
|
**What this pack already has, and what it is not.** `agent-sync` gives leases, race-free id
|
|
238
|
-
reservation and a run journal: it decides *who may write this file right now
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
reservation and a run journal: it decides *who may write this file right now* — precisely,
|
|
239
|
+
via a per-file **resource claim**, because a task lease authorizes the task and not the file
|
|
240
|
+
(SY-04), and its guarantee is a real cross-machine compare-and-swap only under the git lease
|
|
241
|
+
backend, advisory otherwise. That is coordination, and it is not shared memory — it says
|
|
242
|
+
nothing about what an agent should be allowed to *read*, or whose experiential memory is
|
|
243
|
+
trustworthy enough to act on. An agent system that needs both needs both.
|
|
242
244
|
|
|
243
245
|
**The design rule:** make shared writes **attributed and scoped**. An entry carries who
|
|
244
246
|
wrote it and under what role, and a reader may weigh it accordingly. Unattributed shared
|
|
@@ -18,7 +18,8 @@ long-term behaviour is a consequence of those frequencies, not of separate boxes
|
|
|
18
18
|
- [3. Evolution — consolidation](#3-evolution--consolidation)
|
|
19
19
|
- [4. Evolution — updating, and the stability–plasticity dilemma](#4-evolution--updating-and-the-stabilityplasticity-dilemma)
|
|
20
20
|
- [5. Evolution — forgetting](#5-evolution--forgetting)
|
|
21
|
-
- [6.
|
|
21
|
+
- [6. The temporal evidence lifecycle](#6-the-temporal-evidence-lifecycle)
|
|
22
|
+
- [7. What this pack already implements](#7-what-this-pack-already-implements)
|
|
22
23
|
|
|
23
24
|
## 1. Formation — five ways to turn experience into an entry
|
|
24
25
|
|
|
@@ -134,7 +135,38 @@ constraint, many memory systems avoid directly deleting certain memories."*
|
|
|
134
135
|
Deletion remains a **correctness and privacy** operation — a person asking to be forgotten
|
|
135
136
|
is not a capacity decision, and `memory-architecture.md` §8 covers it.
|
|
136
137
|
|
|
137
|
-
## 6.
|
|
138
|
+
## 6. The temporal evidence lifecycle
|
|
139
|
+
|
|
140
|
+
One record's timeline, and the two events that are NOT on it:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
born ──▶ corroborated ──▶ superseded ──▶ (restored)
|
|
144
|
+
│ (independent (reversible: un-supersede puts it back;
|
|
145
|
+
│ provenance is_active=False, nothing was deleted
|
|
146
|
+
│ only) superseded_by)
|
|
147
|
+
└──▶ expired (volatile fact past its freshness window —
|
|
148
|
+
out of default retrieval WHATEVER its confidence,
|
|
149
|
+
verified included)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **Retrieval is not a lifecycle event.** Reading a record — or the agent
|
|
153
|
+
restating it in its own words — moves nothing: not confidence, not
|
|
154
|
+
freshness, not activity. A self-generated repeat scored as confirmation is
|
|
155
|
+
the compounding error `patterns.md` → Confidence Management refuses; only
|
|
156
|
+
evidence with independent provenance corroborates.
|
|
157
|
+
- **Supersession is an annotation, and it is reversible.** A new dated fact
|
|
158
|
+
sets `is_active=False` + `superseded_by` on the old one and KEEPS it: the
|
|
159
|
+
chain is walkable from either end, an explicit query still reaches the old
|
|
160
|
+
value, and restoring it (the correction turned out wrong) is clearing two
|
|
161
|
+
fields, not resurrecting a deleted row. The dated history is the audit
|
|
162
|
+
trail of what the system believed when.
|
|
163
|
+
- **`verified` is about confidence, never about time.** A volatile fact —
|
|
164
|
+
a quota, a price, a rate limit — carries a freshness window from its
|
|
165
|
+
`validity` field, and past that window it leaves default retrieval even at
|
|
166
|
+
confidence 1.0. Verification exempts a record from confidence DECAY;
|
|
167
|
+
nothing exempts a fact about the present from the present.
|
|
168
|
+
|
|
169
|
+
## 7. What this pack already implements
|
|
138
170
|
|
|
139
171
|
Stated so this file is read as an extension and not as a replacement:
|
|
140
172
|
|
|
@@ -146,9 +178,9 @@ Stated so this file is read as an extension and not as a replacement:
|
|
|
146
178
|
| Forgetting, time-based | `patterns.md` → Confidence Management |
|
|
147
179
|
| Global integration, cross-scope | `patterns.md` → Cross-Resource Learning Transfer |
|
|
148
180
|
| **Frequency-based forgetting** | **nowhere — and the long-tail trap above is why that is a deliberate omission rather than a gap to close carelessly** |
|
|
149
|
-
| **Temporal annotation instead of deletion** |
|
|
181
|
+
| **Temporal annotation instead of deletion** | `patterns.md` → Conflict Resolution — supersession sets `is_active=False` + `superseded_by` and the old record stays in history, reversible and reachable by explicit query |
|
|
150
182
|
| **Dual-phase updating** | **nowhere** — the pack updates inline |
|
|
151
183
|
|
|
152
|
-
The
|
|
184
|
+
The remaining two are named as absent rather than quietly added: each is a real change to a
|
|
153
185
|
mechanism that is in production, and this file's job is to say what the options are, not to
|
|
154
186
|
change `patterns.md` from a survey.
|
|
@@ -364,26 +364,36 @@ class LearningAnalyzer:
|
|
|
364
364
|
|
|
365
365
|
## Confidence Management
|
|
366
366
|
|
|
367
|
+
Confidence measures **corroboration, not match frequency**: it moves only on
|
|
368
|
+
evidence with independent provenance — another session, another agent, an
|
|
369
|
+
observed outcome. Retrieving a record, or the agent restating it in its own
|
|
370
|
+
words, bumps nothing: a self-generated repeat scored as confirmation is how an
|
|
371
|
+
early mistake compounds into a "high-confidence" one.
|
|
372
|
+
|
|
367
373
|
```
|
|
368
374
|
LEARNING CONFIDENCE:
|
|
369
|
-
Initial:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
+
Initial: 0.6
|
|
376
|
+
Corroborated: +0.1 (cap 1.0) — independent provenance only;
|
|
377
|
+
retrieval and self-repetition move nothing
|
|
378
|
+
Applied: tracked (times_applied counter — a usage stat, not evidence)
|
|
379
|
+
Contradicted: handled by the contradiction gate below, never a bare -0.3
|
|
380
|
+
on a keyword match
|
|
381
|
+
Stale (30d): -0.02/month
|
|
382
|
+
Deactivated: below 0.2 — demoted from default retrieval, kept in history
|
|
375
383
|
|
|
376
384
|
SESSION NOTE CONFIDENCE:
|
|
377
|
-
Initial:
|
|
378
|
-
|
|
379
|
-
Verified:
|
|
380
|
-
|
|
381
|
-
|
|
385
|
+
Initial: 0.7
|
|
386
|
+
Corroborated: +0.1 (cap 1.0), same provenance rule
|
|
387
|
+
Verified: +0.15 — exempt from confidence DECAY, not from validity:
|
|
388
|
+
a volatile fact past its freshness window leaves default
|
|
389
|
+
retrieval whatever its confidence says
|
|
390
|
+
Stale (60d): -0.1 per cycle
|
|
391
|
+
Floor: 0.1
|
|
382
392
|
|
|
383
393
|
INSIGHT CONFIDENCE:
|
|
384
394
|
Initial: 0.5
|
|
385
395
|
Resurfaced: +0.05
|
|
386
|
-
|
|
396
|
+
Corroborated: +0.15
|
|
387
397
|
Dismissed: -0.2
|
|
388
398
|
Stale (30d): -0.05 per cycle
|
|
389
399
|
Expired: below 0.15
|
|
@@ -393,53 +403,83 @@ INSIGHT CONFIDENCE:
|
|
|
393
403
|
|
|
394
404
|
## Fuzzy Deduplication Pattern
|
|
395
405
|
|
|
396
|
-
|
|
406
|
+
**Similarity proposes; the contradiction gate disposes.** Lexical similarity
|
|
407
|
+
is CANDIDATE RETRIEVAL only — it has no side effects. The measured
|
|
408
|
+
counterexample that fixed this rule: *"Always allow external sharing of
|
|
409
|
+
customer data"* and *"Never allow external sharing of customer data"* score
|
|
410
|
+
`SequenceMatcher` similarity **0.8791** — far above any threshold — and the
|
|
411
|
+
old on-match behaviour (bump confidence, keep the longer text, reactivate)
|
|
412
|
+
would have REINFORCED the stale instruction with the user's own correction,
|
|
413
|
+
and kept "Always" because it is one word longer.
|
|
397
414
|
|
|
398
415
|
```python
|
|
399
416
|
from difflib import SequenceMatcher
|
|
400
417
|
|
|
401
418
|
THRESHOLD = 0.75 # learnings/notes; 0.80 for insights
|
|
402
419
|
|
|
403
|
-
async def
|
|
420
|
+
async def find_candidates(session, connection_id, category, subject, text):
|
|
421
|
+
"""Returns candidates for the contradiction gate. Nothing else happens
|
|
422
|
+
here: no confidence bump, no text replacement, no reactivation."""
|
|
404
423
|
candidates = await load_existing(session, connection_id, category, subject)
|
|
405
424
|
text_lower = text.strip().lower()
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
# On match: bump confidence +0.1, keep longer text, set is_active=True
|
|
414
|
-
# On no match: create new entry
|
|
425
|
+
return [c for c in candidates
|
|
426
|
+
if SequenceMatcher(None, c.text.strip().lower(), text_lower).ratio()
|
|
427
|
+
>= THRESHOLD]
|
|
428
|
+
|
|
429
|
+
# Every candidate goes through the contradiction gate below.
|
|
430
|
+
# Only the gate's verdict decides merge / supersede / coexist / create.
|
|
415
431
|
```
|
|
416
432
|
|
|
417
433
|
---
|
|
418
434
|
|
|
419
435
|
## Conflict Resolution Pattern
|
|
420
436
|
|
|
421
|
-
|
|
437
|
+
A memory record carries five mandatory fields beside its text, and the gate
|
|
438
|
+
reads THEM — never the prose:
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
MEMORY RECORD:
|
|
442
|
+
entity what the statement is about ("customer-data-sharing")
|
|
443
|
+
attribute which property of it ("external-sharing-policy")
|
|
444
|
+
scope where it applies ("project-A" | "global" | …)
|
|
445
|
+
provenance who/what asserted it, when (session, agent, outcome, user)
|
|
446
|
+
validity observed_at + volatile|stable (+ freshness window if volatile)
|
|
447
|
+
value the normalized position ("allow" | "deny" | "30s" | …)
|
|
448
|
+
```
|
|
422
449
|
|
|
423
450
|
```python
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
451
|
+
def contradiction_gate(old, new):
|
|
452
|
+
if (old.entity, old.attribute) != (new.entity, new.attribute):
|
|
453
|
+
return "unrelated" # similarity alone never merges anything
|
|
454
|
+
if not scopes_overlap(old.scope, new.scope):
|
|
455
|
+
return "coexist" # a correction wins only in its own scope
|
|
456
|
+
if values_compatible(old.value, new.value):
|
|
457
|
+
return "corroborates" # +confidence iff provenance is independent
|
|
458
|
+
return "contradicts"
|
|
459
|
+
|
|
460
|
+
def apply_verdict(verdict, old, new):
|
|
461
|
+
if verdict == "contradicts":
|
|
462
|
+
# Temporal supersession, reversible: the old record STAYS in history.
|
|
463
|
+
old.is_active = False
|
|
464
|
+
old.superseded_by = new.id # never deleted, never bumped
|
|
465
|
+
return create(new) # starts at its own initial confidence
|
|
466
|
+
if verdict == "corroborates" and independent(old.provenance, new.provenance):
|
|
467
|
+
old.confidence = min(1.0, old.confidence + 0.1)
|
|
468
|
+
old.provenance.append(new.provenance)
|
|
469
|
+
return old
|
|
470
|
+
if verdict in ("coexist", "unrelated"):
|
|
471
|
+
return create(new) # both live; different scope or subject
|
|
472
|
+
return old # self-repetition: no change at all
|
|
441
473
|
```
|
|
442
474
|
|
|
475
|
+
Keyword heuristics (negation flips, `always`/`never` pairs) may FLAG a pair
|
|
476
|
+
for the gate; they never decide it. The second measured counterexample is
|
|
477
|
+
why: *"Use Python"* and *"Never use production credentials"* share `use` and
|
|
478
|
+
a negation flip, and the old keyword rule could supersede one with the other
|
|
479
|
+
— two statements about different entities entirely. A number or unit change
|
|
480
|
+
("timeout is 30s" → "timeout is 60s") is a contradiction the negation
|
|
481
|
+
heuristic cannot see and the value comparison catches.
|
|
482
|
+
|
|
443
483
|
---
|
|
444
484
|
|
|
445
485
|
## Cross-Resource Learning Transfer
|