Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content
Learning from outcomes

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₂, …])
  1. Capture — every evidence item in a recall() response carries an id. Hold on to the IDs of the entries your agent actually used.
  2. Act — run the task as normal.
  3. 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).
  4. Attribute — call record_outcome() with a primary reference_id, the full entry_ids list, a signal in [-1.0, 1.0], and verified_in_production=True when 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 call

feedback() 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.

ℹ️Note

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:

# (1) Recall, and capture the entry IDs behind the answer.
answer = client.recall(
    session_id="nimbus:ari:ticket-1042",
    query="How do we fix CSV export timeouts on large reports?",
    entry_types=["lesson", "fact"],
)
entry_ids = [e["id"] for e in answer.get("evidence", [])]
 
# (2) Act — Ari ships the batched-exporter fix.
deploy_fix()
 
# (3) Objective verdict: the production export job, not the model's opinion.
verdict = nightly_export_completed()          # True / False from the pipeline
 
# (4) Credit exactly the memories that contributed.
client.record_outcome(
    session_id="nimbus:ari:ticket-1042",
    reference_id=entry_ids[0],                # the entry the decision hinged on
    outcome="success" if verdict else "failure",
    signal=0.8 if verdict else -0.8,
    entry_ids=entry_ids,                      # every entry that informed the fix
    verified_in_production=verdict,           # verdict came from the live pipeline
    rationale="Nightly export completed in 41s (was timing out at 300s)",
)

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).

ℹ️Note

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

⚠️Warning

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

SituationDo instead
No objective verdict exists for the taskSkip the outcome; let reflection learn from the evidence alone
The only available judge is the model itselfSame — an unverified guess recorded as fact poisons ranking
One-off task in a throwaway runAttribution pays off through repetition; a run nobody revisits earns nothing from it
You want per-step credit inside one runrecord_step_outcome, then run-level attribution at the end

Related