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:
| Field | Meaning |
|---|---|
content | The guidance itself, written to be injected into a future prompt |
lesson_type | success, failure, observation, rule, or preference |
scope | run, session, or global — how widely it applies (see the scope ladder) |
importance | low, medium, high, critical |
conditions | When the lesson applies ("regex change touching the export pipeline") |
rationale | Why the reflection pass believes it |
lesson_id | Stable 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.
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):
| Scope | Wire field | Python reflect() | JS reflect() | Rust ReflectOptions |
|---|---|---|---|---|
| Whole run (default) | run_id | ✓ (session_id=) | ✓ (session_id) | ✓ (run_id) |
| Include linked runs | include_linked_runs | ✓ | ✓ | ✓ |
| Last N items only | last_n_items | ✓ | — | ✓ |
| One step's evidence | step_id | — | — | ✓ |
| Since a checkpoint | checkpoint_id | — | — | ✓ |
| Fold in step outcomes | include_step_outcomes | — | — | ✓ |
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 hereServer-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:
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_itemsfor 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
ruleentry; 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
- The learning loop — where reflection sits between attribution and injection
- The outcome attribution loop — the verdicts that make reflection's input meaningful
- Runs, sessions, and scopes — runs vs sessions, and the ladder lessons climb
- The memory model — lessons among the other entry types
- Step-level outcomes — the dense signals
include_step_outcomesfolds in - Control HTTP reference — the wire contract for scoped reflection