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

Reflection to lessons

A finished run is full of evidence — traces, observations, outcomes — that no future run will ever read raw. reflect() distills that evidence into lessons: compact, typed, condition-tagged guidance that later queries inject automatically. Use this pattern at natural boundaries — a resolved ticket, a completed task, a closed session — so the next run starts where this one ended instead of rediscovering it.

How it works

Reflection reads the run's evidence (including any recorded outcomes, which tell it what worked, not just what happened) and extracts lessons. Each lesson carries:

FieldMeaning
contentThe guidance itself, written to be injected into a future prompt
lesson_typesuccess, failure, observation, rule, or preference
scoperun, session, or global — how widely it applies (see the scope ladder)
importancelow, medium, high, critical
conditionsWhen the lesson applies ("regex change touching the export pipeline")
rationaleWhy the reflection pass believes it
lesson_idStable ID, populated once persisted — pass it straight to record_outcome(reference_id=...)

Fresh lessons don't get blind trust. A validation gate scores each candidate against the run's evidence: it becomes active once its score crosses the acceptance threshold, is rejected at a low floor, and stays pending in between — exactly the behavior described in The learning loop. Lessons that keep proving useful (via outcome attribution) climb the scope ladder from run toward global.

You don't have to call reflect() yourself on every run: hosted instances can enable an auto-reflection cadence (after every N ingests, or on a negative-outcome streak), and the learn module reflects automatically when a scoped run ends.

💡Tip

Use the lesson_id returned by reflect() directly with record_outcome(). The lesson index has propagation delay — listing lessons immediately after reflecting can come back empty even though the lessons were stored.

Choosing what to reflect on

The reflect request scopes down four ways beyond the whole run. Typed support varies by SDK (unchanged through 0.12.0):

ScopeWire fieldPython reflect()JS reflect()Rust ReflectOptions
Whole run (default)run_id✓ (session_id=)✓ (session_id)✓ (run_id)
Include linked runsinclude_linked_runs
Last N items onlylast_n_items
One step's evidencestep_id
Since a checkpointcheckpoint_id
Fold in step outcomesinclude_step_outcomes
ℹ️Note

The fields marked — are wire-level fields on the reflect request that the typed helper does not forward. In JS, use the raw passthrough: client.advanced.reflect({ run_id, step_id: "step-2", include_step_outcomes: true }). In Python, client.advanced.reflect({...}) works from SDK v0.12.0 (the advanced domain gained every op); on 0.11.0 it does not mirror reflect, so send the field directly over the transport (POST /v2/control/reflect). Rust forwards all of them from ReflectOptions. See Step-wise reflection parameters.

Reflect on session close

A session is the lifecycle wrapper around runs (see Runs, sessions, and scopes), and closing one is the natural reflection boundary.

With the Python module helpers, the session context manager does it for you — reflection fires on clean exit:

import mubit
 
mubit.init(api_key=API_KEY, agent_id="ari")
 
with mubit.session("ari", session_id="nimbus:ari:ticket-1042"):
    ...  # wrapped LLM calls, remember()/recall() helpers
# auto_reflect=True (the default) triggers reflection here

Server-managed control sessions expose the same hook on the close call — reflect_on_close runs a final reflection before the session is closed:

client.advanced.close_session({
    "session_id": session_id,
    "reflect_on_close": True,
})

(JS: client.advanced.closeSession({...}); Rust: client.close_session(json!({...})) — the close op is payload-style in all three, wire field names as shown.)

Walkthrough

Ari resolves the flaky "export to CSV" ticket after two failed approaches and one that finally worked, with outcomes recorded along the way. Closing the loop turns the run into guidance:

result = client.reflect(session_id="nimbus:ari:ticket-1042")
 
print(result["summary"])                     # what the pass concluded
for lesson in result["lessons"]:
    print(lesson["lesson_type"], lesson["scope"], "-", lesson["content"])
    # e.g. failure  session - Streaming the full report into memory times out
    #                          past ~200k rows; batch the export instead.
    #      success  session - Batched exports (10k rows/chunk) complete reliably;
    #                          verify with the multi-line CSV fixture first.

The response also carries confidence, lessons_stored, and a degraded flag (reflection fell back without the LLM — treat the lessons as provisional).

On Ari's next export ticket, nothing extra is required: the accepted lessons ride into recall() and get_context() as the relevance-gated, token-budgeted lesson_overlay — even from another run — and the batching lesson changes the first attempt instead of the third. When that attempt succeeds in production, one record_outcome(reference_id=lesson_id, outcome="success", verified_in_production=True) pushes the lesson up the scope ladder toward Sana's org-wide digests.

When not to use it

  • Mid-task, every few calls — reflection is an LLM-backed pass; on a hot path prefer the instance's auto-reflection cadence, last_n_items for a cheap incremental pass, or reflect once at the boundary.
  • For hard invariants — reflection produces validated guidance, not guarantees. A rule that must always hold should be written explicitly as a rule entry; see Guardrails from failures.
  • Before outcomes exist — reflecting on a run with no recorded outcomes still works, but the lessons can only describe what happened, not what worked. Record the verdict first (the outcome attribution loop), then reflect.

Related