Write evidence back
An agent that reads but never writes leaves the corpus exactly as good as the day you seeded it. Writing back is what turns a retrieval system into one that learns.
What to write
Section titled “What to write”Conclusions, not transcripts. A corpus of logs is a corpus of noise, and noise costs you twice — once in retrieval quality, once in the tokens spent carrying it.
| Write | Do not write |
|---|---|
“Reconcile job needs fixtures regenerated when billing/reconcile.py changes” | the CI log |
| “Rolled back 2.4.0 — connection pool exhaustion under load” | the deploy transcript |
| “Superseded: the legacy queue is no longer used” | the whole design thread |
One sentence a future agent can act on, scoped to where it is true.
The call
Section titled “The call”vd.client.call_tool("evidence.ingest", evidence_args( agent="release-bot", items=[{ "source_type": "incident", "summary": "2.4.0 rolled back — connection pool exhaustion at 3x normal load", "scope_key": "service:checkout", }],))_, err := client.Ingest(ctx, "release-bot", []wire.EvidenceItem{{ SourceType: "incident", Summary: "2.4.0 rolled back — connection pool exhaustion at 3x normal load", ScopeKey: "service:checkout",}}, wire.IngestOptions{})The set you may send is
SourceTypePublic: run, pr, ci, deploy,
incident, doc, review, correction.
Immutable, and corrected by adding
Section titled “Immutable, and corrected by adding”Evidence is never edited or deleted. Correct by writing a correction that supersedes the earlier
claim — both survive, and an audit of a run from before the correction still shows what was true
then. That is what makes the audit trail worth having.
Idempotency, and the trap inside it
Section titled “Idempotency, and the trap inside it”Identity is derived from content, so replaying identical bytes does not create a second record. A retry after a response that was actually delivered is therefore safe.
The trap: reusing an idempotency key is necessary and not sufficient. The server derives dedupe
identity from the key together with the content, so an attempt that re-stamps any field —
event_ts above all — is different content and appends a second record even though the key matched.
Serialize the body once per logical call and replay those exact bytes. Nothing stamped per attempt: not
event_ts, not a timestamp insidemetadata, not a regenerated key.
The SDKs do this for you. If you build the call yourself, do not rebuild the payload inside the retry loop — a fresh timestamp per attempt is the subtle version of this bug, and the hash chain makes the duplicate permanent.
A caller-supplied idempotency_key is preserved, never overwritten: if you are deduping against
your own system you have already chosen the identity.
Batches partly succeed
Section titled “Batches partly succeed”A batch can be partly accepted. You get 200 with accepted[] and rejected[] — a successful
call with a mixed result.
Do not retry the whole batch. Replaying it re-sends records the server already committed, and your intent was “retry what failed”, not “send everything again”. Resubmit the rejected subset as a new logical call with its own frozen payload. The SDKs deliberately do not retry a partial batch.
A known gap, stated rather than hidden: a rejected record carries index, code and message
but no retryable flag, so you cannot distinguish a permanently invalid record from one that hit
a transient dependency without interpreting the code yourself. That is a deficiency in our contract,
not something you are missing.
Where in the loop
Section titled “Where in the loop”Both paths — acted and held:
try: result = do_the_thing() outcome = f"Applied: {result.summary}"except Exception as e: outcome = f"Failed: {e}"
vd.client.call_tool("evidence.ingest", evidence_args( agent="release-bot", items=[{"source_type": "run", "summary": outcome, "scope_key": scope}],))Failures are the valuable ones. A corpus that only records successes cannot warn anyone about anything.