The outcome attribution loop
Retrieval tells your agent which memories look relevant; it never finds out which ones actually helped. The outcome attribution loop closes that gap: capture the entry IDs behind each decision, get an objective verdict on the task, and feed the verdict back to exactly those entries with record_outcome(). Use this pattern in any agent that acts repeatedly on the same kind of task — it is the single highest-leverage habit for making run 50 better than run 1.
How it works
Four moves, one loop:
recall() ──► evidence [id₁, id₂, …] ──► agent acts ──► objective verdict
▲ (capture these) (test, gate, user)
│ │
└── future ranking shifts ◄── knowledge_confidence ◄── record_outcome(
entry_ids=[id₁, id₂, …])- Capture — every evidence item in a
recall()response carries anid. Hold on to the IDs of the entries your agent actually used. - Act — run the task as normal.
- Verdict — obtain an objective result: a test passed, a deploy gate cleared, the user explicitly confirmed. Never the model grading its own output (see the anti-pattern below).
- Attribute — call
record_outcome()with a primaryreference_id, the fullentry_idslist, asignalin[-1.0, 1.0], andverified_in_production=Truewhen the verdict came from live production.
Each credited entry's reinforcement counters update, its knowledge_confidence rises or falls, and future retrieval re-ranks accordingly — a memory that keeps producing failures stops winning recall, and a battle-tested one surfaces with the trust to match. The field-by-field contract is in The learning loop; how the credit shows up at read time is in Trust, confidence, and staleness.
The zero-code path
If you use the learn module, the loop is two lines. The wrapped LLM clients already track which entries were recalled for each call, so feedback() resolves the IDs for you:
import mubit.learn
mubit.learn.init(agent_id="ari") # wraps OpenAI / Anthropic / LiteLLM / Google GenAI clients
# ... your existing agent code runs unchanged ...
mubit.learn.feedback(good=True) # credits the entries recalled for the most recent callfeedback() takes exactly one signal source — good=True/False (maps to ±1.0), score= an explicit reward in [-1, 1], or response=/error= to derive the signal from a provider result. entry_ids= overrides the default (the entries recalled for the current span, falling back to the run's last recall), and verified_in_production=True is only ever set here, never automatically. The auto path deliberately records no outcome on its own: an HTTP 200 from the LLM says nothing about memory quality.
Automatic trace capture and span-level entry tracking are Python-only. The JS and Rust learn modules still expose the same one-liner for loop closure: learn.feedback({ good: true }) in JS, and session.feedback(1.0) (or feedback_entries(...) with explicit IDs and the production flag) in Rust — each credits the entries recalled for the most recent wrapped call. See the learn module.
Walkthrough
Nimbus's support agent Ari picks up another "CSV export times out" ticket, recalls what the fleet knows, ships a fix, and lets the nightly export job — not the model — decide whether the memories deserved credit:
reference_id is the entry the outcome is about — a lesson from an earlier reflect(), or the top evidence item that drove the decision. entry_ids carries everything else that contributed; the server skips unknown or out-of-scope IDs and never double-counts the primary. verified_in_production is the strongest trust signal an entry can earn — reserve it for verdicts observed in live production, not tests or staging (see Production verification).
The outcome request also accepts an idempotency_key so a retried call is applied at most once. From SDK v0.12.0 the typed helpers forward it directly in all three languages (record_outcome(..., idempotency_key="...") / recordOutcome({ ..., idempotency_key }) / RecordOutcomeOptions.idempotency_key); on 0.11.0 and earlier it is a wire-level field no typed helper forwards — send it directly over the transport (POST /v2/control/outcome). See the outcome request shape.
Never let the model grade itself
Do not route the model's self-assessment ("that looks correct to me") into record_outcome(). A model rewarding its own output is reward-hacking your memory: confidently wrong answers get credited, the entries behind them gain knowledge_confidence, and retrieval starts preferring exactly the memories that mislead. Every signal should trace to something the model cannot flatter — a test result, a gate check, a downstream metric, or an explicit user confirmation. If no objective verdict exists, record nothing: reflection still learns from the raw evidence without a fabricated reward.
Step-level rewards
Run-level outcomes say whether the run worked; step-level outcomes say which step mattered. record_step_outcome attaches a per-step signal (plus an optional directive_hint stating what should have been different) that reflection folds into step-attributed lessons. From SDK v0.12.0 it is a typed helper in all three languages — client.record_step_outcome(step_id=..., outcome=..., ...) / client.recordStepOutcome({...}) / RecordStepOutcomeOptions in Rust. On 0.11.0 the Python/JS calls are top-level wire pass-throughs with the same call shape (wire field names, ambient run ID auto-injected), so the code is identical either way. See Step-level outcomes for the full recipe.
When not to use it
| Situation | Do instead |
|---|---|
| No objective verdict exists for the task | Skip the outcome; let reflection learn from the evidence alone |
| The only available judge is the model itself | Same — an unverified guess recorded as fact poisons ranking |
| One-off task in a throwaway run | Attribution pays off through repetition; a run nobody revisits earns nothing from it |
| You want per-step credit inside one run | record_step_outcome, then run-level attribution at the end |
Related
- The learning loop — the outcome field reference and where attribution sits in the loop
- Trust, confidence, and staleness — how credited outcomes surface as
knowledge_confidenceat read time - Reflection to lessons — distilling the attributed run into reusable lessons
- Guardrails from failures — what negative attribution builds over time
- Self-optimizing prompts — attributed outcomes as the optimizer's raw material
- Step-level outcomes — dense process rewards within a run
- Support agent loop — the loop end-to-end in a working agent
- Control HTTP reference — the wire contract