# Mubit Documentation — full text > Generated from the same sources as https://docs.mubit.ai/llms.txt. Sections appear in navigation order. --- # What is Mubit > Mubit enables continual learning for AI agents, powered by durable memory and cross-agent knowledge sharing. Mubit enables continual learning for AI agents, powered by durable memory and cross-agent knowledge sharing. It stores what agents experience, extracts reusable lessons, and surfaces the right context before every LLM call — so agents get better over time without retraining. Memory and coordination work together: agents share knowledge through handoffs, learn from outcomes, and carry lessons across sessions. ## Why Mubit - **[Agents that learn](/sdk/how-mubit-works)** — Every interaction becomes memory. Mubit extracts lessons from what worked and what didn't, then surfaces them before the next LLM call. - **[Context that fits](/sdk/retrieve-data)** — Token-budgeted context assembly gives your LLM exactly the right facts, lessons, and rules — no overflow, no guessing. - **[Agents that coordinate](/recipes/multi-agent-shared-state)** — Register agents, scope memory access, and pass work between them with structured handoffs and feedback. - **[Works with your stack](/sdk/framework-integrations)** — Drop-in adapters for CrewAI, LangGraph, LangChain, LlamaIndex, Google ADK, Agno, Vercel AI SDK, and MCP. Or use the SDK directly. ## Pick your path - **[Add memory to your agent](/getting-started)** — Two lines of setup. Your LLM calls automatically learn. - **[Give your agent a learning loop](/patterns/outcome-attribution-loop)** — Close the loop between what your agent recalled and how the task went. - **[Build a multi-agent team](/patterns/memory-backed-handoff)** — Handoffs, shared team memory, and subagent isolation. - **[Understand the memory model](/concepts/memory-model)** — Semantic, episodic, procedural, and working memory — and the entry types behind them. - **[Drop into your framework](/sdk/framework-integrations)** — CrewAI, LangGraph, LangChain, LlamaIndex, ADK, Agno, Vercel AI, MCP. - **[API reference](/api-reference/control-http)** — Full HTTP and gRPC control surface. ## Two ways to use Mubit - **[SDK-first](/getting-started)** — Embed Mubit in your app with `remember`, `recall`, `getContext`, `reflect`. Your code owns the agent config. Best for prototypes and CI-driven workflows. - **[Managed Mubit](/sdk/projects-and-agents)** — Configure agents declaratively as **Projects** + **Agent Cards**. Version prompts and skills server-side with candidate → active promotion. Best for teams that want review and rollback. --- # Quickstart > Make your first authenticated Mubit call in under 60 seconds. Make your first authenticated Mubit call in under 60 seconds. The terminal on the right runs the same code — a real round-trip against `api.mubit.ai` showing every payload the SDK returns. Switch the terminal between Python, Node, and Rust with the buttons in the title bar. ## Get your API key Sign in to the [Mubit console](https://console.mubit.ai) and create a key from **Settings → API keys**. Keys look like `mbt___` — three segments separated by underscores. The instance segment routes your call to the right region; the key id is safe to log; the secret is not. ```bash title=".env" MUBIT_API_KEY="mbt___" MUBIT_ENDPOINT="https://api.mubit.ai" ``` The SDK reads both env vars on `Client()`. Set `MUBIT_TRANSPORT="grpc"` if you need lower latency from a backend service — see [gRPC Transport](/sdk/grpc-transport). ## Install #### Python ```bash pip install mubit-sdk python-dotenv ``` #### Node.js ```bash npm install @mubit-ai/sdk dotenv ``` #### Rust ```bash cargo add mubit-sdk dotenvy tokio --features tokio/full ``` #### Other ```bash bun add @mubit-ai/sdk # Bun pnpm add @mubit-ai/sdk # pnpm yarn add @mubit-ai/sdk # Yarn deno add npm:@mubit-ai/sdk # Deno ``` ## Authenticate and make your first call The script below stores a memory in one session and recalls it from a second session — proving cross-session memory works without any local cache. The right-hand terminal runs this exact flow. #### Python ```python title="v1_support.py" import os from dotenv import load_dotenv import mubit load_dotenv() client = mubit.Client(endpoint=os.environ["MUBIT_ENDPOINT"]) client.set_api_key(os.environ["MUBIT_API_KEY"]) client.remember( session_id="support:taylor:s1", agent_id="support-agent", user_id="taylor-1", content="Taylor prefers concise written updates on Friday afternoons; no phone calls.", intent="lesson", lesson_scope="global", ) answer = client.recall( session_id="support:taylor:s2", agent_id="support-agent", user_id="taylor-1", query="how does Taylor like updates?", entry_types=["lesson"], ) print(answer["final_answer"]) ``` #### Node.js ```js title="v1_support.mjs" import "dotenv/config"; import { Client } from "@mubit-ai/sdk"; const client = new Client({ endpoint: process.env.MUBIT_ENDPOINT }); client.setApiKey(process.env.MUBIT_API_KEY); await client.remember({ session_id: "support:taylor:s1", agent_id: "support-agent", user_id: "taylor-1", content: "Taylor prefers concise written updates on Friday afternoons; no phone calls.", intent: "lesson", lesson_scope: "global", }); const answer = await client.recall({ session_id: "support:taylor:s2", agent_id: "support-agent", user_id: "taylor-1", query: "how does Taylor like updates?", entry_types: ["lesson"], }); console.log(answer.final_answer); ``` #### Rust ```rust title="src/main.rs" use mubit_sdk::{Client, ClientConfig, RecallOptions, RememberOptions}; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); let client = Client::new(ClientConfig::from_env())?; let mut r = RememberOptions::new( "Taylor prefers concise written updates on Friday afternoons; no phone calls." ); r.run_id = Some("support:taylor:s1".into()); r.agent_id = Some("support-agent".into()); r.user_id = Some("taylor-1".into()); r.intent = Some("lesson".into()); r.lesson_scope = Some("global".into()); client.remember(r).await?; let mut q = RecallOptions::new("how does Taylor like updates?"); q.run_id = Some("support:taylor:s2".into()); q.agent_id = Some("support-agent".into()); q.user_id = Some("taylor-1".into()); q.entry_types = vec!["lesson".into()]; let answer = client.recall(q).await?; println!("{}", answer["final_answer"]); Ok(()) } ``` #### curl ```bash curl -X POST https://api.mubit.ai/v2/control/ingest \ -H "Authorization: Bearer $MUBIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "run_id": "support:taylor:s1", "agent_id": "support-agent", "items": [ { "item_id": "taylor-friday-updates", "content_type": "text/plain", "text": "Taylor prefers concise written updates on Friday afternoons; no phone calls.", "intent": "lesson", "lesson_scope": "global", "user_id": "taylor-1", "source": "agent" } ] }' curl -X POST https://api.mubit.ai/v2/control/query \ -H "Authorization: Bearer $MUBIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "run_id": "support:taylor:s2", "agent_id": "support-agent", "user_id": "taylor-1", "query": "how does Taylor like updates?", "entry_types": ["lesson"], "mode": "AGENT_ROUTED", "limit": 5 }' ``` A successful `recall` returns roughly: ```json { "final_answer": "Taylor prefers to receive updates in a concise written format delivered on Friday afternoons, and explicitly dislikes receiving phone calls.", "confidence": 1.0, "mode": "agent_routed", "evidence": [ { "id": "f7bfcb26-…", "score": 1.0, "content": "Taylor prefers concise written updates on Friday afternoons; no phone calls." } ] } ``` Didn't work? See [Troubleshooting](/sdk/troubleshooting). The two most common causes are a stale `MUBIT_API_KEY` and an HTTP/gRPC endpoint mismatch. > **Note:** **Cross-session footgun:** facts (`intent="fact"`) are session-local. To recall a memory in a different session keyed by `user_id`, store it as a `lesson` with `lesson_scope="global"` — that's why the example above uses `intent="lesson"`. See [SDK helpers](/sdk/sdk-helpers#cross-session-recall) for the full set. ## Pick your path Most readers want the drop-in. Switch only when you outgrow it. - **[Drop-in (Recommended)](#drop-in-mubitlearn)** — `mubit.learn.init()` auto-instruments your existing `anthropic` / `openai` / `litellm` / `google-genai` calls. Lessons inject before the call, reflection runs on session end. - **[Manual SDK](/sdk/sdk-helpers)** — Call `remember`, `recall`, `get_context`, `archive`, `dereference` yourself when you want explicit control over sessions, agents, and metadata. - **[Framework adapter](/sdk/framework-integrations)** — Plug Mubit into the framework's own memory interface. CrewAI, LangGraph, LangChain, LlamaIndex, ADK, Agno, Vercel AI SDK, MCP. ### Drop-in: `mubit.learn` ```python title="learn_quickstart.py" import os import mubit.learn import anthropic mubit.learn.init( api_key=os.environ["MUBIT_API_KEY"], agent_id="code-reviewer", auto_reflect=True, ) # Existing Anthropic call now learns: lessons inject pre-call, reflection runs on session end. resp = anthropic.Anthropic().messages.create( model="claude-haiku-4-5-20251001", max_tokens=300, messages=[{"role": "user", "content": "Review this diff..."}], ) ``` `mubit.learn.init()` patches `anthropic.Anthropic`, `openai.OpenAI`, LiteLLM, and Google GenAI client classes in place. See [LLM provider support](/sdk/llm-providers) for the full matrix and per-provider notes. ## The core loop, end to end Everything in Mubit hangs off one loop: store → recall → act → record the real outcome → recall better next time. Here it is in five calls: ```python title="core_loop.py" import mubit client = mubit.Client(endpoint="https://api.mubit.ai") client.set_api_key("mbt_...") client.set_run_id("onboarding-demo") # 1. Store a lesson (any experience works — traces, facts, observations) client.learned("Always check the customer's plan tier before quoting limits.") # 2. Recall before acting — evidence comes back with ids result = client.recall(query="What should I check before quoting a limit?") evidence_ids = [e["id"] for e in result.get("evidence", [])] # 3. Act on it (your agent does its thing)... # 4. Close the loop with the REAL outcome — a test result, a gate, # a user confirmation — credited to the exact memories used. client.record_outcome( reference_id=evidence_ids[0], outcome="success", signal=1.0, entry_ids=evidence_ids, ) # 5. Next recall ranks what actually worked higher. That's the whole trick. ``` Step 4 is what separates Mubit from a vector store: outcomes attach to the *specific memories that were used*, so useful knowledge gains confidence and misleading knowledge decays. The full pattern — including what never to feed it — is in [The outcome-attribution loop](/patterns/outcome-attribution-loop). ## What to do next - Get the mental model in [Concepts](/concepts) — [the memory model](/concepts/memory-model), [retrieval semantics](/concepts/retrieval-semantics), and [the learning loop](/concepts/learning-loop). - Pick a [pattern](/patterns) that matches your problem — handoffs, shared team memory, bi-temporal facts, context assembly. - Inspect the full helper surface in [SDK helpers](/sdk/sdk-helpers) — `remember`, `recall`, `get_context`, `archive`, `dereference`. - Add multi-agent coordination with [register_agent + handoff + feedback](/recipes/multi-agent-shared-state). - Use [temporal queries](/sdk/sdk-methods#temporal-queries) to filter by when events happened, not when they were ingested. - Plug into your framework via [Framework integrations](/sdk/framework-integrations). - Browse the full control surface at [Control HTTP reference](/api-reference/control-http). _(Interactive terminal demo — view the live page for the animated walkthrough.)_ --- # Concepts > How Mubit's memory model works, what the learning loop does, and how it compares to vector DBs, prompt caches, and RAG. How Mubit's memory model works, what the learning loop does, and how it compares to vector DBs, prompt caches, and RAG. Mubit's job is to give AI agents memory that persists, evolves, and is queryable across runs. This page is an evaluator's overview — read it to decide whether Mubit fits your problem before you write any integration code. ## The memory model Mubit organizes memory by **entry type**. Each type has different retention, retrieval, and access semantics. | Entry type | What it stores | Lifetime | Recall scope | | --- | --- | --- | --- | | **fact** | Atomic statements grounded in observation (e.g. "Customer Taylor uses SSO") | Run-scoped by default | Same `session_id` + `agent_id` | | **lesson** | Generalized takeaways from runs (e.g. "Always flag bare `except:`") | Configurable: `run` / `session` / `global` | Cross-session if `lesson_scope="global"` | | **rule** | Hard constraints that must always apply (e.g. "Never PII-log a `user.ssn`") | Persistent | Cross-session | | **trace** | Per-call records of what happened (LLM call, tool call, response) | Run-scoped | Same `session_id` | | **archive_block** | Exact artifacts you'll need byte-for-byte later (diffs, SQL, raw outputs) | Persistent | Cross-session via `reference_id` | Storing a fact and asking the wrong session to recall it returns nothing. Storing a global lesson and asking any session keyed by the same `user_id` returns it. This is the most common shape of "why didn't my recall work" — see [SDK helpers → cross-session recall](/sdk/sdk-helpers#cross-session-recall). ### More entry types for richer agents The five types above are what most agents start with. The full taxonomy is larger — multi-agent coordination, tool-call audit, and process-reward learning each have their own entry types. | Entry type | What it captures | | --- | --- | | **observation** | Raw agent observations during a run | | **tool_input** / **tool_output** | LLM tool call request and response | | **reflection** | Notes a reflection pass produced before lessons crystallize | | **task_result** | Outcome of a discrete sub-task | | **log** | Free-form structured log entries | | **checkpoint** | Snapshot of context for later restore — see [SDK methods → helper catalog](/sdk/sdk-methods#current-helper-catalog) | | **step_outcome** | Per-step success/failure for process-reward learning — see [Step-level outcomes](/recipes/step-level-outcomes) | | **mental_model** | Curated high-priority summary, retrieved first | | **handoff** | Inter-agent task handoff record — see [Lane-scoped memory](/recipes/lane-scoped-memory) | | **feedback** | Verdict on a handoff | ## The learning loop The four-step loop is what differentiates Mubit from a passive store. Read [How Mubit works](/sdk/how-mubit-works) for the full discussion. 1. **Write** Your agent calls `remember()` to store facts, traces, or observations as they happen. 2. **Retrieve** Before the next LLM call, `recall()` finds relevant evidence and `get_context()` assembles a token-budgeted context block. 3. **Reflect** After a run completes, `reflect()` extracts reusable lessons from what happened. Fresh lessons start as candidates and are validated before they're trusted — a lesson stays pending until enough evidence accrues, then is promoted (run → session → global) or marked stale. See [How Mubit works](/sdk/how-mubit-works) for details. 4. **Reinforce** `record_outcome()` feeds success / failure signals back into the lesson store, strengthening what worked and weakening what didn't. The agent gets better over multiple runs without retraining the model. ## How Mubit compares | If you need… | Reach for… | Mubit's role | | --- | --- | --- | | Similarity search over a static corpus | A vector DB (Pinecone, Weaviate, pgvector) | Not the right tool — Mubit is for **mutable** memory with outcomes | | Prompt-level caching of identical requests | A prompt cache (Anthropic prompt cache, Helicone) | Orthogonal — Mubit can sit alongside | | Stateless RAG against a fixed knowledge base | A RAG framework (LlamaIndex, LangChain) | Use the framework + Mubit as its persistent store via the [adapters](/sdk/framework-integrations) | | Memory that persists across sessions and evolves | **Mubit** | The whole product | | Multi-agent coordination with scoped access | **Mubit** (`register_agent`, `handoff`, `feedback`) | Native primitives | ## Trust and operations - **Authentication.** All calls use a bearer API key formatted as `mbt___`. The instance segment routes to a region; the key id is safe to log; the secret is not. See [Authentication](/sdk/authentication). - **Scopes.** Each agent registers with explicit `read_scopes` and `write_scopes` (`fact`, `lesson`, `rule`, `archive_block`, etc). The runtime enforces them. - **Rate limits.** Per-instance, per-route. See [Rate limits](/sdk/rate-limits). - **Audit.** Every write produces an activity entry. See [Activity & audit trail](/sdk/activity-audit). - **Data residency.** Instance data is stored in the region where the instance is provisioned (EU or US), and the instance segment of your API key routes requests to that region. ## Where to go next - **[How Mubit works (deep dive)](/sdk/how-mubit-works)** — The full memory model, control-plane vs. core-plane, query and context controls, and a worked helper-first example. - **[Quickstart](/getting-started)** — Decided to integrate? Make your first authenticated call in under 60 seconds. - **[Framework integrations](/sdk/framework-integrations)** — Plug Mubit into CrewAI, LangGraph, LangChain, LlamaIndex, ADK, Agno, Vercel AI SDK, or MCP. - **[Pricing](https://mubit.ai/pricing)** — Plans and per-instance limits. --- # Release channels The SDKs ship on two channels from the same source. Which one you are on is decided by how you install, and you stay on it until you change that. | | Stable | Pre-release | | --- | --- | --- | | Python | `pip install mubit-sdk` | `pip install --pre mubit-sdk` | | JavaScript | `npm install @mubit-ai/sdk` | `npm install @mubit-ai/sdk@next` | | Rust | `cargo add mubit-sdk` | `cargo add mubit-sdk@0.14.0-rc.1` | | Docs | [Stable](/) | [Pre-release](/pre-release) | ## Stable What every ordinary install resolves to. Breaking changes land here only on a major version, and the documentation at the site root describes this channel. ## Pre-release The next SDK line, published to the same registries and downloadable by anyone — but never the default. A pre-release is what you install when you want to try the next line early, or when you are running the local evaluation bundle, whose SDKs are cut from it. Nobody lands on it by accident. `pip` skips pre-releases unless you pass `--pre` or pin the exact version, `cargo` will not resolve one for an ordinary version requirement, and on npm the pre-release sits under the `next` dist-tag while `latest` stays on the last stable release. Pin the exact version if you want a pre-release to stay put: ```bash pip install 'mubit-sdk==0.14.0rc1' npm install @mubit-ai/sdk@0.14.0-rc.1 cargo add mubit-sdk@0.14.0-rc.1 ``` :::note PyPI stores versions in their normalized form, so the release cut as `0.14.0-rc.1` appears there as `0.14.0rc1`. It is the same release under the spelling each ecosystem expects. ::: ## Moving between them The wire contract is the same on both channels, so a pre-release client talks to a stable instance and the other way round. Moving back is a plain install of the stable version — there is no migration step and nothing is written to your instance in a pre-release-only format. The [migration guide](/pre-release/sdk/migration) covers what changed in the pre-release line, and [versioning](/pre-release/sdk/versioning) covers the compatibility rules. --- # Framework Integrations > Native Mubit memory adapters for popular agent frameworks. This page is a reference index; full walkthroughs live on the per-framework pages. Native Mubit memory adapters for popular agent frameworks. This page is a reference index; full walkthroughs live on the per-framework pages. Mubit ships native adapters for popular agent frameworks. Each adapter plugs into the framework's own memory or store interface, so agents pick up persistent semantic memory and cross-session learning without restructuring how they're built. Pick a framework, copy the minimal snippet, then jump to the full walkthrough on the framework's page. ## At a glance | Framework | Language | Adapter pattern | Install | | --- | --- | --- | --- | | **CrewAI** | Python | `StorageBackend` for unified Memory | `pip install mubit-crewai[crewai]` | | **LangGraph** | Python | `BaseStore` with batch ops | `pip install mubit-langgraph[langgraph]` | | **LangGraph** | JS | `BaseStore` with async ops | `npm install @mubit-ai/langgraph` | | **LangChain** | Python | `BaseMemory` / `MubitChatMemory` | `pip install mubit-langchain[langchain]` | | **LlamaIndex** | Python | `BaseMemory` via `MubitChatMemoryBuffer` | `pip install mubit-llama-index[llama-index]` | | **Google ADK** | Python | `BaseMemoryService` for Runner | `pip install mubit-adk[adk]` | | **Agno** | Python | `MemoryDb` + `Toolkit` | `pip install mubit-agno[agno]` | | **AutoGen** | Python | `autogen_core.memory.Memory` provider + group-chat hook | `pip install "mubit-autogen[autogen]"` | | **Agent Lightning** | Python | `Hook` on `Trainer` + reward emitter | `pip install "mubit-agent-lightning[agent-lightning]"` | | **Vercel AI SDK** | JS | `wrapLanguageModel()` middleware | `npm install @mubit-ai/ai-sdk` | | **MCP** | Any | 21 tools over stdio transport | `npm install @mubit-ai/mcp` | All adapters use the canonical Mubit SDK transport internally. Every Python adapter exposes the full MAS surface — see [Feature parity matrix](#feature-parity-matrix). > **Note:** **Version compatibility:** these adapters depend on the canonical `mubit-sdk` via `mubit-integration-base`. Always upgrade integration packages alongside the core SDK. --- ## CrewAI Routes CrewAI's unified Memory system through Mubit. Agent observations persist across runs and are queryable across crews. ```bash pip install mubit-crewai[crewai] ``` ```python title="crewai_minimal.py" from mubit_crewai import MubitCrewMemory from crewai import Crew, Process memory = MubitCrewMemory(api_key="mbt_...", session_id="crew-run-1") crew = Crew( agents=[researcher, writer], tasks=[research_task, write_task], process=Process.sequential, memory=memory.as_crew_memory(), ) crew.kickoff(inputs={"topic": "AI safety"}) ``` Full walkthrough → [/sdk/integrations/crewai](/sdk/integrations/crewai) --- ## LangGraph Use Mubit as a persistent store for LangGraph `StateGraph` nodes. Each node reads or writes through `PutOp` / `SearchOp`. ```bash pip install mubit-langgraph[langgraph] ``` ```python title="langgraph_minimal.py" from mubit_langgraph import MubitStore from langgraph.store.base import PutOp, SearchOp store = MubitStore(api_key="mbt_...") NS = ("memories", "user-1", "session-1") store.batch([PutOp(namespace=NS, key="finding-1", value={"text": "...", "intent": "lesson"})]) results = store.batch([SearchOp(namespace_prefix=NS, query="security issues", limit=5)]) graph.compile(store=store) ``` Full walkthrough → [/sdk/integrations/langgraph](/sdk/integrations/langgraph) --- ## LangChain Drop-in `BaseMemory` for any LangChain chain. Loads context before each call and saves interactions after; cross-session memory is automatic via Mubit's semantic retrieval. ```bash pip install mubit-langchain[langchain] ``` ```python title="langchain_minimal.py" from mubit_langchain import MubitChatMemory from langchain_openai import ChatOpenAI memory = MubitChatMemory(api_key="mbt_...", session_id="chat-1") llm = ChatOpenAI(model="gpt-4o-mini") context = memory.load_memory_variables({"input": "What happened yesterday?"}) # build messages with context["history"], call LLM memory.save_context({"input": question}, {"output": response}) ``` Full walkthrough → [/sdk/integrations/langchain](/sdk/integrations/langchain) --- ## LlamaIndex Exposes `MubitChatMemoryBuffer`, a `BaseMemory`-style adapter that backs LlamaIndex chat memory with Mubit's persistent semantic retrieval across sessions. ```bash pip install mubit-llama-index[llama-index] ``` ```python title="llamaindex_minimal.py" from llama_index.core import Settings from mubit_llama_index import MubitChatMemoryBuffer Settings.memory = MubitChatMemoryBuffer(api_key="mbt_...", session_id="session-1") ``` Full walkthrough → [/sdk/integrations/llamaindex](/sdk/integrations/llamaindex) --- ## Google ADK Plug Mubit into ADK's `Runner` as a `BaseMemoryService`. Session events are auto-ingested; memory search enriches agent context on the next turn. ```bash pip install mubit-adk[adk] ``` ```python title="google_adk_minimal.py" from mubit_adk import MubitMemoryService from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService memory = MubitMemoryService(api_key="mbt_...") runner = Runner( agent=root_agent, app_name="travel", session_service=InMemorySessionService(), memory_service=memory, ) ``` Full walkthrough → [/sdk/integrations/google-adk](/sdk/integrations/google-adk) --- ## Agno Two integration surfaces: `MemoryDb` for Agno's built-in memory system, and a `Toolkit` with LLM-callable memory tools. ```bash pip install mubit-agno[agno] ``` ```python title="agno_minimal.py" from agno.agent import Agent from agno.memory.v2.memory import Memory from mubit_agno import MubitAgnoMemory mubit = MubitAgnoMemory(api_key="mbt_...", session_id="run-1") agent = Agent( name="Assistant", memory=Memory(db=mubit.as_memory_db()), tools=[mubit.as_toolkit()], enable_agentic_memory=True, ) agent.run("What do we know about the production database?") ``` Full walkthrough → [/sdk/integrations/agno](/sdk/integrations/agno) --- ## AutoGen `MubitMemory` implements `autogen_core.memory.Memory`: attach it to an `AssistantAgent` and recall is injected as a `SystemMessage` before every model turn, while new content persists via `add()`. `MubitGroupChatHook` forwards multi-agent handoff/feedback signals. Targets `autogen-core` / `autogen-agentchat` 0.7.x (not the legacy `pyautogen` 0.2 line). ```bash pip install "mubit-autogen[autogen]" ``` ```python title="autogen_minimal.py" from autogen_agentchat.agents import AssistantAgent from autogen_ext.models.openai import OpenAIChatCompletionClient from mubit_autogen import MubitMemory memory = MubitMemory(api_key="mbt_...", session_id="support-1", agent_id="support") agent = AssistantAgent( name="support", model_client=OpenAIChatCompletionClient(model="gpt-4o"), memory=[memory], # Mubit recall injected before every model turn ) ``` Full walkthrough → [/sdk/integrations/autogen](/sdk/integrations/autogen) --- ## Agent Lightning Bridges Agent Lightning rollout rewards into Mubit's attribution loop. `MubitHook` (a subclass of `agentlightning.Hook`) recalls lessons at `on_rollout_start` and credits the recalled `entry_ids` via `record_outcome` at `on_rollout_end`; `MubitRewardEmitter` gives custom training loops explicit `emit_reward` / `emit_event` / `emit_step_outcome` calls. ```bash pip install "mubit-agent-lightning[agent-lightning]" ``` ```python title="agl_minimal.py" import agentlightning as agl from mubit_agent_lightning import MubitHook hook = MubitHook(api_key="mbt_...", session_id="my-rl-run") trainer = agl.Trainer(..., hooks=[hook]) # recall at rollout start, credit at rollout end trainer.fit(my_lit_agent, ...) ``` Full walkthrough → [/sdk/integrations/agent-lightning](/sdk/integrations/agent-lightning) --- ## Vercel AI SDK Middleware that wraps any AI SDK model with automatic memory injection and interaction capture. ```bash npm install @mubit-ai/ai-sdk ``` ```js title="vercel_ai_sdk_minimal.mjs" import { wrapLanguageModel, generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { mubitMemoryMiddleware } from "@mubit-ai/ai-sdk"; const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: mubitMemoryMiddleware({ apiKey: process.env.MUBIT_API_KEY, sessionId: "session-1", agentId: "support-agent", }), }); await generateText({ model, prompt: "How do I reset my password?" }); ``` Full walkthrough → [/sdk/integrations/vercel-ai-sdk](/sdk/integrations/vercel-ai-sdk) --- ## MCP Exposes Mubit as 21 tools over MCP stdio transport. Usable by any MCP-compatible agent (Claude Desktop, Cursor, custom agents). ```bash npm install @mubit-ai/mcp ``` ```bash title="mcp_minimal.sh" MUBIT_API_KEY=$MUBIT_API_KEY MUBIT_ENDPOINT=https://api.mubit.ai npx @mubit-ai/mcp ``` Tools available: `mubit_remember`, `mubit_recall`, `mubit_context`, `mubit_forget`, `mubit_archive`, `mubit_dereference`, `mubit_reflect`, `mubit_learned`, `mubit_lessons`, `mubit_checkpoint`, `mubit_outcome`, `mubit_step_outcome`, `mubit_strategies`, `mubit_register_agent`, `mubit_list_agents`, `mubit_handoff`, `mubit_feedback`, `mubit_diagnose`, `mubit_memory_health`, `mubit_ingest_status`, `mubit_status`. Full walkthrough → [/sdk/integrations/mcp](/sdk/integrations/mcp) --- ## Common MAS extensions Every adapter exposes these Mubit-specific methods beyond the base framework interface: | Method | Purpose | | --- | --- | | `checkpoint()` | Save a snapshot of memory state | | `record_outcome()` | Record success / failure with an RL-style signal | | `surface_strategies()` | Cluster recurring lessons into reusable strategy descriptors | | `register_agent()` | Register an agent with role, scopes, capabilities | | `handoff()` | Transfer control between agents (requires `task_id`) | | `feedback()` | Submit a verdict on a handoff | | `diagnose()` | Surface failure-path lessons for debugging | | `get_context()` | Fetch a token-budgeted context block | | `reflect()` | Extract lessons from session evidence | | `lessons()` | List lessons with optional filtering | | `archive()` | Store an exact reusable artifact with a stable `reference_id` | | `dereference()` | Fetch exact content by `reference_id` | ## Feature parity matrix All Python adapters expose the full Mubit MAS surface through the shared `mubit-integration-base` client. JS/TS adapters expose the same surface in `camelCase`. | Method | CrewAI | LangGraph (Py) | LangChain | LlamaIndex | Google ADK | Agno | AutoGen | Agent Lightning | Vercel AI | MCP | | --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | | `remember` / `ingest` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ `add()` | ✅ `emit_event` | ✅ auto | ✅ `mubit_remember` | | `recall` / `query` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ auto + `query()` | ✅ auto (hook) + `recall()` | ✅ auto | ✅ `mubit_recall` | | `get_context` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | via client | via client | ✅ | ✅ `mubit_context` | | `checkpoint` | ✅ | ✅ | ✅ | via client | ✅ | ✅ | via client | via client | via client | ✅ `mubit_checkpoint` | | `record_outcome` | ✅ | ✅ | ✅ | via client | ✅ | ✅ | ✅ | ✅ `emit_reward` | via client | ✅ `mubit_outcome` | | `surface_strategies` | ✅ | ✅ | ✅ | via client | ✅ | ✅ | via client | via client | via client | ✅ `mubit_strategies` | | `register_agent` / `list_agents` | ✅ | ✅ | ✅ | via client | ✅ | ✅ | via client | via client | via client | ✅ | | `handoff` / `feedback` | ✅ | ✅ | ✅ | via client | ✅ | ✅ | ✅ group-chat hook | via client | via client | ✅ | | `diagnose` | ✅ | ✅ | ✅ | via client | ✅ | ✅ | via client | via client | via client | ✅ `mubit_diagnose` | | `reflect` | ✅ | ✅ | ✅ | via client | ✅ | ✅ | via client | via client | ✅ on run end | ✅ `mubit_reflect` | | `archive` / `dereference` | ✅ | ✅ | ✅ | via client | ✅ | ✅ | via client | via client | via client | ✅ | **"via client"** means the adapter forwards the method to its underlying `MubitControlClient` (e.g. `MubitChatMemoryBuffer(...).record_outcome(...)` on LlamaIndex, `mubit.client.diagnose(...)` on Vercel AI SDK). The framework-facing surface stays focused on the memory contract the framework expects, while the full MAS coordination remains available in the same process. --- # How Mubit works > How your agents go from stateless to self-improving with Mubit's memory loop. How your agents go from stateless to self-improving with Mubit's memory loop. Mubit is a memory engine for AI agents. It stores what your agents learn, retrieves the right context before each LLM call, and extracts reusable lessons so agents improve over time. Everything runs behind a single control-plane boundary — your agent talks to Mubit, Mubit talks to storage. ## Overview The core loop has four steps: 1. **Write** Your agent calls `remember()` to store facts, traces, or observations as they happen. 2. **Retrieve** Before the next LLM call, `recall()` finds relevant evidence and `getContext()` assembles a token-budgeted context block with rules, lessons, and facts. 3. **Reflect** After a run completes, `reflect()` extracts reusable lessons from what happened. Recurring lessons get promoted from run-scoped to session-scoped to global. A freshly reflected lesson may start as a pending candidate and is only promoted to active once its evidence score crosses the acceptance threshold (default 0.6; low-scoring ones are rejected at or below 0.25 and down-weighted), so newly learned lessons are not always surfaced instantly. This validation gate is on by default and can be disabled with `MUBIT_CONTROL_LESSON_VALIDATION_ENABLED`. 4. **Reinforce** `recordOutcome()` feeds success/failure signals back into the lesson store, strengthening what worked and weakening what didn't. Over multiple runs, agents accumulate lessons that make them better at the task — without retraining the LLM. ## Component model Mubit exposes three API surfaces. Most application logic belongs on the control plane. | Surface | Responsibility | Typical use | | --------- | ------------------------------------------------------------------------------------------------ | -------------------------------- | | `control` | Memory lifecycle, context assembly, diagnostics, learning loop, coordination, and planning state | Default application path | | `core` | Direct search, sessions, scratch memory, and specialized low-level primitives | Advanced or specialized features | | `auth` | User and API key lifecycle | Admin and provisioning workflows | - Start with helper-first control usage: `remember`, `recall`, `getContext`, `checkpoint`, `reflect`, `recordOutcome`. - Introduce `core.*` only when you need direct-lane or branch/session primitives. - Keep `auth.*` out of end-user request handlers. ## Execution contract 1. Write memory with `remember()` or raw `control.ingest`. 2. If you use raw ingest, poll `control.get_ingest_job` until `done=true` before freshness-critical reads. 3. Read with `recall()` or assemble context with `getContext()` / `get_context()`. 4. Checkpoint before compaction. 5. Reflect and record outcomes when the attempt finishes. ## Query and context controls that matter | Control | Why it matters | | ---------------------------- | --------------------------------------------------------------------- | | `run_id` / `session_id` | Defines the memory scope | | `mode` on context | Chooses `full`, `summary`, or `sections` context assembly | | `max_token_budget` | Keeps context within the active model budget | | `entry_types` | Restricts retrieval to facts, lessons, rules, traces, and other types | | `diagnose` / `memory_health` | Explains weak retrieval and low-quality memory | ## Minimal helper-first example ```python title="helper_flow.py" client.remember( session_id="support:acme:ticket-42", agent_id="support-agent", content="Customer Taylor prefers concise Friday updates.", intent="fact", ) answer = client.recall( session_id="support:acme:ticket-42", query="What preference do we already know for Taylor?", ) context = client.get_context( session_id="support:acme:ticket-42", query="Draft the next response.", mode="summary", max_token_budget=300, ) ``` ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | ------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------- | | Recent write missing from later context | Raw ingest completion was ignored | Gate reads on `done=true` or use `remember()` | | Context is too long or too noisy | No explicit context mode or budget | Use `getContext()` / `get_context()` with `mode` and `max_token_budget` | | The system does not improve across attempts | Reflection or outcomes are missing | Pair `reflect()` with `recordOutcome()` | | Security boundary drift | `auth` or direct `core` calls leaked into the request path | Keep application logic on the control plane | ## Deep dives - [Data-plane memory component](/sdk/data-plane-memory) - [Control-plane state runtime component](/sdk/control-plane) ## Next steps - Extend the same project with [Add data](/sdk/add-data). - Extend retrieval behavior with [Retrieve data](/sdk/retrieve-data). --- # The memory model > Mubit's four cognitive memory types, the full entry-type taxonomy, who writes each type, and how assembled context prioritizes them. Mubit's four cognitive memory types, the full entry-type taxonomy, who writes each type, and how assembled context prioritizes them. Everything a Mubit-backed agent remembers is stored as a typed **entry**. Read this page when you're deciding what to write (which entry type fits the thing your agent just learned) or debugging what came back (why a lesson outranked a trace in assembled context). The [Concepts overview](/concepts) covers the five types most agents start with; this page is the complete map. The mental model has two axes. The **cognitive type** says what *kind* of knowledge something is — a fact about the world, a record of what happened, a way of doing things, or in-flight task state. The **entry type** is the concrete unit you write and retrieve. Every entry type maps onto one cognitive type, and that mapping determines how the entry is retained, retrieved, and prioritized. ## The four cognitive memory types | Cognitive type | What it holds | Nimbus example | | --- | --- | --- | | **Semantic** | Facts and knowledge about the world, independent of any one episode | "Acme Corp is on the Scale plan" | | **Episodic** | Traces and history — what happened, when, in which run | The full record of how Ari resolved ticket 1042 | | **Procedural** | Workflows, skills, and rules — how to do things and what must never happen | The step-by-step CSV-export fix; "never issue refunds above $500 without review" | | **Working** | In-flight state and attention for the *current* task | Which ticket Ari is triaging right now, and what's been tried so far | Semantic, episodic, and procedural entries live in long-term memory and survive runs. Working memory is a separate, mutable, run-scoped store — see [Working memory vs long-term memory](/concepts/working-memory-vs-long-term) for the full comparison. ## Entry types The complete taxonomy, with who typically writes each type and who reads it back: | Entry type | Cognitive type | Written by | Typical reader | | --- | --- | --- | --- | | `fact` | Semantic | You (`remember(intent="fact")`) or ingestion classification | Context assembly, recall | | `mental_model` | Semantic | Runtime — consolidation promotes recurring observations | Context assembly — searched and injected first | | `trace` | Episodic | Ingestion (captured run activity) | Reflection; recall when reconstructing what happened | | `archive_block` | Episodic (exact) | You (`archive()`) | You, via `dereference()` | | `observation` | Episodic | You or ingestion | Reflection; consolidation into mental models | | `tool_input` / `tool_output` | Episodic | Ingestion (auto-captured tool calls) or you | Reflection | | `reflection` | Episodic | Reflection pass | Later reflection, before lessons crystallize | | `task_result` | Episodic | You | Reflection; recall | | `step_outcome` | Episodic | You (`record_step_outcome()`) | Process-reward learning during reflection | | `handoff` | Episodic | You (`handoff()`) | The receiving agent | | `feedback` | Episodic | You (`feedback()`) | The agent that made the handoff; reflection | | `log` | Episodic | You | Audit and debugging | | `lesson` | Procedural | Reflection (`reflect()`), or you explicitly | Context assembly — injected ahead of facts | | `rule` | Procedural | You | Context assembly — always-on constraints | | `workflow` | Procedural | Runtime — induced from successful traces | Context assembly — reusable procedures | | `checkpoint` | Working (durable snapshot) | You (`checkpoint()`) | You, when restoring after compaction | See the [helper catalog](/sdk/sdk-methods#current-helper-catalog) for the methods named above. ## The taxonomy in one support ticket At Nimbus, Ari (the support agent) picks up ticket 1042 — Acme Corp's CSV export is timing out. One ticket touches five entry types: - While Ari works, its in-flight state — active ticket, hypotheses tried, next step — lives in **working memory**, not in a durable entry at all. - Along the way Ari confirms the account details and stores **"Acme Corp is on the Scale plan"** as a `fact` — true independent of this ticket, useful in every future one. - When the ticket resolves, the full record of what happened — the reproduction, the row-count check, the fix — persists as a `trace`. - Reflection over that trace extracts the reusable insight as a `lesson`: *"When an export times out, check the report row count before retrying — over 500k rows needs the batched exporter."* - After the same fix works across several tickets, consolidation induces a `workflow`: the placeholder-abstracted, step-by-step CSV-export fix procedure any agent can follow. Same episode, five different retention and retrieval behaviors — that's the point of typing entries instead of storing undifferentiated text. ## Context priority When `get_context()` assembles a context block, evidence is grouped into sections with a fixed priority order. Distilled, high-trust knowledge comes first; raw history comes last: **mental models → active rules → lessons → workflows → archived artifacts → handoffs → feedback → facts → observations → working memory → traces → goals → checkpoints → logs** Within a section, entries sort by importance (critical > high > medium > low). Under a token budget, low-priority sections are the first to be trimmed. An abridged assembled block for Ari looks like: ```text ## Mental Models - Acme Corp support profile: Scale plan, CSV-heavy usage, exports peak on Mondays ## Active Rules - Never issue refunds above $500 without reviewer approval ## Lessons Learned - When an export times out, check the report row count before retrying ## Known Facts - Acme Corp is on the Scale plan (since May 3) ## Current State - active_ticket = "TCK-1042" - attempts = 2 ``` The section names are the real ones the runtime emits; the layout is abridged. Working memory appearing near the end is deliberate — it is situational scratch state, not validated knowledge. ## Verbatim vs semantic entries One entry type is retrieved differently from all the others. `archive_block` is **verbatim**: stored exactly, addressed by `reference_id`, and fetched back byte-for-byte with `dereference()` — the right tool for diffs, generated SQL, or raw outputs you'll re-execute. Every other entry type is **semantic**: retrieved by similarity to your query, which means paraphrase-tolerant recall but no byte-exactness guarantee. If losing a character matters, archive it — see [Exact references](/sdk/sdk-helpers#exact-references). ## Related - [Concepts overview](/concepts) — the evaluator's summary of the memory model and learning loop - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — where entries are visible and how knowledge widens - [Retrieval semantics](/concepts/retrieval-semantics) — how entries come back out at query time - [Working memory vs long-term memory](/concepts/working-memory-vs-long-term) — the mutable store behind the "Current State" section - [How Mubit works](/sdk/how-mubit-works) — the write → retrieve → reflect → reinforce loop - [Step-level outcomes](/recipes/step-level-outcomes) — process-reward learning with `step_outcome` entries - [Lane-scoped memory](/recipes/lane-scoped-memory) — handoffs and feedback in multi-agent systems --- # Retrieval semantics > How a recall query becomes an answer — routing, retrieval lanes, fusion scoring, budget tiers, and the explain breakdown. How a recall query becomes an answer — routing, retrieval lanes, fusion scoring, budget tiers, and the explain breakdown. When your agent calls `recall()`, Mubit does more than a vector search. The query is routed, candidates are gathered from several independent retrieval lanes, the lanes are fused into a single ranked evidence list, and an answer is synthesized with citations back into that list. This page explains each stage so you can predict what a query will return — and tune it when it doesn't. **Mental model:** one query fans out into parallel retrieval lanes; fusion scoring merges them into one ranked evidence list; synthesis reads that list and cites it. Every knob on the request (`entry_types`, `limit`, `rank_by`, `budget`, temporal bounds) shapes exactly one of those stages. ## Routing Every query carries a `mode`: | Mode | What happens | | --- | --- | | `agent_routed` (default) | A router agent classifies the query and may expand it into a small set of alternative phrasings (variants) to improve semantic recall over paraphrased memories. Variants are grounded in the input — the router never invents new concepts — and are capped (default 3). Evidence is then synthesized into a `final_answer`. | | `direct_bypass` | Skips the router and answer synthesis; runs the direct semantic-search lane and returns raw evidence. Lower latency, no LLM in the loop. | ## Retrieval lanes Candidates are gathered from independent lanes. Each returned evidence item is tagged with the lane that produced it in its `retrieval_mode` field: | `retrieval_mode` | Lane | | --- | --- | | `semantic` | Embedding similarity search over long-term memory (the main lane; also blends lexical/sparse term matching in fusion) | | `working_memory` | Live working-memory variables and goals for the run (on by default; disable with `include_working_memory=False`) | | `chronological` | Temporal scan — activated when the request sets `min_timestamp`/`max_timestamp`; filters by each entry's occurrence time (falling back to ingest time) | | `trace_scan` | Direct scan over run traces | | `exact_reference` | Exact lookup by a stable `reference_id` (byte-for-byte re-read) | | `checkpoint` | Stored context checkpoints | | `rule_overlay` | Rules injected regardless of similarity — hard constraints always ride along | | `lesson_overlay` | Cross-run lessons injected by relevance (see [The learning loop](/concepts/learning-loop)) | | `archive_overlay` | Archived artifacts surfaced alongside semantic hits | | `recency_fallback` | Most-recent entries returned when no lane produced a confident match | ## Fusion scoring Each candidate's final `score` is a weighted blend of component scores: - **Semantic** — embedding similarity to the query (and its variants). - **Lexical** — term/sparse match strength. - **Recency** — newer entries score higher. Recency decays exponentially with a half-life (default 7 days: an entry scores half as much at 7 days old, a quarter at 14). - **Reinforcement** — entries with positive outcome history (see [`record_outcome`](/concepts/learning-loop#outcomes-close-the-loop)) get a multiplicative boost; a fully-reinforced, production-verified entry can reorder above fresher near-duplicates. Two adjustments apply on top: entries whose stored `env_tags` intersect the request's `env_tags` get a relevance boost (conflicting tags get a small penalty), and superseded entries take a 0.5x stale penalty. `rank_by` shifts the blend: | `rank_by` | Behavior | Weights (semantic / lexical / recency) | | --- | --- | --- | | `relevance` (default) | Semantic-dominant | current default weights | | `freshness` | Recency boosted | 0.50 / 0.10 / 0.40 | | `balanced` | Even split | 0.60 / 0.15 / 0.25 | ## Budget tiers `budget` controls the latency/quality trade-off of candidate gathering: | `budget` | Behavior | | --- | --- | | `low` | Skips deep graph traversal, fewer ANN candidates — fastest | | `mid` (default) | Standard retrieval | | `high` | Deeper graph traversal, more candidates, optional reranking — best recall | ## Shaping the candidate set | Request field | Effect | | --- | --- | | `entry_types` | Only evidence of these types is returned (e.g. `["fact", "lesson"]`) | | `limit` | Maximum evidence items (default 5) | | `min_timestamp` / `max_timestamp` | Inclusive occurrence-time window (unix seconds); activates the temporal lane | | `env_tags` | Caller's stack context, e.g. `["lang:python:3.12"]` — boosts stack-matching entries | | `agent_id` | Filters evidence to the types in that agent's registered `read_scopes` | | `user_id` / `lane` | Scope to one user's memories / one coordination lane | | `prefer_current_run` | Current-run evidence only; skips cross-run lesson overlay | | `evidence_only` | Skip answer synthesis, return evidence only — saves the per-query LLM call | > **Note:** From SDK v0.12.0 the typed `recall()` helpers forward `budget`, `rank_by`, `explain`, `min_timestamp`/`max_timestamp`, and `evidence_only` directly in all three languages (`client.recall(query=..., explain=True)`). On earlier SDK versions these are wire-level fields the typed Python helper does not forward — send them directly over the transport (`POST /v2/control/query` with e.g. `{"run_id": ..., "query": ..., "explain": true}`) or via `client.advanced.query({...})`. `entry_types`, `env_tags`, `limit`, and the scope fields are `recall()` kwargs on every version. See [Control HTTP API](/api-reference/control-http). ## Explain Set `explain: true` on the request and every evidence item carries an `ExplainInfo` breakdown. A returned evidence item looks like this: ```json { "id": "8f2c1a9e-...", "content": "CSV export fails when column headers contain commas; quote headers before export.", "source": "agent", "score": 0.81, "run_id": "support:acme:ticket-118", "entry_type": "lesson", "retrieval_mode": "lesson_overlay", "reference_id": "ltm:lesson:8f2c1a9e", "referenceable": true, "is_stale": false, "superseded_by": "", "knowledge_confidence": 0.74, "explain_info": { "semantic_score": 0.78, "lexical_score": 0.42, "recency_score": 0.31, "temporal_decay_factor": 0.62, "stale_penalty_applied": false, "temporal_intent_detected": false, "rank_by_mode": "relevance", "fusion_weights_used": { "semantic": 1.0, "lexical": 0.25, "recency": 0.1 } } } ``` `score` is retrieval relevance for this query; `knowledge_confidence` is durable reliability of the entry itself — different axes. See [Trust, confidence, and staleness](/concepts/trust-and-confidence). ## Citations When the response includes a synthesized `final_answer`, the `citations` field lists 0-based indices into `evidence` that the answer is grounded in. Indices are validated server-side against the evidence length, so every value is safe to dereference — use them to render citations or audit grounding. An empty list means the model cited nothing or abstained. ## Example: Ari checks for a known error Nimbus's support agent Ari gets a ticket about a failed CSV export and asks memory before answering: ```python result = client.recall( session_id="support:acme:ticket-241", query="Have we seen this CSV export error before?", entry_types=["lesson", "fact", "trace"], env_tags=["service:export", "format:csv"], limit=5, ) for i in result.get("citations", []): print(result["evidence"][i]["content"]) ``` The recurring CSV-export bug has produced lessons in earlier runs, so the `lesson_overlay` lane surfaces them even though this ticket is a brand-new run — that cross-run carry-over is the point of the overlay lanes. ## Related - [The learning loop](/concepts/learning-loop) — how lessons get into the overlay in the first place - [The memory model](/concepts/memory-model) — the entry types behind `entry_types` filtering - [Trust, confidence, and staleness](/concepts/trust-and-confidence) — deciding whether to act on returned evidence - [Retrieve data](/sdk/retrieve-data) and [Query patterns](/sdk/query-patterns) — SDK-level how-to - [Temporal memory patterns](/recipes/temporal-memory-patterns) — recipes using time-bounded recall - [Control HTTP API](/api-reference/control-http) — the full wire-level query request --- # The learning loop > How traces become lessons, lessons earn trust through outcomes, and the runtime maintains memory quality underneath. How traces become lessons, lessons earn trust through outcomes, and the runtime maintains memory quality underneath. Mubit's learning loop is what makes an agent better on run 50 than on run 1 without retraining the model. Raw experience goes in as traces, outcomes attach reward signals to the memories that produced them, reflection distills reusable lessons, and those lessons are injected into future queries — with a promotion ladder deciding how widely each lesson applies. Read this page to understand what each stage does and which parts run automatically. **Mental model:** the loop is *experience → attribution → distillation → injection → promotion*. You drive the first three with API calls; injection and promotion happen at query time and in the background. ## The five stages 1. **Ingest experience** Your agent writes what happened — `remember()` for facts and observations, raw ingest for traces. Ingestion is asynchronous: an ingestion agent classifies each item into a typed entry (fact, trace, observation, …). If you need a write to be immediately recallable, poll the ingest job until `done=true` (see [How Mubit works](/sdk/how-mubit-works#execution-contract)). 2. **Record outcomes** When an attempt succeeds or fails, `record_outcome()` attaches the result to the memories that produced it. This is the reward signal everything downstream feeds on. 3. **Reflect** `reflect()` distills the run's evidence into lessons — each with a `lesson_type` (success, failure, observation, rule, preference), a `scope` (run, session, global), an `importance` (low → critical), and `conditions` describing when it applies. A validation gate scores each fresh lesson: it becomes active once its evidence score crosses the acceptance threshold (default 0.6), is rejected at or below 0.25, and stays pending in between. An auto-reflection cadence can be enabled per instance so reflection fires after every N ingests or on a negative-outcome streak, without an explicit call. 4. **Lesson injection** On later queries, relevant lessons ride along as a `lesson_overlay` — even across runs. The overlay is relevance-gated (embedding similarity between the query and stored lesson embeddings) and token-budgeted (default ~1,600 tokens; the first item is always admitted so one long critical lesson cannot starve the overlay). Rules always inject; lessons compete on relevance. 5. **Promotion** Lessons that keep proving useful climb the scope ladder: run → session → global. Promotion to org-wide sharing is shadow-gated — a candidate lesson is trialled against a control group and only promoted when the outcome delta justifies it. ## Outcomes close the loop `record_outcome()` is the highest-leverage call in the loop — without it, reflection has only "what happened", never "what worked". | Field | Meaning | | --- | --- | | `reference_id` (required) | The lesson or entry ID this outcome is about | | `outcome` | `"success"`, `"failure"`, `"partial"`, `"neutral"` | | `signal` | Numeric reward in `[-1.0, 1.0]` — positive reinforces, negative penalizes | | `entry_ids` | Additional entry IDs that contributed (multi-entry attribution). Each listed entry's reinforcement counters update; the primary `reference_id` is never double-counted | | `verified_in_production` | Marks the outcome as observed in live production, not a test — the strongest trust signal (see [Trust, confidence, and staleness](/concepts/trust-and-confidence)) | | `rationale` | Free-text why, kept for audit | For dense per-step signals inside a single run, use `record_step_outcome()` — see [Step-level outcomes](/recipes/step-level-outcomes). > **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 `record_outcome()` helpers forward it directly in all three languages (`record_outcome(..., idempotency_key="...")`); on earlier versions it is a wire-level field — send it directly over the transport (`POST /v2/control/outcome`). ## What the runtime maintains for you Alongside the loop you drive, hosted instances run always-on maintenance (each is per-instance configurable): | Feature | What it does | | --- | --- | | Write-time lesson reconciliation | A re-learned near-duplicate lesson becomes a recurrence bump on the canonical entry instead of a new copy, so outcome history never splits across duplicates. | | Bi-temporal supersession | When a belief is superseded, the old entry is excluded from current-state answers but kept for history (flagged `is_stale`, with `superseded_by` pointing at its replacement). | | Sleep-time consolidation | While a run is idle, near-duplicate guidance entries missed at write time are merged and clusters of entity facts are distilled into consolidated observations. | | Workflow induction | Repeated successful traces of the same procedure are distilled into reusable workflow entries. | | Guardrail distillation | Repeated failure traces are distilled into preventive lessons with trigger preconditions. | | Utility-weighted forgetting (opt-in) | Aged episodic evidence that is never retrieved or reinforced is moved to an archive tier — reversible, and entries stay reachable by ID. | ## Nimbus example: the bad regex becomes a guardrail Nimbus's support agent Ari ships a regex fix that takes down the export pipeline. The incident run contains the failure traces; closing the loop turns it into prevention: ```python # The fix failed in production — attribute the failure to the memories used. client.record_outcome( session_id="support:incident-0142", reference_id=lesson_id, # returned by an earlier reflect() outcome="failure", signal=-0.9, rationale="Regex change broke multi-line CSV rows in the export pipeline", verified_in_production=True, entry_ids=recalled_entry_ids, # every entry that informed the fix ) # Distill the incident into lessons. lessons = client.reflect(session_id="support:incident-0142") ``` Reflection produces a `failure` lesson ("test regex changes against multi-line CSV fixtures before deploying") with conditions attached. Because similar failure traces recur, guardrail distillation reinforces it into a preventive lesson. Weeks later, when Ari recalls context for another regex change, the guardrail arrives via `lesson_overlay` — and this time Rex, the reviewer agent, blocks the deploy until the fixture test passes. When it holds up, a `record_outcome(..., outcome="success", verified_in_production=True)` strengthens the lesson toward global scope. > **Note:** **Configuration.** The maintenance features are instance configuration, controlled by `MUBIT_CL_*` environment variables on the instance (e.g. `MUBIT_CL_BITEMPORAL=0` to disable supersession, `MUBIT_CL_FORGET=1` to opt in to forgetting, `MUBIT_CL_OVERLAY_TOKEN_BUDGET` to resize the lesson overlay). The lesson validation gate is likewise instance-level (`MUBIT_CONTROL_LESSON_VALIDATION_ENABLED`). On hosted instances the defaults above are already in effect. ## Related - [Retrieval semantics](/concepts/retrieval-semantics) — how injected lessons and other lanes are fused at query time - [The memory model](/concepts/memory-model) — the entry types the loop reads and writes - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — the scope ladder that promotion climbs - [Trust, confidence, and staleness](/concepts/trust-and-confidence) — how reinforcement and verification show up as `knowledge_confidence` - [Step-level outcomes](/recipes/step-level-outcomes) — process-reward signals within a run - [Support agent loop](/recipes/support-agent-loop) — the loop end-to-end in a working agent - [How Mubit works](/sdk/how-mubit-works) — the loop in the context of the whole system --- # Runs, sessions, and scopes > How run_id isolates memory, how sessions wrap runs, and how knowledge widens from run to session, global, and org scope. How `run_id` isolates memory, how sessions wrap runs, and how knowledge widens from run to session, global, and org scope. Every piece of memory in Mubit belongs to a **run**, and every query names the run it's asking on behalf of. Read this page when you're designing how your agents partition memory — per ticket, per user, per agent — and how validated knowledge should escape those partitions. Getting scoping right up front is the difference between clean isolation and "why didn't my recall find it." The mental model: **runs isolate, scopes share.** A `run_id` is a hard boundary — writes land in it, queries read from it. The scope ladder (run → session → global → org) is the controlled path by which individual entries earn visibility beyond their run. ## Runs: the unit of isolation A run is the memory scope for one unit of work. At Nimbus, Ari (the support agent) opens a fresh run per ticket — `nimbus:ari:ticket-1042` — so evidence from one ticket never bleeds into another. Every write and every query carries the run id; two runs share nothing by default. > **Note:** In the SDK helpers the kwarg is named `session_id` for historical reasons — `remember(session_id="nimbus:ari:ticket-1042", ...)` sends `run_id` on the wire. Whenever a helper asks for `session_id`, it means the run id. ## Sessions: the lifecycle wrapper A **session** is a lifecycle object that wraps a run — it tracks status, creation and close time, and ingest activity. Create one (`client.advanced.create_session(...)`) to get a server-managed run with auto-generated ids; close it (`client.advanced.close_session(...)`) when the work is done. Closing with `reflect_on_close=True` triggers a final reflection pass over the run, so lessons are extracted at the natural end of the work rather than on a timer. Runs don't require sessions — you can use bare run ids everywhere — but sessions give you an explicit close moment, which is the right hook for end-of-task reflection. (Core *branch* sessions — reversible scratch forks — are a different primitive; see [Sessions & branching](/sdk/sessions-and-branching).) ## The scope ladder Entries that carry a scope (lessons are the main case) are visible beyond their run according to a four-rung ladder. Wider scope also earns a retrieval-confidence bonus — broadly validated knowledge outranks single-run knowledge: | Scope | Visible to | How memory gets there | Confidence bonus | | --- | --- | --- | --- | | `run` | Only the writing run | Direct write (the default) | +0.00 | | `session` | Related runs | Direct write (`lesson_scope="session"`) or promotion | +0.05 | | `global` | Every run on the instance | Direct write (`lesson_scope="global"`) or promotion | +0.10 | | `org` | Every instance in the tenant | **Promotion only — never a direct write** | +0.15 | Promotion is recurrence-driven: a lesson that keeps re-emerging across runs becomes a candidate for the next rung. Two gates protect the wider scopes: - **Shadow A/B validation.** A candidate doesn't promote on recurrence alone — it *auditions*. Runs that would see it at the wider scope split deterministically into an exposed arm (lesson injected) and a control arm (withheld); outcomes accrue per arm, and the promotion only lands once each arm has enough samples and the exposed arm is doing no worse. - **Distinct actors.** Widening scope multiplies an entry's injection reach, so it requires outcome provenance from multiple distinct principals. A single agent reinforcing its own lesson cannot push it into shared scope — below the bar the lesson is quarantined until its provenance broadens. This is how Nimbus's CSV-export lesson travels: Ari learns it in one ticket run, it recurs, survives its shadow audition, and eventually lands at org scope — where Sana (the analytics agent, on a different instance in the same tenant) picks it up while compiling the weekly digest, having never handled a ticket. ## Run linking Sometimes one run legitimately needs to read another — Rex (the reviewer agent) auditing Ari's work is the canonical case. Linking makes a run's memory reachable from another run without merging them; the reader opts in per query with `include_linked_runs`: ```python # Ari worked the ticket in its own run client.remember( session_id="nimbus:ari:ticket-1042", # run_id on the wire agent_id="ari", content="Export timed out at 500k rows; batched exporter resolved it", intent="observation", ) # Rex reviews in a separate run, with Ari's run linked in client.advanced.link_run(run_id="nimbus:rex:review-7", linked_run_id="nimbus:ari:ticket-1042") evidence = client.recall( session_id="nimbus:rex:review-7", query="What did Ari already try for the CSV export timeout?", include_linked_runs=True, ) ``` `reflect()` accepts `include_linked_runs` too, so a review run can extract lessons from the combined history. Unlink with `client.advanced.unlink_run(...)` — see [Run management](/sdk/sdk-methods#run-management). ## Lanes: channels inside a shared run When several agents share one run, **lanes** give each a named channel. Write with `lane="planning"` and queries filtered to that lane (`lane=` on `recall()`, `lane_filter` on the wire) see only planning-lane evidence — the planner's scratch reasoning stays out of the executor's context while shared, unlaned entries remain visible to all. Agents declare their lanes at registration (`register_agent(..., shared_memory_lanes=[...])`). See the [lane-scoped memory recipe](/recipes/lane-scoped-memory) for a full walkthrough. ## Pinning retrieval to the active run Scope overlays mean a query can surface session- and global-scoped knowledge alongside run-local evidence. When you want the current task to dominate — mid-task, where this run's own recent evidence matters more than accumulated general knowledge — pass `prefer_current_run=True` on `recall()` to weight retrieval toward the active run. ## Users: partitioning by end-user `user_id` partitions memory per end-user, orthogonally to runs. Pass it on writes and queries and entries are scoped to that user across all their runs — Acme's preferences surface in every Acme conversation and never in another customer's. This is the standard mechanism for cross-session recall keyed to a person; see [cross-session recall](/sdk/sdk-helpers#cross-session-recall). > **Note:** **Configuration.** Promotion gating is instance configuration: `MUBIT_CL_SHADOW_PROMOTION` (shadow validation, on by default), `MUBIT_CL_SHADOW_MIN_PER_ARM` (samples per arm before a decision), and `MUBIT_CL_PROMOTION_MIN_ACTORS` (distinct-principal bar for widening). Org scope additionally requires the instance to be configured with a tenant namespace. ## Related - [The memory model](/concepts/memory-model) — the entry types that flow through these scopes - [The learning loop](/concepts/learning-loop) — the promotion pipeline that widens a lesson's scope - [Working memory vs long-term memory](/concepts/working-memory-vs-long-term) — the run-scoped mutable store - [Sessions & branching](/sdk/sessions-and-branching) — checkpoints and core branch sessions - [Lane-scoped memory](/recipes/lane-scoped-memory) — multi-agent isolation inside one run - [Multi-agent shared state](/recipes/multi-agent-shared-state) — coordination patterns on a shared run - [How Mubit works](/sdk/how-mubit-works) — where scoping sits in the memory loop --- # Working memory vs long-term memory > The mutable, decaying scratch state a run holds for the current task, versus the durable typed entries that survive it — and the checkpoint bridge between them. The mutable, decaying scratch state a run holds for the current task, versus the durable typed entries that survive it — and the checkpoint bridge between them. Mubit keeps two stores with deliberately opposite behavior. **Working memory** is what the run is thinking about *right now*: mutable variables that expire, decay, and get pruned. **Long-term memory** is what the instance knows: durable typed entries that survive runs and feed retrieval. Read this page when you're unsure which side something belongs on, or when in-flight state is leaking into places it shouldn't outlive. The mental model: working memory is the whiteboard, long-term memory is the filing cabinet. You scribble freely on the whiteboard while the task is live; only what's worth keeping gets written up and filed. Mubit enforces that discipline mechanically — the whiteboard erases itself. ## The two stores compared | | Working memory | Long-term memory | | --- | --- | --- | | **Unit** | Named variables (JSON values), a context stack, and active goals | Typed entries — see [the memory model](/concepts/memory-model) | | **Scope** | One `run_id`, held by the runtime | Run-scoped by default; widens via [the scope ladder](/concepts/runs-and-scopes) | | **Mutability** | Mutated in place — set, update, remove | Written once, then reinforced, superseded, or archived by the learning loop | | **Lifetime** | The current task: TTL expiry, relevance decay, pruning | Survives runs | | **Retrieval** | Injected into assembled context as the "Current State" section | Similarity retrieval plus scope overlays | | **What ends it** | TTL, relevance pruning below threshold, circuit break | Explicit deletion (`forget()`, `delete_run`) or consolidation-driven archival | Mechanically, a working-memory variable can be **ephemeral** — created with a TTL and an optional decay rate. Expired variables are removed on access; decayed ones fall below a relevance threshold and get pruned. The runtime can hand pruned variables to long-term memory before discarding them, which is the general shape of the system: scratch state either dies quietly or graduates into a durable entry. The **context stack** tracks nested task contexts (push on entering a subtask, pop on return), so "what am I in the middle of" survives even deep detours. ## What goes where Put it in **working memory** when it's only true for this task, changes as you work, and would be noise in any other run. Put it in **long-term memory** when a future run — or another agent — should be able to retrieve it. At Nimbus, while Ari triages ticket 1042, the in-flight state is pure working memory: `active_ticket = "TCK-1042"`, `attempts = 2`, `current_hypothesis = "row-count limit"`. None of that should exist next week. What the triage *produces* is long-term: the resolution `trace`, the "Acme Corp is on the Scale plan" `fact`, and the durable `lesson` — *check the report row count before retrying a timed-out export*. The whiteboard is erased; the filing cabinet grew by three entries. ## How working memory reaches the prompt You don't query working memory separately. `recall()` and `get_context()` fold the run's live variables into their results by default — `include_working_memory=True` — where they appear as the **Current State** section of the assembled context block: ```python context = client.get_context( session_id="nimbus:ari:ticket-1042", query="Draft the next reply to Acme.", include_working_memory=True, # default — pass False to exclude scratch state max_token_budget=800, ) ``` In the [context priority order](/concepts/memory-model#context-priority), Current State lands near the end — after mental models, rules, lessons, and facts, just before raw traces. In-flight state informs the next step; it doesn't compete with validated knowledge. Pass `include_working_memory=False` when you want retrieval over durable knowledge only. > **Note:** Working memory is populated by the runtime as the run progresses. There is also a wire-level surface for setting variables directly (`POST /v2/control/variables/set`, plus `get`/`list`/`delete`) that the typed SDK helpers do not forward — send it over the transport directly if you need explicit variable writes. ## Checkpoints: the bridge The deliberate way to make working context durable is `checkpoint()` — it writes a `checkpoint` entry (a Working-type snapshot in long-term memory) that survives whatever happens to the live run state: ```python client.checkpoint( session_id="nimbus:ari:ticket-1042", label="pre-compaction", context_snapshot="Triage state: timeout reproduced at 500k rows; batched-exporter fix pending verification.", ) ``` Checkpoint before compacting your LLM conversation, before a risky transition, or before handing off — the snapshot is retrievable later even from a different session. This is the standard pre-compaction move; see [Sessions & branching](/sdk/sessions-and-branching) for how checkpoints pair with reversible core branch sessions. ## Circuit break: clear, but snapshot first When a run's state has gone bad — a loop, a poisoned hypothesis — `circuit_break()` resets working memory atomically. It does not just wipe the whiteboard: it first snapshots the live variables into a durable, run-scoped entry tagged with the break reason, then clears the state and emits a `CIRCUIT_BROKEN` event. ```python client.circuit_break(run_id="nimbus:ari:ticket-1042", reason="repeated identical retries") ``` The response includes the snapshot's id, so if the reset was a mistake the pre-break state is recoverable from long-term memory. This is the involuntary version of the same bridge: nothing leaves working memory without the option of leaving a durable record behind. ## Related - [The memory model](/concepts/memory-model) — the durable entry types and their context priority - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — the `run_id` boundary both stores live inside - [Sessions & branching](/sdk/sessions-and-branching) — checkpoints vs reversible scratch branches - [How Mubit works](/sdk/how-mubit-works) — where both stores sit in the memory loop - [SDK helpers](/sdk/sdk-helpers) — `checkpoint()`, `get_context()`, and friends --- # Trust, confidence, and staleness > How an agent should decide whether to act on a memory — score vs knowledge_confidence, staleness and supersession, production verification, and memory health. How an agent should decide whether to act on a memory — score vs knowledge_confidence, staleness and supersession, production verification, and memory health. Retrieval tells you a memory is *relevant*; it does not tell you the memory is *right*. Mubit puts both signals on every evidence item so your agent can separate "this matches my question" from "this is safe to act on". This page explains each trust signal, where it comes from, and how to combine them into a decision. **Mental model:** `score` answers "is this about my query?"; `knowledge_confidence` answers "how reliable is this knowledge in general?"; `is_stale` answers "is this still the current belief?". They are independent axes — read all three before acting. ## Two different axes: score vs knowledge_confidence | | `score` | `knowledge_confidence` | | --- | --- | --- | | Measures | Retrieval relevance **for this query** | Durable reliability of the entry itself, in `[0, 1]` | | Computed from | Fusion of semantic, lexical, and recency components at query time (see [Retrieval semantics](/concepts/retrieval-semantics)) | Reinforcement history from outcomes, lesson scope weight (run < session < global < org), the production-verification flag, and recency — minus a contradiction penalty when opposing lessons are known | | Changes when | You change the query, `rank_by`, or temporal bounds | Outcomes are recorded, the lesson is promoted, or contradictions appear | | Use it to | Order candidates for this question | Weight how much to trust each candidate, independent of topicality | A hot take from five minutes ago can have a high `score` and low `knowledge_confidence`; a battle-tested global lesson can have modest `score` on a tangential query and high `knowledge_confidence`. Neither substitutes for the other. ## Staleness and supersession Mubit's belief store is bi-temporal: it distinguishes when something *happened* (occurrence time) from when Mubit *learned it* (ingest time), and it never silently overwrites a belief — a new belief **supersedes** the old one. On each evidence item: - `is_stale: true` — this entry has been superseded by a newer entry. - `superseded_by` — the ID of the entry that replaced it (empty when not stale). Superseded beliefs are excluded from current-state answer synthesis (kept for history). When stale entries do appear in the evidence list, they are deprioritized in ranking (a 0.5x penalty, visible as `stale_penalty_applied` in the explain breakdown) and returned for transparency — prefer non-stale entries for answers, and follow `superseded_by` to the current belief. Time-bounded queries (`min_timestamp`/`max_timestamp`) filter on occurrence time, so history stays queryable. ## Production verification `verified_in_production` is the strongest trust signal an entry can carry. It is set by you, through the outcome path: `record_outcome(..., verified_in_production=True)` declares that the lesson was applied and verified in a live production environment — not a test or staging run. Production-verified entries receive a ranking boost and surface with higher `knowledge_confidence`. Reserve it for genuinely live verification; it is the signal other agents will lean on hardest. See [The learning loop](/concepts/learning-loop#outcomes-close-the-loop). ## Response-level signals - **`degraded: true`** — answer synthesis fell back to a deterministic, no-LLM digest of the evidence (for example, the LLM was unavailable). The evidence list is still real retrieval output; treat `final_answer` as a grouped summary rather than reasoning, and prefer reading the evidence directly. - **`citations`** — 0-based indices into `evidence` that the synthesized answer is grounded in, validated server-side. An answer whose claims you can't trace to a citation should be treated as ungrounded. - **`confidence`** — the synthesizer's own confidence in the answer, distinct from any per-evidence field. ## The decision table | Signals | Read it as | Action | | --- | --- | --- | | High `score`, high `knowledge_confidence`, not stale | Relevant and reliable | Act on it | | High `score`, low `knowledge_confidence` | On-topic but unproven | Treat as a hypothesis — verify before acting, then `record_outcome` | | Low `score`, high `knowledge_confidence` | Reliable but probably off-topic | Don't force it into this answer | | `is_stale: true` | Superseded belief | Follow `superseded_by` to the current entry; use the stale one only for history | | `verified_in_production: true` in metadata, high `knowledge_confidence` | Battle-tested | Safe default when candidates conflict | | Response `degraded: true` | Synthesis fallback | Rely on the evidence list, not `final_answer` | | Claim without a citation index | Ungrounded synthesis | Verify against evidence before repeating it | ## Memory health: the hygiene dashboard `memory_health()` aggregates the same signals across a whole scope, so you can audit memory quality instead of discovering it one query at a time: ```python health = client.memory_health( session_id="support:acme", stale_threshold_days=30, ) ``` | Response field | What it tells you | | --- | --- | | `entry_counts` | Entries per type — spot runaway trace growth or missing lessons | | `stale_entries` | Entries older than the threshold or superseded | | `contradictions` | Pairs of similar lessons with opposing types — these drag `knowledge_confidence` down until resolved | | `low_confidence_count` | Entries below the confidence floor | | `promotion_candidates` | Lessons that have earned a move up the scope ladder | | `section_health` | Per-section count, average confidence, and average age | ## Nimbus example: the Acme plan change On May 3, customer Acme Corp upgrades from the Starter plan to the Scale plan. Nimbus's memory already holds the fact "Acme Corp is on the Starter plan"; ingesting the new plan fact supersedes it rather than deleting it. Afterwards, Ari's recall for "what plan is Acme on?" is synthesized from the current belief only, and the old fact comes back flagged: ```json { "id": "3d90c2f1-...", "content": "Acme Corp is on the Starter plan.", "entry_type": "fact", "score": 0.77, "knowledge_confidence": 0.31, "is_stale": true, "superseded_by": "b41e88a0-...", "retrieval_mode": "semantic" } ``` Ari answers from the Scale-plan fact. Sana, the analytics agent, can still reconstruct April's state for her weekly digest by querying with a `max_timestamp` before May 3 — the superseded fact was excluded from current answers, not erased. ## Related - [Retrieval semantics](/concepts/retrieval-semantics) — where `score` and the explain breakdown come from - [The learning loop](/concepts/learning-loop) — how outcomes and promotion build `knowledge_confidence` - [Temporal memory patterns](/recipes/temporal-memory-patterns) — querying history with time bounds - [How Mubit works](/sdk/how-mubit-works) — `diagnose` and `memory_health` in the wider system --- # Associative Retrieval > Use Mubit routed retrieval and context assembly to produce grounded, scoped answers and reusable prompt context. Use Mubit routed retrieval and context assembly to produce grounded, scoped answers and reusable prompt context. Associative retrieval in Mubit is not just vector similarity. It is the control-plane contract that combines scoped memory selection, evidence ranking, and answer or context synthesis. For most applications, the primary retrieval surfaces are: - `recall()` for answer-oriented retrieval - `getContext()` / `get_context()` for prompt-ready context assembly - raw `control.query` when you need wire-level control ## Mental model - Default to the routed retrieval path. - Keep `run_id` / `session_id` stable. - Use `entry_types` when you need only facts, lessons, or rules. - Use explicit context budgeting when the downstream model window matters. ## Minimal helper-first example ```json title="query_payload.json" { "run_id": "support-thread-42", "query": "What customer preferences are already known?", "mode": "agent_routed", "limit": 5, "entry_types": ["fact", "lesson", "rule"] } ``` > **Note:** The query/recall response includes a `citations` array — 0-based indices into `evidence` marking which items grounded `final_answer` (empty when the answer cites no specific evidence, such as an abstention). It is a response field, not a request parameter, and is validated server-side so every index is a valid position in `evidence`. Use it to render citations or audit answer grounding. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Sparse evidence | Scope mismatch or weak memory writes | Keep one stable run scope and tag memory intentionally | | Context is too wide | No token budget or type restriction | Use `getContext()` budgeting and `entry_types` | | Retrieval still looks weak | Memory quality degraded | Use `diagnose()` and `memoryHealth()` / `memory_health()` | ## Next steps - Compare retrieval styles at [Associative retrieval vs RAG](/sdk/associative-retrieval-vs-rag). - Implement retrieval patterns at [Retrieve data](/sdk/retrieve-data). --- # Agent-decided Retrieval vs RAG > Why Mubit lets the agent decide retrieval mode and compartment instead of forcing you to hard-code a RAG pipeline. Why Mubit lets the agent decide retrieval mode and compartment instead of forcing you to hard-code a RAG pipeline. Traditional RAG forces every retrieval decision into application code: you pick the index, choose the similarity metric, set the top-k, decide which documents to stuff into the prompt, and wire it all together before the model ever sees the query. When requirements change you re-engineer the pipeline. Mubit takes the opposite approach. The agent decides the retrieval mode and the memory compartment at query time. You call `control.query` with `mode: "agent_routed"`, and the runtime selects the best retrieval lane — semantic search, temporal scan, sparse recall, SDM, graph traversal, or a combination — based on the query and the data it already holds. You don't hard-code the retrieval path; the agent routes itself. ## What this looks like in practice ```python # One call. The runtime picks the lane. response = client.advanced.query({ "run_id": "support:agent:ticket-42", "query": "What did the customer say about billing last week?", "mode": "agent_routed", }) ``` Compare with a typical RAG setup where you would write: embed the query → hit a vector store → rank results → truncate to context window → inject into prompt template → call LLM. Each step is a place where retrieval quality can silently degrade, and every step is your code to maintain. ## How this works | Concern | RAG pipeline (you own it) | Mubit agent-routed (runtime owns it) | | --------------------------- | --------------------------- | ------------------------------------------------------- | | Which index to query | Hard-coded per endpoint | Runtime selects lane from query semantics | | Top-k and ranking | Fixed or tuned per use-case | Adaptive per memory compartment | | Context assembly | Your prompt-stuffing logic | Runtime returns ranked results; agent consumes | | Adding a new retrieval mode | New code path + deploy | Already available — runtime routes when useful | | Observability | You instrument each hop | Built-in: `routing_summary` and `mode` on the response, plus `retrieval_mode`, `metadata_json`, and `score` on each evidence item | ## When to override Agent-routed retrieval is the right default. Override it when you have an explicit reason: - **`direct_lane: "semantic_search"`** — force pure vector similarity when you know the answer is a nearest-neighbor match. - **Explicit RAG** — when you need to assemble context from multiple external sources before calling Mubit, or when your compliance rules require auditable retrieval steps. In this case, use `control.ingest` / `control.batch_insert` to load data and `control.query` with a specific `direct_lane` to retrieve it. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | ------------------------------------ | --------------------------------------------------- | ------------------------------------------------------------ | | Inconsistent answers across requests | Switching between agent-routed and hard-coded lanes | Pick one default mode and stick with it | | Hard incident triage | Retrieval path not logged | Inspect `routing_summary` on the response and `retrieval_mode` / `metadata_json` on each evidence item | | Missing deterministic relationships | Relying on similarity for structured facts | Ingest explicit facts and use control filters or graph-aware recall | | Context window overflow | Returning too many results | Set `limit` on query; let the agent prune | ## Next steps - Build retrieval contracts at [Retrieve data](/sdk/retrieve-data). - Review permissions governance at [Data guardrails](/sdk/data-guardrails). --- # SDK Helpers > Explicit helper methods for memory, context assembly, and the learning loop — when you outgrow the mubit.learn drop-in. Explicit helper methods for memory, context assembly, and the learning loop — when you outgrow the mubit.learn drop-in. The `mubit.learn` drop-in is the right choice for most readers. Use the helpers below when you need explicit control over what gets stored, when, and under which session/agent identity. > **Note:** Automatic trace capture via `mubit.learn` is Python-only. In JS, `learn.init` gives you lesson injection plus loop closure via `feedback()`; in Rust, use an explicit `learn::LearnSession` (`enrich_messages` / `record` / `end`). The helpers on this page exist in all three SDKs. ## Helper map | Method | Purpose | When to reach for it | | --- | --- | --- | | `remember()` | Default write path for a single logical memory item | Storing facts, lessons, or observations | | `recall()` | Answer-oriented retrieval (returns a synthesized `final_answer` plus `evidence` and `citations`) | Asking the memory a question | | `get_context()` | Assemble a token-budgeted context block to inject into your next LLM call | Before any prompt where memory matters | | `archive()` | Store an exact artifact with a stable `reference_id` | Anything you'll need recovered byte-for-byte later | | `dereference()` | Fetch the exact content for a `reference_id` | Recover an archived artifact | | `reflect()` | Extract reusable lessons from session evidence | End of a run/session, or on demand | | `lessons()` | List lessons with optional filtering | Audit, debugging, surfacing strategies | | `checkpoint()` | Snapshot memory state with a label | Before compaction or risky transitions | | `record_outcome()` | Tag a `reference_id` (and any contributing `entry_ids`) with `success` / `failure` and a rationale | RL-style reinforcement signal | ## Minimal usage #### Python ```python title="getting_started.py" import os from mubit import Client run_id = "support:acme:ticket-42" client = Client( api_key=os.environ["MUBIT_API_KEY"], run_id=run_id, transport=os.getenv("MUBIT_TRANSPORT", "auto"), ) client.remember( session_id=run_id, agent_id="support-agent", content="Customer Taylor prefers concise Friday updates.", intent="fact", metadata={"customer": "taylor", "source": "quickstart"}, ) answer = client.recall( session_id=run_id, query="What update style does Taylor want?", entry_types=["fact", "lesson", "rule"], ) context = client.get_context( session_id=run_id, query="Draft the next customer update.", mode="summary", max_token_budget=300, ) print(answer["final_answer"]) print(context["section_summaries"]) ``` #### Node / TS ```ts title="getting_started.ts" import { Client } from "@mubit-ai/sdk"; const runId = "support:acme:ticket-42"; const client = new Client({ api_key: process.env.MUBIT_API_KEY, run_id: runId, transport: process.env.MUBIT_TRANSPORT ?? "auto", }); await client.remember({ session_id: runId, agent_id: "support-agent", content: "Customer Taylor prefers concise Friday updates.", intent: "fact", metadata: { customer: "taylor", source: "quickstart" }, }); const answer = await client.recall({ session_id: runId, query: "What update style does Taylor want?", entry_types: ["fact", "lesson", "rule"], }); const context = await client.getContext({ session_id: runId, query: "Draft the next customer update.", mode: "summary", max_token_budget: 300, }); console.log(answer.final_answer); console.log(context.section_summaries); ``` #### Rust ```rust title="src/main.rs" use mubit_sdk::{Client, ClientConfig, GetContextOptions, RecallOptions, RememberOptions}; use serde_json::json; #[tokio::main] async fn main() -> anyhow::Result<()> { let run_id = "support:acme:ticket-42"; // from_env() reads MUBIT_ENDPOINT / MUBIT_API_KEY / MUBIT_TRANSPORT. let client = Client::new(ClientConfig::from_env().run_id(run_id))?; let mut fact = RememberOptions::new("Customer Taylor prefers concise Friday updates."); fact.run_id = Some(run_id.to_string()); fact.agent_id = Some("support-agent".to_string()); fact.intent = Some("fact".to_string()); fact.metadata = Some(json!({"customer": "taylor", "source": "quickstart"})); client.remember(fact).await?; let mut recall = RecallOptions::new("What update style does Taylor want?"); recall.run_id = Some(run_id.to_string()); recall.entry_types = vec!["fact".into(), "lesson".into(), "rule".into()]; let answer = client.recall(recall).await?; let mut ctx = GetContextOptions::default(); ctx.run_id = Some(run_id.to_string()); ctx.query = Some("Draft the next customer update.".to_string()); ctx.mode = Some("summary".to_string()); ctx.max_token_budget = Some(300); let context = client.get_context(ctx).await?; println!("{}", answer["final_answer"]); println!("{}", context["section_summaries"]); Ok(()) } ``` The `recall()` (and `query()`) response includes a `citations` array — 0-based indices into `evidence` marking which items grounded `final_answer` (empty when the answer cites no specific evidence). ## Cross-session recall `recall()` queries are scoped to the session by default. To read memory from a different session for the same user: - Store the memory as a **lesson** with `lesson_scope="global"`. - On recall, pass the same `user_id` and `entry_types=["lesson"]`. #### Python ```python client.remember( session_id="s1", agent_id="support-agent", user_id="taylor-1", content="Taylor prefers concise written updates on Friday afternoons.", intent="lesson", lesson_scope="global", ) # Different session, same user_id — recall returns the lesson. client.recall( session_id="s2", agent_id="support-agent", user_id="taylor-1", query="how does Taylor like updates?", entry_types=["lesson"], ) ``` #### Node / TS ```ts await client.remember({ session_id: "s1", agent_id: "support-agent", user_id: "taylor-1", content: "Taylor prefers concise written updates on Friday afternoons.", intent: "lesson", lesson_scope: "global", }); // Different session, same user_id — recall returns the lesson. await client.recall({ session_id: "s2", agent_id: "support-agent", user_id: "taylor-1", query: "how does Taylor like updates?", entry_types: ["lesson"], }); ``` #### Rust ```rust use mubit_sdk::{RecallOptions, RememberOptions}; let mut lesson = RememberOptions::new( "Taylor prefers concise written updates on Friday afternoons.", ); lesson.run_id = Some("s1".to_string()); lesson.agent_id = Some("support-agent".to_string()); lesson.user_id = Some("taylor-1".to_string()); lesson.intent = Some("lesson".to_string()); lesson.lesson_scope = Some("global".to_string()); client.remember(lesson).await?; // Different session, same user_id — recall returns the lesson. let mut recall = RecallOptions::new("how does Taylor like updates?"); recall.run_id = Some("s2".to_string()); recall.agent_id = Some("support-agent".to_string()); recall.user_id = Some("taylor-1".to_string()); recall.entry_types = vec!["lesson".to_string()]; client.recall(recall).await?; ``` `intent="fact"` memories are session-local even if `user_id` matches. This trips up almost everyone the first time — call it out in your design doc. ## Context modes `get_context(mode=...)` returns different shapes: | Mode | Returns | Use when | | --- | --- | --- | | `"full"` | Single concatenated `context_block` string | You want to drop the result straight into a system message | | `"summary"` | `section_summaries[]` with `top_item_preview` per section | You want to render structured context (e.g., a card UI) | | `"sections"` | Only the explicitly requested sections (pass `sections=[...]`) | You want to interleave sections with your own prose | `max_token_budget` is enforced after assembly. If the budget is too small, the lowest-score evidence is dropped first. ## Exact references `archive()` and `dereference()` are the exact-recovery pair. Use them for anything semantic recall is the wrong tool for — original diffs, raw tool outputs, generated SQL you'll re-execute later. #### Python ```python archived = client.archive( session_id=run_id, agent_id="support-agent", content="Original billing diff and remediation note", artifact_kind="billing_postmortem", labels=["billing", "exact"], ) # Later — possibly in a different session — fetch the exact content back. exact = client.dereference( session_id=run_id, reference_id=archived["reference_id"], ) ``` #### Node / TS ```ts const archived = await client.archive({ session_id: runId, agent_id: "support-agent", content: "Original billing diff and remediation note", artifact_kind: "billing_postmortem", labels: ["billing", "exact"], }); // Later — possibly in a different session — fetch the exact content back. const exact = await client.dereference({ session_id: runId, reference_id: archived.reference_id, }); ``` #### Rust ```rust use mubit_sdk::{ArchiveOptions, DereferenceOptions}; let mut archive = ArchiveOptions::new( "Original billing diff and remediation note", "billing_postmortem", ); archive.run_id = Some(run_id.to_string()); archive.agent_id = Some("support-agent".to_string()); archive.labels = vec!["billing".to_string(), "exact".to_string()]; let archived = client.archive(archive).await?; // Later — possibly in a different session — fetch the exact content back. let mut deref = DereferenceOptions::new(archived["reference_id"].as_str().unwrap_or_default()); deref.run_id = Some(run_id.to_string()); let exact = client.dereference(deref).await?; ``` `archive()` requires the `archive_block` write scope; `dereference()` requires the matching read scope. Register your agent with both if it needs to round-trip artifacts. ## Reflection and outcomes #### Python ```python ref = client.reflect(session_id=run_id) for L in ref["lessons"]: print(f"[{L['lesson_type']}] {L['content']}") client.record_outcome( session_id=run_id, reference_id="ticket-42", outcome="success", rationale="Customer confirmed the SSO bypass resolved the ticket.", entry_ids=["entry-7f3", "entry-9a1"], ) ``` #### Node / TS ```ts const ref = await client.reflect({ session_id: runId }); for (const lesson of ref.lessons ?? []) { console.log(`[${lesson.lesson_type}] ${lesson.content}`); } await client.recordOutcome({ session_id: runId, reference_id: "ticket-42", outcome: "success", rationale: "Customer confirmed the SSO bypass resolved the ticket.", entry_ids: ["entry-7f3", "entry-9a1"], }); ``` #### Rust ```rust use mubit_sdk::{RecordOutcomeOptions, ReflectOptions}; let reflection = client .reflect(ReflectOptions { run_id: Some(run_id.to_string()), ..Default::default() }) .await?; if let Some(lessons) = reflection["lessons"].as_array() { for lesson in lessons { println!("[{}] {}", lesson["lesson_type"], lesson["content"]); } } let mut outcome = RecordOutcomeOptions::new("ticket-42", "success"); outcome.run_id = Some(run_id.to_string()); outcome.rationale = "Customer confirmed the SSO bypass resolved the ticket.".to_string(); outcome.entry_ids = vec!["entry-7f3".to_string(), "entry-9a1".to_string()]; client.record_outcome(outcome).await?; ``` `reflect()` extracts new lessons from the session's traces. `record_outcome()` is the reinforcement signal — recurring `success` lessons get promoted from run-scoped to session-scoped to global. Pass `entry_ids` to attribute the outcome to every recalled entry that contributed, not just the primary `reference_id` (which is never double-counted). List each contributing entry — for example the `evidence` items that `citations` flagged as grounding the answer you acted on. ## When to drop down to `client.advanced.*` raw methods Reach for `client.advanced.*` (Python/JS; in Rust, raw ops are payload-style methods directly on `Client`, e.g. `client.ingest(...)`) only when you need: - Async ingest with explicit job polling (`advanced.ingest` + `advanced.get_ingest_job`). - Raw wire payloads for tooling/observability that needs the full response shape. - Direct access to `core.*` lanes (search, scratch, branching) that the helpers wrap. Most application code never needs this layer. See the [Control HTTP reference](/api-reference/control-http) for the full surface. --- # LLM Provider Support > Which LLM client libraries mubit.learn auto-instruments, and what each integration captures. Which LLM client libraries mubit.learn auto-instruments, and what each integration captures. `mubit.learn.init()` patches your existing LLM client classes in place — no wrapper code required. The table below lists the libraries that are auto-instrumented today. | Provider | Python | Node.js | | --- | --- | --- | | **OpenAI** | `openai` | `openai` | | **Anthropic** | `anthropic` | `@anthropic-ai/sdk` | | **Google Gemini** | `google-generativeai` | `@google/generative-ai` | | **LiteLLM** | `litellm` | — | | **Vercel AI SDK** | — | `ai` (via `@mubit-ai/ai-sdk` middleware) | What gets patched is concrete: the constructor of the provider's client class. Subsequent calls to `.messages.create()`, `.chat.completions.create()`, etc. flow through Mubit before and after the network call. ## What auto-instrumentation captures For every wrapped call: - **Pre-call:** relevant lessons retrieved from Mubit and injected into the system message (or as a synthetic system message if none exists). - **The call itself:** sent to the provider unchanged. - **Post-call:** the request, response, model name, latency, and token usage are recorded as a `trace` in the active session. On session end (or `auto_reflect=True`), `reflect()` runs and extracts new lessons from the session's traces. ## Verifying instrumentation ```python import anthropic, mubit.learn mubit.learn.init(api_key="mbt_...", agent_id="my-agent") client = anthropic.Anthropic() print(client._mubit_learn_wrapped) # True ``` A wrapped client carries `_mubit_learn_wrapped = True`. If `False`, the patch didn't take — usually because `mubit.learn.init()` was called *after* the client was constructed. Patch first, instantiate second. ## Turning instrumentation off There is no per-call opt-out today. To stop capturing entirely, call `mubit.learn.uninstrument()` — it restores the original LLM client behavior so subsequent calls flow straight to the provider: ```python import mubit.learn mubit.learn.uninstrument() # global — restores unpatched client behavior resp = anthropic.Anthropic().messages.create(...) # not captured ``` If you only need a few calls recorded (raw HTTP, an unsupported library, or a utility call you want logged on your terms), skip `init()`-style auto-instrumentation for those and capture them explicitly: ```python import mubit.learn mubit.learn.capture( messages=[{"role": "user", "content": "estimate cost"}], response=resp_text, model="claude-3-5-sonnet", ) ``` > **Note:** A scoped per-call bypass context manager is not available yet. If you need system-utility calls (cost estimates, eval pipelines, retries) excluded from evidence without disabling instrumentation globally, open a feature request against `mubit-sdk`. ## Provider-specific notes ### OpenAI - Both sync (`OpenAI`) and async (`AsyncOpenAI`) clients are patched. - Streaming responses are captured incrementally; the full assembled output is recorded. - Tool/function call traces include the tool name and arguments. ### Anthropic - Both sync (`Anthropic`) and async (`AsyncAnthropic`) clients are patched. - `messages.create` is wrapped; the deprecated `completions.create` is not. - Tool-use blocks are captured as part of the response trace. ### Google Gemini - The `google-generativeai` client is patched on first import after `init()`. - Multi-turn `ChatSession` instances inherit instrumentation from their parent client. ### LiteLLM (Python only) - LiteLLM's unified `completion()` and `acompletion()` calls are patched. - The provider-specific client behind LiteLLM is not double-wrapped. ### Vercel AI SDK (Node only) - Use `mubitMemoryMiddleware` from `@mubit-ai/ai-sdk` with `wrapLanguageModel()` rather than `learn.init()`. The Node `learn` API auto-instruments the lower-level provider SDKs but not the AI SDK abstraction. - Full example: see [Framework integrations → Vercel AI SDK](/sdk/integrations/vercel-ai-sdk). ## Adding a provider Open an issue or a PR against `mubit-sdk` with the client class name and the call surface that should be wrapped (constructor + the methods that hit the network). Most providers take less than 50 lines of patch code; the existing per-provider patches (`mubit/learn/_openai_learn.py`, `mubit/learn/_anthropic_learn.py`) are the reference pattern to follow. --- # Add data > Use helper-first writes for normal Mubit memory flows, and fall back to raw ingest only when you need explicit async ingest control. Use helper-first writes for normal Mubit memory flows, and fall back to raw ingest only when you need explicit async ingest control. Most Mubit integrations should start with `remember()`. Use raw `control.ingest` only when you need explicit async ingest behavior, job polling, or bulk payload control. For normal agent memory writes, the helper path is simpler and stays aligned with the current SDK contract. ## Recommended write contract 1. Keep one stable `session_id` / `run_id` per workflow, claim, ticket, or task. 2. Use `remember()` for facts, traces, lessons, rules, handoffs, feedback, and tool artifacts. 3. Use raw `control.ingest` when you need explicit async ingestion lifecycle control. 4. Stamp `agent_id`, `intent`, and metadata so later diagnose, reflect, and context assembly stay useful. ## Helper-first write examples #### Python ```python title="add_data_helper.py" def add_data_helper(client, run_id: str, text: str): client.remember( session_id=run_id, agent_id="support-agent", content=text, intent="fact", metadata={"source": "support", "channel": "ticket"}, ) ``` #### Node / TS ```ts title="add_data_helper.ts" export async function addDataHelper(client, runId: string, text: string) { await client.remember({ session_id: runId, agent_id: "support-agent", content: text, intent: "fact", metadata: { source: "support", channel: "ticket" }, }); } ``` #### Rust ```rust title="add_data_helper.rs" use mubit_sdk::RememberOptions; use serde_json::json; pub async fn add_data_helper( client: &mubit_sdk::Client, run_id: &str, text: &str, ) -> Result<(), Box> { let mut remember = RememberOptions::new(text); remember.run_id = Some(run_id.to_string()); remember.agent_id = Some("support-agent".to_string()); remember.intent = Some("fact".to_string()); remember.metadata = Some(json!({"source":"support","channel":"ticket"})); client.remember(remember).await?; Ok(()) } ``` ## When raw ingest is still the right choice Use raw `client.advanced.ingest` (Python/JS; `client.ingest(...)` payload-style in Rust) when you need one of these explicitly: - async job polling through `get_ingest_job` - controlled batch writes through `/v2/control/ingest` or `/v2/control/batch_insert` - wire-level debugging of serialized payloads - compatibility with systems that already generate ingest payload JSON directly > **Note:** `control.ingest` and `control.batch_insert` accept at most 1000 items per request; larger requests return 400 (InvalidArgument) — chunk bigger writes. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Important memory never shows up later | `intent` or metadata missing | Tag writes with `intent`, `agent_id`, and useful metadata | | Helper writes feel too implicit | You need job-level control | Switch that path to raw `control.ingest` | | Cross-task contamination | `session_id` changes per request | Keep one deterministic task/session mapping | ## Next steps - Read with [Retrieve data](/sdk/retrieve-data). - Use the helper catalog at [SDK methods](/sdk/sdk-methods). --- # Retrieve data > Use recall for answer-oriented retrieval and getContext for model-ready context assembly, with raw query only for explicit control. Use recall for answer-oriented retrieval and getContext for model-ready context assembly, with raw query only for explicit control. Mubit exposes two default read patterns: - `recall()` when you want Mubit to answer a question from memory - `getContext()` / `get_context()` when you want a context block to send to your own model Use raw `control.query` when you need explicit control over low-level request fields or want to debug wire-level behavior. ## Retrieval decision model | Need | Recommended method | | --- | --- | | Answer a question from stored memory | `recall()` | | Build a prompt-ready context block | `getContext()` / `get_context()` | | Inspect memory quality and gaps | `diagnose()` and `memoryHealth()` / `memory_health()` | | Explicit raw control over query payload | `client.advanced.query(...)` | | Compatibility-only direct lanes | `client.core.*` advanced routes when explicitly enabled | ## Helper-first retrieval examples #### Python ```python title="retrieve_helper.py" def retrieve_helper(client, run_id: str, question: str): answer = client.recall( session_id=run_id, query=question, entry_types=["fact", "lesson", "rule"], ) context = client.get_context( session_id=run_id, query=question, mode="summary", max_token_budget=400, ) return answer, context ``` #### Node / TS ```ts title="retrieve_helper.ts" export async function retrieveHelper(client, runId: string, question: string) { const answer = await client.recall({ session_id: runId, query: question, entry_types: ["fact", "lesson", "rule"], }); const context = await client.getContext({ session_id: runId, query: question, mode: "summary", max_token_budget: 400, }); return { answer, context }; } ``` #### Rust ```rust title="retrieve_helper.rs" use mubit_sdk::{GetContextOptions, RecallOptions}; pub async fn retrieve_helper( client: &mubit_sdk::Client, run_id: &str, question: &str, ) -> Result<(serde_json::Value, serde_json::Value), Box> { let mut recall = RecallOptions::new(question); recall.run_id = Some(run_id.to_string()); recall.entry_types = vec!["fact".to_string(), "lesson".to_string(), "rule".to_string()]; let answer = client.recall(recall).await?; let mut context = GetContextOptions::default(); context.run_id = Some(run_id.to_string()); context.query = Some(question.to_string()); context.mode = Some("summary".to_string()); context.max_token_budget = Some(400); let context = client.get_context(context).await?; Ok((answer, context)) } ``` ## What the helper path buys you - `recall()` uses the current control-plane query contract without you assembling the raw payload manually. - `getContext()` / `get_context()` gives you sectioned, cache-friendly context assembly with optional budget control. - `diagnose()` and `memoryHealth()` / `memory_health()` help you understand why retrieval is weak before you start changing prompts blindly. > **Note:** The `recall()` / `query` response includes a `citations` array — 0-based indices into `evidence` marking which items grounded `final_answer` (empty when the answer abstains or cites no specific evidence). Use them to render source attributions or audit answer grounding. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Answer ignores an important lesson | Entry types or context budget too broad | Restrict entry types and use `getContext()` budgeting | | Retrieval feels noisy | You are mixing compatibility-only direct lanes with routed paths | Standardize one default read path per workflow | | You need wire-level parity checks | Helper abstracts too much | Drop to raw `client.advanced.query(...)` for that case | ## Next steps - Inspect the helper and raw surface split at [SDK methods](/sdk/sdk-methods). - Review the full public control surface at [Control HTTP reference](/api-reference/control-http). --- # Query patterns > Use routed retrieval, direct semantic search, and control filters for structured Mubit retrieval. Use routed retrieval, direct semantic search, and control filters for structured Mubit retrieval. Mubit provides several retrieval patterns depending on your use case: | Pattern | When to use | | --- | --- | | `control.query` with `mode: "agent_routed"` | Default retrieval path — routes through the control plane for answer-oriented results | | `control.query` with `entry_types`, `include_working_memory`, `agent_id`, `user_id` | Scoped retrieval with fine-grained filters | | `control.context` | Model-ready context block with token budgeting before an LLM call | | `client.core.search(...)` | Direct semantic search when you need raw core hits (policy-gated) | ## Routed retrieval example #### Python ```python answer = client.recall( session_id="claim-123", query="What changed in the claimant's story after the second interview?", ) ``` #### Node / TS ```ts const answer = await client.recall({ session_id: "claim-123", query: "What changed in the claimant's story after the second interview?", }); ``` #### Rust ```rust use mubit_sdk::RecallOptions; let mut recall = RecallOptions::new( "What changed in the claimant's story after the second interview?", ); recall.run_id = Some("claim-123".to_string()); let answer = client.recall(recall).await?; ``` ## Structured control query example #### Python ```python result = client.advanced.query( run_id="claim-123", query="Show failure lessons from prior fraud reviews", entry_types=["lesson", "rule"], include_working_memory=False, mode="agent_routed", ) ``` #### Node / TS ```ts const result = await client.advanced.query({ run_id: "claim-123", query: "Show failure lessons from prior fraud reviews", entry_types: ["lesson", "rule"], include_working_memory: false, mode: "agent_routed", }); ``` #### Rust ```rust use serde_json::json; // Raw control ops are payload-style methods directly on `Client` in Rust. // Enum values are case-sensitive on the gRPC wire — use uppercase. let result = client .query(json!({ "run_id": "claim-123", "query": "Show failure lessons from prior fraud reviews", "entry_types": ["lesson", "rule"], "include_working_memory": false, "mode": "AGENT_ROUTED", })) .await?; ``` ## Direct semantic search example #### Python ```python results = client.core.search( query_text="windshield replacement invoice", k=5, run_id="claim-123", ) ``` #### Node / TS ```ts const results = await client.core.search({ query_text: "windshield replacement invoice", k: 5, run_id: "claim-123", }); ``` #### Rust ```rust use serde_json::json; let results = client .core .search(json!({ "query_text": "windshield replacement invoice", "k": 5, "run_id": "claim-123", })) .await?; ``` ## Guidance - Prefer `control.query` for app and agent retrieval. - Prefer `control.context` before an LLM step. - Use direct semantic search only for advanced compatibility cases where you need raw core hits. - Treat `user_id` as a logical sub-scope tag only. It narrows retrieval inside the authenticated actor boundary and never expands access. --- # SDK methods > Three integration layers — from zero-config closed-loop to full low-level control. Three integration layers — from zero-config closed-loop to full low-level control. Mubit SDKs expose three layers: | Layer | What it does | When to use | | --- | --- | --- | | **Learn** (`mubit.learn` / `learn`) | Auto-ingest all LLM interactions + auto-inject lessons + auto-reflect | Zero-config closed-loop — agents learn with one line of setup | | **Helpers** on `Client` | ~18 explicit methods for memory, context, reflection, multi-agent | Fine-grained control over what gets remembered and when | | **Raw domains** (`auth`, `advanced`, `core`) | 1:1 endpoint mappings | Wire debugging, async job polling, advanced routes | Start with learn or helpers. Drop to raw domains only when you need exact payload control. ## Learn module (closed-loop) Before each LLM call, the learn module retrieves relevant lessons from Mubit and injects them into the system message. After the call, the interaction is ingested automatically. On run end, reflection extracts new lessons. #### Python ```python import mubit.learn mubit.learn.init(api_key="mbt_...", agent_id="my-agent") # All OpenAI/Anthropic/LiteLLM calls now auto-inject lessons + auto-ingest. @mubit.learn.run(agent_id="planner", auto_reflect=True) def plan_task(task): return openai.OpenAI().chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": task}], ).choices[0].message.content ``` #### Node / TS ```js import * as learn from "@mubit-ai/sdk/learn"; const runManager = learn.init({ apiKey: "mbt_...", agentId: "my-agent" }); // learn.wrapOpenaiLearn(client, { learnConfig, lessonCache, learnClient, runManager }); const result = await learn.withRun({ agentId: "planner" }, async () => { return client.chat.completions.create({ ... }); }); ``` #### Rust ```rust use mubit_sdk::learn::{LearnSession, LearnConfig}; let session = LearnSession::new(LearnConfig::from_env().agent_id("my-agent")).await; let enriched = session.enrich_messages(&messages).await; // Make LLM call with enriched... session.record("response", "gpt-4o", 1500.0).await; session.end().await; ``` ## Helper method bundles | Use case | Methods | What they do | | --- | --- | --- | | Basic memory | `remember`, `recall` | Ingest content with intent classification; semantic query with evidence scoring | | Prompt context | `getContext` / `get_context` | Token-budgeted context block for LLM injection (rules → lessons → facts) | | Exact artifacts | `archive`, `archiveBlock` / `archive_block`, `dereference` | Bit-exact storage with stable reference IDs; retrieval without semantic search | | Run lifecycle | `checkpoint`, `reflect`, `recordOutcome` / `record_outcome`, `recordStepOutcome` / `record_step_outcome` | Durable state; LLM lesson extraction; reinforcement feedback; per-step process rewards | | Ingest lifecycle | `client.advanced.getIngestJob` / `client.advanced.get_ingest_job`, `client.advanced.getRunIngestStats` / `client.advanced.get_run_ingest_stats` | Poll async ingest jobs ([details](#ingest-job-tracking)); per-run counters. `remember()` and `ingest()` return a `job_id` — call `get_ingest_job(job_id)` until `done=True` before reading the same run. | | Multi-agent | `registerAgent` / `register_agent`, `listAgents` / `list_agents`, `handoff`, `feedback` | Scoped access per agent; task transfer | | Diagnostics | `memoryHealth` / `memory_health`, `diagnose`, `surfaceStrategies` / `surface_strategies`, `forget` | Staleness metrics; error debugging; lesson clustering; deletion | ## The learning loop ``` 1. remember() → ingest facts, traces, lessons 2. record_step_outcome() → per-step process reward signals (optional, for dense RL) 3. reflect() → LLM extracts lessons from evidence (recurring lessons promote: run → session → global) 4. get_context() → retrieve relevant lessons for the next call 5. record_outcome() → reinforce what worked at the run level ``` With learn, steps 1–3 happen automatically. With helpers, you orchestrate them yourself. > **Note:** Reflected lessons may start as `pending` candidates and only become `active` once enough evidence accrues — a lesson's score must cross the accept threshold (default ~0.6) before it is surfaced in retrieval. Low-scoring candidates (~≤0.25) are marked `rejected` and de-prioritized as stale. The control stream emits `context.lesson_validation_passed` / `context.lesson_validation_failed` alongside `context.lesson_promoted`. This validation gate is a server-side default; set `MUBIT_CONTROL_LESSON_VALIDATION_ENABLED=false` for the legacy "store immediately as active" behavior. ## Current helper catalog #### Python - `remember` - `recall` - `archive` - `archive_block` - `dereference` - `get_context` - `memory_health` - `diagnose` - `reflect` - `forget` - `checkpoint` - `register_agent` - `list_agents` - `record_outcome` - `record_step_outcome` - `learned` - `surface_strategies` - `handoff` - `feedback` - `client.advanced.get_ingest_job`, `client.advanced.get_run_ingest_stats` — poll async ingest and inspect per-run counters - `mubit.auto` for automatic trace capture (ingestion only) - `mubit.learn` for closed-loop auto-inject + auto-ingest + auto-reflect (supports `auto_extract=True` for heuristic extraction) #### Node / TS - `remember` - `recall` - `archive` - `archiveBlock` - `dereference` - `getContext` - `memoryHealth` - `diagnose` - `reflect` - `forget` - `checkpoint` - `registerAgent` - `listAgents` - `recordOutcome` - `recordStepOutcome` (typed options object from v0.12.0; same `client.recordStepOutcome({...})` call shape as the 0.11 pass-through) - `learned` - `surfaceStrategies` - `handoff` - `feedback` - `client.advanced.getIngestJob`, `client.advanced.getRunIngestStats` — poll async ingest and inspect per-run counters (advanced domain) - `learn.init`, `learn.withRun`, `learn.startRun` for closed-loop memory #### Rust - `remember` - `recall` - `archive` - `archive_block` - `dereference` - `get_context` - `memory_health` - `diagnose` - `reflect` - `forget` - `checkpoint` - `register_agent` - `list_agents` - `record_outcome` - `record_step_outcome` - `learned` - `surface_strategies` - `handoff` - `feedback` - `get_ingest_job`, `get_run_ingest_stats` — payload-style directly on `Client`; poll async ingest and inspect per-run counters - `learn::LearnSession` with `enrich_messages` / `record` / `end` for closed-loop memory ## Step-level outcomes Record per-step process rewards for dense RL signal within a run. Use after each agentic step, then reflect with the step outcomes folded in. ```python client.record_step_outcome( step_id="tool_call_1", step_name="search_api", outcome="success", signal=0.8, rationale="Found the correct document on first try", directive_hint="Keep using search before browsing", ) ``` The Python `reflect()` helper only accepts `session_id`, `include_linked_runs`, `user_id`, and `last_n_items` — there is no `include_step_outcomes` kwarg. To fold step outcomes into reflection from Python, use the raw passthrough `client.advanced.reflect({...})` (available from SDK v0.12.0, when the `advanced` domain gained every op; on 0.11.0 it does not mirror `reflect` — send `POST /v2/control/reflect` directly). In JS use the raw passthrough, and in Rust set `ReflectOptions.include_step_outcomes`: ```js // Later: reflect with step outcomes included (raw passthrough) await client.advanced.reflect({ run_id: "my-run", include_step_outcomes: true }); ``` ## Lane-scoped memory Lanes partition memory within a shared run so each agent sees only relevant entries. ```python # Ingest into a specific lane client.remember(content="Planning output: task A depends on B", intent="fact", lane="planning") # Query only the planning lane result = client.recall(query="task dependencies", lane="planning") # Register an agent with lane participation client.register_agent(agent_id="planner", role="planner", shared_memory_lanes=["planning", "shared"]) ``` > **Note:** `lane` (MAS memory isolation) is distinct from `direct_lane` (core data-plane retrieval routing). They serve different purposes and do not interact. ## Step-wise reflection Scope reflection to recent evidence for incremental lesson extraction. ```python # Reflect over only the 5 most recent items client.reflect(last_n_items=5) ``` To reflect with step outcomes included, use the wire `include_step_outcomes` field (Python: `client.advanced.reflect({...})` from SDK v0.12.0, direct gRPC/HTTP on earlier versions), Rust's `ReflectOptions.include_step_outcomes`, or the JS raw passthrough: ```js await client.advanced.reflect({ run_id: "my-run", include_step_outcomes: true }); ``` ## Auto-extraction in learn Enable heuristic extraction of rules, lessons, and facts from LLM responses without an extra LLM call. ```python mubit.learn.init( api_key="mbt_...", agent_id="my-agent", auto_extract=True, # extract structured items from LLM responses extraction_mode="heuristic", # no LLM call needed ) ``` ## When to use what | Scenario | Use | | --- | --- | | Agents should learn with zero code | `mubit.learn.init()` (Python), `learn.init()` (JS), `LearnSession::new()` (Rust) | | Passive trace capture only | `mubit.auto.instrument()` (Python only) | | Control exactly what gets remembered | `client.remember()` + `client.recall()` | | Token-budgeted context for prompts | `client.get_context()` / `client.getContext()` | | Multiple agents with scoped access | `client.register_agent()` + `client.handoff()` | | Bit-exact artifact storage | `client.archive()` + `client.dereference()` | | Wire-level debugging | `client.advanced.*` / `client.core.*` | ## When to use raw control methods directly Use `client.advanced.*` (Python/JS; Rust exposes the same long-tail ops payload-style directly on `Client`) when you need one of these explicitly: - `advanced.ingest` plus `advanced.get_ingest_job` job polling - `advanced.batch_insert` - exact raw request/response debugging against HTTP or gRPC - advanced or compatibility state-management routes - `advanced.get_ingest_job`, `advanced.get_run_ingest_stats` for job polling - `advanced.list_run_history`, `advanced.link_run`, `advanced.unlink_run` for run management (`delete_run` stays top-level) - `advanced.context_snapshot` / `advanced.contextSnapshot` for full context snapshots ### Ingest job tracking `remember()` and the raw `ingest()` are **asynchronous** — they return a `job_id` and return immediately. If the next step in your code reads the same run (for example, `recall()` or `get_context()`), poll `get_ingest_job(job_id)` until `done=True` to guarantee the new item is visible. Framework adapters that advertise "synchronous put" semantics (e.g. `MubitStore` in `mubit-langgraph`) do this polling for you; the raw SDK does not. Retrieve per-run statistics with `get_run_ingest_stats()` when you want aggregate counts without scanning activity. #### Python ```python # Submit + poll until the job is durable job_resp = client.advanced.ingest({"run_id": "my-run", "items": [{"text": "…", "intent": "fact"}]}) while True: status = client.advanced.get_ingest_job(run_id="my-run", job_id=job_resp["job_id"]) if status.get("done"): break # Per-run aggregate stats stats = client.advanced.get_run_ingest_stats(run_id="my-run") ``` #### Node / TS ```js const { job_id } = await client.advanced.ingest({ run_id: "my-run", items: [{ text: "…", intent: "fact" }] }); while (!(await client.advanced.getIngestJob({ run_id: "my-run", job_id: job_id })).done) { // yield back briefly; in production use a small backoff } const stats = await client.advanced.getRunIngestStats({ run_id: "my-run" }); ``` > **Note:** For bulk writes, prefer one `ingest({ items: [...] })` call with a list of items over many `remember()` calls — it is one job instead of N. Each ingest call accepts at most 1000 items; chunk larger workloads into multiple ingest calls (requests over the cap return 400 / InvalidArgument). ### Run management List run history, link, unlink, and delete runs. The server clamps the `limit` on run-history listing to a max of 1000 (default 100). #### Python ```python # List recent run history (advanced domain) runs = client.advanced.list_run_history(limit=100) # Link a child run to a parent client.advanced.link_run(run_id="parent-run", linked_run_id="child-run") # Unlink client.advanced.unlink_run(run_id="parent-run", linked_run_id="child-run") # Delete a run and its data client.delete_run(run_id="old-run") ``` #### Node / TS ```js const runs = await client.advanced.listRunHistory({ limit: 100 }); await client.advanced.linkRun({ run_id: "parent-run", linked_run_id: "child-run" }); await client.advanced.unlinkRun({ run_id: "parent-run", linked_run_id: "child-run" }); await client.deleteRun({ run_id: "old-run" }); // deleteRun is a top-level helper ``` ### Context snapshot Retrieve a full context snapshot for a run, including working memory, attention state, and active goals. #### Python ```python snapshot = client.advanced.context_snapshot(run_id="my-run") ``` #### Node / TS ```js const snapshot = await client.advanced.contextSnapshot({ run_id: "my-run" }); ``` ## Temporal and quality features ### Occurrence time Mubit tracks two time dimensions for every memory entry: **ingestion time** (when the system learned it) and **occurrence time** (when the event actually happened). Set `occurrence_time` to record when an event happened, separate from when it was ingested. #### Python From SDK v0.12.0 the Python `remember()` helper forwards `occurrence_time` directly (`client.remember(..., occurrence_time=...)`). On earlier versions route it through the raw `ingest` passthrough, which forwards arbitrary item fields: ```python import time # Event happened 3 days ago, ingested now client.advanced.ingest({ "run_id": "my-run", "items": [{ "text": "New CI/CD pipeline reduced deployment time by 60%.", "intent": "fact", "occurrence_time": int(time.time()) - 86400 * 3, }], }) # Historical event from January 2025 client.advanced.ingest({ "run_id": "my-run", "items": [{ "text": "Server migration to AWS completed with zero downtime.", "intent": "fact", "occurrence_time": 1736899200, # Jan 15 2025 UTC }], }) ``` #### Node / TS From SDK v0.12.0 the JS `remember()` helper forwards a top-level `occurrence_time` (or `occurrenceTime`) option directly. On earlier versions route it through the raw `advanced.ingest` passthrough, which forwards arbitrary item fields: ```js // Event happened 3 days ago, ingested now await client.advanced.ingest({ run_id: "my-run", items: [{ text: "New CI/CD pipeline reduced deployment time by 60%.", intent: "fact", occurrence_time: Math.floor(Date.now() / 1000) - 86400 * 3, }], }); // Historical event from January 2025 await client.advanced.ingest({ run_id: "my-run", items: [{ text: "Server migration to AWS completed with zero downtime.", intent: "fact", occurrence_time: 1736899200, // Jan 15 2025 UTC }], }); ``` In Rust, `RememberOptions.occurrence_time` (or `RememberBuilder::occurrence_time`) forwards the field through the typed helper. ### Temporal queries Use `min_timestamp` and `max_timestamp` to filter evidence to a specific time window. The filter checks `occurrence_time` first, falling back to ingestion time. #### Python From SDK v0.12.0 the Python `recall()` helper forwards `min_timestamp`/`max_timestamp` directly (`client.recall(..., min_timestamp=..., max_timestamp=...)`). On earlier versions route temporal filters through the raw `query` passthrough, which forwards arbitrary body fields: ```python # "What happened in January 2025?" results = client.advanced.query( run_id="my-run", query="What technical changes were made?", min_timestamp=1735689600, # Jan 1 2025 max_timestamp=1738367999, # Jan 31 2025 ) for evidence in results["evidence"]: print(f" {evidence['content'][:80]}") ``` #### Node / TS ```js const results = await client.recall({ query: "What technical changes were made?", min_timestamp: 1735689600, max_timestamp: 1738367999, }); results.evidence.forEach(e => console.log(e.content.slice(0, 80))); ``` Without temporal bounds, queries like "What happened last week?" use natural language temporal intent detection and prioritize entries by occurrence time in the recency ranking. ### Search budget The `budget` parameter controls the depth of retrieval. Use `"low"` for real-time agents and `"high"` for accuracy-critical offline analysis. | Budget | Behavior | Typical latency | | --- | --- | --- | | `"low"` | Fewer candidates, skip deep traversal | < 500ms | | `"mid"` | Standard retrieval (default) | 500ms–2s | | `"high"` | More candidates, deeper graph traversal | 1–5s | #### Python From SDK v0.12.0 the Python `recall()` helper forwards `budget` directly (`client.recall(..., budget="low")`). On earlier versions route it through the raw `query` passthrough: ```python # Fast retrieval for a real-time chatbot fast = client.advanced.query(run_id="my-run", query="user question", budget="low") # Deep retrieval for a research report deep = client.advanced.query(run_id="my-run", query="comprehensive analysis topic", budget="high") ``` #### Node / TS ```js // Fast const fast = await client.recall({ query: "user question", budget: "low" }); // Deep const deep = await client.recall({ query: "analysis topic", budget: "high" }); ``` ### Staleness detection When a newer fact contradicts an older one, Mubit marks the older entry as stale and deprioritizes it in ranking. The staleness metadata is available in evidence responses. #### Python ```python results = client.recall(query="Where is the office?") for evidence in results["evidence"]: stale = evidence.get("is_stale", False) status = " [STALE]" if stale else "" print(f" {evidence['content'][:60]}{status}") ``` #### Node / TS ```js const results = await client.recall({ query: "Where is the office?" }); results.evidence.forEach(e => { const status = e.is_stale ? " [STALE]" : ""; console.log(` ${e.content.slice(0, 60)}${status}`); }); ``` > **Note:** Stale entries are still returned for transparency. The ranking penalty ensures they appear below the current fact. Filter them out in your application if you only want current information. ### Mental models The `mental_model` entry type stores consolidated entity summaries that are prioritized over raw facts in context assembly. Use this for entities your agent tracks over time. #### Python ```python client.remember( content="Alice Chen is a senior engineer specializing in distributed systems. " "She prefers async communication and reviews PRs within 24 hours.", intent="mental_model", metadata={"entity": "alice chen", "consolidated": True}, ) ``` #### Node / TS ```js await client.remember({ content: "Alice Chen is a senior engineer specializing in distributed systems. " + "She prefers async communication and reviews PRs within 24 hours.", intent: "mental_model", metadata: { entity: "alice chen", consolidated: true }, }); ``` Mental models are returned with higher priority than individual facts in `recall()` and `get_context()`. Update them periodically as your agent learns more about an entity. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | SDK usage becomes inconsistent across teams | Raw and helper paths mixed arbitrarily | Set helpers as the default integration contract | | Debugging a route contract is awkward | Helper layer hides wire details | Use the raw `client.advanced.*` call for that investigation | | Docs and examples drift from SDK reality | Helpers undocumented | Treat the top-level helper surface as the public default | ## Next steps - See the concrete helper flow at [Getting started](/getting-started). - See the wire contract at [Control HTTP reference](/api-reference/control-http). --- # CrewAI Integration > Route CrewAI's unified Memory system through Mubit. Agent observations persist across runs and crews. Route CrewAI's unified Memory system through Mubit. Agent observations persist across runs and crews. The `mubit-crewai` adapter exposes a `StorageBackend` that CrewAI's unified Memory accepts. Once wired up, every agent observation in every crew run is queryable across crews — without changing how the crew is built. ```bash pip install mubit-crewai[crewai] ``` ## Minimal usage ```python title="crewai_minimal.py" from mubit_crewai import MubitCrewMemory from crewai import Crew, Process memory = MubitCrewMemory(api_key="mbt_...", session_id="crew-run-1") crew = Crew( agents=[researcher, writer], tasks=[research_task, write_task], process=Process.sequential, memory=memory.as_crew_memory(), ) result = crew.kickoff(inputs={"topic": "AI safety"}) # Extended Mubit features memory.checkpoint("Research phase complete") memory.record_outcome("task-1", "success") memory.handoff("researcher", "writer", "Here are findings", requested_action="execute") ``` ## Full example: Support ticket triage crew A 3-agent crew (classifier, researcher, responder) that processes customer support tickets. Each agent learns from previous triage outcomes through Mubit memory. ```python title="support_triage/main.py" from crewai import Agent, Task, Crew, Process from mubit_crewai import MubitCrewMemory memory = MubitCrewMemory( endpoint="https://api.mubit.ai", api_key="mbt_...", session_id="triage-001", agent_id="crewai-triage", ) # Register agents for MAS coordination for agent_def in [ {"agent_id": "classifier", "role": "ticket-classifier"}, {"agent_id": "researcher", "role": "solution-researcher"}, {"agent_id": "responder", "role": "response-drafter"}, ]: memory.register_agent(**agent_def) classifier = Agent( role="Support Ticket Classifier", goal="Classify tickets by severity and category", backstory="Experienced support lead who identifies severity and escalation triggers.", llm="openai/gpt-4o-mini", ) researcher = Agent( role="Solution Researcher", goal="Find relevant past solutions from Mubit memory", backstory="Knowledge specialist who searches for similar tickets and resolution patterns.", llm="openai/gpt-4o-mini", ) responder = Agent( role="Customer Response Drafter", goal="Draft empathetic, actionable replies", backstory="Senior success agent who turns frustrated customers into advocates.", llm="openai/gpt-4o-mini", ) classify_task = Task( description="Classify this ticket: {ticket}", expected_output="Severity, category, key issues, escalation flags.", agent=classifier, ) research_task = Task( description="Research solutions for: {ticket}", expected_output="Past cases, known solutions, systemic patterns.", agent=researcher, ) respond_task = Task( description="Draft a response for: {ticket}", expected_output="Professional, empathetic customer response.", agent=responder, ) crew = Crew( agents=[classifier, researcher, responder], tasks=[classify_task, research_task, respond_task], process=Process.sequential, memory=memory.as_crew_memory(), ) result = crew.kickoff(inputs={"ticket": "Duplicate charge on Pro subscription..."}) # Post-run: handoffs, checkpoint, outcome memory.handoff("classifier", "researcher", "Classification complete.", requested_action="continue") memory.handoff("researcher", "responder", "Research complete.", requested_action="execute") memory.checkpoint(snapshot="Triage complete.", label="triage-complete") memory.record_outcome(reference_id="triage-001", outcome="success", rationale="Ticket resolved.") ``` ## Extended Mubit features `MubitCrewMemory` exposes the full MAS surface alongside the framework hook: ```python memory.register_agent("...", role="...", read_scopes=[...], write_scopes=[...]) memory.handoff("from-agent", "to-agent", "...", requested_action="review") memory.checkpoint("...", label="...") memory.record_outcome("...", "success", rationale="...") memory.surface_strategies() # cluster recurring success lessons memory.diagnose("error text", error_type="...", limit=10) # failure-path lessons for debugging ``` See [SDK helpers](/sdk/sdk-helpers) for the meaning of each method. ## Gotchas - **Memory scope is per `session_id`.** A new `session_id` is a fresh crew memory; reuse the id to share memory across kickoffs. - **CrewAI's built-in short-term and long-term memories are both routed through Mubit.** No need to configure them separately. - **Sequential vs. hierarchical processes** both work; `Process.hierarchical` benefits especially from `register_agent()` so handoffs are queryable. ## Version compatibility | `mubit-crewai` | `mubit-sdk` | `crewai` | | --- | --- | --- | | `0.5.x` | `>= 0.6.0` | `>= 0.86` | When upgrading the core SDK, also upgrade `mubit-crewai` to a matching minor. --- # LangGraph Integration > Use Mubit as a persistent BaseStore for LangGraph StateGraphs. Each node reads or writes through PutOp / SearchOp. Use Mubit as a persistent BaseStore for LangGraph StateGraphs. Each node reads or writes through PutOp / SearchOp. The `mubit-langgraph` adapter implements LangGraph's `BaseStore` interface. Pass it to `graph.compile(store=...)` and your graph nodes can read or write memory through the standard `PutOp` / `SearchOp` ops without any LangGraph-specific Mubit knowledge. ```bash pip install mubit-langgraph[langgraph] ``` ## Minimal usage ```python title="langgraph_minimal.py" from mubit_langgraph import MubitStore from langgraph.graph import StateGraph from langgraph.store.base import PutOp, SearchOp store = MubitStore(api_key="mbt_...") NS = ("memories", "user-1", "session-1") # In graph nodes, use the store directly store.batch([PutOp(namespace=NS, key="finding-1", value={"text": "...", "intent": "lesson"})]) results = store.batch([SearchOp(namespace_prefix=NS, query="security issues", limit=5)]) # MAS extensions store.register_agent(NS, agent_id="reviewer", role="code-reviewer") store.checkpoint(NS, snapshot="Review complete") store.handoff(NS, from_agent_id="planner", to_agent_id="reviewer", content="...", requested_action="review") graph.compile(store=store) ``` ## Full example: Code review pipeline A `StateGraph` with planner, reviewer loop, and summarizer. The Mubit store persists findings across steps and across sessions. ```python title="code_review/main.py" from langgraph.graph import StateGraph, START, END from langgraph.store.base import PutOp, SearchOp from mubit_langgraph import MubitStore store = MubitStore(endpoint="https://api.mubit.ai", api_key="mbt_...") NAMESPACE = ("memories", "code-reviewer", "review-session") # Register agents for agent_id, role in [ ("planner", "review-planner"), ("reviewer", "item-reviewer"), ("summarizer", "review-summarizer"), ]: store.register_agent(NAMESPACE, agent_id=agent_id, role=role) def planner_node(state): past = store.batch([SearchOp(namespace_prefix=NAMESPACE, query="code review checklist", limit=3)])[0] # ... LLM call to generate checklist ... store.checkpoint(NAMESPACE, snapshot=f"Checklist created with {len(checklist)} items") return {"checklist": checklist, "current_idx": 0, "findings": []} def reviewer_node(state): item = state["checklist"][state["current_idx"]] # ... LLM call to evaluate item ... store.batch([PutOp( namespace=NAMESPACE, key=f"finding-{state['current_idx']}", value={"text": finding, "intent": "lesson"}, )]) return {"findings": state["findings"] + [finding], "current_idx": state["current_idx"] + 1} def summarizer_node(state): context = store.get_context(NAMESPACE, query="code review findings", max_token_budget=4096) # ... LLM call to synthesize final review ... store.record_outcome(NAMESPACE, reference_id="review-001", outcome="success", rationale="Review completed.") return {"final_review": review} graph = StateGraph(ReviewState) graph.add_node("planner", planner_node) graph.add_node("reviewer", reviewer_node) graph.add_node("summarizer", summarizer_node) graph.add_edge(START, "planner") graph.add_edge("planner", "reviewer") graph.add_conditional_edges("reviewer", should_continue, {"reviewer": "reviewer", "summarizer": "summarizer"}) graph.add_edge("summarizer", END) result = graph.compile().invoke({ "code_diff": "...", "checklist": [], "current_idx": 0, "findings": [], "final_review": "", }) ``` ## Extended Mubit features `MubitStore` exposes the full MAS surface. Every method takes the namespace as its first positional argument: ```python store.register_agent(NS, agent_id="...", role="...", read_scopes=[...], write_scopes=[...]) store.handoff(NS, from_agent_id="...", to_agent_id="...", content="...", requested_action="review") store.feedback(NS, handoff_id="...", verdict="approve", from_agent_id="...") store.checkpoint(NS, snapshot="...", label="...") store.record_outcome(NS, reference_id="...", outcome="success", rationale="...") store.surface_strategies(NS) store.diagnose(NS, error_text="...") store.archive(NS, content="...", artifact_kind="...") store.dereference(NS, reference_id="...") store.get_context(NS, query="...", max_token_budget=...) ``` ## Gotchas - **Namespace is the source of truth for scope.** Use a stable tuple shape across nodes (`(memories, user_id, session_id)`) so `SearchOp` finds writes from earlier nodes. - **`PutOp.value` must be a dict.** Cast strings before storing — the adapter rejects raw strings. - **`SearchOp` returns a list of `Item` objects.** Each `Item.value` is the dict you stored. - **JS adapter:** `npm install @mubit-ai/langgraph` provides the same surface async, with `camelCase` method names. ## Version compatibility | `mubit-langgraph` | `mubit-sdk` | `langgraph` | | --- | --- | --- | | `0.5.x` | `>= 0.6.0` | `>= 0.2.40` | --- # LangChain Integration > Drop-in BaseMemory for any LangChain chain. Cross-session memory works automatically through Mubit's semantic retrieval. Drop-in BaseMemory for any LangChain chain. Cross-session memory works automatically through Mubit's semantic retrieval. `mubit-langchain` provides `MubitChatMemory`, a `BaseMemory` subclass that any LangChain chain accepts. It auto-loads context before each call and saves the interaction after; cross-session retrieval works through Mubit's semantic recall, not LangChain's local buffer. ```bash pip install mubit-langchain[langchain] ``` ## Minimal usage ```python title="langchain_minimal.py" from mubit_langchain import MubitChatMemory from langchain_openai import ChatOpenAI memory = MubitChatMemory(api_key="mbt_...", session_id="chat-1") llm = ChatOpenAI(model="gpt-4o-mini") # Manual loop — useful for understanding the contract context = memory.load_memory_variables({"input": "What happened yesterday?"}) # build messages with context["history"], call LLM memory.save_context({"input": question}, {"output": response}) ``` ## Full example: Research assistant with cross-session memory A multi-turn research conversation across two sessions. Session 2 retrieves relevant facts from Session 1. ```python title="research_assistant/main.py" from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, SystemMessage from mubit_langchain import MubitChatMemory llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3) # Session 1: learn about a topic memory_s1 = MubitChatMemory( api_key="mbt_...", session_id="research-s1", agent_id="research-assistant", ) for question in [ "What caused the 2008 financial crisis?", "How did subprime mortgages contribute?", ]: context = memory_s1.load_memory_variables({"input": question}) messages = [SystemMessage(content="You are a research assistant.")] if context.get("history"): messages.extend(context["history"]) messages.append(HumanMessage(content=question)) response = llm.invoke(messages) memory_s1.save_context({"input": question}, {"output": response.content}) # Session 2: new session, cross-session memory works automatically memory_s2 = MubitChatMemory( api_key="mbt_...", session_id="research-s2", agent_id="research-assistant", ) context = memory_s2.load_memory_variables({"input": "What about crisis prevention?"}) # Session 2's context now includes relevant facts from Session 1. ``` ## Extended LangChain components `MubitChatMemory` implements only the `BaseMemory` contract (`load_memory_variables` / `save_context`). The Mubit control surface (reflect, record_outcome, surface_strategies, handoffs, and so on) lives on the SDK `Client`, not on the memory object — call those through `Client` directly when you need them. For LangChain-native wiring, `mubit-langchain` ships two additional components: > **Note:** Both components hold their own Mubit connection — pass `api_key` and `session_id` directly. They do not read state from a `MubitChatMemory` instance. **`MubitLessonRetriever`** — a LangChain `BaseRetriever` that surfaces validated strategies as retrievable documents. It wraps the SDK's `surface_strategies` call and returns each strategy as a `Document` from `get_relevant_documents`, so you can drop it into any retrieval chain: ```python from mubit_langchain import MubitLessonRetriever retriever = MubitLessonRetriever(api_key="mbt_...", session_id="chat-1") docs = retriever.get_relevant_documents("How should I structure the migration?") # docs -> [Document(page_content=, metadata={...}), ...] ``` **`MubitCallbackHandler`** — a LangChain callback handler that records the outcome of each chain run back into Mubit. Add it to a chain's `callbacks` list; on completion it calls `record_outcome` with `success_signal` (default `0.8`) and on error with `failure_signal` (default `-0.8`), so reflection learns from real runs: ```python from mubit_langchain import MubitCallbackHandler from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate handler = MubitCallbackHandler( api_key="mbt_...", session_id="chat-1", success_signal=0.8, failure_signal=-0.8, ) # Attach the handler to a chain — it records the outcome on on_chain_end / # on_chain_error. A bare llm.invoke() only emits on_llm_* events, which this # handler does not observe, so the outcome would never be recorded. chain = ChatPromptTemplate.from_template("Summarize: {input}") | ChatOpenAI(model="gpt-4o-mini") response = chain.invoke({"input": "the migration plan"}, config={"callbacks": [handler]}) ``` ## Gotchas - **`memory_key` defaults to `"history"`.** If your chain expects a different key, pass `MubitChatMemory(memory_key="chat_history", ...)`. - **`return_messages=True`** is the default — `context["history"]` is a list of `BaseMessage` objects, not a single string. - **Session vs. user memory:** for cross-session recall keyed by user, pass `user_id="…"` to `MubitChatMemory` and store with `intent="lesson"`, `lesson_scope="global"`. See [SDK helpers → cross-session recall](/sdk/sdk-helpers#cross-session-recall). - **`load_memory_variables()` makes a network call.** Don't call it inside tight loops. ## Version compatibility | `mubit-langchain` | `mubit-sdk` | `langchain` | | --- | --- | --- | | `0.5.x` | `>= 0.6.0` | `>= 0.3.0` | --- # LlamaIndex Integration > Use Mubit as the persistent chat memory for LlamaIndex chat engines and agents. One memory pool across sessions. Use Mubit as the persistent chat memory for LlamaIndex chat engines and agents. One memory pool across sessions. The `mubit-llama-index` adapter exposes a single class: - `MubitChatMemoryBuffer` — a drop-in substitute for LlamaIndex's `BaseMemory`. It duck-types the `get` / `get_all` / `put` / `set` / `reset` contract and forwards every turn to Mubit, so conversation history survives across processes and sessions. Assign it to `Settings.memory` (or pass it as the `memory=` argument to a chat engine) and LlamaIndex stores and retrieves chat history through Mubit. ```bash pip install mubit-llama-index[llama-index] ``` ## Minimal usage ```python title="llamaindex_minimal.py" from llama_index.core import Settings from mubit_llama_index import MubitChatMemoryBuffer Settings.memory = MubitChatMemoryBuffer( endpoint="https://api.mubit.ai", api_key="mbt_...", session_id="li-1", agent_id="llama_index", token_limit=2048, ) ``` ## Full example: Document-grounded chat with persistent memory A chat engine that answers from indexed docs and remembers every exchange. Session 2 resumes the same Mubit-backed conversation, so the prior turns are surfaced even in a fresh process. ```python title="llama_rag/main.py" import os from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, Settings from llama_index.llms.openai import OpenAI from mubit_llama_index import MubitChatMemoryBuffer endpoint = os.environ.get("MUBIT_ENDPOINT", "https://api.mubit.ai") api_key = os.environ["MUBIT_API_KEY"] Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0.2) memory = MubitChatMemoryBuffer( endpoint=endpoint, api_key=api_key, session_id="llama-chat-s1", agent_id="llama_index", token_limit=2048, ) documents = SimpleDirectoryReader("./docs").load_data() index = VectorStoreIndex.from_documents(documents) chat_engine = index.as_chat_engine(chat_mode="context", memory=memory) # Session 1: a few turns, all persisted to Mubit for question in [ "What's our deployment process?", "Who approves production releases?", ]: response = chat_engine.chat(question) print(f"Q: {question}\nA: {response}\n") # Close the reinforcement loop directly on the memory buffer memory.record_outcome( outcome="success", signal=1.0, reference_id="llama-rag-turn-2", rationale="Retrieval surfaced approval workflow correctly.", ) # Session 2: same session_id — Mubit replays the prior conversation memory_s2 = MubitChatMemoryBuffer( endpoint=endpoint, api_key=api_key, session_id="llama-chat-s1", # same session → cross-process continuity agent_id="llama_index", token_limit=2048, ) chat_engine_s2 = index.as_chat_engine(chat_mode="context", memory=memory_s2) print(chat_engine_s2.chat("Remind me — who approves prod releases?")) ``` ## Recording outcomes The buffer carries one convenience method beyond the `BaseMemory` contract: `record_outcome`, which forwards a chain-level result to Mubit so it can reinforce the memories that produced it. ```python memory.record_outcome( outcome="success", # required signal=1.0, # required reinforcement strength reference_id="turn-2", # optional; defaults to "global" rationale="Answer matched the approval workflow.", # optional ) ``` `session_id`, `agent_id`, and `user_id` are taken from the buffer you constructed, so you only pass the outcome itself. ## Gotchas - **`session_id` controls cross-session continuity.** Construct a new `MubitChatMemoryBuffer` with the same `session_id` and Mubit replays the prior conversation — even in a separate process. - **`reset()` clears the session.** It deletes the current session's working memory, so the next `get()` starts empty. - **Need the full control surface?** The memory buffer covers chat history and outcomes. For agent registration, handoffs, reflection, or checkpoints, use the canonical `mubit-sdk` `Client` against the same `session_id` (see the Python SDK reference). ## Version compatibility | `mubit-llama-index` | `mubit-sdk` | `llama-index` | | --- | --- | --- | | `0.5.x` | `>= 0.6.0` | `>= 0.11.0` | --- # Google ADK Integration > Plug Mubit into ADK's Runner as a BaseMemoryService. Session events auto-ingest; memory search enriches agent context. Plug Mubit into ADK's Runner as a BaseMemoryService. Session events auto-ingest; memory search enriches agent context. The `mubit-adk` adapter implements ADK's `BaseMemoryService`. Pass it to `Runner(memory_service=…)` and every session event is automatically ingested into Mubit; memory search enriches agent context on the next turn. ```bash pip install mubit-adk[adk] ``` ## Minimal usage ```python title="google_adk_minimal.py" from mubit_adk import MubitMemoryService from google.adk.agents import LlmAgent, SequentialAgent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService memory_service = MubitMemoryService(api_key="mbt_...") agent = SequentialAgent(name="coordinator", sub_agents=[flight_agent, hotel_agent, planner_agent]) runner = Runner( agent=agent, app_name="travel", session_service=InMemorySessionService(), memory_service=memory_service, ) # MAS extensions await memory_service.checkpoint(app_name="travel", user_id="user-1", snapshot="Plan complete") await memory_service.register_agent(user_id="user-1", agent_id="planner", role="itinerary") ``` ## Full example: Travel planner A `SequentialAgent` with Gemini, tool calling, and MAS coordination. Three agents (flight finder, hotel finder, itinerary planner) collaborate through Mubit memory. ```python title="travel_planner/main.py" from google.adk.agents import LlmAgent, SequentialAgent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from mubit_adk import MubitMemoryService mubit_memory = MubitMemoryService(endpoint="https://api.mubit.ai", api_key="mbt_...") flight_finder = LlmAgent( name="flight_finder", model="gemini-3.1-flash-lite-preview", description="Finds the best flights", instruction="Search for flights and recommend the best option.", tools=[search_flights], output_key="flight_results", ) hotel_finder = LlmAgent( name="hotel_finder", model="gemini-3.1-flash-lite-preview", description="Finds accommodations", instruction="Search for hotels and recommend the best option.", tools=[search_hotels], output_key="hotel_results", ) itinerary_planner = LlmAgent( name="itinerary_planner", model="gemini-3.1-flash-lite-preview", description="Creates day-by-day itinerary", instruction="Combine flight and hotel info into a complete travel plan.", output_key="final_itinerary", ) coordinator = SequentialAgent( name="travel_coordinator", sub_agents=[flight_finder, hotel_finder, itinerary_planner], ) runner = Runner( agent=coordinator, app_name="travel", session_service=InMemorySessionService(), memory_service=mubit_memory, ) # After the pipeline runs, record Mubit operations await mubit_memory.register_agent(user_id="user-1", agent_id="flight_finder", role="finder") await mubit_memory.checkpoint(app_name="travel", user_id="user-1", snapshot="Plan complete") await mubit_memory.record_outcome( app_name="travel", user_id="user-1", reference_id="travel-001", outcome="success", rationale="Itinerary confirmed by user.", ) ``` ## Extended Mubit features `MubitMemoryService` is async — every method returns a coroutine. ```python await memory_service.register_agent(user_id="...", agent_id="...", role="...", read_scopes=[...], write_scopes=[...]) await memory_service.handoff(user_id="...", from_agent_id="...", to_agent_id="...", task_id="...", content="...", requested_action="review") await memory_service.feedback(user_id="...", handoff_id="...", verdict="approve", from_agent_id="...") await memory_service.checkpoint(app_name="...", user_id="...", snapshot="...", label="...") await memory_service.record_outcome(app_name="...", user_id="...", reference_id="...", outcome="success", rationale="...") await memory_service.surface_strategies(session_id="...") await memory_service.diagnose(user_id="...", error_text="...") await memory_service.archive(app_name="...", user_id="...", content="...", artifact_kind="...") await memory_service.dereference(user_id="...", reference_id="...") ``` ## Gotchas - **`app_name` + `user_id`** form the namespace ADK uses internally; the adapter joins them into a Mubit session id. - **Auto-ingestion is opt-out, not opt-in.** Set `auto_ingest_events=False` on `MubitMemoryService(...)` if you want to capture only what you call manually. - **Gemini streaming responses** are captured incrementally; the assembled output is what gets stored. ## Version compatibility | `mubit-adk` | `mubit-sdk` | `google-adk` | | --- | --- | --- | | `0.5.x` | `>= 0.6.0` | `>= 1.0.0` | --- # Agno Integration > Use Mubit as a persistent memory backend and toolkit for Agno agents. MemoryDb plus an LLM-callable Toolkit. Use Mubit as a persistent memory backend and toolkit for Agno agents. MemoryDb plus an LLM-callable Toolkit. The `mubit-agno` adapter exposes two integration surfaces: - `MemoryDb` — Agno's built-in memory persistence layer. - `Toolkit` — LLM-callable tools (`mubit_remember`, `mubit_recall`, `mubit_reflect`, `mubit_checkpoint`, `mubit_diagnose`). Use `MemoryDb` to give Agno's existing memory system durable storage; use the Toolkit when you want the LLM itself to drive memory operations. ```bash pip install mubit-agno[agno] ``` ## Minimal usage ```python title="agno_minimal.py" from agno.agent import Agent from agno.models.openai import OpenAIChat from agno.memory.v2.memory import Memory from mubit_agno import MubitAgnoMemory mubit = MubitAgnoMemory(api_key="mbt_...", session_id="run-1") agent = Agent( name="Assistant", model=OpenAIChat(id="gpt-4o"), memory=Memory(db=mubit.as_memory_db()), tools=[mubit.as_toolkit()], enable_agentic_memory=True, ) result = agent.run("What do we know about the production database?") # Extended Mubit features mubit.checkpoint("Research done", "Completed infra review") mubit.record_outcome("infra-recall", "success", rationale="Correct recall") mubit.reflect() mubit.archive("SELECT * FROM users WHERE active", "sql_query", labels=["infra"]) ref = mubit.dereference("ref_abc123") ``` ## Full example: Basic agent with persistent memory An Agno agent backed by Mubit. Memories persist across sessions, enabling cross-conversation learning. ```python title="basic.py" import os from agno.agent import Agent from agno.models.openai import OpenAIChat from agno.memory.v2.memory import Memory from mubit_agno import MubitAgnoMemory endpoint = os.environ.get("MUBIT_ENDPOINT", "https://api.mubit.ai") api_key = os.environ["MUBIT_API_KEY"] mubit = MubitAgnoMemory( endpoint=endpoint, api_key=api_key, session_id="basic-example", user_id="demo-user", ) agent = Agent( name="Assistant", model=OpenAIChat(id="gpt-4o"), memory=Memory(db=mubit.as_memory_db()), tools=[mubit.as_toolkit()], enable_agentic_memory=True, instructions=[ "You have access to Mubit memory tools.", "Use mubit_remember to store important information.", "Use mubit_recall to search for relevant memories.", ], ) # Run 1: store knowledge agent.run( "Remember that our production database is PostgreSQL 16 " "running on AWS RDS in us-east-1, and we use Redis for caching.", session_id="session-1", ) mubit.checkpoint("Initial learning", "Stored infrastructure facts") # Run 2: recall knowledge (cross-session) agent.run("What database do we use in production?", session_id="session-2") mubit.record_outcome( "infrastructure-recall", "success", rationale="Agent correctly recalled database details from memory", ) ``` ## Full example: Multi-agent coordination Two Agno agents — a researcher and a reviewer — share a Mubit run. The researcher hands off findings; the reviewer evaluates, surfaces strategies, and records the outcome. ```python title="multi_agent.py" import os from agno.agent import Agent from agno.models.openai import OpenAIChat from agno.memory.v2.memory import Memory from mubit_agno import MubitAgnoMemory endpoint = os.environ.get("MUBIT_ENDPOINT", "https://api.mubit.ai") api_key = os.environ["MUBIT_API_KEY"] SESSION = "agno-coord-1" researcher_mem = MubitAgnoMemory( endpoint=endpoint, api_key=api_key, session_id=SESSION, agent_id="researcher", ) reviewer_mem = MubitAgnoMemory( endpoint=endpoint, api_key=api_key, session_id=SESSION, agent_id="reviewer", ) # Register both agents so handoffs and scopes are explicit for mem, agent_id, role, scopes in [ (researcher_mem, "researcher", "solution-researcher", ["fact", "lesson"]), (reviewer_mem, "reviewer", "quality-reviewer", ["fact", "lesson", "rule"]), ]: mem.register_agent(agent_id, role=role, read_scopes=scopes, write_scopes=scopes) researcher = Agent( name="Researcher", model=OpenAIChat(id="gpt-4o-mini"), memory=Memory(db=researcher_mem.as_memory_db()), tools=[researcher_mem.as_toolkit()], enable_agentic_memory=True, ) reviewer = Agent( name="Reviewer", model=OpenAIChat(id="gpt-4o-mini"), memory=Memory(db=reviewer_mem.as_memory_db()), tools=[reviewer_mem.as_toolkit()], enable_agentic_memory=True, ) # Researcher produces findings and hands off to reviewer findings = researcher.run( "Investigate why tenant billing discrepancies spiked this week. " "Store the top 3 causes as facts." ) researcher_mem.handoff( from_agent_id="researcher", to_agent_id="reviewer", content=str(findings.content)[:1000], requested_action="review", ) # Reviewer consumes the handoff, evaluates, feeds back, surfaces strategies review = reviewer.run( "Review the researcher's billing findings. Call out anything weak and " "use mubit_strategies to see what has worked before." ) reviewer_mem.feedback( handoff_id=findings.run_id, verdict="approve", comments=str(review.content)[:1000], from_agent_id="reviewer", ) # Close the loop: reflect and record outcome reviewer_mem.reflect() reviewer_mem.record_outcome( reference_id="billing-discrepancy-review", outcome="success", rationale="Reviewer confirmed findings matched stored rules.", ) strategies = reviewer_mem.surface_strategies(lesson_types=["success"], max_strategies=3) print(f"Reusable strategies: {len(strategies.get('strategies', []))}") ``` ## Extended Mubit features `MubitAgnoMemory` exposes the full MAS surface as instance methods: ```python mubit.register_agent("agent-id", role="...", read_scopes=[...], write_scopes=[...]) mubit.handoff(from_agent_id="...", to_agent_id="...", content="...", requested_action="review") mubit.feedback(handoff_id="...", verdict="approve", from_agent_id="...") mubit.checkpoint(snapshot="...", label="...") mubit.record_outcome(reference_id="...", outcome="success", rationale="...") mubit.reflect() mubit.surface_strategies(lesson_types=["success"], max_strategies=3) mubit.diagnose(query="...") mubit.archive(content="...", artifact_kind="...", labels=[...]) mubit.dereference(reference_id="...") mubit.memory_health() ``` ## Gotchas - **The Toolkit and the MemoryDb are independent.** Pass both to `Agent(...)` if you want both Agno's automatic memory and the LLM-callable tools. - **`enable_agentic_memory=True`** enables Agno's read-write memory loop. Without it the LLM can't call the tools. - **`session_id` is the Mubit scope.** Reuse it across `agent.run()` calls to share memory. ## Version compatibility | `mubit-agno` | `mubit-sdk` | `agno` | | --- | --- | --- | | `0.5.x` | `>= 0.6.0` | `>= 1.0.0` | --- # AutoGen Integration > Attach Mubit as an AutoGen Memory provider. Recall is injected before every model turn; outcomes credit the exact memories that helped. Attach Mubit as an AutoGen Memory provider. Recall is injected before every model turn; outcomes credit the exact memories that helped. The `mubit-autogen` adapter exposes two integration surfaces: - `MubitMemory` — an implementation of `autogen_core.memory.Memory`, so it attaches directly to an `AssistantAgent(memory=[...])`. AutoGen calls `update_context()` before each model turn: Mubit recalls relevant context and injects it as a `SystemMessage`, and new assistant/user content is persisted via `add()`. - `MubitGroupChatHook` — forwards multi-agent handoff and feedback signals into Mubit so team activity is queryable alongside memory. > **Note:** The adapter targets the current AutoGen API — `autogen-core` / `autogen-agentchat` 0.7.x (`AssistantAgent`, `autogen_core.memory.Memory`). It does not work with the legacy `pyautogen` 0.2 line (`ConversableAgent` and friends); migrate to AgentChat first if you are still on 0.2. ```bash pip install "mubit-autogen[autogen]" # AutoGen over-pins protobuf<6 while mubit-sdk needs >=6.33 — apply after install: pip install -U "protobuf>=6.33.4" ``` ## Minimal usage ```python title="autogen_minimal.py" import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_ext.models.openai import OpenAIChatCompletionClient from mubit_autogen import MubitMemory memory = MubitMemory( endpoint="https://api.mubit.ai", api_key="mbt_...", session_id="support-1", # the Mubit run that backs this memory agent_id="support-assistant", ) agent = AssistantAgent( name="support", model_client=OpenAIChatCompletionClient(model="gpt-4o"), memory=[memory], # Mubit recall is injected before every model turn ) async def main() -> None: result = await agent.run(task="Customer says checkout 500s on Safari.") print(result.messages[-1].content) asyncio.run(main()) ``` ## What gets captured and injected - **Injected** — before each model turn, `update_context()` recalls memory relevant to the latest message and appends it to the model context as a single `SystemMessage`. Recalled lessons, facts, and prior traces arrive without any prompt plumbing on your side. - **Captured** — `add()` persists content via Mubit `remember`. User-role content lands with a `context` intent, everything else as a `trace`, unless you set `metadata["intent"]` explicitly. Pass a stable `metadata["idempotency_key"]` so a retried write is deduplicated server-side instead of double-counting reinforcement. - **Attributable** — every recalled `MemoryContent` keeps its source `reference_id` in `metadata`. Pull those IDs off a query result with `extract_entry_ids()` and feed them into `record_outcome(entry_ids=...)` so the memories that actually helped get credited. See [The learning loop](/concepts/learning-loop) for why this matters. Reinforcement is explicit, not implicit: the adapter emits no neutral per-reply signal. You close the loop yourself when you know the real outcome. ## Cross-session recall `session_id` maps to the Mubit run — the memory scope. Reuse the same `session_id` across processes and days and the agent recalls what earlier sessions stored; use a fresh `session_id` for isolated memory. `clear()` deletes the backing run entirely. ## Full example: Support assistant with the attribution loop Recall prior tickets, answer a new one grounded in them, then credit the recalled entries with the real outcome. ```python title="support_assistant.py" import asyncio import os from autogen_agentchat.agents import AssistantAgent from autogen_core.memory import MemoryContent, MemoryMimeType from autogen_ext.models.openai import OpenAIChatCompletionClient from mubit_autogen import MubitMemory, extract_entry_ids NEW_TICKET = ( "Checkout fails with a 500 error on Safari. Works fine in Chrome. " "I'm on a Pro plan and need to renew today." ) async def main() -> None: memory = MubitMemory( endpoint=os.environ.get("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], session_id="support-assistant", # stable -> cross-session recall agent_id="support-assistant", ) model_client = OpenAIChatCompletionClient(model="gpt-4o-mini") # 1. RECALL — query directly so we can capture the reference IDs. # (The same recall is auto-injected into the model context during agent.run.) recalled = await memory.query(NEW_TICKET, limit=5) entry_ids = memory.extract_entry_ids(recalled) # 2. ACT — the attached memory injects recalled context as a SystemMessage. agent = AssistantAgent( name="support_assistant", model_client=model_client, memory=[memory], system_message="Use recalled prior tickets to give a concrete resolution.", ) result = await agent.run(task=NEW_TICKET) answer = result.messages[-1].content # Persist the new resolution so future sessions can recall it (idempotent). await memory.add(MemoryContent( content=f"[safari-500] Resolution: {answer}", mime_type=MemoryMimeType.TEXT, metadata={"intent": "trace", "idempotency_key": "resolution-safari-500"}, )) # 3. RECORD — credit every recalled entry that contributed. memory.record_step_outcome( step_id="answer-ticket", step_name="draft-resolution", outcome="success", ) memory.record_outcome( outcome="success", entry_ids=entry_ids, # credit the recalled entries verified_in_production=True, # boost lessons confirmed in live use rationale="Resolved the Safari 500 using prior gateway lessons.", ) await model_client.close() await memory.close() asyncio.run(main()) ``` On the first run there is nothing to recall yet; on the second, the agent recalls the stored resolution and the reinforced entries rank higher. ## Full example: Multi-agent handoffs Two agents — a researcher and a reviewer — share one Mubit run. `MubitGroupChatHook` forwards the handoff and the reviewer's verdict into Mubit so they are queryable by [`diagnose` and the MAS surface](/sdk/sdk-helpers). ```python title="multi_agent.py" import asyncio import os from autogen_agentchat.agents import AssistantAgent from autogen_ext.models.openai import OpenAIChatCompletionClient from mubit_autogen import MubitMemory, MubitGroupChatHook endpoint = os.environ.get("MUBIT_ENDPOINT", "https://api.mubit.ai") api_key = os.environ["MUBIT_API_KEY"] SESSION = "autogen-team-1" researcher_mem = MubitMemory( endpoint=endpoint, api_key=api_key, session_id=SESSION, agent_id="researcher", ) reviewer_mem = MubitMemory( endpoint=endpoint, api_key=api_key, session_id=SESSION, agent_id="reviewer", ) hook = MubitGroupChatHook(endpoint=endpoint, api_key=api_key, session_id=SESSION) async def main() -> None: model_client = OpenAIChatCompletionClient(model="gpt-4o-mini") researcher = AssistantAgent( name="researcher", model_client=model_client, memory=[researcher_mem], ) reviewer = AssistantAgent( name="reviewer", model_client=model_client, memory=[reviewer_mem], ) findings = await researcher.run( task="Investigate why billing discrepancies spiked this week." ) handoff = hook.emit_handoff( from_agent_id="researcher", to_agent_id="reviewer", task_id="billing-review-1", content=str(findings.messages[-1].content)[:1000], requested_action="review", ) review = await reviewer.run(task="Review the researcher's billing findings.") hook.emit_feedback( handoff_id=handoff.get("handoff_id", "billing-review-1"), from_agent_id="reviewer", verdict="approve", comments=str(review.messages[-1].content)[:1000], ) await model_client.close() asyncio.run(main()) ``` ## Extended Mubit features Beyond the five `Memory` methods (`update_context`, `query`, `add`, `clear`, `close`), `MubitMemory` exposes the reinforcement surface: ```python ids = memory.extract_entry_ids(result, cited_only=False) # reference_ids off a query result memory.extract_citations(result) # 0-based indices of cited evidence memory.record_outcome(outcome="success", entry_ids=ids, verified_in_production=True, rationale="...") memory.record_step_outcome(step_id="...", step_name="...", outcome="success", signal=0.8, directive_hint="...") ``` `extract_entry_ids` accepts a `MemoryQueryResult` or a raw recall dict; pass `cited_only=True` to credit only the evidence the answer was grounded in. See [step-level outcomes](/recipes/step-level-outcomes) for when per-step credit is worth it. ## Configuration | Parameter | Default | Purpose | | --- | --- | --- | | `endpoint` | `http://127.0.0.1:3000` | Mubit HTTP endpoint | | `api_key` | `""` | Mubit API key | | `session_id` | `"default"` | Mubit run that backs this memory | | `user_id` | `""` | Optional per-user scoping | | `agent_id` | `"autogen-agent"` | Agent identity for ingest/recall | | `limit` | `10` | Max memories recalled per turn | | `entry_types` | `None` | Restrict recall to specific entry types | ## Gotchas - **`memory=[...]` takes a list.** AutoGen accepts multiple memory providers; pass `MubitMemory` as one element. - **The `Memory` methods are async; the reinforcement methods are not.** `query`/`add`/`clear`/`close` must be awaited, while `record_outcome`, `record_step_outcome`, `emit_handoff`, and `emit_feedback` are plain synchronous calls. - **`clear()` deletes the run.** It is AutoGen's reset semantics, but on Mubit it removes the whole backing run — don't call it to "tidy up" a session you want to recall later. - **Recall and persistence fail open.** A Mubit hiccup returns empty recall or skips the write rather than breaking the agent turn. - **protobuf pin.** AutoGen pins `protobuf<6`; `mubit-sdk` needs `>=6.33`. Run `pip install -U "protobuf>=6.33.4"` after installing both. ## Version compatibility | `mubit-autogen` | `mubit-sdk` | `autogen-core` / `autogen-agentchat` | | --- | --- | --- | | `0.6.x` | `>= 0.9.0, < 1.0` | `>= 0.7.5, < 0.8` | --- # Agent Lightning Integration > Bridge Agent Lightning rollout rewards into Mubit. Rewards flow back to the recalled lessons that shaped each rollout via record_outcome. Bridge Agent Lightning rollout rewards into Mubit. Rewards flow back to the recalled lessons that shaped each rollout via `record_outcome`. Agent Lightning is an RL/optimization framework for agents: it runs rollouts and credits reward back to whatever shaped them. That makes it a natural fit for Mubit's attribution loop — a reward should credit the *recalled lessons* that influenced a rollout, not just the run as a whole. The `mubit-agent-lightning` adapter provides two surfaces: - `MubitHook` — a subclass of `agentlightning.Hook`. Register it on a `Trainer` and the loop runs itself: recall at rollout start, credit at rollout end. - `MubitRewardEmitter` — an explicit emitter for custom training loops or inside `LitAgent.rollout`, with reward, event, and step-outcome methods. ```bash pip install "mubit-agent-lightning[agent-lightning]" ``` This pulls `mubit-sdk>=0.9.0` and `agentlightning>=0.3,<0.4`. ## Minimal usage ```python title="agl_minimal.py" import agentlightning as agl from mubit_agent_lightning import MubitHook hook = MubitHook( endpoint="https://api.mubit.ai", api_key="mbt_...", session_id="my-rl-run", recall_query="", # empty -> grounds recall in each rollout's input verified_in_production=True, # flag lessons confirmed in live use for a boost ) trainer = agl.Trainer(..., hooks=[hook]) trainer.fit(my_lit_agent, ...) ``` No wiring inside the agent is required — the hook rides the rollout lifecycle. ## How the hook closes the loop - **`on_rollout_start`** — recalls relevant Mubit lessons (using `recall_query`, or the rollout's own input text when the query is empty) and stashes the recalled `entry_ids` plus grounding citations keyed by `rollout_id`. - **`on_rollout_end`** — pulls the rollout's final reward out of the collected spans (`agl.find_final_reward`) and forwards it to `record_outcome`, crediting the stashed `entry_ids`. Reward `>= 0` records a `success` outcome, negative records a `failure`; the raw value is carried as the `signal`. Both lifecycle methods fail open: a Mubit hiccup never breaks a rollout. On the next recall, the lessons that earned reward rank higher — see [The learning loop](/concepts/learning-loop). ## Full example: Explicit emitter loop For custom training loops, `MubitRewardEmitter` gives you each step of the loop explicitly: recall, act, credit. ```python title="reward_rollout.py" import os from mubit_agent_lightning import ( MubitRewardEmitter, extract_citations, extract_entry_ids, ) TASK = "What is the safe way to retry a failed tool call?" emitter = MubitRewardEmitter( endpoint=os.environ.get("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], session_id="agent-lightning-demo", agent_id="reward-rollout-demo", ) # --- recall: pull memory to ground the rollout --- recall = emitter.recall(TASK, limit=10) entry_ids = extract_entry_ids(recall) # IDs of every recalled lesson citations = extract_citations(recall) # which ones grounded the answer # --- act: your agent solves the task using recall["evidence"] --- evidence = recall.get("evidence", []) reward = run_rollout(TASK, evidence) # your rollout; returns a scalar reward # --- intra-rollout (process) credit for a single step --- emitter.emit_step_outcome( "retrieval", 0.8, step_name="recall-and-ground", rationale="recall returned usable grounding for the task", directive_hint="prefer reference_id lessons over raw traces", entry_ids=entry_ids, ) # --- close the loop: reward flows back to the recalled lessons --- emitter.emit_reward_for_recall( reward, recall, verified_in_production=True, rationale="task solved using recalled lessons", ) # --- durable event with server-side dedup on retry --- emitter.emit_event( "rollout_summary", f"solved {TASK!r} with reward {reward}", idempotency_key="demo:rollout-001:event-1", ) ``` `emit_reward_for_recall(reward, recall)` is shorthand for `emit_reward(reward, entry_ids=extract_entry_ids(recall))`; pass `cited_only=True` to credit only the evidence the answer was actually grounded in. ## Emitter surface ```python emitter.recall(query, limit=10, entry_types=None) # -> raw recall dict emitter.emit_reward(reward, entry_ids=[...], # -> record_outcome verified_in_production=True, reference_id=None, rationale="") emitter.emit_reward_for_recall(reward, recall_result, # recall + credit in one call cited_only=False) emitter.emit_event(kind, content, intent="trace", # -> remember idempotency_key=None) emitter.emit_step_outcome(step_id, signal, # -> record_step_outcome step_name="", entry_ids=[...], directive_hint=None) ``` Re-exported helpers: `extract_entry_ids(result, cited_only=...)`, `extract_citations(result)`, `normalize_metadata(evidence)`. `MubitTracer` is retained as a backward-compatibility alias of `MubitRewardEmitter` (adds the legacy `trace_reward(content, reward)` convenience). See [step-level outcomes](/recipes/step-level-outcomes) for how per-step signals are used at recall time. ## Configuration | Parameter | Default | Purpose | | --- | --- | --- | | `endpoint` | `http://127.0.0.1:3000` | Mubit HTTP endpoint | | `api_key` | `""` | Mubit API key | | `session_id` | `"agent-lightning"` (hook) / `"default"` (emitter) | Mubit run the rollouts write to | | `agent_id` | `"agent-lightning"` | Agent identity for ingest/recall | | `recall_query` | `""` (hook only) | Fixed recall query; empty grounds recall in each rollout's input | | `recall_limit` | `10` (hook only) | Max lessons recalled per rollout | | `verified_in_production` | `None` (hook only) | Forwarded on every `record_outcome` | | `mirror_to_agl` | `True` (emitter only) | Also mirror rewards/events into Agent Lightning's span store via `agl.emit_reward` / `agl.emit_message` | ## Gotchas - **Reward sign picks the outcome label.** `reward >= 0` records `success`, negative records `failure`; the exact float is preserved as `signal`. Shape your reward accordingly if you use zero as "neutral". - **`reference_id` defaults to the run-level `"global"` sentinel.** You don't have to map every reward to a single lesson — `entry_ids` carries the per-entry attribution. - **`mirror_to_agl=True` double-reports by design.** The emitter writes to Mubit *and* to Agent Lightning's own span store so the trainer's reward accounting still sees the value. Set it to `False` in loops where the framework already captured the reward. - **Everything fails open.** Recall or record errors inside `MubitHook` are swallowed so a Mubit outage cannot break training. - **Use `idempotency_key` on retried `emit_event` writes.** At-least-once delivery with the same token dedupes server-side instead of double-counting reinforcement. ## Version compatibility | `mubit-agent-lightning` | `mubit-sdk` | `agentlightning` | | --- | --- | --- | | `0.6.x` | `>= 0.9.0, < 1.0` | `>= 0.3, < 0.4` | The `Hook` ABC (`on_rollout_start` / `on_rollout_end`), the `LitAgent` rollout API, and the module-level `emit_reward` / `emit_message` / `find_final_reward` helpers the adapter conforms to were verified against `agentlightning` 0.3.0. --- # Vercel AI SDK Integration > Middleware that wraps any AI SDK model with automatic memory injection and interaction capture. Middleware that wraps any AI SDK model with automatic memory injection and interaction capture. `@mubit-ai/ai-sdk` provides `mubitMemoryMiddleware`, a middleware factory you pass to Vercel AI SDK's `wrapLanguageModel()`. The wrapped model auto-injects relevant lessons before each call, captures the interaction, and reflects on session end. ```bash npm install @mubit-ai/ai-sdk ``` ## Minimal usage ```js title="vercel_ai_sdk_minimal.mjs" import { wrapLanguageModel, generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { mubitMemoryMiddleware } from "@mubit-ai/ai-sdk"; const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: mubitMemoryMiddleware({ apiKey: process.env.MUBIT_API_KEY, sessionId: "session-1", agentId: "support-agent", }), }); const { text } = await generateText({ model, prompt: "How do I reset my password?", }); // Lessons auto-injected before call; interaction auto-captured after. ``` ## Full example: FAQ bot with cross-session learning A multi-session FAQ bot with a knowledge-base tool. Session 2 benefits from a lesson captured in Session 1. ```js title="faq_bot/index.mjs" import { generateText, tool, wrapLanguageModel } from "ai"; import { openai } from "@ai-sdk/openai"; import { z } from "zod"; import { mubitMemoryMiddleware, MubitClient } from "@mubit-ai/ai-sdk"; const mubitClient = new MubitClient({ apiKey: process.env.MUBIT_API_KEY }); const lookupKnowledgeBase = tool({ description: "Search the knowledge base for help articles", parameters: z.object({ query: z.string() }), execute: async ({ query }) => findBestArticle(query), }); // Session 1: answer a couple of FAQ questions const model1 = wrapLanguageModel({ model: openai("gpt-4o-mini"), middleware: mubitMemoryMiddleware({ sessionId: "faq-session-1", agentId: "faq-bot", mubitClient, }), }); for (const question of [ "How do I reset my password?", "What are system requirements?", ]) { const { text } = await generateText({ model: model1, tools: { lookupKnowledgeBase }, maxSteps: 3, prompt: question, }); console.log(`Q: ${question}\nA: ${text}`); } // Ingest a lesson between sessions await mubitClient.control.ingest({ run_id: "faq-session-1", items: [ { item_id: "faq-2fa-cross-sell", content_type: "text/plain", text: "Users who ask about password reset often also need 2FA help.", intent: "lesson", }, ], }); // Session 2: new session benefits from memory const model2 = wrapLanguageModel({ model: openai("gpt-4o-mini"), middleware: mubitMemoryMiddleware({ sessionId: "faq-session-2", agentId: "faq-bot", mubitClient, }), }); const { text } = await generateText({ model: model2, tools: { lookupKnowledgeBase }, maxSteps: 3, prompt: "I'm having trouble logging in", }); // Response references password / 2FA from Session 1. ``` ## Extended Mubit features The middleware focuses on inject + capture. The full MAS surface is on the `MubitClient` you pass in (or that the middleware constructs internally — pass your own to share it across middlewares): ```js await mubitClient.registerAgent({ session_id, agent_id, role, read_scopes, write_scopes }); await mubitClient.handoff({ session_id, task_id, from_agent_id, to_agent_id, content, requested_action }); await mubitClient.feedback({ session_id, handoff_id, verdict, from_agent_id }); await mubitClient.checkpoint({ session_id, context_snapshot, label }); await mubitClient.recordOutcome({ session_id, reference_id, outcome, rationale }); await mubitClient.reflect({ session_id }); await mubitClient.surfaceStrategies({ session_id, lesson_types, max_strategies }); await mubitClient.diagnose({ session_id, error_text }); await mubitClient.archive({ session_id, content, artifact_kind }); await mubitClient.dereference({ session_id, reference_id }); await mubitClient.getContext({ session_id, query, mode, max_token_budget }); ``` ## Gotchas - **`maxSteps`** matters: tool-calling roundtrips count as separate model calls. The middleware injects lessons for each. - **Streaming responses** (`streamText`) are supported; the assembled output is captured on stream completion. - **Reflection on session end** is opt-in. Pass `autoReflect: true` to the middleware, or call `mubitClient.reflect(...)` yourself when the session ends. - **No CommonJS:** `@mubit-ai/ai-sdk` ships ESM-only. ## Version compatibility | `@mubit-ai/ai-sdk` | `mubit-sdk` (Node) | `ai` | | --- | --- | --- | | `0.5.x` | `>= 0.6.0` | `>= 4.0.0` | --- # MCP Server > Expose Mubit as 21 tools over the Model Context Protocol stdio transport. Usable from Claude Desktop, Cursor, and any MCP client. Expose Mubit as 21 tools over the Model Context Protocol stdio transport. Usable from Claude Desktop, Cursor, and any MCP client. `@mubit-ai/mcp` is a Model Context Protocol server that exposes Mubit as a tool surface. Drop it into any MCP-compatible agent (Claude Desktop, Cursor, custom agents) and the agent can read or write memory through standard MCP tool calls — no Mubit-specific code in the agent itself. ```bash npm install @mubit-ai/mcp ``` ## Minimal usage ```bash title="mcp_minimal.sh" MUBIT_API_KEY=mbt_... MUBIT_ENDPOINT=https://api.mubit.ai npx -y @mubit-ai/mcp ``` The server reads its configuration from environment variables only — there are no `--api-key` / `--endpoint` flags. Set `MUBIT_API_KEY` and `MUBIT_ENDPOINT` (defaults to `http://127.0.0.1:3000` when unset), and optionally `MUBIT_DEFAULT_SESSION_ID` / `MUBIT_DEFAULT_USER_ID` to supply defaults for tool calls that omit them. The server speaks MCP over stdio; configure your MCP client to spawn it with the same environment. ### Claude Desktop config ```json title="~/Library/Application Support/Claude/claude_desktop_config.json" { "mcpServers": { "mubit": { "command": "npx", "args": ["-y", "@mubit-ai/mcp"], "env": { "MUBIT_API_KEY": "mbt_...", "MUBIT_ENDPOINT": "https://api.mubit.ai" } } } } ``` ### Cursor config ```json title=".cursor/mcp.json" { "mcpServers": { "mubit": { "command": "npx", "args": ["-y", "@mubit-ai/mcp"], "env": { "MUBIT_API_KEY": "mbt_...", "MUBIT_ENDPOINT": "https://api.mubit.ai" } } } } ``` ## Full tool surface **Core memory:** `mubit_remember`, `mubit_recall`, `mubit_context`, `mubit_archive`, `mubit_dereference`, `mubit_reflect`, `mubit_lessons`, `mubit_forget`, `mubit_status` **MAS and learning loop:** `mubit_checkpoint`, `mubit_outcome`, `mubit_step_outcome`, `mubit_strategies`, `mubit_learned`, `mubit_register_agent`, `mubit_list_agents`, `mubit_handoff`, `mubit_feedback` **Observability:** `mubit_memory_health`, `mubit_diagnose`, `mubit_ingest_status` ## Common flows ```text Checkpoint + reflection: mubit_checkpoint → mubit_reflect → mubit_lessons MAS coordination: mubit_register_agent → mubit_handoff → mubit_feedback Failure diagnosis: mubit_diagnose → mubit_memory_health → mubit_recall RAG with exact recovery: mubit_archive → mubit_dereference ``` ## Per-tool input schemas Each tool accepts the same arguments as the corresponding SDK helper. For example, `mubit_remember` takes `session_id`, `agent_id`, `content`, `intent`, `metadata`, etc. Inspect `tools/list` from your MCP client for the full schemas. ## Gotchas - **Stdio only.** SSE transport is on the roadmap; stdio is the only option today. - **One server, one instance.** Each spawn binds to a single Mubit instance (the one in your `MUBIT_API_KEY`). Use multiple `mcpServers` entries if you need multiple instances. - **Idempotency keys.** When you supply an `idempotency_key` (or derive one from a stable item id), the server dedups so the signal/write is not re-applied — until that key is evicted from a bounded in-memory cache (~100k keys, oldest-first). There is no fixed 24-hour TTL and no `record_id` is returned. Note for MCP specifically: the server auto-generates a fresh item id per tool call, so two identical calls are **not** automatically deduplicated unless you pass an explicit `idempotency_key`. - **`mubit_forget` is exposed by default.** It deletes a lesson by id (`lesson_id`) or an entire session/run (`session_id`) — there is no `--allow-forget` flag or gating. If deletes should be blocked, omit `mubit_forget` at the MCP-client level (for example, by filtering the tool list your client advertises). ## Version compatibility | `@mubit-ai/mcp` | `mubit-sdk` (Node) | `@modelcontextprotocol/sdk` | | --- | --- | --- | | `0.5.x` | `>= 0.6.0` | `>= 0.6.0` | --- # Projects, Agents, Skills, Prompts > The Managed Mubit resource model — group agents into Projects, version their prompts and skills, and ship changes safely with a candidate → active promotion flow. The Managed Mubit resource model — group agents into Projects, version their prompts and skills, and ship changes safely with a candidate → active promotion flow. With **Managed Mubit**, agents aren't just transient run-time identities — they're **first-class resources**. A Project groups related agents, each agent owns a versioned system prompt and a set of skills, and everything can be inspected, rolled out, and rolled back without touching your deployed SDK code. This page is the SDK view of that resource model. Every operation below is also available point-and-click in the **Mubit Console** at [console.mubit.ai](https://console.mubit.ai) — inline `In the console` notes under each section below show the exact route and buttons. For the raw wire contract see [Control HTTP](/api-reference/control-http#managed-resources-projects-agents-skills-prompts) and [Control gRPC](/api-reference/control-grpc#managed-resources-projects-agents-skills-prompts). ## The hierarchy ``` Project ├── Agent Card (name, role, description, active system prompt) │ ├── PromptVersion* (one active; N candidates awaiting approval) │ └── Skill* (tools/playbooks attached to the agent) │ └── SkillVersion* (candidate → active lifecycle) └── Run history (outcomes aggregated per run) ``` - **Project** — the top-level workspace. Maps 1:1 to a Mubit instance in hosted deployments. - **Agent Card** — the configuration surface for one agent: identity + prompt + attached skills. - **PromptVersion** — a single version of an agent's system prompt. Exactly one is `active` at a time; others sit as `candidate`, `retired`, or `archived`. - **SkillVersion** — same lifecycle applied to a skill definition (parameters schema + instructions). ## Projects ```python Python # List projects accessible to this API key projects = client.advanced.list_projects() # Create a project project = client.advanced.create_project( name="triage-demo", description="Customer-support triage pilot", ) project_id = project["project"]["project_id"] # Update / delete client.advanced.update_project(project_id=project_id, description="Live now") client.advanced.delete_project(project_id=project_id) # tears down the backing instance ``` ```javascript JavaScript const { project } = await client.advanced.createProject({ name: "triage-demo", description: "Customer-support triage pilot", }); const projectId = project.project_id; await client.advanced.updateProject({ project_id: projectId, description: "Live now" }); await client.advanced.deleteProject({ project_id: projectId }); ``` ```rust Rust let resp = client.create_project(json!({ "name": "triage-demo", "description": "Customer-support triage pilot" })).await?; let project_id = resp["project"]["project_id"].as_str().unwrap(); ``` > **Note:** **In the console:** `Projects` tab in the left sidebar lists every project you can access. `New Project` opens the creation form; each row links to `/app/projects/` where you can rename, describe, or delete the project from its **Settings** tab. ## Agent Definitions Every Agent Card you see in the console is an `AgentDefinition` row in the control plane. ```python Python # Create an agent inside a project agent = client.advanced.create_agent_definition( project_id=project_id, agent_id="triage", role="customer triage agent", description="Routes escalations and captures recurring issues", system_prompt_content="You are a concise, empathetic triage agent...", ) # List agents in a project agents = client.advanced.list_agent_definitions(project_id=project_id) # Update role / description / prompt client.advanced.update_agent_definition( project_id=project_id, agent_id="triage", description="Now also handles billing escalations", ) ``` > **Tip:** The `system_prompt_content` you pass on creation becomes the first `active` PromptVersion for the agent. Subsequent changes flow through `set_prompt` (manual) or `optimize_prompt` (LLM-generated candidate). > **Note:** **In the console:** open a project → **Agents** tab. Each row links to the agent's page at `/app/projects//agents/` with sub-tabs for **Identity**, **Prompts**, **Skills**, **Runs**, **Memory**, and **Play**. `New Agent` creates an `AgentDefinition` and mints the first active `PromptVersion` in one step. ## PromptVersion lifecycle Prompts don't just change — they **version**. Every write mints a new `PromptVersion` row, and exactly one version is `active` at any time. ### Statuses | Status | Meaning | | --- | --- | | `active` | The version currently served to retrieval / inference | | `candidate` | Awaiting approval. Visible in the console's "Pending Optimization" card; gets a diff view vs. active | | `retired` | Previously active, superseded by a newer version | | `archived` | Manually archived (won't show up in default listings) | ### Sources | Source | How it was created | | --- | --- | | `manual` | Written via `set_prompt` | | `optimization` | Minted by `optimize_prompt` — the control plane synthesises a candidate from recent outcomes | | `rollback` | Restored from a retired version | ### Typical flow ```python Python # Manual edit — activates immediately client.advanced.set_prompt( agent_id="triage", content="Updated system prompt...", activate=True, ) # Fetch current active prompt — response is { version: PromptVersion } current = client.advanced.get_prompt(agent_id="triage") # List all versions for an agent versions = client.advanced.list_prompt_versions(agent_id="triage") # Ask the control plane to propose a candidate from recent outcomes candidate = client.optimize_prompt( agent_id="triage", project_id=project_id, # llm override: gRPC transport only; forwarded by the 0.11 pass-through. # From SDK v0.12.0 use client.advanced.optimize_prompt({..., "llm": {...}}) llm={"provider": "anthropic", "model": "claude-sonnet-4-6"}, ) # Returns: { success, candidate, optimization_summary, confidence, activated } # Compare two versions (returns unified diff_text) diff = client.advanced.get_prompt_diff( agent_id="triage", version_a_id=current["version"]["version_id"], version_b_id=candidate["candidate"]["version_id"], ) # Approve the candidate client.advanced.activate_prompt_version( agent_id="triage", version_id=candidate["candidate"]["version_id"], ) ``` ```javascript JavaScript await client.advanced.setPrompt({ agent_id: "triage", content: "...", activate: true }); const versions = await client.advanced.listPromptVersions({ agent_id: "triage" }); const candidate = await client.optimizePrompt({ agent_id: "triage", project_id: projectId, }); await client.advanced.activatePromptVersion({ agent_id: "triage", version_id: candidate.candidate.version_id, }); ``` Each `PromptVersion` carries outcome aggregates (`avg_outcome_score`, `outcome_count`) so you can decide whether a candidate is actually an improvement before promoting it. See [Prompt Optimization Lifecycle](/recipes/prompt-optimization) for the end-to-end workflow. ### From the console The same lifecycle is available without writing code. Open an agent's **Prompts** tab at `/app/projects//agents//prompts`. | You want to… | Where to click | | --- | --- | | Edit the active prompt by hand | `Edit` button on the **Active System Prompt** card → `Save & Create Version` (source: `manual`) | | Ask the optimizer for a candidate | `Suggest Optimization` button (sparkles icon) on the same card. Creates a new row with `status: candidate`, `source: optimization`, auto-expanded to show the new prompt | | Review a candidate's diff against active | `Review` on the pending-candidate banner, or `Compare` in the Version History table → opens `/app/projects//agents//compare/` with a unified diff and the candidate's optimization summary | | Approve a candidate | `Approve` in the banner or `Approve & Activate` on the compare page. Flips candidate → active and previous active → retired atomically | | Roll back | Find a `retired` row in Version History → `Compare` to confirm → approve that version. The activation is recorded with `source: rollback` | > **Note:** **The console uses the instance's default optimizer model.** If you need a specific provider/model/temperature for a single optimize run, pass the `llm` override from the SDK — `client.optimize_prompt(..., llm={"provider": ..., "model": ..., "temperature": ...})` on SDK 0.11.0, or the raw op `client.advanced.optimize_prompt({..., "llm": {...}})` from v0.12.0 (the typed helper does not forward `llm`) — the console does not expose that override today. > > The per-call `llm` override is honored **only over the gRPC transport**. The HTTP `optimize_prompt` route hardcodes `llm: None` and silently ignores any override in the body, so over HTTP every optimize run uses the instance's default optimizer model. ## Skills Skills are tools or playbooks attached to a project (and optionally bound to a specific agent). They version identically to prompts. ```python Python # Create a tool skill skill = client.advanced.create_skill( project_id=project_id, agent_id="triage", # optional — omit for project-level shared skill name="send-email", description="Send a templated customer email", parameters_schema='{"type":"object","properties":{"to":{"type":"string"},"template":{"type":"string"}}}', instructions="Use for routine replies. Do not use for escalations.", skill_type="tool", # or "playbook" ) # List skills in a project skills = client.advanced.list_skills(project_id=project_id) # Ask the control plane to propose an improved version candidate = client.optimize_skill( project_id=project_id, skill_id=skill["skill"]["skill_id"], ) # Diff + activate just like prompts diff = client.advanced.get_skill_diff( skill_id=skill["skill"]["skill_id"], version_a_id="...", version_b_id="...", ) client.advanced.activate_skill_version( skill_id=skill["skill"]["skill_id"], version_id=candidate["candidate"]["version_id"], ) ``` `skill_type`: - `"tool"` — a callable function with a parameters schema. - `"playbook"` — a longer-form text description of a procedure the agent should follow. ### From the console Skills follow the same candidate → active → retired flow as prompts, wrapped in a UI at `/app/projects//skills/`: | You want to… | Where to click | | --- | --- | | Edit **Description**, **Parameters Schema**, or **Instructions** by hand | `Edit` on the **Active Definition** card → three dedicated fields → `Save & Create Version` | | Ask the optimizer for a candidate | `Suggest Optimization` on the same card. The diff view at `/app/projects//skills//compare/` shows changes across all three fields in one unified diff | | Approve | `Approve` on the pending-candidate banner, or `Approve & Activate` on the compare page | | Roll back | Pick a retired version from Version History → `Compare` → approve | Shared project-level skills live under `/app/projects//skills`; agent-scoped skills appear under the owning agent at `/app/projects//agents//skills`. ## Run history per project Every run your agents execute gets a row in the project's run history with aggregates: `lessons_extracted`, `prompt_changes`, `avg_outcome_score`, `outcome_count`, `ingest_count`. Use this to track how an agent's behavior evolves over time without querying raw memory. ```python Python runs = client.advanced.list_run_history(project_id=project_id, limit=50) run = client.advanced.get_run_history(project_id=project_id, run_id="run-abc") ``` > **Note:** **In the console:** each agent has a **Runs** tab (`/app/projects//agents//runs`) that renders the same run history with filters and drill-down. The project-level **Logs** tab (`/app/projects//logs`) spans all agents in the project. ## Try a prompt or skill before shipping it Every agent page has a **Play** tab at `/app/projects//agents//play`. It runs a real query against the project's instance and shows the response, retrieved memory, and invoked skills — the same trace an SDK call would see. Use it as a smoke test after activating a new prompt or skill version. > **Note:** **Caveat:** Play runs against whichever version is currently `active`, so you can dry-run a freshly-activated version but not a still-`candidate` one. To pressure-test a candidate specifically, briefly activate it in a non-production project, or use the advanced-panel `direct_bypass` mode to skip prompt routing. ## When to use the resource model vs. raw `run_id` | Use the resource model when… | Stick with raw `run_id` when… | | --- | --- | | You want to version prompts + skills and roll back | You're prototyping against a single unmanaged endpoint | | Multiple teammates or CI workflows need to coordinate on an agent's config | Your agent config lives entirely in your application code | | You want the console's "Pending Optimization" review UX | You don't need a human-in-the-loop approval step | | You want per-project billing / observability boundaries | You only have one project and never expect to grow | ## Related pages - [Prompt Optimization Lifecycle (recipe)](/recipes/prompt-optimization) — end-to-end: capture outcomes → optimize → diff → activate. - [Control HTTP — Managed resources](/api-reference/control-http#managed-resources-projects-agents-skills-prompts) — raw wire contract. - [Control gRPC — Managed resources](/api-reference/control-grpc#managed-resources-projects-agents-skills-prompts) — proto RPCs. - [Authentication](/sdk/authentication) — API keys authorise against a specific project's instance. --- # Sessions & Branching > Use checkpoints for durable task continuity and core sessions for reversible scratch branches. Use checkpoints for durable task continuity and core sessions for reversible scratch branches. Current Mubit uses two distinct ideas here: - **control-plane checkpoints** for durable task continuity and compaction survival - **core session branches** for temporary scratch or what-if work These are not the same thing. Checkpoints preserve the durable task path. Core sessions give you a temporary branch you can inspect, commit, or drop. ## Decision model | Requirement | Primitive | | --- | --- | | Preserve the main task before compaction | `checkpoint()` | | Try a temporary branch and discard it | `core.create_session`, `core.drop_session` | | Keep a reviewed branch | `core.commit_session` | | Keep a short-lived scratch buffer | `core.add_memory` with `ttl_seconds` | ## Minimal implementation example #### Python ```python title="sessions_and_branching.py" run_id = "claims:planner:claim-42" client.checkpoint( session_id=run_id, label="before-scratch-branch", context_snapshot="Planner is about to compare two claim settlement strategies.", ) base = client.core.create_session({}) branch = client.core.create_session({"parent_session_id": base["session_id"]}) branch_id = branch["session_id"] client.core.commit_session({"session_id": branch_id, "merge_strategy": "overwrite"}) client.core.drop_session({"session_id": branch_id}) ``` #### Node / TS ```ts title="sessions_and_branching.ts" const runId = "claims:planner:claim-42"; await client.checkpoint({ session_id: runId, label: "before-scratch-branch", context_snapshot: "Planner is about to compare two claim settlement strategies.", }); const base = await client.core.createSession({}); const branch = await client.core.createSession({ parent_session_id: base.session_id }); await client.core.commitSession({ session_id: branch.session_id, merge_strategy: "overwrite", }); await client.core.dropSession({ session_id: branch.session_id, }); ``` #### Rust ```rust title="sessions_and_branching.rs" use mubit_sdk::CheckpointOptions; use serde_json::json; let run_id = "claims:planner:claim-42"; let mut checkpoint = CheckpointOptions::new( "Planner is about to compare two claim settlement strategies.", ); checkpoint.label = Some("before-scratch-branch".to_string()); checkpoint.run_id = Some(run_id.to_string()); client.checkpoint(checkpoint).await?; let base = client.core.create_session(json!({})).await?; let branch = client.core.create_session(json!({ "parent_session_id": base["session_id"] })).await?; let branch_id = branch["session_id"].as_str().unwrap(); client.core.commit_session(json!({ "session_id": branch_id, "merge_strategy": "overwrite" })).await?; client.core.drop_session(json!({ "session_id": branch_id })).await?; ``` ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Main path becomes hard to restore | No checkpoint was taken | Checkpoint before the scratch branch | | Branch writes leak into the main path | Branch was committed too early | Commit only reviewed branches | | Scratch context disappears too soon | TTL or session lifecycle too short | Adjust branch lifetime intentionally | ## Next steps - Apply the full branching workflow at [Branching memory](/recipes/branching-memory). - Add coordination at [Multi-agent shared state](/recipes/multi-agent-shared-state). --- # Pub-Sub & Semantic Subscriptions > Build event-driven workflows with run-scoped control streams and semantic subscriptions on the data plane. Build event-driven workflows with run-scoped control streams and semantic subscriptions on the data plane. Polling state endpoints does not scale for reactive systems. Mubit exposes two streaming surfaces so workers wake on state changes instead of spinning. ## Decision model | Stream | Scope | Method | Best use | | --- | --- | --- | --- | | Run-scoped control events | `run_id` | `client.advanced.subscribe(...)` (Python/JS; `client.subscribe(...)` in Rust) — SSE or gRPC | Workflow coordination inside one run: ingestion completion, checkpoints, lesson validation and promotion, handoffs, outcome recording, drift signals | | Core data-plane pub-sub | User + filter | `client.core.subscribe_events(...)` (SSE or gRPC) | Topic-triggered automation across broader state: wake on node insert/update/delete, wake when a newly-written node crosses a semantic-similarity threshold | Both surfaces stream events as they happen. The subscription lives for the lifetime of the HTTP or gRPC stream — on disconnect the server cleans up the subscription automatically. ## Control event stream Run-scoped events fire for every meaningful control-plane state change: ~24 types covering ingest, reflection, checkpointing, outcome recording, handoffs, prompt activations, drift, project/agent/skill CRUD, run-monitor signals. Filter with `event_types` to keep the stream narrow. Lesson lifecycle events flow through this stream too, and reflect a gated candidate lifecycle. A freshly reflected lesson enters as a **pending candidate** and is not trusted immediately — it stays pending until it passes a held-out validation gate. Watch for `context.lesson_validation_passed` (candidate marked active) and `context.lesson_validation_failed` (candidate rejected and down-weighted) alongside `context.lesson_promoted`; promotion now reflects a validated lesson rather than an unconditional one. ```python Python events = client.advanced.subscribe({ "run_id": run_id, "event_types": ["context.ingest_completed", "context.checkpoint_created"], }) for event in events: print(event["type"], event["payload"]) ``` ```ts Node / TS const stream = await client.advanced.subscribe({ run_id: runId, event_types: ["context.ingest_completed", "context.checkpoint_created"], }); for await (const event of stream) { console.log(event.type, event.payload); } ``` Events include `id`, `type`, `run_id`, `agent_id`, `payload` (event-specific JSON), and `created_at`. Recent events are replayable from Redis Streams (default retention 10,000 per run, env-configurable). > **Note:** Transport caveat (Python): over gRPC the control stream yields parsed dict events as shown above; over the HTTP/SSE fallback the Python SDK yields raw SSE lines — strip the `data: ` prefix and `json.loads` each line yourself. The JS SDK parses SSE lines into objects on both transports. ## Core semantic subscriptions `core.subscribe_events` opens an SSE (or server-streaming gRPC) connection that delivers pub-sub events as they are published by the storage layer. Every event yielded by the SDK has the same shape across Python, JavaScript, and Rust — and across both HTTP and gRPC transports. The first event on the stream is always `subscribed`; after that, events are `node.inserted`, `node.updated`, `node.deleted`, or `memory.added`. ### Event shape Every event is a dict / object with a `type` discriminator plus the fields relevant to that type. The SDK wrappers parse `metadata_json` / `entry_json` wire strings into native `metadata` / `entry` objects and strip proto3 scalar defaults so callers see a tight shape: | `type` | Fields | | --- | --- | | `subscribed` | `subscription_id` | | `node.inserted` / `node.updated` | `node_id`, `run_id`, `metadata`, `created_at`, `updated_at` | | `node.deleted` | `node_id` | | `memory.added` | `session_id`, `entry` | Example stream: ```json {"type":"subscribed","subscription_id":42} {"type":"node.inserted","node_id":7,"run_id":"r1","metadata":{"intent":"fact"},"created_at":1714000000,"updated_at":1714000000} {"type":"node.deleted","node_id":7} ``` ```python Python # Fire only when a node whose vector is near the "billing escalation" embedding lands. events = client.core.subscribe_events({ "filter_type": "semantic", "query_text": "billing escalation", "threshold": 0.82, }) for event in events: if event["type"] == "subscribed": print("subscription id:", event["subscription_id"]) continue if event["type"] == "node.inserted": node_id = event["node_id"] # Wake the agent loop, page an on-call, enqueue a follow-up recall, … handle_billing_escalation(node_id) ``` ```rust Rust use serde_json::json; use tokio_stream::StreamExt; let mut stream = client.core.subscribe_events(json!({ "filter_type": "semantic", "query_text": "billing escalation", "threshold": 0.82 })).await?; while let Some(event) = stream.next().await { let evt = event?; if evt["type"] == "node.inserted" { let node_id = evt["node_id"].as_u64().unwrap_or(0); // react… } } ``` ### Filter types | `filter_type` | Wakes on | | --- | --- | | `"all"` | Any node insert/update/delete (use for admin tooling, not production) | | `"node"` | Events touching a single `node_id` (needs `node_id` in the request) | | `"semantic"` | Inserts/updates whose vector exceeds `threshold` cosine similarity to the encoded `query_text`. Default threshold `0.8`. | | `"session"` | `memory.added` events scoped to one session id | ACL filtering runs server-side: subscribers only see events for nodes they have permission to read. ## Cleanup and lifecycle - Close the SSE response / drop the gRPC stream to end the subscription. The server unsubscribes automatically — you do not need to call `unsubscribe` explicitly. - `POST /v2/core/pubsub/unsubscribe { subscription_id }` exists for admin tooling or for cleaning up subscriptions whose originating client was killed before it could close the stream. - `POST /v2/core/pubsub/list` returns the active subscription IDs for the calling user plus the server-wide count — useful for monitoring. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Too many events | Broad filter (`"all"`) or a low semantic threshold | Narrow `filter_type`, raise `threshold`, or filter client-side by `event_types` on the control stream | | Missing events | Subscriber scoped to a different `run_id` (control stream) or subscribed before the data plane had permission to read the emitting node (core stream) | Verify the producer and subscriber share a `run_id`; check ACLs on the node class you expect to see | | SSE stream idle for minutes | Proxy / load balancer idle timeout | The server emits SSE keep-alives, but some proxies still cut idle connections — configure the proxy for long-lived SSE or fall back to the gRPC surface | | Core subscribe returns 404 / denied | Data-plane access policy not enabled for the route | `/v2/core/pubsub/*` sits under the core route policy. See [Core Direct Lanes and Policy](/api-reference/core-direct-lanes) for the flag set and rollout guidance | ## Next steps - Add planner/specialist coordination at [Sessions and branching](/sdk/sessions-and-branching). - Review state APIs at [State management endpoints](/api-reference/state-management). - See the exact HTTP request/response shapes at [Core Direct Lanes — PubSub](/api-reference/core-direct-lanes#pubsub). --- # Activity & Audit Trail > Browse, export, and append chronological activity entries for observability and compliance. Browse, export, and append chronological activity entries for observability and compliance. Mubit tracks all memory operations as a chronological activity trail. Use it for observability, debugging, compliance audit, and manual trace annotation. Activity is distinct from semantic retrieval (`recall` / `query`). It provides exact chronological ordering — what happened and when — rather than ranked relevance. ## Browse activity List recent activity entries for a run with optional type and agent filters. > **Note:** Activity has no dedicated SDK helper. Browse, export, and append are reachable only over raw HTTP (`POST /v2/control/activity`, `/v2/control/activity/export`, `/v2/control/activities/append`) or gRPC (`ListActivity`, `ExportActivity`, `AppendActivity`). The examples below use the HTTP transport; authenticate with `Authorization: Bearer `. `POST /v2/control/activity` returns entries sorted newest-first (`sort` defaults to `"desc"`). `limit` defaults to 100 and is clamped to 500. Each entry uses the proto field names — note `entry_type` and `created_at` (not `type`/`timestamp`). #### HTTP ```bash curl -sS http://localhost:3000/v2/control/activity \ -H "Authorization: Bearer $MUBIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "run_id": "support:acme:ticket-42", "limit": 50, "entry_types": ["fact", "lesson"] }' ``` Response (`ListActivityResponse`): ```json { "entries": [ { "id": "evt_01H...", "run_id": "support:acme:ticket-42", "agent_id": "support-agent", "entry_type": "fact", "content": "Customer on enterprise plan since 2023", "created_at": "2026-06-03T14:02:11Z" } ], "next_page_token": "", "total_visible": 1 } ``` #### Python ```python import os, requests resp = requests.post( "http://localhost:3000/v2/control/activity", headers={"Authorization": f"Bearer {os.environ['MUBIT_API_KEY']}"}, json={ "run_id": "support:acme:ticket-42", "limit": 50, "entry_types": ["fact", "lesson"], }, ) activity = resp.json() for entry in activity["entries"]: print(f"{entry['created_at']} [{entry['entry_type']}] {entry['content'][:80]}") ``` #### Node / TS ```js const resp = await fetch("http://localhost:3000/v2/control/activity", { method: "POST", headers: { Authorization: `Bearer ${process.env.MUBIT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ run_id: "support:acme:ticket-42", limit: 50, entry_types: ["fact", "lesson"], }), }); const activity = await resp.json(); for (const entry of activity.entries) { console.log(`${entry.created_at} [${entry.entry_type}] ${entry.content.slice(0, 80)}`); } ``` ## Export activity Export the full activity trail for offline analysis or compliance archival. The request takes the same filters as the browse call (`run_id`, `user_id`, `agent_id`, `entry_types`, `created_after`, `created_before`, `sort`) — there is no `format` request field. The response always returns JSONL, with the format string and serialized entries in the response's `format` and `content` fields. #### HTTP ```bash curl -sS http://localhost:3000/v2/control/activity/export \ -H "Authorization: Bearer $MUBIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "run_id": "support:acme:ticket-42" }' ``` Response (`ExportActivityResponse`): ```json { "format": "jsonl", "content": "{\"id\":\"evt_01H...\",\"entry_type\":\"fact\",...}\n", "entry_count": 1 } ``` #### Python ```python import os, requests resp = requests.post( "http://localhost:3000/v2/control/activity/export", headers={"Authorization": f"Bearer {os.environ['MUBIT_API_KEY']}"}, json={"run_id": "support:acme:ticket-42"}, ) export = resp.json() # export["content"] holds the JSONL-formatted activity entries (export["format"] == "jsonl") ``` #### Node / TS ```js const resp = await fetch("http://localhost:3000/v2/control/activity/export", { method: "POST", headers: { Authorization: `Bearer ${process.env.MUBIT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ run_id: "support:acme:ticket-42" }), }); const exported = await resp.json(); // exported.content holds the JSONL-formatted entries (exported.format === "jsonl") ``` ## Append activity Manually append an activity trace — useful for logging external observations, human annotations, or system events that aren't captured automatically. Each request appends a **single** `activity` record (not a list). The record carries `type` and `payload` (required) plus optional `tool_name`, `input_ref`, `output_ref`, `ts`, and `agent_id`. Issue one request per record. #### HTTP ```bash curl -sS http://localhost:3000/v2/control/activities/append \ -H "Authorization: Bearer $MUBIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "run_id": "support:acme:ticket-42", "agent_id": "support-agent", "activity": { "type": "observation", "payload": "Customer escalated via phone" } }' ``` Response: `{ "success": true, "event_id": "evt_01H..." }` #### Python ```python import os, requests resp = requests.post( "http://localhost:3000/v2/control/activities/append", headers={"Authorization": f"Bearer {os.environ['MUBIT_API_KEY']}"}, json={ "run_id": "support:acme:ticket-42", "agent_id": "support-agent", "activity": { "type": "observation", "payload": "Customer escalated via phone", }, }, ) resp.raise_for_status() ``` #### Node / TS ```js await fetch("http://localhost:3000/v2/control/activities/append", { method: "POST", headers: { Authorization: `Bearer ${process.env.MUBIT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ run_id: "support:acme:ticket-42", agent_id: "support-agent", activity: { type: "observation", payload: "Customer escalated via phone", }, }), }); ``` ## Activity vs. semantic query | | `ListActivity` | `recall` / `query` | | --- | --- | --- | | **Ordering** | Chronological (exact timestamps) | Relevance-ranked | | **Use case** | Audit, debugging, compliance | Context retrieval for LLM prompts | | **Filters** | Type, agent, time range | Semantic similarity, entry types | | **Output** | Raw entries as ingested | Answer with evidence scoring | Use activity for "what happened" questions. Use query/recall for "what's relevant" questions. ## API reference | Transport | Endpoint | | --- | --- | | HTTP | `POST /v2/control/activity` (list), `POST /v2/control/activity/export` (export), `POST /v2/control/activities/append` (append) | | gRPC | `ListActivity`, `ExportActivity`, `AppendActivity` | See [Control HTTP reference](/api-reference/control-http) and [Control gRPC reference](/api-reference/control-grpc) for full request/response schemas. ## Next steps - See [SDK methods](/sdk/sdk-methods) for the full method catalog. - See [Control HTTP reference](/api-reference/control-http#activity-routes-request-shapes) for request/response field details. --- # Authentication (API key etc) > Configure Mubit API-key authentication correctly for secure and stable SDK integrations. Configure Mubit API-key authentication correctly for secure and stable SDK integrations. Mubit runtime access uses API keys across SDK, HTTP, and gRPC surfaces. Correct key handling is operationally critical for developer teams. Most authentication incidents in AI platform integrations come from key and environment drift, not protocol design. Keep endpoint-key pairing explicit, keep keys server-side only, and run controlled rotation workflows. This gives predictable auth behavior across development, staging, and production. ## API key model - Key format: `mbt___` - Header: `Authorization: Bearer ` Canonical environment setup: ```bash title=".env" MUBIT_API_KEY="mbt___" ``` ## Key lifecycle endpoints | Route | Purpose | | ----------------------------------------------- | --------------------------- | | `POST /v2/core/auth/users` | Create user and initial key | | `POST /v2/core/auth/users/:username/rotate_key` | Rotate user key | | `POST /v2/core/auth/users/:username/revoke_key` | Revoke active key | | `GET /v2/core/auth/users` | List users | | `GET /v2/core/auth/users/:username` | Inspect user | > **Warning:** Never expose Mubit API keys in browser clients. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | ------------------------- | ----------------------------------------- | ------------------------------------------------ | | Invalid key format errors | Truncated or malformed key | Re-issue and validate full `mbt_...` value | | Repeated 401/403 | Wrong key, revoked key, or wrong endpoint | Verify endpoint-key pairing and rotate if needed | | Partial rollout failures | Mixed key versions after rotation | Stage rollout and confirm complete deploy | ## Next steps - Review access guardrails at [Data guardrails](/sdk/data-guardrails). - Review ops posture at [Observability, consistency, and operations](/sdk/observability). --- # Data Guardrails (ACL related) > Protect memory access with route-level policy gates and node-level ACL governance. Protect memory access with route-level policy gates and node-level ACL governance. Data protection in Mubit has two layers: route policy controls external lane access, and ACL controls node-level visibility. If your team treats guardrails as a late-stage concern, failures usually appear first in production as policy denials or unexpected visibility. Define direct-lane policy and ACL ownership early. Then enforce those choices in code paths and operational runbooks. ## Guardrail layer 1: route policy - `/v2/core/health` and `/v2/core/auth/*` are always available. - `/v2/core/search` requires `MUBIT_CORE_ENABLE_DIRECT_SEARCH`. - Other external `/v2/core/*` routes are denied by default middleware. ## Guardrail layer 2: ACL operations | Route | Purpose | | -------------------------- | ----------------------------------- | | `POST /v2/core/acl/grant` | Grant read/write/delete permissions | | `POST /v2/core/acl/revoke` | Remove permissions | | `POST /v2/core/acl/check` | Verify permission state | ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --------------------- | ------------------------- | ----------------------------------------------------- | | Direct method denied | Lane policy off | Use routed control query or enable lane intentionally | | Unexpected visibility | ACL model incomplete | Validate grant/revoke/check flow | | Broad blast radius | Policies enabled globally | Roll out lane flags per environment | ## Next steps - See policy details at [Core direct lanes policy](/api-reference/core-direct-lanes). - See authentication setup at [Authentication](/sdk/authentication). --- # SDK Configuration Reference > Environment variables, transport selection, endpoint defaults, and connection options for Mubit SDKs. Environment variables, transport selection, endpoint defaults, and connection options for Mubit SDKs. All Mubit SDKs (Python, Node.js, Rust) share a consistent configuration model based on environment variables and constructor options. ## Environment variables | Variable | Required | Default | Description | | --- | --- | --- | --- | | `MUBIT_API_KEY` | yes | — | API key in format `mbt___` | | `MUBIT_ENDPOINT` | no | `https://api.mubit.ai` | Base endpoint for both HTTP and gRPC (SDK resolves protocol automatically) | | `MUBIT_TRANSPORT` | no | `auto` | Transport selection: `auto`, `http`, or `grpc` | | `MUBIT_HTTP_ENDPOINT` | no | `https://api.mubit.ai` | Override HTTP-specific endpoint | | `MUBIT_GRPC_ENDPOINT` | no | `grpc.api.mubit.ai:443` | Override gRPC-specific endpoint | When `MUBIT_ENDPOINT` is set, it serves as the default for both HTTP and gRPC. The protocol-specific overrides (`MUBIT_HTTP_ENDPOINT`, `MUBIT_GRPC_ENDPOINT`) take precedence when set. ## Transport selection The `transport` option controls how the SDK communicates with Mubit: | Value | Behavior | | --- | --- | | `auto` (default) | SDK probes the endpoint and selects gRPC if available, falling back to HTTP | | `http` | Force HTTP/REST transport | | `grpc` | Force gRPC transport | #### Python ```python from mubit import Client # Auto transport (default) client = Client(api_key="mbt_...") # Force gRPC client = Client(api_key="mbt_...", transport="grpc") # Force HTTP with custom endpoint client = Client( api_key="mbt_...", transport="http", endpoint="https://custom.api.mubit.ai", ) ``` #### Node / TS ```js import { Client } from "@mubit-ai/sdk"; // Auto transport (default) const client = new Client({ api_key: process.env.MUBIT_API_KEY }); // Force gRPC const client = new Client({ api_key: "mbt_...", transport: "grpc" }); // Force HTTP with custom endpoint const client = new Client({ api_key: "mbt_...", transport: "http", endpoint: "https://custom.api.mubit.ai", }); ``` #### Rust ```rust use mubit_sdk::{Client, ClientConfig}; // From environment (reads MUBIT_API_KEY, MUBIT_ENDPOINT, etc.) let client = Client::new(ClientConfig::from_env())?; // synchronous — no .await // Explicit gRPC let config = ClientConfig::new("grpc.api.mubit.ai:443") .api_key("mbt_...") .transport("grpc"); let client = Client::new(config)?; ``` ## Endpoint defaults | Context | HTTP endpoint | gRPC endpoint | | --- | --- | --- | | **Hosted (production)** | `https://api.mubit.ai` | `grpc.api.mubit.ai:443` | | **Hosted (dev)** | `https://api.dev.mubit.ai` | `grpc.api.dev.mubit.ai:443` | | **Local** | `http://127.0.0.1:3000` | `127.0.0.1:50051` | Both hosted endpoints use TLS. Local endpoints are plaintext by default. ## Constructor options Beyond environment variables, SDK constructors accept these options: | Option | Type | Description | | --- | --- | --- | | `api_key` | string | API key (overrides `MUBIT_API_KEY`) | | `endpoint` | string | Base endpoint (overrides `MUBIT_ENDPOINT`) | | `transport` | string | Transport selection (overrides `MUBIT_TRANSPORT`) | | `run_id` | string | Default run ID auto-injected into all run-scoped operations (overrides `MUBIT_RUN_ID`) | | `http_endpoint` | string | HTTP-specific endpoint override (overrides `MUBIT_HTTP_ENDPOINT`) | | `grpc_endpoint` | string | gRPC-specific endpoint override (overrides `MUBIT_GRPC_ENDPOINT`) | | `timeout_ms` | number | Per-call timeout in milliseconds (default 30000) | Default `agent_id` / `user_id` are not `Client` constructor options — they are set on `mubit.init()` / `mubit.learn.init()` (Python) and `learn.init()` (JS), or passed per call. ## Resolution order Configuration resolves in this order (first wins): 1. Explicit constructor argument 2. Protocol-specific env var (`MUBIT_HTTP_ENDPOINT` / `MUBIT_GRPC_ENDPOINT`) 3. Base env var (`MUBIT_ENDPOINT`) 4. Built-in default (`https://api.mubit.ai`) ## Next steps - See [Getting started](/getting-started) for quick setup. - See [gRPC Transport Guide](/sdk/grpc-transport) for gRPC-specific guidance. - See [Control HTTP reference](/api-reference/control-http) for the full HTTP API surface. --- # gRPC Transport Guide > When to use gRPC vs HTTP, endpoint configuration, TLS, and proto file locations for Mubit SDKs. When to use gRPC vs HTTP, endpoint configuration, TLS, and proto file locations for Mubit SDKs. Mubit supports both HTTP/REST and gRPC transports. All SDK methods work identically regardless of transport — the choice affects performance characteristics, not API surface. ## When to use gRPC vs HTTP | Factor | gRPC | HTTP | | --- | --- | --- | | **Latency** | Lower per-call overhead (binary protocol, persistent connections) | Slightly higher due to JSON serialization | | **Streaming** | Native bidirectional streaming (`Subscribe`, `StreamSearch`, `WatchMemory`) | SSE for events, no bidirectional streaming | | **Debugging** | Requires gRPC tooling (grpcurl, Bloom RPC) | curl, browser dev tools | | **Proxies/CDNs** | Requires HTTP/2-aware infrastructure | Works everywhere | | **Browser clients** | Requires grpc-web proxy | Native fetch/XMLHttpRequest | **Recommendation:** Use gRPC for backend services where you want lower latency and streaming. Use HTTP for browser clients, serverless functions, or environments with HTTP/1.1-only proxies. ## Configuration ### Environment variables ```bash title=".env" MUBIT_TRANSPORT="grpc" MUBIT_GRPC_ENDPOINT="grpc.api.mubit.ai:443" ``` ### SDK constructor #### Python ```python from mubit import Client client = Client( api_key="mbt_...", transport="grpc", # endpoint defaults to grpc.api.mubit.ai:443 ) ``` #### Node / TS ```js import { Client } from "@mubit-ai/sdk"; const client = new Client({ api_key: "mbt_...", transport: "grpc", // endpoint defaults to grpc.api.mubit.ai:443 }); ``` #### Rust ```rust use mubit_sdk::{Client, ClientConfig}; let config = ClientConfig::new("grpc.api.mubit.ai:443") .api_key("mbt_...") .transport("grpc"); let client = Client::new(config)?; // synchronous — no .await ``` ## Endpoints | Environment | gRPC endpoint | TLS | | --- | --- | --- | | Hosted (production) | `grpc.api.mubit.ai:443` | Yes (system CA) | | Hosted (dev) | `grpc.api.dev.mubit.ai:443` | Yes (system CA) | | Local | `127.0.0.1:50051` | No (plaintext) | TLS is handled automatically by the SDK — no certificate configuration is needed for hosted endpoints. ## Proto file locations The proto definitions are available in the repository: | Proto file | Service | Description | | --- | --- | --- | | `proto/mubit/v1/core.proto` | `CoreService` | Core data-plane operations (insert, search, memory, sessions) | | `proto/mubit/v1/control.proto` | `ControlService` | Control-plane operations (ingest, query, context, reflection, coordination) | These can be used with any gRPC code generator if you need a client outside the official SDKs. ## Streaming RPCs gRPC enables streaming RPCs not available over HTTP: | RPC | Direction | Purpose | | --- | --- | --- | | `Subscribe` (Control) | Server → Client | Stream control-plane events in real-time | | `StreamSearch` (Core) | Server → Client | Stream search results as they're found | | `WatchMemory` (Core) | Server → Client | Watch for scratchpad memory changes | #### Python ```python # Stream control events — over gRPC each yielded item is a parsed dict. # (Over the HTTP/SSE fallback the Python SDK yields raw SSE lines instead — # strip the "data: " prefix and json.loads each line.) for event in client.advanced.subscribe(run_id="my-run"): print(f"Event: {event['type']} — {event['payload']}") ``` #### Node / TS ```js // Stream control events for await (const event of client.advanced.subscribe({ run_id: "my-run" })) { console.log(`Event: ${event.type} — ${event.payload}`); } ``` #### Rust ```rust use futures::StreamExt; // Stream control events — each item is a serde_json::Value let mut stream = client.subscribe(serde_json::json!({ "run_id": "my-run" })).await?; while let Some(event) = stream.next().await { let event = event?; println!("Event: {} — {}", event["type"], event["payload"]); } ``` ## Auto transport When `transport` is set to `"auto"` (the default), the SDK: 1. Checks if a gRPC connection can be established to the endpoint 2. Falls back to HTTP if gRPC is unavailable 3. Caches the result for subsequent calls This works well for most deployments. Set an explicit transport when you need deterministic behavior or want to avoid the initial probe. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Connection refused on port 50051 | gRPC server not running or wrong endpoint | Verify endpoint; for local, ensure `make run-mubit` is running | | TLS handshake failure | Self-signed cert or wrong CA | For hosted endpoints, ensure system CA store is current | | `UNIMPLEMENTED` error | Proto version mismatch | Update SDK to latest version | | `DEADLINE_EXCEEDED` | Network timeout | Check connectivity; increase timeout in client config | | Fields missing in response | `keepCase` not set (JS SDK < v0.4.1) | Update `@mubit-ai/sdk` to >= v0.4.1 | ## Next steps - See [SDK Configuration Reference](/sdk/sdk-configuration) for all config options. - See [Control gRPC reference](/api-reference/control-grpc) for the full RPC surface. --- # Observability, Consistency, and Operations > Operate Mubit safely with ingest/query/context telemetry, memory diagnostics, checkpoint discipline, and explicit agent coordination traces. Operate Mubit safely with ingest/query/context telemetry, memory diagnostics, checkpoint discipline, and explicit agent coordination traces. Production memory systems need more than good demos. Mubit gives you explicit lifecycle boundaries and diagnostics so you can tell whether the system is learning, compacting safely, and retrieving the right evidence. ## Operational checklist | Area | Track this | | --- | --- | | Freshness | Ingest accepted-to-done latency | | Retrieval | Query and context latency, weak-evidence rates | | Learning loop | Reflection volume, outcome recording coverage, surfaced strategies | | Memory quality | `memory_health` results, contradictions, stale entries | | Compaction safety | Checkpoint cadence and checkpoint failures | | Coordination | Handoff and feedback visibility across agents | ## Consistency model - Keep deterministic `run_id` / `session_id` mapping across writes and reads. - Use `getContext` rather than reconstructing large prompts manually. - Treat checkpoints as explicit lifecycle boundaries. - Use `diagnose` and `memory_health` before changing retrieval prompts or weights. > **Note:** Reflected lessons do not become trusted the moment they are extracted. Each one enters long-term memory as a `pending` candidate and is only promoted to `active` once outcome evidence pushes its score past the accept threshold (default `0.6`); a candidate that scores at or below the reject threshold (default `0.25`) is marked `rejected`, while anything in between stays `pending` until more evidence arrives. The control stream emits `context.lesson_validation_passed` / `context.lesson_validation_failed` alongside `context.lesson_promoted`, so you can watch candidate-vs-active counts diverge. The gate is on by default and can be toggled with `MUBIT_CONTROL_LESSON_VALIDATION_ENABLED` (set it to `0`/`false`/`off` to fall back to storing lessons immediately). ## LLM telemetry Mubit tracks all internal LLM calls (ingestion routing, query synthesis, reflection, snapshots) with Prometheus metrics available at the `/metrics` endpoint. | Metric | Labels | Description | | --- | --- | --- | | `mubit_llm_calls_total` | task, provider, model, success | Total LLM call count by task and outcome | | `mubit_llm_call_duration_seconds` | task, provider | Call latency histogram (buckets: 0.1s–30s) | | `mubit_llm_tokens_total` | task, provider, token_type | Token consumption (prompt vs completion) | | `mubit_llm_retries_total` | task, provider | Retry attempts due to rate limits or errors | | `mubit_agent_degraded_total` | agent, reason | Agent fallbacks to heuristic mode | Storage health is also tracked: | Metric | Description | | --- | --- | | `mubit_storage_compaction_pending` | Pending compaction work (0 = healthy) | | `mubit_storage_write_stall` | Whether writes are being throttled (0 or 1) | | `mubit_disk_total_bytes` | Total disk capacity | | `mubit_disk_used_bytes` | Disk space used | | `mubit_disk_usage_pct` | Disk usage percentage | These metrics can be scraped by Prometheus at a 15-second interval. The LLM Activity page in the user console provides a dashboard view. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Learning appears inactive | Reflections exist but outcomes are never recorded | Record outcomes against reflected lessons/rules | | Important details disappear after long runs | No checkpoint before compaction | Save checkpoints before summarization or window resets | | Debugging memory quality is slow | No memory diagnostics in the workflow | Add `memory_health` and `diagnose` to incident review | ## Next steps - Review route-level contracts at [Control HTTP reference](/api-reference/control-http). - Apply the learning loop at [Support agent memory loop](/recipes/support-agent-loop). --- # Retries and Idempotency > When to retry Mubit calls, how to make writes safe to repeat, and what the SDK does for you automatically. When to retry Mubit calls, how to make writes safe to repeat, and what the SDK does for you automatically. ## What the SDK retries automatically By default the SDK retries transient failures with exponential backoff and jitter: - `mubit.ServerError` — any `5xx` (`500`/`503`) from the server. - `mubit.TransportError` whose `.code` is transient: `UNAVAILABLE`, `DEADLINE_EXCEEDED`, `RESOURCE_EXHAUSTED`, `ABORTED`, `INTERNAL`, `CANCELLED`, `CONNECTION_ERROR`, or `TIMEOUT` (includes network-level failures before any response arrived). It does **not** retry `AuthError` (`401`), `ValidationError` (`400`/`409`), or `UnsupportedFeatureError`. Those are caller errors — retrying makes them worse. Retries are tuned process-wide through environment variables (there is no per-call or per-client `RetryPolicy` object): | Env var | Default | Meaning | | --- | --- | --- | | `MUBIT_RETRY_ATTEMPTS` | `3` | Total attempts including the first (min `1`). | | `MUBIT_RETRY_BASE_MS` | `200` | Base delay in ms. | | `MUBIT_RETRY_CAP_MS` | `5000` | Maximum delay per retry. | | `MUBIT_RETRY_JITTER` | `0.2` | ± jitter fraction (`0.0` disables jitter). | Backoff is exponential (factor 2) off `MUBIT_RETRY_BASE_MS`, capped at `MUBIT_RETRY_CAP_MS`. ```bash title=".env" MUBIT_RETRY_ATTEMPTS=5 MUBIT_RETRY_BASE_MS=500 ``` ## Idempotency keys `remember()` (and the underlying `control.ingest`) carry an idempotency key so a repeated write returns the existing entry instead of creating a duplicate. If you don't pass one, the key defaults to the item id (`item_id`, else an auto-generated `remember-`). Pin it explicitly to dedupe across retries from a queue worker: ```python client.remember( session_id=run_id, agent_id="support-agent", content="…", intent="fact", idempotency_key=f"ticket-{ticket_id}-fact-1", ) ``` `record_outcome` also accepts an `idempotency_key`, so a retried outcome write reinforces once rather than double-counting — a typed kwarg/option from SDK v0.12.0 in all three languages; on earlier versions it is a wire-level field (send it via `POST /v2/control/outcome` directly). Other writes are naturally idempotent by their own ids (e.g. `archive` keys on the block id, `register_agent` on the agent id). ## When to retry yourself The SDK's built-in retries cover most cases. Wrap a longer outer budget (queue workers, batch jobs) only when you need one: - **Retry** transient `TransportError` and `ServerError`. - **Don't retry** `AuthError`, `ValidationError`/`AlreadyExistsError`, or `UnsupportedFeatureError` — fix the call instead. ## Recommended pattern ```python from mubit import ServerError, TransportError import time, random _TRANSIENT = {"UNAVAILABLE", "DEADLINE_EXCEEDED", "RESOURCE_EXHAUSTED", "ABORTED", "INTERNAL", "CANCELLED", "CONNECTION_ERROR", "TIMEOUT"} def with_retry(fn, max_attempts=4, base_ms=300): for attempt in range(max_attempts): try: return fn() except ServerError: pass except TransportError as e: if getattr(e, "code", None) not in _TRANSIENT: raise time.sleep(base_ms * (2 ** attempt) * (0.8 + random.random() * 0.4) / 1000) raise RuntimeError("retries exhausted") ``` ## See also - [Errors](/api-reference/errors) — status codes and the SDK exception taxonomy - [Rate limits](/sdk/rate-limits) — input caps and overload behavior --- # Rate Limits > The input caps Mubit enforces, how it behaves under load, and how the SDK backs off. The input caps Mubit enforces, how it behaves under load, and how the SDK backs off. > **Note:** The SDK-facing data and control runtime does **not** currently enforce per-route request quotas and does **not** return `X-RateLimit-*` or `Retry-After` headers. The limits that apply today are the input-size caps and the overload behavior below. Quota headers may be added later — don't build clients that depend on them yet. ## Input caps (enforced today) | Limit | Where it applies | Over the limit | | --- | --- | --- | | **1000 items** per request | `control.ingest`, `control.batch_insert` | `400` (`InvalidArgument`) | | **1000** results | `list_run_history`, `list_projects`, `list_skills` `limit` | Silently clamped to 1000 | | `llm_override.timeout_ms` ∈ **[1000, 600000] ms** | prompt / skill optimize, query overrides | Clamped into range | Chunk bulk writes into ≤1000-item batches and paginate large lists. ## Overload behavior When a backend dependency is saturated, calls fail with a **retryable status** rather than a quota error: - `429` (`ResourceExhausted`) — an upstream dependency (e.g. an LLM provider used by `reflect`/`query`) is throttling. Mubit applies its own outbound rate limiting to those providers. - `503` (`Unavailable` / `FailedPrecondition`) — a backend is temporarily unavailable. Both are safe to retry with backoff. The SDK already retries them — there is no `Retry-After` header to honor, so use the SDK's exponential backoff. See [Retries](/sdk/retries). > **Note:** Per-tenant request limits **do** apply to the platform / instance-management API (the console control plane), configured by the operator via `MUBIT_PLATFORM_RATE_LIMIT_REQUESTS_PER_MINUTE`. That governs instance CRUD and admin traffic — not your `remember` / `recall` / `query` data calls. ## Reducing pressure - Batch writes with `control.ingest` (≤1000 items) instead of N synchronous `remember()` calls. - Cache `get_context()` within a single LLM turn — the same context is usually safe to reuse across tool calls in that turn. - Bound result sizes with `recall(limit=N)` to cut downstream token spend. ## See also - [Errors](/api-reference/errors) — status codes and what each means - [Retries](/sdk/retries) — backoff and idempotency --- # Troubleshooting > Common issues, error messages, and solutions for Mubit SDK and API usage. Common issues, error messages, and solutions for Mubit SDK and API usage. ## Authentication ### Invalid API key **Symptom:** `401 Unauthorized` or `UNAUTHENTICATED` gRPC error. **Causes and fixes:** | Cause | Fix | | --- | --- | | Malformed key | API keys follow the format `mbt___`. Verify all four segments are present. | | Wrong instance | The instance tag in the key must match the endpoint's instance. Check your `MUBIT_API_KEY`. | | Revoked key | Keys can be revoked via `revokeUserApiKey`. Generate a new key with `rotateUserApiKey`. | | Missing env var | Ensure `MUBIT_API_KEY` is set in your environment or passed to the SDK constructor. | ## Transport and connectivity ### Connection refused **Symptom:** `ECONNREFUSED` or `Connection refused`. | Context | Fix | | --- | --- | | Local development | Ensure Mubit is running (`make run-mubit`). Default ports: HTTP 3000, gRPC 50051. | | Hosted endpoint | Check network connectivity to `api.mubit.ai`. Verify no firewall blocking HTTPS/443. | | gRPC transport | Verify `MUBIT_GRPC_ENDPOINT` points to the correct host:port. | ### Transport selection issues **Symptom:** Unexpected transport used, or auto-detection fails. Set `MUBIT_TRANSPORT` explicitly to `"http"` or `"grpc"` instead of relying on `"auto"`. The auto probe can fail in restricted network environments. ### gRPC field names wrong (JS SDK) **Symptom:** Response fields use camelCase instead of snake_case proto names, or vice versa. **Fix:** Update `@mubit-ai/sdk` to the latest version. Current SDKs decode proto responses with `keepCase: false` by default, so fields surface as `camelCase`. ## Memory and retrieval ### Recall returns empty results | Cause | Fix | | --- | --- | | Data not yet ingested | Ingest is async. Wait for the ingest job to complete (poll `getIngestJob`). | | Wrong `run_id` | `recall` scopes by `run_id`. Ensure you're querying the same run. | | Entry types filter too narrow | Broaden `entry_types` or omit it to search all types. | | Lane mismatch | If using lanes, ensure `lane_filter` matches the `lane` used at ingest time. | ### Context block is empty or too small | Cause | Fix | | --- | --- | | Token budget too low | The default `max_token_budget` is 0, which means no cap. If you set a small budget, raise it; 0 imposes no limit. | | No relevant evidence | Ingest more data or broaden the query. | | Wrong mode | Try different `mode` values: `"full"`, `"summary"`, `"sections"`. | | Run has no data | Verify the `run_id` has ingested data with `getRunIngestStats` / `getIngestJob`, or query the activity log directly via `POST /v2/control/activity`. | ### Lessons not appearing after reflect | Cause | Fix | | --- | --- | | Lesson is still a pending candidate | Reflected lessons are gated: they enter as `pending` and are promoted to `active` once their validation score crosses the accept threshold (default 0.6). Record outcomes against the lesson to accrue evidence, or disable the gate with `MUBIT_CONTROL_LESSON_VALIDATION_ENABLED=0`. | | No outcomes recorded | Pair `reflect()` with `recordOutcome()` for stronger lesson extraction. | | Too few data points | Reflection needs sufficient evidence. Ingest more interactions before reflecting. | | Step outcomes not included | Set `include_step_outcomes: true` on the wire reflect request (`client.advanced.reflect({...})` in JS, and in Python from SDK v0.12.0; direct HTTP/gRPC on earlier Python versions — the typed `reflect()` helper does not forward it). | ## Runs and scoping ### Run scope conflicts **Symptom:** Data from one run appears in another, or data is missing. - Runs are the primary isolation boundary. Each `run_id` scopes its own data. - Linked runs (`linkRun`) share data intentionally. Check if runs are linked. - Inspect active runs and their relationships with `client.advanced.listRunHistory(...)` (JS) / `client.advanced.list_run_history(...)` (Python), or `GET /v2/control/runs`. ### Cannot delete a run **Symptom:** `deleteRun` fails or data persists. - Run deletion is permanent and removes all associated data. - Ensure no active ingest jobs are running for the run. - Both control and core planes have `deleteRun` — the control version handles both. ## Lesson promotion ### How lesson promotion works Lessons are promoted through scopes based on recurrence: 1. **Run-scoped** — extracted from a single run's evidence 2. **Session-scoped** — promoted when the same lesson appears across multiple runs 3. **Global** — promoted when recurring across sessions Promotion happens automatically during `reflect()`. You can inspect lesson scope with `client.lessons(...)` (`lessons()` in JS). ## Integration packages ### Framework adapter not connecting All integration packages (`mubit-crewai`, `mubit-langgraph`, etc.) use the core SDK internally. Verify: 1. `MUBIT_API_KEY` is set 2. `MUBIT_ENDPOINT` points to a reachable Mubit instance 3. The adapter package version matches your SDK version ## Getting help - SDK issues: [github.com/mubit-ai/ricedb/issues](https://github.com/mubit-ai/ricedb/issues) - API reference: [Control HTTP](/api-reference/control-http) | [Control gRPC](/api-reference/control-grpc) - Configuration: [SDK Configuration Reference](/sdk/sdk-configuration) --- # Full Fledged Support Agent > Build an end-to-end support agent with helper-first memory, compaction-safe context, coordination, and reinforcement. Build an end-to-end support agent with helper-first memory, compaction-safe context, coordination, and reinforcement. This page shows one complete support-agent shape per SDK using the **current recommended Mubit flow**: 1. register the agents involved 2. store facts, traces, and preferences 3. assemble context before responding 4. checkpoint before compaction or risky transitions 5. coordinate specialist review with handoff and feedback 6. reflect and record outcomes so future cases improve ## Environment ```bash title=".env" MUBIT_API_KEY="mbt___" MUBIT_ENDPOINT="https://api.mubit.ai" ``` ## Full implementation #### Python ```python import os from mubit import Client RUN_ID = "support:acme:ticket-42" client = Client( endpoint=os.getenv("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], run_id=RUN_ID, transport="http", ) client.register_agent(session_id=RUN_ID, agent_id="support", role="support") client.register_agent(session_id=RUN_ID, agent_id="billing", role="billing") client.remember( session_id=RUN_ID, agent_id="support", content="Customer Taylor wants concise Friday updates and is asking about invoice INV-7781.", intent="fact", metadata={"customer": "taylor", "ticket": 42}, ) client.remember( session_id=RUN_ID, agent_id="billing", content="Invoice INV-7781 has a stale tax table mismatch on line item 4.", intent="trace", metadata={"invoice": "INV-7781", "source": "billing-review"}, ) context = client.get_context( session_id=RUN_ID, query="Draft the next customer-safe update with the billing root cause and next step.", mode="full", max_token_budget=800, ) checkpoint = client.checkpoint( session_id=RUN_ID, agent_id="support", label="pre-response-compaction", context_snapshot=context.get("context_block", ""), ) handoff = client.handoff( session_id=RUN_ID, task_id="ticket-42-billing-review", from_agent_id="support", to_agent_id="billing", content="Confirm the billing root cause for invoice INV-7781 before we reply.", requested_action="review", ) feedback = client.feedback( session_id=RUN_ID, handoff_id=handoff["handoff_id"], from_agent_id="billing", verdict="approve", comments="Root cause confirmed: stale tax table mismatch on line item 4.", ) reflection = client.reflect(session_id=RUN_ID) # Use the lesson id returned inline on the reflect response — avoids racing # with the vector-store index when listing immediately after a write. lesson_id = next( (l.get("lesson_id") for l in (reflection.get("lessons") or []) if l.get("lesson_id")), None, ) if lesson_id: client.record_outcome( session_id=RUN_ID, agent_id="support", reference_id=lesson_id, outcome="success", signal=0.75, rationale="The support response used the stored customer preference and the validated billing root cause.", ) strategies = client.surface_strategies( session_id=RUN_ID, lesson_types=["success", "failure"], max_strategies=5, ) print({ "checkpoint_id": checkpoint.get("checkpoint_id"), "handoff_id": handoff.get("handoff_id"), "feedback_id": feedback.get("feedback_id"), "lessons_stored": reflection.get("lessons_stored"), "strategy_count": len(strategies.get("strategies", [])), }) ``` #### Node / TS ```ts import { Client } from "@mubit-ai/sdk"; const RUN_ID = "support:acme:ticket-42"; const client = new Client({ endpoint: process.env.MUBIT_ENDPOINT ?? "https://api.mubit.ai", api_key: process.env.MUBIT_API_KEY, run_id: RUN_ID, transport: "http", }); await client.registerAgent({ session_id: RUN_ID, agent_id: "support", role: "support" }); await client.registerAgent({ session_id: RUN_ID, agent_id: "billing", role: "billing" }); await client.remember({ session_id: RUN_ID, agent_id: "support", content: "Customer Taylor wants concise Friday updates and is asking about invoice INV-7781.", intent: "fact", metadata: { customer: "taylor", ticket: 42 }, }); await client.remember({ session_id: RUN_ID, agent_id: "billing", content: "Invoice INV-7781 has a stale tax table mismatch on line item 4.", intent: "trace", metadata: { invoice: "INV-7781", source: "billing-review" }, }); const context = await client.getContext({ session_id: RUN_ID, query: "Draft the next customer-safe update with the billing root cause and next step.", mode: "full", max_token_budget: 800, }); const checkpoint = await client.checkpoint({ session_id: RUN_ID, agent_id: "support", label: "pre-response-compaction", context_snapshot: context.context_block ?? "", }); const handoff = await client.handoff({ session_id: RUN_ID, task_id: "ticket-42-billing-review", from_agent_id: "support", to_agent_id: "billing", content: "Confirm the billing root cause for invoice INV-7781 before we reply.", requested_action: "review", }); const feedback = await client.feedback({ session_id: RUN_ID, handoff_id: handoff.handoff_id, from_agent_id: "billing", verdict: "approve", comments: "Root cause confirmed: stale tax table mismatch on line item 4.", }); const reflection = await client.reflect({ session_id: RUN_ID }); // lesson_id is populated inline on each ReflectLesson so callers don't need // a round-trip through client.lessons() which can race with index // propagation immediately after a write. const lessonId = reflection.lessons?.find((l) => l.lesson_id)?.lesson_id; if (lessonId) { await client.recordOutcome({ session_id: RUN_ID, agent_id: "support", reference_id: lessonId, outcome: "success", signal: 0.75, rationale: "The support response used the stored customer preference and the validated billing root cause.", }); } const strategies = await client.surfaceStrategies({ session_id: RUN_ID, lesson_types: ["success", "failure"], max_strategies: 5, }); console.log({ checkpoint_id: checkpoint.checkpoint_id, handoff_id: handoff.handoff_id, feedback_id: feedback.feedback_id, lessons_stored: reflection.lessons_stored, strategy_count: strategies.strategies?.length ?? 0, }); ``` #### Rust ```rust use mubit_sdk::{ CheckpointOptions, Client, ClientConfig, FeedbackOptions, GetContextOptions, HandoffOptions, RecordOutcomeOptions, ReflectOptions, RegisterAgentOptions, RememberOptions, SurfaceStrategiesOptions, }; use serde_json::json; use std::env; let run_id = "support:acme:ticket-42".to_string(); let config = ClientConfig::new("https://api.mubit.ai") .api_key(env::var("MUBIT_API_KEY")?) .run_id(&run_id); let client = Client::new(config)?; let mut support = RegisterAgentOptions::new("support"); support.run_id = Some(run_id.clone()); support.role = "support".to_string(); client.register_agent(support).await?; let mut billing = RegisterAgentOptions::new("billing"); billing.run_id = Some(run_id.clone()); billing.role = "billing".to_string(); client.register_agent(billing).await?; let mut support_fact = RememberOptions::new( "Customer Taylor wants concise Friday updates and is asking about invoice INV-7781.", ); support_fact.run_id = Some(run_id.clone()); support_fact.agent_id = Some("support".to_string()); support_fact.intent = Some("fact".to_string()); support_fact.metadata = Some(json!({"customer":"taylor","ticket":42})); client.remember(support_fact).await?; let mut billing_trace = RememberOptions::new( "Invoice INV-7781 has a stale tax table mismatch on line item 4.", ); billing_trace.run_id = Some(run_id.clone()); billing_trace.agent_id = Some("billing".to_string()); billing_trace.intent = Some("trace".to_string()); billing_trace.metadata = Some(json!({"invoice":"INV-7781","source":"billing-review"})); client.remember(billing_trace).await?; let mut context = GetContextOptions::default(); context.run_id = Some(run_id.clone()); context.query = Some("Draft the next customer-safe update with the billing root cause and next step.".to_string()); context.mode = Some("full".to_string()); context.max_token_budget = Some(800); let context = client.get_context(context).await?; let mut checkpoint = CheckpointOptions::new( context.get("context_block").and_then(|v| v.as_str()).unwrap_or(""), ); checkpoint.label = Some("pre-response-compaction".to_string()); checkpoint.run_id = Some(run_id.clone()); checkpoint.agent_id = Some("support".to_string()); let checkpoint = client.checkpoint(checkpoint).await?; let mut handoff = HandoffOptions::new( "ticket-42-billing-review", "support", "billing", "Confirm the billing root cause for invoice INV-7781 before we reply.", ); handoff.run_id = Some(run_id.clone()); handoff.requested_action = "review".to_string(); let handoff = client.handoff(handoff).await?; let handoff_id = handoff.get("handoff_id").and_then(|v| v.as_str()).unwrap().to_string(); let mut feedback = FeedbackOptions::new(handoff_id.clone(), "approve"); feedback.run_id = Some(run_id.clone()); feedback.from_agent_id = Some("billing".to_string()); feedback.comments = "Root cause confirmed: stale tax table mismatch on line item 4.".to_string(); let feedback = client.feedback(feedback).await?; let mut reflect = ReflectOptions::default(); reflect.run_id = Some(run_id.clone()); let reflection = client.reflect(reflect).await?; // Read the lesson id inline from the reflect response — avoids racing with // the vector-store index when listing immediately after a write. let lesson_id = reflection["lessons"] .get(0) .and_then(|l| l["lesson_id"].as_str()); if let Some(lesson_id) = lesson_id { let mut outcome = RecordOutcomeOptions::new(lesson_id, "success"); outcome.run_id = Some(run_id.clone()); outcome.agent_id = Some("support".to_string()); outcome.signal = 0.75; outcome.rationale = "The support response used the stored customer preference and the validated billing root cause.".to_string(); client.record_outcome(outcome).await?; } let mut strategies = SurfaceStrategiesOptions::default(); strategies.run_id = Some(run_id.clone()); strategies.lesson_types = vec!["success".to_string(), "failure".to_string()]; strategies.max_strategies = 5; let strategies = client.surface_strategies(strategies).await?; println!("{} {} {} {} {}", checkpoint["checkpoint_id"], handoff["handoff_id"], feedback["feedback_id"], reflection["lessons_stored"], strategies["strategies"] ); ``` > **Note:** A freshly reflected lesson may enter long-term memory as a `pending` candidate (the validation gate is on by default, accept threshold `0.6`) and is not immediately surfaced until enough evidence accrues. The outcomes you record against it are exactly how it accrues that evidence and gets promoted to `active` — the control stream emits `context.lesson_validation_passed` / `context.lesson_validation_failed` alongside `context.lesson_promoted`. Operators can disable the gate with `MUBIT_CONTROL_LESSON_VALIDATION_ENABLED=0`. > **Note:** The answer was grounded on more than the lesson alone, so attribute the win to every recalled entry that informed it. `record_outcome` accepts `entry_ids` (a list of the recalled entry ids — e.g. the customer fact and the billing trace) so each one's reinforcement counters update, not just the lesson `reference_id` (which is never double-counted). `entry_ids` is available in the Python, Node, and Rust `record_outcome` helpers. ## Operational notes - Start helper-first and drop to raw `client.advanced.*` only where helper coverage is intentionally lower (`client.lessons(...)` for explicit lesson listing stays top-level). - `mubit.auto` is the public zero-friction path if you want Python LLM traces to feed this same memory loop automatically. ## Next steps - Read the route contract at [Control HTTP reference](/api-reference/control-http). - Use the narrower examples at [Support agent memory loop](/recipes/support-agent-loop) and [Multi-agent shared state](/recipes/multi-agent-shared-state). --- # Examples / Cookbook > Apply Mubit concepts with one staged SDK project that evolves from a single support agent to a multi-agent system. Start with the complete multi-SDK implementation first, then use cookbooks for focused patterns. Full implementation page: [Full-fledged support agent](/sdk/full-support-agent) Then apply recipes in this order: 1. [Support agent memory loop](/recipes/support-agent-loop) 2. [Multi-agent shared state](/recipes/multi-agent-shared-state) 3. [Building event-driven agents](/recipes/event-driven-agents) 4. [Branching what-if simulations](/recipes/branching-memory) ## Framework integration examples Complete example apps with real LLM calls showing Mubit integrated with popular agent frameworks: | Example | Framework | What it shows | | --- | --- | --- | | [Support Ticket Triage](/sdk/framework-integrations#crewai) | CrewAI | 3-agent crew with handoffs, checkpoints, and outcome recording | | [Code Review Pipeline](/sdk/framework-integrations#langgraph) | LangGraph | StateGraph with conditional reviewer loop and Mubit store | | [Research Assistant](/sdk/framework-integrations#langchain) | LangChain | Multi-turn conversation with cross-session memory | | [Travel Planner](/sdk/framework-integrations#google-adk) | Google ADK | SequentialAgent with Gemini tool calling | | [FAQ Bot](/sdk/framework-integrations#vercel-ai-sdk) | Vercel AI SDK | Middleware auto-injection with knowledge-base tool | ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Recipes work alone but fail together | Run identity not carried between stages | Keep deterministic run IDs by role/task | | Progression feels disconnected | Recipe order skipped | Follow the recipe order above and reuse the same project folder | ## Next steps - Recheck foundations at [How Mubit works](/sdk/how-mubit-works). - Use route-level references at [Control HTTP reference](/api-reference/control-http). --- # The outcome attribution loop > Close the causal loop between the memories an agent used and whether the task actually succeeded — objective verdicts, entry-level credit, and the one-line zero-code path. 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](#never-let-the-model-grade-itself)). 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](/concepts/learning-loop#outcomes-close-the-loop); how the credit shows up at read time is in [Trust, confidence, and staleness](/concepts/trust-and-confidence). ## 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: ```python 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](/sdk/sdk-methods#learn-module-closed-loop). ## 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: #### Python ```python # (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)", ) ``` #### Node / TS ```js const answer = await client.recall({ session_id: "nimbus:ari:ticket-1042", query: "How do we fix CSV export timeouts on large reports?", entry_types: ["lesson", "fact"], }); const entryIds = (answer.evidence ?? []).map((e) => e.id); // ... deploy the fix, then read the verdict from the pipeline ... const verdict = await nightlyExportCompleted(); await client.recordOutcome({ session_id: "nimbus:ari:ticket-1042", reference_id: entryIds[0], outcome: verdict ? "success" : "failure", signal: verdict ? 0.8 : -0.8, entry_ids: entryIds, verified_in_production: verdict, rationale: "Nightly export completed in 41s (was timing out at 300s)", }); ``` #### Rust ```rust use mubit_sdk::{RecallOptions, RecordOutcomeOptions}; let mut recall = RecallOptions::new("How do we fix CSV export timeouts on large reports?"); recall.run_id = Some("nimbus:ari:ticket-1042".into()); recall.entry_types = vec!["lesson".into(), "fact".into()]; let answer = client.recall(recall).await?; let entry_ids: Vec = answer["evidence"] .as_array() .map(|items| items.iter().filter_map(|e| e["id"].as_str().map(String::from)).collect()) .unwrap_or_default(); // ... deploy the fix, read the verdict from the pipeline ... let mut outcome = RecordOutcomeOptions::new(entry_ids[0].clone(), "success"); outcome.run_id = Some("nimbus:ari:ticket-1042".into()); outcome.signal = 0.8; outcome.entry_ids = entry_ids; // the primary id is never double-counted outcome.verified_in_production = true; outcome.rationale = "Nightly export completed in 41s (was timing out at 300s)".into(); client.record_outcome(outcome).await?; ``` `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](/concepts/trust-and-confidence#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](/api-reference/control-http#run-level-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](/recipes/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](/patterns/reflection-to-lessons) 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`](/recipes/step-level-outcomes), then run-level attribution at the end | ## Related - [The learning loop](/concepts/learning-loop#outcomes-close-the-loop) — the outcome field reference and where attribution sits in the loop - [Trust, confidence, and staleness](/concepts/trust-and-confidence) — how credited outcomes surface as `knowledge_confidence` at read time - [Reflection to lessons](/patterns/reflection-to-lessons) — distilling the attributed run into reusable lessons - [Guardrails from failures](/patterns/guardrails-from-failures) — what negative attribution builds over time - [Self-optimizing prompts](/patterns/self-optimizing-prompts) — attributed outcomes as the optimizer's raw material - [Step-level outcomes](/recipes/step-level-outcomes) — dense process rewards within a run - [Support agent loop](/recipes/support-agent-loop) — the loop end-to-end in a working agent - [Control HTTP reference](/api-reference/control-http#run-level-outcome-request-shape) — the wire contract --- # Reflection to lessons > Distill a completed run into reusable, validation-gated lessons — scoping options per SDK, lesson anatomy, reflect-on-session-close, and where lessons resurface. 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](/concepts/runs-and-scopes#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](/concepts/learning-loop#the-five-stages). Lessons that keep proving useful (via [outcome attribution](/patterns/outcome-attribution-loop)) 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): | 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` | — | — | ✓ | > **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](/api-reference/control-http#step-wise-reflection-parameters). ## Reflect on session close A *session* is the lifecycle wrapper around runs (see [Runs, sessions, and scopes](/concepts/runs-and-scopes#sessions-the-lifecycle-wrapper)), 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: ```python 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: ```python 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: #### Python ```python 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. ``` #### Node / TS ```js const result = await client.reflect({ session_id: "nimbus:ari:ticket-1042", include_linked_runs: true, // fold in the escalation run Rex handled }); for (const lesson of result.lessons ?? []) { console.log(lesson.lesson_type, lesson.scope, "-", lesson.content); } ``` #### Rust ```rust use mubit_sdk::ReflectOptions; let mut options = ReflectOptions::default(); options.run_id = Some("nimbus:ari:ticket-1042".into()); options.include_step_outcomes = Some(true); // use recorded step rewards as evidence let result = client.reflect(options).await?; ``` 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](/concepts/runs-and-scopes#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](/patterns/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](/patterns/outcome-attribution-loop)), then reflect. ## Related - [The learning loop](/concepts/learning-loop) — where reflection sits between attribution and injection - [The outcome attribution loop](/patterns/outcome-attribution-loop) — the verdicts that make reflection's input meaningful - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — runs vs sessions, and the ladder lessons climb - [The memory model](/concepts/memory-model#entry-types) — lessons among the other entry types - [Step-level outcomes](/recipes/step-level-outcomes) — the dense signals `include_step_outcomes` folds in - [Control HTTP reference](/api-reference/control-http#step-wise-reflection-parameters) — the wire contract for scoped reflection --- # Guardrails from failures > Turn failures into preventive memory — explicit rules, failure attribution, automatic guardrail distillation, and diagnose() as the "have I hit this before?" check. An agent that merely logs its failures repeats them; an agent whose failures become **preventive memory** doesn't. This pattern turns each failure into something that changes future behavior — a rule that always injects, a confidence penalty on the memories that misled, or a distilled guardrail lesson with trigger conditions. Use it whenever a class of failure is expensive enough that "we'll notice next time" isn't good enough. ## How it works Failures harden into guardrails at three tiers, from fully manual to fully automatic: | Tier | You do | Memory gets | | --- | --- | --- | | **Explicit rule** | Write the invariant yourself | A `rule` entry — always injected, exempt from relevance competition | | **Attributed failure** | `record_outcome(outcome="failure")` on the entries that misled | Confidence penalties that demote bad guidance in ranking | | **Automatic distillation** | Nothing — instance feature | Repeated failure-credited traces distilled into guardrail lessons with trigger preconditions | **Tier 1 — explicit.** When you already know the invariant, don't wait for the system to learn it. A `rule` entry is a hard constraint: context assembly injects it in the always-on Active Rules section (see [context priority](/concepts/memory-model#context-priority)), it never has to win a relevance contest, and it is authored — never produced or promoted by the automatic loop. ```python client.remember( session_id="nimbus:ari:support", content="Never deploy a regex change to the export pipeline without a green run " "of the multi-line CSV fixture suite.", intent="rule", ) ``` **Tier 2 — attributed.** When a task fails, penalize the memories that led it there. `record_outcome(outcome="failure", signal=-0.8, entry_ids=[...])` drops each entry's `knowledge_confidence`, so the same misleading guidance stops outranking better candidates. This is the negative half of [the outcome attribution loop](/patterns/outcome-attribution-loop). **Tier 3 — automatic.** Hosted instances run guardrail distillation as an always-on maintenance feature (per-instance configurable): when similar failure-credited traces recur, they are distilled into a preventive lesson with trigger preconditions — "before doing X under condition Y, check Z" — that competes into the lesson overlay of future queries. You feed it simply by doing tier 2 consistently. ## `diagnose()`: have I hit this error before? Guardrails also need a read path at the moment of failure. `diagnose()` takes the error your agent just hit and retrieves the failure lessons that match it — returning `failure_lessons` (scored evidence items), a `summary`, and `total_failure_lessons`. Call it in your error handler, before the retry: #### Python ```python hits = client.diagnose( session_id="nimbus:ari:ticket-2107", error_text="Export worker crashed: catastrophic regex backtracking on multi-line row", error_type="RegexTimeout", # optional coarse class limit=5, ) print(hits["summary"]) for lesson in hits["failure_lessons"]: print(lesson["content"]) ``` #### Node / TS ```js const hits = await client.diagnose({ session_id: "nimbus:ari:ticket-2107", error_text: "Export worker crashed: catastrophic regex backtracking on multi-line row", error_type: "RegexTimeout", limit: 5, }); console.log(hits.summary); ``` #### Rust ```rust use mubit_sdk::DiagnoseOptions; let mut options = DiagnoseOptions::new( "Export worker crashed: catastrophic regex backtracking on multi-line row", ); options.run_id = Some("nimbus:ari:ticket-2107".into()); options.error_type = Some("RegexTimeout".into()); options.limit = 5; let hits = client.diagnose(options).await?; ``` ## Walkthrough: the bad regex becomes a guardrail Ari ships a regex "fix" that takes down Nimbus's export pipeline. Here is the incident becoming prevention, tier by tier: ```python # Tier 2 — the incident run attributes the failure to the memories used. client.record_outcome( session_id="nimbus:incident-0142", reference_id=misleading_lesson_id, # the lesson that suggested the quick fix outcome="failure", signal=-0.9, entry_ids=recalled_entry_ids, # everything that informed the change verified_in_production=True, # it failed in prod — that is a verdict too rationale="Regex change broke multi-line CSV rows in the export pipeline", ) # Reflection distills the incident into a failure lesson with conditions. client.reflect(session_id="nimbus:incident-0142") # Tier 1 — the postmortem writes the invariant down as a hard rule. client.remember( session_id="nimbus:ari:support", content="Never deploy a regex change to the export pipeline without a green run " "of the multi-line CSV fixture suite.", intent="rule", ) ``` Tier 3 does the rest: because export-regex failures have now recurred, guardrail distillation reinforces the pattern into a preventive lesson with its trigger precondition attached. Weeks later, Ari attempts another regex change. The rule arrives unconditionally in Active Rules; the guardrail lesson arrives via the lesson overlay because the query matches its conditions; and when the deploy still throws, `diagnose(error_text=...)` surfaces the incident lesson at error time — so Rex, the reviewer agent, blocks the deploy until the fixture suite passes. When the guarded attempt succeeds, `record_outcome(..., outcome="success", verified_in_production=True)` strengthens the guardrail instead of the shortcut. > **Note:** `verified_in_production=True` applies to failures as much as successes — "this guidance demonstrably failed in prod" is exactly the trust signal that should demote it hardest. See [Trust, confidence, and staleness](/concepts/trust-and-confidence#production-verification). ## When not to use it - **Transient faults** — a network blip or a 503 is not a lesson; handle it with plain retries and backoff. Persisting one-off noise as failure memory dilutes the guardrails that matter. (For *behavioral* pathologies like retry loops, see [Loop safety](/patterns/loop-safety).) - **Failures with no recurring shape** — distillation and `diagnose()` both work off similarity; a truly unique failure is better captured as a plain observation. - **Secrets in error text** — `error_text` is stored and retrieved like any memory. Scrub tokens and customer data before recording. ## Related - [The outcome attribution loop](/patterns/outcome-attribution-loop) — the failure-crediting mechanics this pattern builds on - [Reflection to lessons](/patterns/reflection-to-lessons) — how the incident run becomes a conditioned failure lesson - [The learning loop](/concepts/learning-loop) — guardrail distillation among the runtime maintenance features - [Trust, confidence, and staleness](/concepts/trust-and-confidence) — how penalties surface as `knowledge_confidence` - [Retrieval semantics](/concepts/retrieval-semantics#example-ari-checks-for-a-known-error) — a `diagnose`-style error lookup traced through retrieval - [The memory model](/concepts/memory-model#entry-types) — `rule` vs `lesson` vs `observation` > **Note:** **Configuration.** Guardrail distillation is instance configuration — `MUBIT_CL_GUARDRAIL_DISTILL` on the instance, enabled by default on hosted instances alongside the other maintenance features listed in [The learning loop](/concepts/learning-loop#what-the-runtime-maintains-for-you). --- # Self-optimizing prompts > Serve prompts and skills as versioned server-side resources that evolve from recorded outcomes — set/get helpers, version lifecycle calls, and champion/challenger machinery. A prompt baked into your deploy artifact can only improve on redeploy. This pattern moves the system prompt (and tool/skill definitions) into Mubit as **versioned server-side resources**: agents fetch the active version at call time, outcomes accrue against it, and the optimizer proposes better candidates from that evidence — reviewed, activated, and rolled back without touching agent code. Use it when your agents already [record outcomes](/patterns/outcome-attribution-loop) and prompt tuning has become a recurring manual chore. ## How it works ``` set_prompt(v1) ──► agents get_prompt() per call ──► outcomes accrue │ activate ◄── review diff ◄── optimize_prompt() ◄─────┘ │ └─► (regression? → rollback to the retired version) ``` Every `set_prompt` creates a new `PromptVersion` (`active`, `candidate`, `retired`, or `archived`); activation is atomic and the displaced version stays addressable for rollback. `optimize_prompt` reads the agent's accumulated lessons and outcomes — the same signal the learning loop runs on — and produces a `candidate` version plus an `optimization_summary` and `confidence`. It never activates by default; you diff, then promote. Skills get the identical lifecycle with `optimize_skill` and the skill-version calls. ## Serving the active prompt The helper layer resolves the agent from the ambient session context and returns the content string directly: #### Python ```python import mubit mubit.init(api_key=API_KEY, agent_id="ari") prompt = mubit.get_prompt() # active version; cached for 60s tools = mubit.get_skills(format="anthropic") # skill defs as tool schemas # formats: "openai" (default), "anthropic", "gemini", "raw" mubit.set_prompt("You are Ari, Nimbus's support agent...", activate=True) # set_prompt busts the local cache so the next get_prompt() sees the new version ``` #### Node / TS ```js // Module-level helper layer (reads the initialized SDK context): import { getPrompt, setPrompt, getSkills } from "@mubit-ai/sdk/helpers"; const prompt = await getPrompt(); // active version content const tools = await getSkills({ format: "openai" }); // ready for the tools= param await setPrompt("You are Ari, Nimbus's support agent...", { activate: true }); ``` > **Note:** The 60-second prompt cache is Python-only at 0.11.0 — the JS `getPrompt()` helper fetches on every call, so add your own memoization on hot paths. Rust has no session-context helper layer; fetch with the payload-style call directly on the client: `client.get_prompt(json!({ "agent_id": "ari" }))` and read `version.content`. Because agents fetch the prompt per call (cache notwithstanding), an activation propagates to the whole fleet within seconds — no redeploy. ## Version lifecycle calls `optimize_prompt` is top-level in all three SDKs (a typed helper from SDK v0.12.0 in Python/JS — `client.optimize_prompt(agent_id=..., auto_activate=False, project_id=...)` / `client.optimizePrompt({...})` — and typed in Rust since 0.11); the surrounding version management lives on the raw surface: | Call | Python | JS | Rust | | --- | --- | --- | --- | | Propose a candidate | `client.optimize_prompt(agent_id=..., project_id=...)` | `client.optimizePrompt({...})` | `client.optimize_prompt(OptimizePromptOptions::new("ari"))` — typed | | Get active / specific version | `client.advanced.get_prompt` | `client.advanced.getPrompt` | `client.get_prompt(json!({...}))` | | List versions | `client.advanced.list_prompt_versions` | `client.advanced.listPromptVersions` | `client.list_prompt_versions(json!({...}))` | | Diff two versions | `client.advanced.get_prompt_diff` | `client.advanced.getPromptDiff` | `client.get_prompt_diff(json!({...}))` | | Activate a version | `client.advanced.activate_prompt_version` | `client.advanced.activatePromptVersion` | `client.activate_prompt_version(json!({...}))` | > **Note:** The propose row is typed everywhere from SDK v0.12.0 (`optimize_prompt` / `optimizePrompt` / Rust's `OptimizePromptOptions`, which predates it); on 0.11.0 the Python/JS calls are wire pass-throughs with the same call shape. The get/list/diff/activate rows remain wire pass-throughs on every version — pass the wire field names (`agent_id`, `version_id`, `version_a_id`/`version_b_id`, `project_id`). In Python and JS the version-management ops live under `client.advanced.*`; in Rust the same long-tail ops sit payload-style directly on `Client`. The compact loop, from Nimbus's nightly cron: ```python resp = client.optimize_prompt(agent_id="ari", project_id=PROJECT, auto_activate=False) if resp["confidence"] >= 0.6: active = client.advanced.get_prompt(agent_id="ari") diff = client.advanced.get_prompt_diff( agent_id="ari", version_a_id=active["version"]["version_id"], version_b_id=resp["candidate"]["version_id"], ) post_for_review(resp["optimization_summary"], diff["diff_text"]) # a human approves → client.advanced.activate_prompt_version( # agent_id="ari", version_id=resp["candidate"]["version_id"]) ``` The candidate's material is the rationales and `directive_hint`s from your recorded outcomes — the better your [attribution hygiene](/patterns/outcome-attribution-loop), the better the candidates. The full runnable workflow — including the console equivalents, shadow testing, and rollback — is [Prompt optimization](/recipes/prompt-optimization). ## Champion/challenger, hands-off Instances can run the comparison for you. With prompt shadow serving enabled, while a candidate exists `get_prompt` alternates deterministically between the champion and the challenger on a time bucket, so the challenger accrues real outcome statistics with no client-side changes. The champion/challenger evaluator then surfaces the winner — and when auto-promotion is enabled, an activated challenger is watched against the displaced champion's baseline score and **automatically rolled back** if post-promotion outcomes regress. Treat this as an instance feature to opt into once manual review has built trust in the loop, not as a starting point. ## When not to use it - **Externally change-controlled prompts** — if the prompt text is reviewed, signed off, and audited outside your systems, it must not drift by optimization. Pin it in your artifact (or keep versions in Mubit but never grant `optimize_prompt` a path to activation). - **Too little signal** — the optimizer can synthesize a confident-looking candidate from a handful of outcomes. Wait for a meaningful sample with some genuine failures before acting on candidates. - **Prompts that encode hard invariants** — safety-critical constraints belong in `rule` entries or code-level checks ([Guardrails from failures](/patterns/guardrails-from-failures)), not in text an optimizer is allowed to rewrite. ## Related - [Prompt optimization](/recipes/prompt-optimization) — the full runnable lifecycle, with console equivalents - [The outcome attribution loop](/patterns/outcome-attribution-loop) — the signal the optimizer feeds on - [Reflection to lessons](/patterns/reflection-to-lessons) — the lessons folded into candidates - [Projects, agents, and skills](/sdk/projects-and-agents) — the resource model behind prompt and skill versions - [Control HTTP reference](/api-reference/control-http) — the prompt lifecycle endpoints on the wire > **Note:** **Configuration.** Shadow serving and its bucket width are instance configuration (`MUBIT_CL_PROMPT_SHADOW`, `MUBIT_CL_PROMPT_SHADOW_BUCKET_SECS`), as is the confidence threshold `auto_activate` must clear server-side. Defaults keep activation deliberate: shadow serving off, candidates surface for review. --- # Loop safety > Detect looping, drift, and stuck runs with run signals, then break the pathology with circuit_break — without losing state. Agents fail in characteristic shapes: retrying the same call forever, drifting off the objective, grinding without progress, or churning through steps that surface nothing new. This pattern detects those shapes with **run signals** and breaks them with **`circuit_break`** — a reset that snapshots working memory to a durable entry before clearing it, so recovery costs a reset, not the run's accumulated state. Use it in any orchestrator that supervises long-running or unattended agents. ## How it works **Detection is advisory; action is yours.** Mubit's monitors flag pathologies but never act on them — no server-side abort, no automatic reset. Two read surfaces: - **Per-recall signals** — every `recall()` response carries a lightweight `signals` block: `repeated` (same normalized query over threshold), `stagnant` (negative-outcome streak), `drift_score`, `novelty_score`. Free to read; check it in the agent's own loop. - **The run signal** — the run-monitor op re-inspects the run on demand and returns a merged verdict for dashboards and supervisors: `is_looping`, `is_drifting`, `is_stuck`, `is_novelty_starved`, plus `loop_score`, `drift_score`, a human-readable `rationale`, a `confidence`, and the `source` that produced it (`rule`, `llm`, or `probe`). The response's `monitor_disabled: true` means no monitor router is configured — treat it as "no pathologies detected". When you decide to act, `circuit_break` performs the reset atomically: it snapshots current working memory into a durable, run-scoped `loop_detected` lesson (tagged with your `reason`), clears working memory, resets the drift-monitor state, and emits a `CIRCUIT_BROKEN` event. The response returns `broken`, the `snapshot_id` of the snapshot entry, and a `summary` — so the pre-break state remains recoverable from long-term memory (see [Circuit break: clear, but snapshot first](/concepts/working-memory-vs-long-term#circuit-break-clear-but-snapshot-first)). **Drift scoring needs a reference point.** `drift_score` compares each query's embedding against the agent's declared objective; without one it reports `-1` (unknown). Supply a client-computed `objective_embedding` when registering the agent to switch it on. > **Note:** SDK surface: the run-signal op is `client.advanced.get_run_signal({...})` in Python and `client.advanced.getRunSignal({...})` in JS; the Rust SDK does not bind it at 0.11.0 — call `POST /v2/control/run-monitor/signal` directly. `circuit_break` is typed in all three languages from SDK v0.12.0 (`client.circuit_break(reason=...)` / `client.circuitBreak({ reason })` / `CircuitBreakOptions` in Rust, which had it earlier); on 0.11.0 the Python/JS calls are top-level wire pass-throughs with the same call shape (wire field names: `run_id`, `reason`, `agent_id`; the client's ambient run ID is auto-injected when `run_id` is omitted), so the code below runs unchanged on either version. `objective_embedding` is a wire-level field on the agent-register request that none of the typed `register_agent` helpers forward — in JS use the raw passthrough `client.advanced.registerAgent({ run_id, agent_id, objective_embedding: [...] })`; in Python use `client.advanced.register_agent({...})` from SDK v0.12.0 (earlier Python versions and Rust: send `POST /v2/control/agents/register` directly). ## Walkthrough Ari is stuck on a ticket, re-issuing the same failing tool call. The Nimbus orchestrator polls the run signal between steps, breaks the loop, and seeds a fresh attempt from durable memory: #### Python ```python run_id = "nimbus:ari:ticket-2214" # Supervisor loop: poll between agent steps. resp = client.advanced.get_run_signal({"run_id": run_id, "agent_id": "ari"}) if not resp.get("monitor_disabled"): signal = resp.get("signal") or {} if signal.get("is_looping") and signal.get("confidence", 0.0) >= 0.7: print(signal["rationale"]) # "same export-status call repeated 6x with identical args" # Break the loop: snapshot → clear → reset, in one call. result = client.circuit_break(run_id=run_id, reason="repeated_tool_call") snapshot_id = result["snapshot_id"] # Seed a fresh attempt. The snapshot lesson is durable and run-scoped, # so the rebuilt briefing already includes what the broken attempt knew. briefing = client.get_context( session_id=run_id, query="Resume ticket 2214: export job stuck in 'queued', retries exhausted", max_token_budget=1500, ) restart_agent(system_extra=briefing["context_block"]) ``` #### Node / TS ```js const runId = "nimbus:ari:ticket-2214"; const resp = await client.advanced.getRunSignal({ run_id: runId, agent_id: "ari" }); const signal = resp.monitor_disabled ? null : resp.signal; if (signal?.is_looping && signal.confidence >= 0.7) { const result = await client.circuitBreak({ run_id: runId, reason: "repeated_tool_call" }); const briefing = await client.getContext({ session_id: runId, query: "Resume ticket 2214: export job stuck in 'queued', retries exhausted", max_token_budget: 1500, }); await restartAgent(briefing.context_block, result.snapshot_id); } ``` #### Rust ```rust use mubit_sdk::{CircuitBreakOptions, GetContextOptions}; // Run-signal polling is wire-only in Rust at 0.11.0 (POST /v2/control/run-monitor/signal). // Once your supervisor decides to act: let result = client .circuit_break(CircuitBreakOptions::new().with_reason("repeated_tool_call")) .await?; let snapshot_id = result["snapshot_id"].as_str().unwrap_or_default(); let mut ctx = GetContextOptions::default(); ctx.run_id = Some("nimbus:ari:ticket-2214".into()); ctx.query = Some("Resume ticket 2214: export job stuck in 'queued', retries exhausted".into()); ctx.max_token_budget = Some(1500); let briefing = client.get_context(ctx).await?; ``` The fresh attempt starts with clean working memory but a briefed context: long-term memory — facts, lessons, and now the tagged snapshot of the broken attempt — survives the break by design. If the reset itself turns out to be wrong, the `snapshot_id` points at the pre-break state. To enable drift detection for Ari, register the agent with an objective embedding (computed with your own embedding model): ```js // JS raw passthrough — the typed register helpers don't forward this field. await client.advanced.registerAgent({ run_id: runId, agent_id: "ari", role: "support", objective_embedding: await embed("Resolve Nimbus support tickets end to end"), }); ``` > **Warning:** Wire the signals into decisions, not reflexes. `is_looping` from a low-confidence `rule` source on a single poll is a prompt to look, not to reset — an agent legitimately polling a slow job can look "repeated". Gate on `confidence`, require the flag to persist across polls, and record the outcome of every break ([the outcome attribution loop](/patterns/outcome-attribution-loop)) so your thresholds themselves improve. ## When not to use it | Situation | Do instead | | --- | --- | | Transient errors (timeouts, 429s, flaky dependency) | Plain retries with backoff — nothing is wrong with the agent's state | | The failure has a recurring, learnable shape | [Guardrails from failures](/patterns/guardrails-from-failures) — prevent it, don't just interrupt it | | You want a deliberate pause point, not a reset | `checkpoint()` — snapshot without clearing ([the bridge](/concepts/working-memory-vs-long-term#checkpoints-the-bridge)) | | `monitor_disabled: true` and you need loop detection | Read the per-recall `signals` block, or enable the run monitor on the instance (below) | ## Related - [Working memory vs long-term memory](/concepts/working-memory-vs-long-term) — what circuit break clears, and what survives it - [Guardrails from failures](/patterns/guardrails-from-failures) — turning the diagnosed pathology into prevention - [The outcome attribution loop](/patterns/outcome-attribution-loop) — crediting whether a break helped - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — the run boundary the snapshot lives in - [SDK helpers](/sdk/sdk-helpers) — when to drop to `client.advanced.*` raw methods > **Note:** **Configuration.** The run monitor is instance configuration: `MUBIT_CONTROL_RUN_MONITOR_ENABLED` turns the router on, and `MUBIT_CONTROL_RUN_MONITOR_MODE` selects `rule` (cheap heuristics, the default), `llm` (semantic loop/drift detection), or `hybrid`. The per-recall `signals` block works independently of the router. --- # Memory-backed handoff > Transfer work between agents as a durable, structured memory entry — with a verdict loop — instead of a raw transcript dump. Transfer work between agents as a durable, structured memory entry — with a verdict loop — instead of a raw transcript dump. A memory-backed handoff moves a task from one agent to another by writing a `handoff` entry into shared memory: who it's from, who it's for, what's being asked, and the decisions made so far. The receiver answers with a `feedback` entry carrying a verdict. Use this when one agent's output needs another agent's review, approval, or continuation — and when "paste the whole conversation into the next agent's prompt" would either blow the context budget or silently drop the one constraint that mattered. ## How it works `handoff()` writes a `handoff` entry into the run, addressed to a specific agent. The receiver responds with `feedback()`, which writes a `feedback` entry referencing the handoff. Both are ordinary memory: they survive the turn, appear in [assembled context](/concepts/memory-model#context-priority) (the `handoffs` and `feedback` sections), and feed reflection later. ``` Ari shared run Rex │ handoff(to=rex, ┌──────────────┐ │ │ requested_action=approve) →│ handoff entry│→ recall(...) ──┤ │ │ trace, trace │→ (full history) │ │ ← await/poll verdict │feedback entry│← feedback(block)│ ``` Two enums shape the exchange (both from the control-plane contract): | Field | Values | Meaning | | --- | --- | --- | | `requested_action` | `review`, `continue`, `approve`, `execute` | What the sender is asking the receiver to do | | `verdict` | `approve`, `request_changes`, `block`, `acknowledge` | The receiver's answer, written by `feedback()` | The design point that makes this better than a transcript dump: **put decisions, constraints, and open questions in the handoff content** — the distilled state the receiver needs, not the raw history. But because the handoff lives in the shared run, the receiver isn't limited to your summary. When a summary might have dropped an implicit decision, the receiver can query the run's full `trace` history directly and check the evidence itself. You get compaction without loss. ## Walkthrough At Nimbus, Ari (the support agent) wants to refund Acme Corp $1,200 for the CSV-export incident — above the $500 rule that requires reviewer approval. Ari hands the action to Rex (the reviewer agent) with `requested_action="approve"`; Rex reads the traces, finds the refund double-counts a credit already applied, and blocks. #### Python ```python title="refund_handoff.py" run_id = "nimbus:ticket-1088" # Ari: decisions, constraints, and open questions — not the transcript. result = client.handoff( session_id=run_id, # run_id on the wire task_id="ticket-1088-refund", from_agent_id="ari", to_agent_id="rex", requested_action="approve", content=( "Refund $1,200 to Acme Corp for the CSV-export incident. " "Decisions: full refund, not partial credit. " "Constraints: over the $500 no-review limit; Acme is on the Scale plan. " "Open question: does the May 3 plan-change credit overlap this?" ), metadata={"amount_usd": 1200, "customer": "acme-corp"}, ) handoff_id = result["handoff_id"] # Rex: discover pending handoffs — a query with entry_types=["handoff"]. pending = client.recall( session_id=run_id, query="pending handoffs for rex", entry_types=["handoff"], evidence_only=True, limit=20, ) # Rex: the summary raised an open question — check the full trace history # instead of trusting it. The traces are in the same run. traces = client.recall( session_id=run_id, query="credits or refunds already applied to Acme's account", entry_types=["trace"], evidence_only=True, ) # → shows Ari applied a $400 goodwill credit on the same incident. # Rex: the verdict itself becomes memory. client.feedback( session_id=run_id, handoff_id=handoff_id, from_agent_id="rex", verdict="block", comments="A $400 goodwill credit was already applied for this incident " "(see run traces). Re-submit for $800 or void the credit first.", ) ``` #### Node / TS ```ts title="refund_handoff.ts" const runId = "nimbus:ticket-1088"; const result = await client.handoff({ session_id: runId, task_id: "ticket-1088-refund", from_agent_id: "ari", to_agent_id: "rex", requested_action: "approve", content: "Refund $1,200 to Acme Corp for the CSV-export incident. " + "Decisions: full refund, not partial credit. " + "Constraints: over the $500 no-review limit. " + "Open question: does the May 3 plan-change credit overlap this?", metadata: { amount_usd: 1200, customer: "acme-corp" }, }); // Rex: discover pending handoffs (recall with entry_types: ["handoff"], // as in the Python tab), then check the full trace history before ruling. const traces = await client.recall({ session_id: runId, query: "credits or refunds already applied to Acme's account", entry_types: ["trace"], evidence_only: true, }); await client.feedback({ session_id: runId, handoff_id: result.handoff_id, from_agent_id: "rex", verdict: "block", comments: "A $400 goodwill credit was already applied for this incident.", }); ``` #### Rust ```rust title="refund_handoff.rs" use mubit_sdk::{FeedbackOptions, HandoffOptions, RecallOptions}; let run_id = "nimbus:ticket-1088"; let mut handoff = HandoffOptions::new( "ticket-1088-refund", "ari", "rex", "Refund $1,200 to Acme Corp for the CSV-export incident. \ Decisions: full refund, not partial credit. \ Constraints: over the $500 no-review limit.", ); handoff.run_id = Some(run_id.to_string()); handoff.requested_action = "approve".to_string(); let result = client.handoff(handoff).await?; let handoff_id = result["handoff_id"].as_str().unwrap_or_default().to_string(); // Rex: check the full trace history before ruling. let mut traces = RecallOptions::new("credits or refunds already applied to Acme"); traces.run_id = Some(run_id.to_string()); traces.entry_types = vec!["trace".to_string()]; traces.evidence_only = Some(true); let evidence = client.recall(traces).await?; let mut verdict = FeedbackOptions::new(handoff_id, "block"); verdict.run_id = Some(run_id.to_string()); verdict.from_agent_id = Some("rex".to_string()); verdict.comments = "A $400 goodwill credit was already applied for this incident.".to_string(); client.feedback(verdict).await?; ``` Rex's `block` is now a durable `feedback` entry: it surfaces in Ari's assembled context, and reflection can later distill it into a lesson ("check for existing credits before computing refund amounts"). The review didn't just gate one action — it became memory. > **Note:** Two conveniences on top of this loop, available in all three SDKs from **v0.12.0**: `receive_handoffs(agent_id=...)` / `receiveHandoffs` is a packaged receiver inbox (the same `entry_types=["handoff"]` query, filtered to handoffs addressed to that agent), and `handoff(..., await_verdict=True)` makes the sender block and poll until a verdict lands (returns the verdict string, or `None` on timeout, default 30s). On earlier SDK versions, use the explicit query shown in the walkthrough — it's the same wire calls. Ari and Rex here share one run. If the reviewer works in its own run instead, link the sender's run and pass `include_linked_runs=True` on the trace query — see [Runs, sessions, and scopes](/concepts/runs-and-scopes#run-linking). ## When not to use it | Situation | Prefer instead | | --- | --- | | One agent calling a subroutine and consuming the result immediately, no review semantics | A plain function/tool call — memory adds nothing | | Two agents interleaving tightly on the same task, every step | One shared run with [lanes](/recipes/lane-scoped-memory) — handoffs per step is ceremony | | Fanning a task out to isolated workers | [Subagent isolation](/patterns/subagent-isolation) — separate runs, linked back | ## Related - [Multi-agent shared state](/recipes/multi-agent-shared-state) — the full recipe: registration, scopes, handoff, feedback, shared context - [The memory model](/concepts/memory-model) — where `handoff` and `feedback` entries sit in the taxonomy and context priority - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — run linking, for cross-run review - [Shared team memory](/patterns/shared-team-memory) — fleet-wide knowledge sharing beyond one handoff - [Control HTTP reference](/api-reference/control-http) — the wire contract for `/v2/control/handoff` and `/v2/control/feedback` --- # Shared team memory > Let a fleet of agents share validated knowledge safely — scoped registration, lanes for isolation, global lessons, and promotion-gated org sharing. Let a fleet of agents share validated knowledge safely — scoped registration, lanes for isolation, global lessons, and promotion-gated org sharing. Shared team memory is the pattern for a fleet where what one agent learns should benefit the others — without every agent seeing every agent's scratch work, and without one agent being able to poison the whole fleet. Use it when you run multiple long-lived agents on one instance (a support agent, a reviewer, an analytics agent) and you keep re-teaching each of them the same things. ## How it works Three mechanisms compose, each answering a different question: | Mechanism | Question it answers | Set where | | --- | --- | --- | | `read_scopes` / `write_scopes` | Which *entry types* can this agent touch? | `register_agent()` | | Lanes (`lane=` on write, `lane` filter on query) | Which *channel* inside a shared run does this belong to? | Per write / per query | | Lesson scope (`lesson_scope=` / `share=`) | How *widely visible* is this knowledge? | Per write, or earned via promotion | Registration declares each agent's contract up front: the entry types it may read and write, and the lanes it participates in (`shared_memory_lanes`). Lanes keep day-to-day work partitioned inside a shared run. Lesson scope is the sharing dial: a lesson written with `lesson_scope="global"` is visible to every run on the instance — the standard way to publish team knowledge. **Org scope is different: it cannot be written, only earned.** A lesson reaches org visibility exclusively through promotion — it must recur, survive a shadow-A/B audition (injected for some runs, withheld from a control arm, promoted only if the exposed arm does no worse), and carry outcome provenance from at least two distinct actors. The full ladder is in [Runs, sessions, and scopes](/concepts/runs-and-scopes#the-scope-ladder). ## Walkthrough Nimbus registers its fleet on one instance: Ari (support) and Sana (analytics) with deliberately different contracts. ```python title="team_memory.py" # Registration: each agent's contract. Sana can read lessons and facts but # cannot write them — an analytics agent should consume knowledge, not mint it. client.register_agent( session_id="nimbus:fleet", agent_id="ari", role="support", read_scopes=["fact", "lesson", "rule", "trace", "handoff", "feedback"], write_scopes=["fact", "trace", "observation", "handoff", "feedback"], shared_memory_lanes=["support", "shared"], ) client.register_agent( session_id="nimbus:fleet", agent_id="sana", role="analytics", read_scopes=["fact", "lesson"], write_scopes=["observation"], shared_memory_lanes=["analytics", "shared"], ) # Day-to-day: lane-scoped writes keep scratch work out of each other's context. client.remember( session_id="nimbus:fleet", agent_id="ari", content="Ticket queue spiking on CSV-export errors again", intent="observation", lane="support", ) # Sharing: Ari's CSV-export lesson has held up across tickets — publish it # instance-wide so every run (including Sana's) sees it. client.remember( session_id="nimbus:ari:ticket-1102", agent_id="ari", content="When an export times out, check the report row count before " "retrying — over 500k rows needs the batched exporter.", intent="lesson", lesson_scope="global", # or the alias share="global" ) # Consumption: Sana, in a completely different run, gets it automatically. digest_context = client.recall( session_id="nimbus:sana:weekly-digest", agent_id="sana", query="known causes of export-related ticket spikes", entry_types=["lesson", "fact"], ) # → the global CSV-export lesson surfaces, with its scope confidence bonus. ``` > **Note:** Every call above exists in all three SDKs: TypeScript `registerAgent` / `remember` / `recall` (same snake_case option fields), Rust `RegisterAgentOptions` / `RememberOptions` / `RecallOptions` — see the [helper catalog](/sdk/sdk-methods#current-helper-catalog). > **Note:** `lane` on `recall()` maps to the wire field `lane_filter`. Context assembly accepts `lane_filter` too, but the typed `get_context()` helper does not forward it — send it via the raw passthrough (`client.advanced.context({...})`) if you need lane-filtered context blocks. Full details in the [lane-scoped memory recipe](/recipes/lane-scoped-memory). ### What promotion quarantine looks like Suppose Ari also writes a plausible-sounding lesson — "skip reproduction steps for Scale-plan customers, they're usually right" — and keeps reinforcing it with its own successful-looking outcomes. It recurs, so it becomes a promotion candidate toward org scope. It then stalls in quarantine: every outcome in its provenance comes from one actor (Ari), and the distinct-actors gate requires at least two. Until Rex or another principal independently validates it, the lesson stays at its written scope. If it does enter a shadow audition and the exposed arm performs worse, the candidate is rejected outright — the control stream emits `context.lesson_promotion_candidate` and `context.lesson_promotion_rejected` events you can watch (see [Event-driven agents](/patterns/event-driven-agents)). ## Why one agent can't widen scope unilaterally Scope is injection reach. A global or org lesson gets injected into contexts of agents that never saw the evidence behind it — they will act on it with borrowed trust. If any single agent could write directly to org scope, one compromised, misconfigured, or merely overconfident agent could steer the entire fleet with a single bad write. That's why the write path is capped: `share="org"` is rejected by the SDK with an explanatory error (the `share=` alias ships in SDK v0.12.0; `lesson_scope=` works on all versions), and the only route to org visibility runs through recurrence, shadow validation, and multi-actor provenance. Treat direct `lesson_scope="global"` writes as a deliberate publishing act by an agent you trust for it — and use `write_scopes` at registration to deny lesson-writing entirely to agents that shouldn't publish (as Nimbus does for Sana). ## When not to use it | Situation | Prefer instead | | --- | --- | | Two agents collaborating on one task, in the moment | A shared run with [handoffs](/patterns/memory-backed-handoff) — team memory is for durable knowledge, not task state | | Isolated workers whose findings merge once | [Subagent isolation](/patterns/subagent-isolation) — per-run isolation with linked synthesis | | Per-customer knowledge | `user_id` partitioning ([cross-run recall](/patterns/cross-run-recall)) — customer facts aren't team lessons | ## Related - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — the scope ladder and promotion gates in full - [The learning loop](/concepts/learning-loop) — how lessons are extracted, validated, and promoted - [Trust and confidence](/concepts/trust-and-confidence) — how scope feeds `knowledge_confidence` at retrieval - [Lane-scoped memory](/recipes/lane-scoped-memory) — the lanes walkthrough, including failure modes - [Multi-agent shared state](/recipes/multi-agent-shared-state) — registration + handoff + feedback end to end --- # Subagent isolation > Orchestrator-worker with isolated contexts but shared durable memory — per-subagent runs, linked back for synthesis, nothing lost to summarization. Orchestrator-worker with isolated contexts but shared durable memory — per-subagent runs, linked back for synthesis, nothing lost to summarization. In this pattern an orchestrator fans work out to subagents that each get their **own `run_id`**, so no subagent's evidence pollutes another's context. Each subagent writes its findings to memory as it works and returns only a distilled summary in-band; the orchestrator links the runs and queries across them to synthesize. Use it when subtasks are independent enough to parallelize and each would generate more evidence than you want in any single context window. ## How it works ``` orchestrator run: nimbus:ari:ticket-1130 │ link_run ×3 ├────────────► sub run A (logs digger) writes observations ├────────────► sub run B (reproducer) writes observations └────────────► sub run C (incident history) writes facts ▲ each returns a short summary in-band recall(include_linked_runs=True) reads across all four runs ``` The division of labor: **isolation by run, durability by memory, synthesis by linking.** - Each subagent works in its own run — a hard boundary, per [Runs, sessions, and scopes](/concepts/runs-and-scopes#runs-the-unit-of-isolation) — and writes findings as `observation` and `fact` entries scoped to that run as it goes. - Only a distilled summary travels back in-band. But unlike plain summarization, nothing is lost: the full evidence sits in the subagent's run, and the orchestrator — or a human debugging the job next week — can recall any subagent's complete findings on demand. - The orchestrator links each subagent run to its own and queries with `include_linked_runs=True`. The response's `consulted_runs` field lists exactly which runs contributed evidence, so you can verify all three subagents were actually heard from. ## Walkthrough At Nimbus, Ari hits a hard ticket — Acme's exports fail only on Mondays — and spawns three research subagents: one digs logs, one attempts reproduction, one checks incident history. ```python title="subagent_research.py" orchestrator_run = "nimbus:ari:ticket-1130" sub_runs = { "logs": "nimbus:ari:ticket-1130:sub-logs", "repro": "nimbus:ari:ticket-1130:sub-repro", "history": "nimbus:ari:ticket-1130:sub-history", } # Each subagent works in its own run and writes findings as it goes. # (Inside the logs subagent:) client.remember( session_id=sub_runs["logs"], agent_id="ari-sub-logs", content="Export worker OOMs correlate with the Monday 06:00 UTC " "analytics batch job — memory headroom drops to ~5%.", intent="observation", ) # ... dozens more observations; only a 3-line summary returns in-band. # Orchestrator: link each subagent run back. # link_run is a raw op — call it via the advanced passthrough # (client.advanced.link_run in Python, client.advanced.linkRun in JS, # client.advanced().link_run(json!({...})) in Rust); there is no typed helper. for sub_run in sub_runs.values(): client.advanced.link_run( run_id=orchestrator_run, linked_run_id=sub_run, ) # Synthesis: one query across the orchestrator run + all linked sub runs. synthesis = client.recall( session_id=orchestrator_run, query="Why do Acme's exports fail on Mondays? Combine log evidence, " "reproduction results, and incident history.", include_linked_runs=True, limit=10, ) # Which runs actually contributed evidence? print(synthesis["consulted_runs"]) # → ["nimbus:ari:ticket-1130", "...:sub-logs", "...:sub-repro", "...:sub-history"] ``` A subagent whose run never appears in `consulted_runs` either found nothing relevant or never wrote its findings down — both worth knowing before you trust the synthesis. `reflect(session_id=orchestrator_run, include_linked_runs=True)` works the same way, extracting lessons from the combined history of all four runs. The payoff shows up later: two weeks on, when someone asks "what exactly did the reproduction attempt rule out?", the answer isn't gone with the summary — it's a `recall()` against `nimbus:ari:ticket-1130:sub-repro`. > **Note:** The same flow works in TypeScript (`client.advanced.linkRun({...})`, `recall({ include_linked_runs: true })`) and Rust (`client.advanced().link_run(json!({...}))`, `RecallOptions.include_linked_runs = true`) — see the [helper catalog](/sdk/sdk-methods#current-helper-catalog). > **Note:** `handoff()` and this pattern compose: if a subagent's conclusion needs review before the orchestrator acts on it, have the subagent write a [memory-backed handoff](/patterns/memory-backed-handoff) instead of just a summary. ## When not to split Isolation has a price: subagents can't see each other's reasoning mid-flight. Decisions that are **tightly coupled** — where subtask B's approach should change based on what subtask A just found — lose coherence when split across contexts; each subagent optimizes locally and the orchestrator inherits the contradictions. For that shape of work, keep **one run** and give each concern a [lane](/recipes/lane-scoped-memory) instead: everyone shares the run's memory, lanes keep the channels tidy, and no evidence has to cross a run boundary to be seen. | Signal | Split into sub-runs | One run + lanes | | --- | --- | --- | | Subtasks independent, parallelizable | Yes | — | | Each subtask generates heavy evidence | Yes | — | | Steps must react to each other's findings | — | Yes | | One coherent decision at the end from interleaved work | — | Yes | ## Related - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — run isolation and linking semantics - [Cross-run recall](/patterns/cross-run-recall) — the same linking machinery for continuity over time - [Lane-scoped memory](/recipes/lane-scoped-memory) — the single-run alternative for coupled work - [Retrieval semantics](/concepts/retrieval-semantics) — how evidence from multiple runs is ranked - [Run management](/sdk/sdk-methods#run-management) — `link_run` / `unlink_run` reference --- # Cross-run recall > Memory that survives and connects runs — automatic global knowledge, explicit run linking, per-user partitioning, and instant run snapshots. Memory that survives and connects runs — automatic global knowledge, explicit run linking, per-user partitioning, and instant run snapshots. Runs isolate by design, but most real work has history: the same customer returns, the same class of bug resurfaces, a new task continues an old one. Cross-run recall is the set of mechanisms by which a fresh run sees what earlier runs learned — some automatic, some explicit. Use this page when you're deciding *which* mechanism carries which kind of continuity, instead of stuffing old transcripts into new prompts. ## How it works Four mechanisms, from implicit to explicit: | Mechanism | What crosses runs | You do | | --- | --- | --- | | Scope ladder | Validated knowledge (lessons at session/global/org scope) | Nothing — surfaces automatically in any run's recall | | `user_id` partitioning | Everything written for one end-user | Pass `user_id` on writes and queries | | Run linking | One specific run's full memory | `client.advanced.link_run` + `include_linked_runs=True` | | Run snapshot | A distilled summary of one run | `client.advanced.context_snapshot` | The scope ladder is the ambient channel: knowledge that has earned session, global, or org scope — chiefly lessons, via direct `lesson_scope=` writes or [promotion](/concepts/runs-and-scopes#the-scope-ladder) — appears in every eligible run's retrieval without any linking. The other three are targeted: per-person, per-run, per-summary. ## Walkthrough Two weeks after the CSV-export incident, Acme Corp opens a new ticket. Ari starts a fresh run — and stitches the history back together with all four mechanisms: ```python title="returning_customer.py" new_run = "nimbus:ari:ticket-1201" old_run = "nimbus:ari:ticket-1042" # the original CSV-export ticket # 1. Automatic: the global-scope lesson from the old incident just shows up. context = client.recall( session_id=new_run, query="Acme export failing again — anything we already know?", entry_types=["lesson", "fact"], ) # → "When an export times out, check the report row count before retrying…" # surfaces even though this run has never seen it. No linking involved. # 2. Per-user: facts written with user_id follow the customer, not the run. client.remember( session_id=new_run, agent_id="ari", content="Acme Corp is on the Scale plan (upgraded May 3)", intent="fact", user_id="acme-corp", ) # Any future run passing user_id="acme-corp" recalls this — see # cross-session recall in the SDK helpers. # 3. Explicit continuity: link the original ticket run and read its evidence. # link_run is a raw op — call it via the advanced passthrough # (client.advanced.link_run in Python, client.advanced.linkRun in JS, # client.advanced().link_run(json!({...})) in Rust); there is no typed helper. client.advanced.link_run(run_id=new_run, linked_run_id=old_run) history = client.recall( session_id=new_run, query="What was the root cause and fix for the previous export failure?", include_linked_runs=True, ) # 4. Instant orientation: "what happened in that run", without replaying it. # context_snapshot is also a raw op (POST /v2/control/context/snapshot). snap = client.advanced.context_snapshot(run_id=old_run) print(snap["snapshot"]["summary"]) # LLM-generated run summary print(snap["snapshot"]["facts"]) # key facts established in the run print(snap["snapshot"]["uncertainties"]) # what was never resolved print(snap["snapshot"]["blockers"]) # what stopped progress print(snap["snapshot"]["next_actions"]) # where the run left off ``` The snapshot response also carries the run's `agents`, event `timeline`, and any `promotions` that originated there. Pass `refresh=True` to force regeneration instead of a cached snapshot. For a debugging human, `context_snapshot` on a two-week-old run answers "where did this leave off?" in one call. > **Note:** All four mechanisms exist in every SDK: TypeScript uses `client.advanced.linkRun({...})` and `client.advanced.contextSnapshot({...})`; Rust uses `client.advanced().link_run(json!({...}))` and `client.advanced().context_snapshot(json!({...}))`. The typed helpers (`recall`, `remember`) take the same option fields shown here. ## Pinning retrieval to the current run Cross-run recall is the default posture — durable learning is the point. But sometimes you need the opposite: deterministic, self-contained retrieval that reflects only what *this* run established. Evaluation harnesses, reproducible tests, and strict-isolation tasks all qualify. Pass `prefer_current_run=True` on `recall()` to pin retrieval to the active run and skip the cross-run lesson overlay: ```python result = client.recall( session_id=new_run, query="current diagnosis for this ticket", prefer_current_run=True, # this run's evidence only; no cross-run lessons ) ``` Leave it off in production agents — withholding validated lessons from a live task is usually a bug, not a feature. ## When not to use it | Situation | Prefer instead | | --- | --- | | The "prior work" is in-flight, same task, multiple agents | One shared run — [lanes](/recipes/lane-scoped-memory) and [handoffs](/patterns/memory-backed-handoff), no cross-run machinery needed | | You need byte-exact artifacts from an old run (a diff, generated SQL) | `archive()` / `dereference()` by `reference_id` — semantic recall doesn't guarantee exactness ([memory model](/concepts/memory-model#verbatim-vs-semantic-entries)) | | Fan-out research whose runs you'll link back the same day | [Subagent isolation](/patterns/subagent-isolation) — same linking, different lifecycle | ## Related - [Runs, sessions, and scopes](/concepts/runs-and-scopes) — the scope ladder, run linking, and `user_id` partitioning in depth - [Subagent isolation](/patterns/subagent-isolation) — run linking for orchestrator-worker fan-out - [Cross-session recall](/sdk/sdk-helpers#cross-session-recall) — the `user_id` helper idiom - [Temporal memory patterns](/recipes/temporal-memory-patterns) — time-windowed queries over long-lived memory - [Control HTTP reference](/api-reference/control-http) — wire contracts for run link and snapshot --- # Event-driven agents > Agents that wake on memory changes instead of polling — the control event stream, what it emits, and when to reach for it. Agents that wake on memory changes instead of polling — the control event stream, what it emits, and when to reach for it. An event-driven agent subscribes to a run's control event stream and acts when memory changes: a handoff lands, reflection finishes, a lesson promotes. Use this when an agent's job is to *react* — polling loops burn tokens and latency re-reading memory that hasn't changed, and the interesting moments (a verdict request, a new lesson) are exactly the ones the stream announces. > **Tip:** This page frames when to use the pattern. For the full walkthrough — wiring a consumer, filtering, rebuilding context after a wakeup — go to the [event-driven agents recipe](/recipes/event-driven-agents). ## How it works The control plane emits an event for every significant memory change in a run. Subscribe over SSE (`GET /v2/control/events/subscribe`) or gRPC (`Subscribe`); each SDK exposes the stream directly: | SDK | Call | Returns | | --- | --- | --- | | Python | `client.advanced.subscribe({"run_id": run_id})` | iterator of events | | JS/TS | `client.advanced.subscribe({ run_id: runId })` | async-iterable stream | | Rust | `client.subscribe(payload).await?` | `ValueStream` | Events carry a `type`, the `run_id`, the acting `agent_id`, and a JSON payload. The types fall into a few families (all namespaced `context.*`): | Family | Example types | | --- | --- | | Memory writes | `context.ingest_completed`, `context.working_memory_updated`, `context.checkpoint_created` | | Learning | `context.reflection_completed`, `context.lesson_promoted`, `context.lesson_validation_passed` / `_failed` | | Coordination | `context.handoff_created`, `context.feedback_received`, `context.agent_registered` | | Outcomes | `context.outcome_recorded`, `context.step_outcome_recorded` | A reactive reviewer is a few lines — Rex sleeps until Ari asks for a verdict: ```python title="rex_wakes_on_handoff.py" for event in client.advanced.subscribe({"run_id": "nimbus:ticket-1088"}): if event.get("type") == "context.handoff_created": review_handoff(event) # recall traces, then feedback() a verdict ``` ## When to use it - **Reactive review agents.** Rex shouldn't poll every run for pending handoffs — `context.handoff_created` wakes him exactly when a [memory-backed handoff](/patterns/memory-backed-handoff) needs a verdict, and `context.feedback_received` wakes the sender. - **Cache invalidation.** If you cache assembled context or derived state, `context.reflection_completed` and `context.working_memory_updated` tell you precisely when the cache went stale — rebuild with `get_context()` on wakeup rather than on a timer. - **Monitoring and audit.** Sana can watch `context.lesson_promoted` and `context.lesson_validation_failed` across the fleet to track what's entering [shared team memory](/patterns/shared-team-memory) — and flag a quarantined promotion before anyone asks. ## When not to use it Don't reach for the stream when a poll would do: a batch job that runs hourly and reads memory once doesn't need a persistent connection, and `handoff(..., await_verdict=True)` already packages the common "block until the verdict arrives" case without any subscription. The stream is per-run — a consumer watching many runs needs a subscription per run, which is a real cost at fleet scale. > **Note:** The core data plane has its own pub-sub with **semantic subscriptions** — fire when a written node crosses a similarity threshold to a query embedding. It's a separate, lower-level surface, available only where the instance operator has enabled it; see [Pub-sub & semantic subscriptions](/sdk/pubsub-and-events) if you have access. ## Related - [Event-driven agents recipe](/recipes/event-driven-agents) — the full implementation walkthrough - [Pub-sub & semantic subscriptions](/sdk/pubsub-and-events) — both event surfaces, decision model, and lifecycle - [Memory-backed handoff](/patterns/memory-backed-handoff) — the coordination artifacts these events announce - [The learning loop](/concepts/learning-loop) — what reflection and promotion events mean - [Control gRPC reference](/api-reference/control-grpc) — the `Subscribe` streaming contract --- # Write-time reconciliation > New writes reconcile against what the store already knows — repetition strengthens the canonical entry instead of appending a duplicate. New writes reconcile against what the store already knows — repetition strengthens the canonical entry instead of appending a duplicate. An append-only memory degrades in a predictable way: every reflection cycle re-states the same stable lesson, every conversation re-states the same fact, and after a month the store holds ten copies of each — bloating retrieval candidates and splitting outcome history across duplicates that should have been one strengthening entry. Write-time reconciliation checks each incoming write against similar existing entries and decides whether it is genuinely new. Use this pattern when your agents run repeatedly over the same domain — which is exactly when memory is worth having. ## How it works Three mechanisms, from fully automatic to fully explicit: **1. Lesson reconciliation (on by default).** When reflection extracts a lesson that near-duplicates an existing lesson in the same run, the write becomes a **recurrence bump** on the canonical entry — its `recurrence_count` rises — instead of a new stored copy. Candidates are proposed by semantic search and confirmed by a deterministic normalized-token gate, so paraphrase restatements ("100-row limit for batch uploads" vs "maximum batch size of 100 rows per upload") reconcile while unrelated lessons about the same system do not. Recurrence is also the signal the promotion ladder reads, so repetition feeds directly into scope promotion — see [The learning loop](/concepts/learning-loop). **2. Fact reconciliation (opt-in instance feature).** When enabled, each ingested fact is compared against similar non-stale facts in the same scope and one of four operations is chosen: | Operation | When | Effect | | --- | --- | --- | | `ADD` | The fact is unrelated to anything stored | Stored as a new entry (the common path — no LLM call) | | `NOOP` | A restatement of an existing fact | Confirmation bump on the canonical entry; no new row | | `UPDATE` | A refinement of an existing fact | The stored entry is updated in place | | `SUPERSEDE` | A changed value that contradicts the stored fact | New entry stored; the old one is marked `is_stale` with `superseded_by` (see [Bi-temporal memory](/patterns/bi-temporal-memory)) | Near-exact restatements short-circuit to `NOOP` deterministically; anything subtler goes to a cheap op-decider model. A malformed decision always degrades to `ADD` — reconciliation is an optimization, never a gate that can lose a write. **3. Explicit developer control.** When *you* know two writes are the same logical record, don't make the store guess: - `upsert_key` on `remember()` (and on raw ingest items) — an existing entry with the same key in the same session/user scope is updated in place instead of duplicated. Deterministic update-in-place for records with a natural identity ("acme:plan"). - `deduplicate: true` on a raw `batch_insert` — collapses exact duplicates (same text, source, and metadata) *within that one request*, so a retried or naively assembled batch doesn't double-write. ## Walkthrough Nimbus's flaky CSV-export bug generates near-identical tickets for weeks. Ari resolves each one and reflects; without reconciliation, every resolution would pin another copy of the same lesson. #### Python ```python import json # Each resolved ticket ends the same way: outcome + reflection. for ticket in ("TCK-1042", "TCK-1057", "TCK-1063"): session = f"support:{ticket}" # ... Ari works the ticket, recalls context, applies the fix ... client.record_outcome( session_id=session, reference_id=fix_lesson_id, outcome="success", signal=0.8, rationale=f"Batched exporter resolved {ticket}", ) client.reflect(session_id=session) # Reflection re-derives "use the batched exporter over 500k rows" every # time — but the store reconciles it into ONE lesson whose recurrence # count rises, instead of three copies. results = client.recall( session_id="support:TCK-1063", query="export timeout fix", entry_types=["lesson"], ) top = results["evidence"][0] meta = json.loads(top.get("metadata_json") or "{}") print(top["content"]) # one canonical lesson print(meta.get("recurrence_count")) # rising with each recurrence # Explicit control: Acme's plan is a keyed record, not a discovery. # Same upsert_key = update in place, never a duplicate. client.remember( session_id="support:acme", content="Acme Corp is on the Scale plan.", intent="fact", upsert_key="acme:plan", ) ``` #### Node / TS ```js // Keyed update-in-place: same upsert_key never duplicates. await client.remember({ session_id: "support:acme", content: "Acme Corp is on the Scale plan.", intent: "fact", upsert_key: "acme:plan", }); // Within-request dedup on a raw batch insert. await client.advanced.batchInsert({ run_id: "support:acme", deduplicate: true, items: rows.map((text, i) => ({ item_id: `row-${i}`, text })), }); ``` #### Rust ```rust use mubit_sdk::{RememberBuilder, SessionScope}; // The builder forwards the upsert key through the typed helper. let options = RememberBuilder::new("Acme Corp is on the Scale plan.") .session(SessionScope { run_id: Some("support:acme".into()), ..Default::default() }) .intent("fact") .upsert("acme:plan") .build(); client.remember(options).await?; ``` ## When not to use it | Situation | Reach for instead | | --- | --- | | Every restatement is itself evidence (e.g. counting how often customers report an issue) | Plain appends — reconciliation would collapse the signal you're measuring. Keep each report as its own entry and aggregate at query time | | The record has a natural identity you already know | `upsert_key` — deterministic beats similarity-gated every time | | You need the *old* value queryable after a change | You already have it: `SUPERSEDE` keeps history (see [Bi-temporal memory](/patterns/bi-temporal-memory)); nothing extra to do | | Duplicates already accumulated before reconciliation was on | [Sleep-time consolidation](/patterns/sleep-time-consolidation) merges near-duplicate guidance missed at write time | ## Related - [The learning loop](/concepts/learning-loop) — where reflection-extracted lessons come from, and how recurrence feeds promotion - [Bi-temporal memory](/patterns/bi-temporal-memory) — what `SUPERSEDE` does with the old belief - [Sleep-time consolidation](/patterns/sleep-time-consolidation) — the idle-time counterpart that catches what write-time missed - [The memory model](/concepts/memory-model) — lessons, facts, and the rest of the entry taxonomy - [Trust, confidence, and staleness](/concepts/trust-and-confidence) — how a single strengthened entry earns `knowledge_confidence` that split duplicates never could > **Note:** **Configuration.** These are instance features. Lesson reconciliation is on by default (`MUBIT_CL_WRITE_RECONCILE=0` disables it; `MUBIT_CL_RECONCILE_MIN_SIM` tunes the recurrence gate, default 0.5). Fact reconciliation is opt-in (`MUBIT_CL_FACT_RECONCILE=1`; `MUBIT_CL_FACT_RECONCILE_MIN_SIM` sets the candidate floor, default 0.75 — deliberately conservative because `UPDATE` rewrites stored content in place). `upsert_key` and `deduplicate` need no configuration. --- # Bi-temporal memory > Separate when something happened from when the agent learned it — and supersede beliefs without deleting the history they came from. Separate when something happened from when the agent learned it — and supersede beliefs without deleting the history they came from. Every entry in Mubit carries two independent times: **ingestion time** (when the store learned it) and **occurrence time** (when the event actually happened). Most memory systems conflate the two, which makes "what did we believe in April?" and "what happened in April?" the same — usually wrong — question. Use this pattern when your agents ingest events after the fact, when beliefs change over time, or when any agent needs to reconstruct a past state instead of just the current one. ## How it works **Two timestamps per entry.** Ingestion time is stamped automatically. Occurrence time you set explicitly on the write; if you don't, the runtime falls back to extracting it from metadata keys (`occurrence_time`, `event_time`, `session_date`) before finally treating ingestion time as the event time. **Time-bounded queries.** `min_timestamp` / `max_timestamp` (inclusive, Unix seconds) restrict evidence to entries whose occurrence time — falling back to ingestion time — lands inside the window. Temporal bounds activate a dedicated temporal retrieval branch, so history is a first-class lane, not a post-filter. **Supersession instead of deletion.** When a belief changes — a new fact contradicts a stored one — the old entry is not overwritten. It is marked `is_stale: true` with `superseded_by` pointing at its replacement (supersessions written by [fact reconciliation](/patterns/write-time-reconciliation) also stamp `invalid_at`, closing the old belief's validity interval): - **Current-state answers exclude it.** Synthesis works from the live belief only. - **Historical and time-bounded queries can still see it.** The past stays queryable. This is what makes the two-timestamp model useful: the store distinguishes "no longer true" from "never happened". How stale entries surface (ranking penalty, `superseded_by` chains, `knowledge_confidence`) is covered in [Trust, confidence, and staleness](/concepts/trust-and-confidence). ## Walkthrough In March, Ari stored "Acme Corp is on the Starter plan". On May 3, Acme upgrades. Ari writes the new plan fact with the upgrade's occurrence time; the Starter fact is superseded, not erased. #### Python ```python # May 3: Acme upgrades. Record WHEN it happened, not just when we heard. # From SDK v0.12.0: occurrence_time is a typed remember() kwarg — # client.remember(..., occurrence_time=1777766400) # On earlier versions, pass it as a metadata key; the server extracts it. client.remember( session_id="support:acme", content="Acme Corp upgraded to the Scale plan.", intent="fact", metadata={"occurrence_time": 1777766400}, # May 3, 2026 UTC ) # Current state: the Scale fact answers; the Starter fact comes back # is_stale with superseded_by pointing at its replacement. now = client.recall(session_id="support:acme", query="What plan is Acme on?") print(now["final_answer"]) # ... Scale plan ... # April state, for Sana's monthly digest: bound the query to occurrence # time. From SDK v0.12.0, recall() forwards min_timestamp / max_timestamp # directly; on earlier versions use the low-level query passthrough: april = client.advanced.query({ "run_id": "support:acme", "query": "What plan was Acme on?", "min_timestamp": 1775001600, # Apr 1, 2026 "max_timestamp": 1777593599, # Apr 30, 2026 }) for e in april["evidence"]: print(e["content"], "[stale]" if e.get("is_stale") else "") # The Starter fact is in-window and returned — flagged stale, but queryable. ``` #### Node / TS ```js // From SDK v0.12.0, remember() forwards occurrence_time (or occurrenceTime) // as a top-level option. On earlier versions pass it as a metadata key; // the server extracts it. await client.remember({ session_id: "support:acme", content: "Acme Corp upgraded to the Scale plan.", intent: "fact", metadata: { occurrence_time: 1777766400 }, // May 3, 2026 UTC }); // recall() forwards temporal bounds directly. const april = await client.recall({ session_id: "support:acme", query: "What plan was Acme on?", min_timestamp: 1775001600, max_timestamp: 1777593599, }); april.evidence.forEach(e => console.log(e.content, e.is_stale ? "[stale]" : "")); ``` #### Rust ```rust use mubit_sdk::{RecallOptions, RememberBuilder, SessionScope}; // The Rust builder forwards occurrence_time through the typed helper. let write = RememberBuilder::new("Acme Corp upgraded to the Scale plan.") .session(SessionScope { run_id: Some("support:acme".into()), ..Default::default() }) .intent("fact") .occurrence_time(1_777_766_400) // May 3, 2026 UTC .build(); client.remember(write).await?; let mut q = RecallOptions::new("What plan was Acme on?"); q.run_id = Some("support:acme".into()); q.min_timestamp = Some(1_775_001_600); q.max_timestamp = Some(1_777_593_599); let april = client.recall(q).await?; ``` > **Note:** From SDK v0.12.0 the typed helpers forward all three fields natively in every language — `remember(..., occurrence_time=...)` and `recall(..., min_timestamp=..., max_timestamp=...)` in Python and JS, matching what Rust's `RememberOptions`/`RecallOptions` already carried. On 0.11.0 and earlier the Python/JS helpers do not forward all of them, which is why the tabs above route through the `occurrence_time` metadata key and the `advanced` passthrough — both routes remain valid on every version (`occurrence_time` is a wire-level ingest-item field; the temporal bounds are wire-level query fields). See [SDK methods → Temporal queries](/sdk/sdk-methods#temporal-queries) for the per-language matrix. ## When not to use it - **Events ingested as they happen.** If ingestion time ≈ occurrence time (a live agent writing what it just did), skip explicit `occurrence_time` — the fallback is already correct. Reserve it for backfills, imported history, and user reports about the past. - **You want the old value gone, not superseded.** Supersession is deliberate retention. For actual removal (a compliance delete, a poisoned entry), use explicit deletion — `forget()` — not a superseding write; see [Utility-weighted forgetting](/patterns/utility-weighted-forgetting) for how deletion differs from archival. - **The "change" is really a refinement.** If the new write clarifies rather than contradicts ("Scale plan, billed annually"), write-time fact reconciliation resolves it as an `UPDATE` instead of a supersession — see [Write-time reconciliation](/patterns/write-time-reconciliation). ## Related - [Temporal memory patterns](/recipes/temporal-memory-patterns) — the recipe: occurrence-time writes, range queries, staleness handling, and budget control end to end - [Trust, confidence, and staleness](/concepts/trust-and-confidence) — reading `is_stale`, `superseded_by`, and confidence signals before acting - [Write-time reconciliation](/patterns/write-time-reconciliation) — the mechanism that decides when a new fact supersedes an old one - [Retrieval semantics](/concepts/retrieval-semantics) — how the temporal branch fuses with semantic and lexical retrieval > **Note:** **Configuration.** Bi-temporal supersession is an instance feature, on by default on hosted instances (`MUBIT_CL_BITEMPORAL=0` disables it — beliefs then coexist without staleness marking). Occurrence-time storage and time-bounded queries are always available and need no configuration. --- # Sleep-time consolidation > Memory reorganizes itself while the agent is idle — duplicates merge, scattered entity facts distill into mental models, and raw evidence is never deleted. Memory reorganizes itself while the agent is idle — duplicates merge, scattered entity facts distill into mental models, and raw evidence is never deleted. Agents write memory under time pressure: mid-task, one observation at a time, with no view of what the last hundred entries add up to. Consolidation is the counterweight — a background worker that uses idle time to turn accumulation into organization. Use this pattern (mostly: understand and observe it, since it's on by default on hosted instances) when a long-lived scope is collecting many entries about the same entities and you want distilled knowledge to emerge without an explicit pipeline. ## How it works The consolidation worker sweeps periodically and only touches scopes that have gone idle — no ingestion activity for a couple of minutes — because reorganizing memory underneath an actively writing agent would be a race. Each sweep does two kinds of work: - **Merge near-duplicate guidance.** Lessons and rules that [write-time reconciliation](/patterns/write-time-reconciliation) missed — paraphrases that arrived through different paths — are merged behind one canonical entry. Merges are deterministic and cheap, so they are not budget-capped. - **Distill entity fact clusters.** When enough entries accumulate about one entity ("Acme Corp" mentioned across dozens of facts and observations), the worker distills the cluster into a consolidated, entity-level summary grounded only in the source entries, with the source IDs kept as provenance. Recurring, high-confidence summaries are promoted into `mental_model` entries — the section injected *first* in assembled context, ahead of rules, lessons, and raw facts (see [Context priority](/concepts/memory-model#context-priority)). Distillation calls an LLM, so each sweep runs under a bounded per-sweep budget of distillation operations — consolidation is deliberately slow and steady rather than exhaustive. The same idle-time worker also hosts workflow induction (see [Procedural memory](/patterns/procedural-memory)), guardrail distillation (see [The learning loop](/concepts/learning-loop#what-the-runtime-maintains-for-you)), and, when enabled, [utility-weighted forgetting](/patterns/utility-weighted-forgetting). **What it never does: delete raw evidence.** Merged duplicates are superseded — flagged with `superseded_by` and a `consolidated_into` pointer — and distilled summaries carry `source_ids` back to the entries they came from. Consolidation adds a distilled layer on top of episodic history; it does not replace it. ## Walkthrough A week of Acme tickets leaves Ari's scope with dozens of scattered Acme facts and observations. You don't call anything — you observe what the worker does with them. ```python # Before: many raw entries, no distilled layer. before = client.memory_health(session_id="support:acme") print(before["entry_counts"]) # {'fact': 38, 'observation': 21, 'trace': 64, 'lesson': 5, ...} # ... the scope goes idle; consolidation sweeps run ... after = client.memory_health(session_id="support:acme") print(after["entry_counts"]) # Consolidated entity summaries appear; near-duplicate guidance collapses # behind canonical entries. # The payoff shows up in assembled context: the distilled Acme profile # now leads the block, before rules, lessons, and raw facts. ctx = client.get_context( session_id="support:acme", query="Acme export ticket", ) print(ctx["context_block"]) ``` ```text ## Mental Models - Acme Corp support profile: Scale plan since May 3, CSV-heavy usage, exports peak on Mondays, prior export-timeout incidents resolved via the batched exporter. ## Lessons Learned - When an export times out, check the report row count before retrying ... ``` Every agent that queries this scope now gets the one-paragraph Acme picture first, instead of re-deriving it from 59 scattered entries — and Sana's weekly digest reads the same distilled profile Ari's tickets built. Two ways to observe the worker over time: 1. **`memory_health()` before and after idle periods** — watch `entry_counts` gain distilled types while near-duplicate guidance collapses behind canonical entries. 2. **`mental_model` entries appearing in `get_context()`** — the "Mental Models" section materializing at the top of context blocks is consolidation paying rent. > **Note:** You can also write `mental_model` entries yourself (`intent="mental_model"`) when you already have a curated entity summary — see [SDK methods → Mental models](/sdk/sdk-methods#mental-models). Consolidation is the automatic path to the same entry type. ## When not to rely on it Consolidation is eventually consistent by design — it waits for idle windows and spends a bounded budget per sweep. Don't lean on it when: | Need | Reach for instead | | --- | --- | | The very next query must see a deduplicated, reconciled store | [Write-time reconciliation](/patterns/write-time-reconciliation) — reconcile synchronously at the write, don't wait for sleep | | A belief must flip the moment a contradicting fact arrives | Bi-temporal supersession at write time — see [Bi-temporal memory](/patterns/bi-temporal-memory) | | You have a curated summary ready now | Write the `mental_model` entry explicitly instead of waiting for distillation | | The scope never goes idle (continuous high-rate ingestion) | Idle-gated sweeps won't touch it; reconcile at write time and consider splitting the scope | ## Related - [The memory model](/concepts/memory-model) — `mental_model` and `observation` in the entry taxonomy, and why mental models lead context assembly - [Write-time reconciliation](/patterns/write-time-reconciliation) — the synchronous counterpart that catches duplicates before they land - [Procedural memory](/patterns/procedural-memory) — workflow induction, the other thing idle sweeps distill - [Utility-weighted forgetting](/patterns/utility-weighted-forgetting) — the opt-in archival pass that shares the same idle worker - [The learning loop](/concepts/learning-loop) — where consolidation sits among the runtime's maintenance features - [Trust, confidence, and staleness](/concepts/trust-and-confidence) — `memory_health()` fields in detail > **Note:** **Configuration.** Consolidation is an instance feature, on by default on hosted instances (`MUBIT_CL_SLEEP_CONSOLIDATE=0` disables it). Instance knobs: `MUBIT_CL_CONSOLIDATE_IDLE_SECS` (how long a scope must be quiet before it's touched, default 120), `MUBIT_CL_CONSOLIDATE_INTERVAL_SECS` (sweep cadence, default 300), and `MUBIT_CL_CONSOLIDATE_MAX_LLM_OPS` (distillation budget per sweep, default 4 — deterministic merges are uncapped). --- # Utility-weighted forgetting > Bounded memory growth without losing what matters — unused episodic evidence archives out reversibly while credited, retrieved memories stay hot. Bounded memory growth without losing what matters — unused episodic evidence archives out reversibly while credited, retrieved memories stay hot. A busy agent fleet writes episodic evidence far faster than it rereads it: traces, logs, tool outputs. Left alone, retrieval wades through an ever-growing candidate pool where the signal never got denser. Utility-weighted forgetting is the opt-in instance feature that moves aged, never-used episodic evidence to an **archive tier** — out of retrieval, still reachable by ID, and reversible. Use this pattern on long-lived, high-volume scopes where most episodes are routine. > **Warning:** **Forgetting ≠ `forget()`.** This page is about the runtime's *archival* feature. The SDK's `forget()` helper is a different, immediate operation: explicit **deletion**. `forget(lesson_id=...)` deletes one lesson; `forget(session_id=...)` deletes a whole session's memory. Deletion is permanent — archival is not. And despite the similar name, `archive_block` entries (written by `archive()`, read by `dereference()`) are exact-storage artifacts, **not** part of the archive tier — forgetting never touches them, and `dereference()` is unaffected. ## How it works The idle-time consolidation worker (the same one behind [sleep-time consolidation](/patterns/sleep-time-consolidation)) runs an archival pass per sweep. An entry moves to the archive tier only if *all* of these hold: - **Episodic evidence only.** Only `trace`, `log`, `tool_input`, `tool_output`, and `step_outcome` entries qualify. Guidance (lessons, rules, mental models), beliefs (facts, observations), handoffs, and `archive_block` artifacts are never archived — beliefs age out via [supersession](/patterns/bi-temporal-memory), guidance via [consolidation](/patterns/sleep-time-consolidation). - **Aged.** Older than the minimum window (default 7 days). Entries with no parsable timestamp are never archived — no evidence of age is not evidence of disuse. - **Never credited.** No positive outcome ever attributed to it (its success and reinforcement counters are zero) — see [The learning loop](/concepts/learning-loop#outcomes-close-the-loop). - **Not recently used.** Not retrieved and not outcome-touched within the window. **A retrieval resets the clock** — being read is being useful. Archived entries leave the retrieval candidate pool and all overlays, but remain reachable by ID (`dereference` / get-by-id), so stored references never dangle. Each sweep archives at most a bounded batch, so a first pass over a large backlog cannot flood the store. **Reversibility.** Archival writes markers; it does not destroy anything. An un-archive lever (`MUBIT_CL_UNFORGET=1`) suppresses forgetting and progressively revives previously archived entries on each sweep until the archive tier drains — a standing rollback, not a one-shot restore. ## Why utility beats TTL A TTL asks one question — *how old is it?* — and deletes the answer. Utility-weighting asks *has it ever mattered?* using signals the store already tracks: | Policy | Keeps | Loses | | --- | --- | --- | | TTL / max-age | Everything recent, useful or not | The 11-month-old incident trace you needed exactly once a year | | Utility-weighted | Anything retrieved or credited — usage resets the clock, outcomes pin it | Only evidence that aged out *and* was never read *and* never earned credit | Because [outcome attribution](/concepts/learning-loop) marks which entries contributed to results, value — not age — decides retention. And it's archival, not deletion, so a wrong call costs a revival, not data. ## Walkthrough Nimbus's support scope accrues thousands of traces from routine, resolved tickets. The incident run's evidence, though older, stays hot: its traces were credited when the failure lesson was distilled, and the lesson itself is guidance — never archivable. ```python # Watch the episodic tier shrink as archival sweeps run (opt-in feature). health = client.memory_health(session_id="support:ari", stale_threshold_days=30) print(health["entry_counts"]) # Routine ticket traces age out of retrieval; credited incident # evidence and every lesson/rule/fact stays. # Distinct operation: explicit, immediate deletion. client.forget(lesson_id=bad_lesson_id) # delete one lesson client.forget(session_id="support:scratch") # delete a session's memory # Exactly one of lesson_id / session_id — deletion is permanent, # unlike archival. # archive() artifacts are exact-storage, not the archive tier: # dereference() keeps working regardless of forgetting. exact = client.dereference(reference_id=sql_ref_id) ``` > **Note:** There is no client call to trigger or tune archival — it is instance configuration (see below), and the SDK surface it touches is only what you observe through `memory_health()` and ID-based reads. `forget()` is available in all three SDKs (JS: `forget({ lesson_id })` / `forget({ session_id })`; Rust: `ForgetOptions` with `lesson_id` or `run_id`). ## When not to use it - **Regulated or audit-bound evidence with hard retention rules.** Archival is a retrieval-tier decision, not a records policy. Manage retention explicitly rather than assuming what archives or survives. - **Low-volume scopes.** If retrieval isn't wading through noise, leave it off — the default is off for a reason. - **You actually need deletion.** Poisoned entries, user data removal: `forget()` (or `delete_run`) deletes; archival deliberately doesn't. - **Evidence that is rarely read but must stay retrievable by content.** Archived entries are ID-reachable, not searchable. If it must surface in future *queries* despite years of disuse, attach an outcome to it (credit pins it) or store it as guidance instead of a trace. ## Related - [The learning loop](/concepts/learning-loop) — outcome attribution, the credit signal that keeps memories alive - [Sleep-time consolidation](/patterns/sleep-time-consolidation) — the idle worker this pass runs in - [Bi-temporal memory](/patterns/bi-temporal-memory) — how *beliefs* age (supersession), as opposed to evidence - [The memory model](/concepts/memory-model) — the episodic entry types this feature owns, and `archive_block` verbatim storage - [Trust, confidence, and staleness](/concepts/trust-and-confidence) — `memory_health()` for watching scope hygiene > **Note:** **Configuration.** Utility-weighted forgetting is an opt-in instance feature: `MUBIT_CL_FORGET=1` enables it. Knobs: `MUBIT_CL_FORGET_MIN_AGE_SECS` (age and quiet window, default 604800 = 7 days), `MUBIT_CL_FORGET_MAX_PER_SWEEP` (archival cap per sweep, default 200 — also the per-sweep revival cap). Rollback: set `MUBIT_CL_UNFORGET=1` to suppress forgetting and progressively revive the archive tier. --- # Procedural memory > Successful behavior becomes a reusable procedure — workflows induced from credited traces, and curated skills served as provider-ready tool schemas. Successful behavior becomes a reusable procedure — workflows induced from credited traces, and curated skills served as provider-ready tool schemas. Facts tell an agent what is true; lessons tell it what to watch out for. Procedural memory answers the third question: *how do we do this here?* Use this pattern when your agents repeat multi-step tasks — you want the second-hundredth ticket to follow a proven procedure, not a fresh derivation. Mubit gives procedures two forms: **induced workflows** the runtime distills from what actually worked, and **curated skills** you define and version deliberately. ## How it works **Induced workflows (on by default).** During idle sweeps, the runtime looks at a run's episodic traces that outcome attribution *positively credited* — not everything that happened, only what was tied to success (see [The learning loop](/concepts/learning-loop#outcomes-close-the-loop)). Once enough credited traces accumulate, it distills them into a `workflow` entry: an ordered, imperative step list with run-specific values abstracted into placeholders (``, ``), plus a title and one-line description kept in metadata so context assembly can inject a compact hint without spending body-sized budget. By default a run qualifies only if at least one credited source was verified in production — unverified success is not enough to mint a procedure. Workflows are semantic entries: later tasks retrieve them by similarity to the task at hand, and they surface in the `Workflows` context section (see [Context priority](/concepts/memory-model#context-priority)). **Curated skills.** Skills are managed resources — named tool or playbook definitions attached to a project (or a specific agent), versioned with a candidate → active promotion flow. Where a workflow is *discovered*, a skill is *decided*: you write it, review its versions, and activate the one agents should use. `skill_type` picks the form: | `skill_type` | What it is | Served as | | --- | --- | --- | | `"tool"` | A callable function definition with a JSON Schema for parameters | A provider-ready tool schema | | `"playbook"` | A markdown procedure — instructions, not a callable | Instructions your agent follows | ### Rules vs workflows vs skills | Form | Written by | Retrieved | Use it for | | --- | --- | --- | --- | | `rule` | You, explicitly | Always injected — an unconditional constraint | "Never issue refunds above $500 without review" | | `workflow` | Runtime — induced from credited traces | By task similarity, in the `Workflows` context section | The CSV-export fix procedure that keeps working | | Skill | You — curated, versioned, activated | Fetched via the skills API as tool schemas / playbooks | The refund playbook every support agent must follow | ## Walkthrough Two procedures at Nimbus. The CSV-export fix is *induced*: Ari resolved enough tickets with credited, production-verified outcomes that the runtime distilled the steps. The refund playbook is *curated*: it encodes policy, so no one waits for it to be discovered. #### Python ```python import mubit # Module-level helpers read the context set up by mubit.init(). mubit.init(api_key=API_KEY, agent_id="support-ari", project_id="nimbus") # ── Induced: the CSV-export workflow appears on its own ────────────── # After several credited runs (record_outcome with success signals, # at least one verified_in_production), an idle sweep induces it. results = mubit.recall( "customer export is timing out", session_id="support:TCK-1101", entry_types=["workflow"], ) print(results["evidence"][0]["content"]) # 1. Ask for the report row count for . # 2. If over rows, switch the export to the batched exporter. # ... # ── Curated: the refund playbook is policy, so write it deliberately ─ mubit.set_skill( "refund_playbook", "How to process a customer refund, including the reviewer gate.", skill_type="playbook", instructions=( "1. Verify the charge in billing.\n" "2. Refunds over $500: hand off to Rex for approval.\n" "3. Apply the refund and confirm to the customer." ), ) # Tool-type skills come back as provider-ready schemas — pass straight # to your LLM call. format: "openai" (default), "anthropic", "gemini", "raw". tools = mubit.get_skills(format="anthropic") # anthropic_client.messages.create(..., tools=tools) ``` #### Node / TS ```js // Module-level helper layer (reads the initialized SDK context): import { getSkills, setSkill } from "@mubit-ai/sdk/helpers"; await setSkill( "refund_playbook", "How to process a customer refund, including the reviewer gate.", { skillType: "playbook", instructions: "1. Verify the charge...\n2. Over $500: hand off to Rex...", projectId: "nimbus", }, ); const tools = await getSkills({ projectId: "nimbus", format: "anthropic" }); // Or drive the raw ops on the client directly: await client.advanced.createSkill({ project_id: "nimbus", name: "issue_refund", description: "Issue a refund for a verified charge.", parameters_schema: JSON.stringify({ type: "object", properties: { charge_id: { type: "string" }, amount: { type: "number" } }, required: ["charge_id", "amount"], }), skill_type: "tool", }); ``` Version management is a raw-ops surface: `client.advanced.list_skill_versions(...)` to inspect, `client.advanced.activate_skill_version(...)` to promote a candidate, and top-level `optimize_skill(...)` to generate a candidate from recent outcomes — the full lifecycle is walked through in [Prompt optimization](/recipes/prompt-optimization) (the same loop works for skills), and the HTTP endpoints are in the [Control HTTP reference](/api-reference/control-http). > **Note:** The skill CRUD operations are low-level raw ops on `client.advanced.*` (dict/object payloads, no typed signatures) in all three SDKs. The `get_skills` / `set_skill` helper layer — with per-provider `format` conversion — exists in Python (module-level, after `mubit.init()`) and JS (`@mubit-ai/sdk/helpers`); the Rust SDK does not ship these helpers at 0.11.0 — use the raw ops and format the tool schemas yourself. ## When not to use it - **The procedure must be byte-exact** (a script, generated SQL, a config to re-execute). Workflows and playbooks are semantic text; store exact artifacts with `archive()` and fetch them back verbatim — see [Verbatim vs semantic entries](/concepts/memory-model#verbatim-vs-semantic-entries). - **One-off tasks.** Induction needs repetition with credited outcomes; a task you'll never repeat has nothing to distill. Record the outcome anyway — it still feeds lessons. - **Constraints, not procedures.** "Never do X" is a `rule` — unconditional and always injected — not a workflow that competes on task similarity. - **You aren't recording outcomes.** Induction feeds on credit. Without `record_outcome()`, no workflow will ever be induced — close the loop first ([The learning loop](/concepts/learning-loop)). ## Related - [The memory model](/concepts/memory-model) — where `workflow` and `rule` sit in the entry taxonomy and context priority - [The learning loop](/concepts/learning-loop) — outcome attribution and verification, the signals induction gates on - [Sleep-time consolidation](/patterns/sleep-time-consolidation) — the idle worker that runs workflow induction - [Write-time reconciliation](/patterns/write-time-reconciliation) — why repeated procedures strengthen instead of duplicate - [Prompt optimization](/recipes/prompt-optimization) — the version → shadow-test → activate lifecycle, for prompts and skills alike - [Control HTTP reference](/api-reference/control-http) — the `/v2/control/skills/*` endpoints > **Note:** **Configuration.** Workflow induction is an instance feature, on by default on hosted instances (`MUBIT_CL_WORKFLOW_INDUCT=0` disables it). Knobs: `MUBIT_CL_WORKFLOW_MIN_TRACES` (credited traces required, default 3), `MUBIT_CL_WORKFLOW_REQUIRE_VERIFIED` (require a production-verified source, default on), `MUBIT_CL_WORKFLOW_MAX_PER_RUN` (live workflows per run, default 4). Skills need no instance configuration. --- # Context assembly > Hand the model one budgeted, prioritized context block instead of stuffing the window — priority fill, token accounting, and progressive disclosure with get_context(). Instead of concatenating everything that might be relevant into the prompt, ask memory for **one budgeted, prioritized block** and inject that. `get_context()` retrieves, ranks, groups, and trims in a single call, so the model sees the most trustworthy knowledge first and the budget is spent deliberately. Use this pattern whenever you build a system prompt per LLM call — which for most agents is every call. ## How it works `get_context()` runs retrieval for your query, groups the evidence into named sections, and fills them in a fixed priority order: distilled, validated knowledge (mental models, rules, lessons) first, raw history last. The full order is documented in [the memory model](/concepts/memory-model#context-priority). Within a section, items sort by descending score. When `max_token_budget` is greater than 0, sections are filled in that priority order until the budget is exhausted — low-priority sections are the first to be dropped. The response accounts for every token: ```json { "context_block": "## Mental Models\n- Acme Corp support profile: ...", "token_estimate": 1421, "budget_used": 1421, "budget_remaining": 79, "evidence_candidates_considered": 23, "evidence_dropped_by_budget": 6, "exact_references_surfaced": 1, "section_summaries": [ { "section_name": "lessons", "item_count": 3, "top_item_preview": "When an export times out...", "estimated_tokens": 210 } ], "empty_reason": "" } ``` `empty_reason` tells you when the block is empty or degraded rather than making you guess: `"no_evidence"` (nothing matched), `"recency_fallback"` (no confident match, most-recent entries returned instead), or `"budget_exhausted"` (the budget was too small to fit anything). Treat a non-empty `empty_reason` as a signal, not an error — a brand-new agent legitimately has no evidence yet. ## Progressive disclosure You don't have to fetch the full block up front. `mode="summary"` returns only `section_summaries` — item counts, top-item previews, and token estimates per section — with an empty `context_block`. That's a cheap overview: the model (or your routing code) can look at what memory *has* before paying for it. Then fetch only the sections that matter with `mode="sections"`: ```python overview = client.get_context( session_id=run_id, query="Acme CSV export timeout", mode="summary", ) # section_summaries says lessons and archive_blocks are where the value is: detail = client.get_context( session_id=run_id, query="Acme CSV export timeout", mode="sections", sections=["lessons", "archive_blocks"], max_token_budget=800, ) ``` Valid section keys: `mental_models`, `active_rules`, `lessons`, `archive_blocks`, `handoffs`, `feedback`, `facts`, `observations`, `working_memory`, `traces`, `goals`, `checkpoints`, `logs`, `other`. See [Context modes](/sdk/sdk-helpers#context-modes) for the shape each mode returns. ## Walkthrough At Nimbus, Ari (the support agent) picks up a new Acme Corp ticket and assembles a 1,500-token block for the first LLM call — small enough to leave room for the ticket itself, large enough for the profile, rules, and lessons that make the reply competent: #### Python ```python context = client.get_context( session_id="nimbus:ari:ticket-1043", query="New Acme Corp ticket: CSV export times out on large reports", max_token_budget=1500, entry_types=["mental_model", "rule", "lesson", "fact"], include_working_memory=True, # default — Current State rides along format="structured", ) if context.get("empty_reason") == "no_evidence": context_block = "" # first ticket ever — proceed without memory else: context_block = context["context_block"] messages = [ {"role": "system", "content": BASE_PROMPT + "\n\n" + context_block}, {"role": "user", "content": ticket_body}, ] ``` #### Node / TS ```js const context = await client.getContext({ session_id: "nimbus:ari:ticket-1043", query: "New Acme Corp ticket: CSV export times out on large reports", max_token_budget: 1500, entry_types: ["mental_model", "rule", "lesson", "fact"], }); const contextBlock = context.empty_reason === "no_evidence" ? "" : context.context_block; const messages = [ { role: "system", content: `${BASE_PROMPT}\n\n${contextBlock}` }, { role: "user", content: ticketBody }, ]; ``` #### Rust ```rust use mubit_sdk::GetContextOptions; let context = client .get_context(GetContextOptions { run_id: Some("nimbus:ari:ticket-1043".into()), query: Some("New Acme Corp ticket: CSV export times out on large reports".into()), max_token_budget: Some(1500), entry_types: vec!["mental_model".into(), "rule".into(), "lesson".into(), "fact".into()], ..GetContextOptions::default() }) .await?; let block = context["context_block"].as_str().unwrap_or_default(); ``` > **Note:** The wire request also accepts a **lane filter** (`lane`, with `lane_filter` as an accepted alias) that restricts assembly to entries tagged with one lane — useful in multi-agent setups. Rust exposes it as `GetContextOptions.lane`; the Python and JS `get_context` helpers do not forward it, so use the raw passthrough — `client.advanced.context({..., "lane": "billing"})` in both — when you need it. See [Lane-scoped memory](/recipes/lane-scoped-memory). This is exactly where the pattern plugs in: the assembled block goes into the system message, ahead of the task input. It is also what the learn module automates — `mubit.learn.init(..., max_token_budget=2048)` runs a budgeted `get_context()` before every LLM call and injects the result, with `entry_types` and `context_sections` knobs for the same shaping shown above (Python; module surfaces vary by language — see [the learn module](/sdk/sdk-methods#learn-module-closed-loop)). ## When not to use it For a single-fact lookup — "what plan is Acme on?" — plain [`recall()`](/sdk/retrieve-data) is cheaper: no block assembly, no section formatting, and `evidence_only=True` skips answer synthesis entirely. Reach for `get_context()` when you're briefing a model; reach for `recall()` when you're answering one question or need the raw scored evidence objects. ## Related - [The memory model](/concepts/memory-model#context-priority) — the full section priority order and what each entry type is - [Retrieval semantics](/concepts/retrieval-semantics) — the lanes and fusion scoring behind the evidence - [Working memory vs long-term memory](/concepts/working-memory-vs-long-term) — why Current State appears near the end of the block - [Context modes](/sdk/sdk-helpers#context-modes) — `full` vs `summary` vs `sections` response shapes - [Compaction survival](/patterns/compaction-survival) — seeding a fresh window from assembled context - [Prompt optimization](/recipes/prompt-optimization) — evolving the base prompt the block is injected into --- # Compaction survival > Persist working context as a durable checkpoint before compacting a long LLM conversation, then seed the fresh window from memory — nothing durable lost. Long-running conversations eventually hit the context-window limit, and the usual escape — summarize and compact — silently discards whatever only existed in the window. This pattern makes compaction safe: `checkpoint()` persists the working context as a durable entry *before* you compact, and the fresh window seeds itself from memory. Use it any time an agent conversation can outlive one context window. ## How it works ``` window fills → checkpoint() → compact → new window seeds from get_context() → continue ``` `checkpoint(context_snapshot=..., label=..., metadata=...)` writes a `checkpoint` entry to long-term memory — a durable snapshot of task state, independent of both the LLM window and the run's live working memory. The response returns `checkpoint_id` and a `token_estimate` for the snapshot. After compaction, the new window recovers the state two ways: - **Assembled**: `get_context()` includes a `checkpoints` section in its [priority order](/concepts/memory-model#context-priority), so a resume query brings the snapshot back alongside rules, lessons, and facts. - **Targeted**: fetch only checkpoints with `mode="sections"`, `sections=["checkpoints"]`, or recall them directly with `entry_types=["checkpoint"]`. Don't confuse this with working memory's own lifecycle. Working-memory variables decay and expire *automatically* — that's the runtime pruning scratch state, not you preserving it (see [Working memory vs long-term memory](/concepts/working-memory-vs-long-term#checkpoints-the-bridge)). A checkpoint is the deliberate act: you choose the moment and the content, and the snapshot survives runs. | | `checkpoint()` | Working-memory decay | | --- | --- | --- | | **Trigger** | You call it (pre-compaction, risky transition, handoff) | TTL expiry, relevance pruning | | **Content** | Whatever snapshot you pass | Live variables, verbatim | | **Lifetime** | Durable — survives runs | The current task | | **Recovery** | `get_context()` / `recall()` | None (or runtime graduation to LTM) | ## Walkthrough At Nimbus, Ari's escalation for Acme Corp's CSV-export incident reaches turn 80 and the window is full. Three attempted fixes, a pending approval from Rex, and the current hypothesis exist only in the conversation. Before compacting: #### Python ```python cp = client.checkpoint( session_id="nimbus:ari:esc-2201", label="pre-compaction-turn-80", context_snapshot=( "Escalation TCK-2201 (Acme CSV export). Reproduced at 500k rows. " "Tried: retry (failed), cache flush (failed), batched exporter (works, " "pending Rex approval). Next: confirm approval, then close with postmortem." ), metadata={"ticket": "TCK-2201", "turn": 80, "pending": "rex-approval"}, ) checkpoint_id = cp["checkpoint_id"] # ... compact the conversation with your framework ... # Fresh window: seed from memory. The checkpoint comes back as its own # section, alongside the rules and lessons the task still needs. seed = client.get_context( session_id="nimbus:ari:esc-2201", query="Resume the Acme CSV export escalation where it left off", max_token_budget=1200, ) messages = [{"role": "system", "content": BASE_PROMPT + "\n\n" + seed["context_block"]}] ``` #### Node / TS ```js const cp = await client.checkpoint({ session_id: "nimbus:ari:esc-2201", label: "pre-compaction-turn-80", context_snapshot: "Escalation TCK-2201 (Acme CSV export). Reproduced at 500k rows. ...", metadata: { ticket: "TCK-2201", turn: 80, pending: "rex-approval" }, }); // ... compact ... const seed = await client.getContext({ session_id: "nimbus:ari:esc-2201", query: "Resume the Acme CSV export escalation where it left off", max_token_budget: 1200, }); ``` #### Rust ```rust use mubit_sdk::CheckpointOptions; let mut opts = CheckpointOptions::new("Escalation TCK-2201 (Acme CSV export). Reproduced at 500k rows. ..."); opts.run_id = Some("nimbus:ari:esc-2201".into()); opts.label = Some("pre-compaction-turn-80".into()); let cp = client.checkpoint(opts).await?; ``` Nothing durable was lost: the compacted turns are gone from the window, but the task state, the pending approval, and the next step were re-derived from memory, not from a lossy summary alone. ## Scoping reflection to "since the checkpoint" The checkpoint also becomes a temporal anchor. The reflect request accepts a `checkpoint_id` so reflection considers only evidence created *after* that checkpoint — useful when the pre-compaction half of the run has already been reflected on. Rust forwards it as a typed field: ```rust use mubit_sdk::ReflectOptions; let lessons = client.reflect(ReflectOptions { run_id: Some("nimbus:ari:esc-2201".into()), checkpoint_id: Some(checkpoint_id), ..ReflectOptions::default() }).await?; ``` > **Note:** `checkpoint_id` is a wire-level field on the reflect request — the typed Python and JS `reflect()` helpers do not forward it. Use the low-level passthrough (`client.advanced.reflect({ run_id, checkpoint_id })` in JS; in Python too from SDK v0.12.0) or, on Python 0.11.0 and earlier, send it directly over the transport (`POST /v2/control/reflect` with `{"run_id": ..., "checkpoint_id": ...}`). ## When not to use it Skip the ceremony when the task fits comfortably in one window, and don't checkpoint every turn — knowledge that is already durable as typed entries (facts, lessons, traces written along the way) needs no snapshot. Checkpoint at the moments that matter: before compaction, before a risky transition, before a handoff. For reversible what-if exploration, a core branch session is the better tool — see [Sessions & branching](/sdk/sessions-and-branching). ## Related - [Working memory vs long-term memory](/concepts/working-memory-vs-long-term) — the decay behavior checkpoints protect against - [The memory model](/concepts/memory-model) — where the `checkpoint` entry type sits in the taxonomy - [Context assembly](/patterns/context-assembly) — the budgeted seeding call in detail - [Sessions & branching](/sdk/sessions-and-branching) — checkpoints vs reversible branch sessions --- # Verbatim vs semantic recall > When you need the exact bytes back, archive; when you need the gist, write semantic entries — and how to combine both with a fact that references the archive id. Semantic memory is paraphrase-prone *by design* — entries are retrieved by similarity and assembled into context, which is exactly right for insights and preferences and exactly wrong for a config file. This pattern is the discipline of deciding, at write time, whether a future reader needs the **exact bytes** or the **gist** — and using `archive()` for the former. Use it whenever your agent produces artifacts that will be re-executed, quoted, or audited. ## How it works `archive(content, artifact_kind, ...)` stores the content as an [`archive_block`](/concepts/memory-model#verbatim-vs-semantic-entries) — immutable, exact, and addressed by the stable `reference_id` the call returns. `dereference(reference_id)` fetches it back byte-for-byte, no similarity search involved. Every other entry type is semantic: retrieved by relevance to a query, scored, trimmed, and reworded downstream — reliable for meaning, not for characters. The two worlds still meet at query time: archived artifacts surface as `archive_overlay` evidence in normal queries when they're relevant, and as the `archive_blocks` section in assembled context — see [Retrieval lanes](/concepts/retrieval-semantics#retrieval-lanes). Evidence with `referenceable: true` carries its `reference_id`, so anything the overlay surfaces can be re-read exactly. | You're storing | Use | Why | | --- | --- | --- | | Code snippets, configs, generated SQL | `archive()` | One changed character breaks it | | Legal text, exact user quotes | `archive()` | Must be reproduced verbatim | | Insights, preferences, patterns | Semantic entries (`remember()`) | Should generalize, reconcile, and rank by relevance | | A working artifact *and* the knowledge around it | Both — archive, then a semantic entry referencing the id | Exact recovery plus similarity-retrievable context | ## Walkthrough At Nimbus, Ari finally lands the batched-exporter config that fixes Acme Corp's CSV-export timeouts. The config must survive byte-for-byte; the *insight* about when to apply it should be semantic so it surfaces on the next similar ticket: #### Python ```python archived = client.archive( session_id="nimbus:ari:ticket-1042", content=exporter_config_text, # the exact config that worked artifact_kind="config", labels=["csv-export", "acme"], metadata={"verified_on": "TCK-1042"}, ) ref = archived["reference_id"] # The semantic half: a lesson that points at the exact artifact. client.remember( content=( "When an Acme CSV export times out on large reports, apply the batched " f"exporter config archived as {ref} — dereference it, never retype it." ), intent="lesson", lesson_type="success", share="session", metadata={"reference_id": ref}, ) # Any later run in scope recovers the artifact exactly: exact = client.dereference(session_id=run_id, reference_id=ref) if exact.get("found"): config_text = exact["evidence"]["content"] # byte-for-byte ``` #### Node / TS ```js const archived = await client.archive({ session_id: "nimbus:ari:ticket-1042", content: exporterConfigText, artifact_kind: "config", labels: ["csv-export", "acme"], }); const exact = await client.dereference({ session_id: runId, reference_id: archived.reference_id, }); ``` #### Rust ```rust use mubit_sdk::{ArchiveBuilder, DereferenceOptions, SessionScope}; let archived = client.archive( ArchiveBuilder::new(exporter_config_text, "config") .session(SessionScope { run_id: Some(run_id.clone()), ..Default::default() }) .label("csv-export") .build(), ).await?; let mut deref = DereferenceOptions::new(archived["reference_id"].as_str().unwrap_or_default()); deref.run_id = Some(run_id.clone()); let exact = client.dereference(deref).await?; ``` The combination is the point. The next timeout ticket retrieves the lesson semantically (a differently-worded query still matches), and the lesson hands the agent a `reference_id` it can dereference for the config with zero paraphrase risk. > **Note:** `archive()` requires the `archive_block` write scope and `dereference()` the matching read scope — register agents with both if they round-trip artifacts. See [Exact references](/sdk/sdk-helpers#exact-references). ## When not to use it Don't archive everything. Archived blocks are immutable and sit outside the learning loop — they are never reconciled, superseded, or promoted the way semantic entries are, so knowledge that should *evolve* (preferences, patterns, lessons) belongs in semantic entries. Archive is for artifacts where fidelity beats adaptability. ## Related - [The memory model](/concepts/memory-model#verbatim-vs-semantic-entries) — where `archive_block` sits in the taxonomy - [Retrieval semantics](/concepts/retrieval-semantics#retrieval-lanes) — the `archive_overlay` and `exact_reference` lanes - [Exact references](/sdk/sdk-helpers#exact-references) — the helper pair and its scopes - [Context assembly](/patterns/context-assembly) — how the `archive_blocks` section reaches the prompt --- # Control HTTP API > Current public Mubit control-plane HTTP routes for memory, context, diagnostics, coordination, and explicit planning state. Current public Mubit control-plane HTTP routes for memory, context, diagnostics, coordination, and explicit planning state. This page documents the current public HTTP control surface. In application code, prefer the helper-first SDK methods where they exist and use these routes as wire-contract and debugging references. ## Core memory routes | Route | Method | Purpose | Recommended SDK surface | | --- | --- | --- | --- | | `/v2/control/ingest` | `POST` | Async ingest for facts, traces, lessons, rules, WM, and other typed memory | `remember()` for normal writes, raw `client.advanced.ingest(...)` when you need job control | | `/v2/control/batch_insert` | `POST` | Compatibility bulk write path | raw `client.advanced.batchInsert(...)` / `client.advanced.batch_insert(...)` | | `/v2/control/ingest/jobs/:job_id` | `GET` | Ingest job status and completion | raw `client.advanced.getIngestJob(...)` / `client.advanced.get_ingest_job(...)` | | `/v2/control/query` | `POST` | Answer-oriented routed retrieval | `recall()` or raw `client.advanced.query(...)` | | `/v2/control/context` | `POST` | Pre-assembled context block with disclosure modes and budget control | `getContext()` / `get_context()` | | `/v2/control/activity` | `POST` | Chronological memory/activity browse surface with scope and type filters | HTTP / gRPC only — no SDK helper | | `/v2/control/activity/export` | `POST` | Export chronological memory/activity as JSONL | direct HTTP or operator export tooling | | `/v2/control/archive` | `POST` | Write an exact reusable artifact block and return a stable `reference_id` | `archive()` / `archive_block()` | | `/v2/control/dereference` | `POST` | Fetch exact stored content by `reference_id` | `dereference()` | | `/v2/control/diagnose` | `POST` | Failure-path lesson surfacing | `diagnose()` | | `/v2/control/memory_health` | `POST` | Memory quality, staleness, contradictions, and section health | `memoryHealth()` / `memory_health()` | | `/v2/control/reflect` | `POST` | Extract lessons and rules from run evidence | `reflect()` | | `/v2/control/lessons` | `POST` | List stored lessons for a run/session scope | raw `client.lessons(...)` | | `/v2/control/lessons/delete` | `POST` | Delete a stored lesson | `forget()` or raw lesson delete | > **Note:** `/v2/control/ingest` and `/v2/control/batch_insert` accept at most 1000 items per request; larger requests return HTTP 400 (InvalidArgument). Chunk bigger writes client-side. ## MAS and learning-loop routes | Route | Method | Purpose | Recommended SDK surface | | --- | --- | --- | --- | | `/v2/control/agents` | `POST` | List registered agents for the run | `listAgents()` / `list_agents()` | | `/v2/control/agents/register` | `POST` | Register an agent identity and its scopes | `registerAgent()` / `register_agent()` | | `/v2/control/checkpoint` | `POST` | Save a pre-compaction or pre-transition checkpoint | `checkpoint()` | | `/v2/control/outcome` | `POST` | Reinforce a lesson or rule after success/failure/partial outcome | `recordOutcome()` / `record_outcome()` | | `/v2/control/step_outcome` | `POST` | Record a per-step process reward signal within a run | `recordStepOutcome()` / `record_step_outcome()` | | `/v2/control/strategies` | `POST` | Surface repeated patterns from stored lessons and rules | `surfaceStrategies()` / `surface_strategies()` | | `/v2/control/handoff` | `POST` | Persist a coordination handoff between agents | `handoff()` | | `/v2/control/feedback` | `POST` | Persist a response to a handoff or review request | `feedback()` | ## Explicit planning and state routes > **Warning:** The **goals**, **actions**, and **decision-cycle** routes are deprecated — track tasks in your orchestration framework (LangGraph, CrewAI) or a task system (Linear, Jira) and persist only the resulting lessons and outcomes in Mubit. The **variables** and **concepts** routes remain supported. All of these are HTTP/gRPC-only — none are exposed as typed SDK helpers. See [State management](/api-reference/state-management). | Route | Method | Purpose | | --- | --- | --- | | `/v2/control/goals/add` | `POST` | Add a goal | | `/v2/control/goals/update` | `POST` | Update a goal | | `/v2/control/goals/list` | `POST` | List goals | | `/v2/control/goals/tree` | `POST` | Retrieve the nested goal tree | | `/v2/control/variables/set` | `POST` | Set an explicit run-scoped variable | | `/v2/control/variables/get` | `POST` | Get a variable | | `/v2/control/variables/list` | `POST` | List variables | | `/v2/control/variables/delete` | `POST` | Delete a variable | | `/v2/control/concepts/define` | `POST` | Define a concept | | `/v2/control/concepts/list` | `POST` | List concepts | | `/v2/control/actions/submit` | `POST` | Record an action | | `/v2/control/actions/log` | `POST` | Read the action log | | `/v2/control/cycles/run` | `POST` | Record a reasoning / planning cycle | | `/v2/control/cycles/history` | `POST` | Read cycle history | ## Run management and agent lifecycle routes | Route | Method | Purpose | Recommended SDK surface | | --- | --- | --- | --- | | `/v2/control/runs` | `GET` | List recent runs | `client.advanced.listRunHistory(...)` / `client.advanced.list_run_history(...)` (project run-history surface) | | `/v2/control/runs/link` | `POST` | Link a child run to a parent run | raw `client.advanced.linkRun(...)` / `client.advanced.link_run(...)` | | `/v2/control/runs/unlink` | `POST` | Unlink a previously linked run | raw `client.advanced.unlinkRun(...)` / `client.advanced.unlink_run(...)` | | `/v2/control/delete_run` | `POST` | Delete a run and its associated data | `client.deleteRun(...)` / `client.delete_run(...)` (top-level helper) | | `/v2/control/agents/heartbeat` | `POST` | Agent liveness heartbeat signal | raw `client.advanced.agentHeartbeat(...)` / `client.advanced.agent_heartbeat(...)` | | `/v2/control/activities/append` | `POST` | Append an activity record to a run | HTTP / gRPC only — no SDK helper | | `/v2/control/context/snapshot` | `POST` | Full context snapshot including working memory and attention | `client.advanced.contextSnapshot(...)` / `client.advanced.context_snapshot(...)` | | `/v2/control/events/subscribe` | `GET` | Stream control events via SSE | raw `client.advanced.subscribe(...)` | | `/v2/control/ingest/stats` | `POST` | Get per-run ingest statistics | raw `client.advanced.getRunIngestStats(...)` / `client.advanced.get_run_ingest_stats(...)` | ## Operating guidance - Prefer helper methods for normal application code. - Use raw route-level control when you need exact request-shape control, ingest-job polling, or contract debugging. - Treat `/v2/control/context` as the default compaction-safe context assembly surface for long-running systems. - Treat `/v2/control/activity` as the chronological audit surface for stored Mubit data. It is separate from semantic `query`. - Use `/v2/control/archive` plus `/v2/control/dereference` when later steps need exact artifact fidelity rather than only semantic discovery. - Use `diagnose`, `memory_health`, and `strategies` before changing prompts blindly. ## Temporal query parameters `POST /v2/control/query` and `POST /v2/control/context` accept temporal filtering and budget control: | Field | Type | Required | Description | | --- | --- | --- | --- | | `min_timestamp` | int64 | no | Lower bound for temporal filter (unix seconds, inclusive). Filters by `occurrence_time` with `created_at` fallback. | | `max_timestamp` | int64 | no | Upper bound for temporal filter (unix seconds, inclusive). | | `budget` | string | no | Search budget tier: `"low"`, `"mid"` (default), `"high"`. Controls latency/quality tradeoff. | ### Staleness fields in query evidence Evidence items in query and context responses may include staleness metadata: | Field | Type | Description | | --- | --- | --- | | `is_stale` | bool | Whether this entry has been superseded by a newer version | | `superseded_by` | string | ID of the superseding entry (empty if not stale) | Stale entries are returned with a ranking penalty. Filter them in your application if you only want current information. ### Occurrence time on ingest `POST /v2/control/ingest` items accept an occurrence time field: | Field | Type | Required | Description | | --- | --- | --- | --- | | `occurrence_time` | int64 | no | When the event actually occurred (unix seconds). Distinct from ingestion time. Used for temporal queries and recency ranking. | ## Evidence provenance and context pressure `/v2/control/query` and `/v2/control/context` now expose reuse provenance on surfaced evidence: - `retrieval_mode`: `semantic`, `exact_reference`, `checkpoint`, `rule_overlay`, `lesson_overlay`, or `working_memory` - `reference_id`: stable exact-reference ID when available - `referenceable`: whether the source can be dereferenced later - `origin_entry_type`: typed LTM source such as `fact`, `lesson`, `rule`, `archive_block`, or `checkpoint` The `/v2/control/query` response also includes a `citations` array — 0-based indices into `evidence` marking which items grounded `final_answer` (empty when the answer cites no specific evidence or was an abstention). Indices are validated server-side against the evidence length, so every value is a valid index into `evidence`. Use them to render citations or audit answer grounding. `/v2/control/context` also reports context-pressure telemetry: - requested token budget - `budget_used` - `budget_remaining` - source counts by entry type - source counts by retrieval mode - `evidence_candidates_considered` - `evidence_dropped_by_budget` - `exact_references_surfaced` ## Run-level outcome request shape `POST /v2/control/outcome` reinforces a lesson or rule after a success/failure/partial outcome. Beyond the core fields (`reference_id`, `outcome`, `signal`, `rationale`, `agent_id`, `user_id`, `verified_in_production`), the wire request accepts two attribution fields: | Field | Type | Required | Description | | --- | --- | --- | --- | | `entry_ids` | string[] | no | Additional contributing memory entry IDs for multi-entry attribution. Each is resolved within the run and scope-checked; unknown or out-of-scope IDs are skipped. The primary `reference_id` is never double-counted if it also appears here | | `idempotency_key` | string | no | Client-supplied dedup token. When set, the outcome is applied at most once for the token — a retry with the same key (e.g. after a transport timeout) returns success WITHOUT re-applying the signal, so reinforcement counts aren't double-counted. Forwarded by the typed `record_outcome()` helpers from SDK v0.12.0; on earlier SDKs it is wire-only — send it directly over the transport | ## Step-level outcome request shape `POST /v2/control/step_outcome` records a per-step process reward signal, complementing the run-level `/v2/control/outcome`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | yes | Session/run identifier | | `step_id` | string | yes | Unique step identifier within the run | | `step_name` | string | no | Human-readable step label | | `outcome` | string | yes | One of `success`, `failure`, `partial`, `neutral` | | `signal` | float | no | Reward signal from -1.0 to 1.0 | | `rationale` | string | no | Why this outcome was assigned | | `directive_hint` | string | no | Hindsight guidance for future runs | | `agent_id` | string | no | Agent that performed the step | | `user_id` | string | no | Logical user scope | | `metadata_json` | string | no | Arbitrary JSON metadata | Response: `{ "step_outcome_id": "...", "accepted": true }`. Use after each agentic step where you want dense reward signal. To produce step-attributed lessons, set `include_step_outcomes` on the reflect request. This is a wire-level `ReflectRequest` field (not a kwarg on the typed `reflect()` helpers): send it via `client.advanced.reflect({ run_id, include_step_outcomes: true })` (JS on all versions; Python from SDK v0.12.0, when the `advanced` domain gained every op), `ReflectOptions.include_step_outcomes` (Rust), or `POST /v2/control/reflect` directly (Python 0.11.0 and earlier). ## Lane parameters Lanes partition memory within a shared run for multi-agent isolation. | Route | Field | Description | | --- | --- | --- | | `/v2/control/ingest` | `lane` (on each item) | Tags ingested items with a named lane | | `/v2/control/query` | `lane_filter` | Retrieves only entries tagged with the specified lane | | `/v2/control/context` | `lane_filter` | Filters context assembly by lane | | `/v2/control/agents/register` | `shared_memory_lanes` | Declares which lanes an agent participates in | Items without a lane are visible to all queries. Items with a lane are visible only when `lane_filter` matches or is empty. > **Note:** `lane` (MAS memory isolation) is distinct from the core data-plane retrieval lane concept used in direct search routing. See [Core Direct Lanes](/api-reference/core-direct-lanes) for the data-plane concept. ## Step-wise reflection parameters `POST /v2/control/reflect` now accepts additional fields for targeted reflection: | Field | Type | Description | | --- | --- | --- | | `step_id` | string | Scope reflection to a specific step | | `last_n_items` | int32 | Reflect only over the N most recent evidence items | | `include_step_outcomes` | bool | Include recorded step outcomes as reflection input | These compose with existing fields (`run_id`, `checkpoint_id`). Use `last_n_items` for incremental after-each-step reflection, and `include_step_outcomes` when step reward signals should influence lesson extraction. ## Activity routes request shapes ### `POST /v2/control/activity` List chronological memory/activity entries. | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | no | Session/run scope. When empty, lists activity across runs visible to the actor | | `user_id` | string | no | Logical user scope | | `agent_id` | string | no | Filter by agent | | `entry_types` | string[] | no | Filter by entry type (e.g. `["fact", "lesson"]`) | | `created_after` | string | no | Inclusive lower-bound timestamp (RFC3339) | | `created_before` | string | no | Inclusive upper-bound timestamp (RFC3339) | | `sort` | string | no | `"desc"` (default, newest first) or `"asc"` | | `limit` | uint32 | no | Page size. Defaults to 100 and is clamped to `[1, 500]` — an omitted or `0` limit resolves to `1`, not 100 | | `page_token` | string | no | Opaque pagination cursor returned as `next_page_token` from the previous page (currently a numeric offset string) | | `exclude_derived` | bool | no | When true, exclude promoted/derived facts from the listing | | `projection` | string | no | `"full"` (default) or `"compact"` (truncates content to 200 chars and strips verbose metadata) | Response: `{ "entries": [...], "next_page_token": "...", "total_visible": int }`. `next_page_token` is empty when the last page has been reached. ### `POST /v2/control/activity/export` Export activity as JSONL. The request takes the same filter fields as the browse call — there is no `format` request field; the response is always JSONL. | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | no | Run scope (same filter semantics as `/v2/control/activity`) | | `user_id` | string | no | Logical user scope | | `agent_id` | string | no | Filter by agent | | `entry_types` | string[] | no | Filter by entry type | | `created_after` / `created_before` | string | no | Inclusive RFC3339 bounds | | `sort` | string | no | `"desc"` (default) or `"asc"` | Response: `{ "format": "jsonl", "content": "", "entry_count": int }`. ### `POST /v2/control/activities/append` Append a manual activity trace. Each request appends a **single** `activity` record — issue one request per record. | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | yes | Run identifier | | `agent_id` | string | no | Agent that produced the record | | `activity` | object | yes | The activity record to append | | `activity.type` | string | yes | Record type (e.g. `"observation"`, `"action"`) | | `activity.payload` | string | yes | Record content | | `activity.tool_name` / `activity.input_ref` / `activity.output_ref` / `activity.ts` | string | no | Optional provenance fields | Response: `{ "success": true, "event_id": "evt_..." }`. ## Run management request shapes ### `POST /v2/control/runs/link` | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | yes | Parent run identifier | | `linked_run_id` | string | yes | Child run to link | Response: `{ "linked": true }`. ### `POST /v2/control/runs/unlink` | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | yes | Parent run identifier | | `linked_run_id` | string | yes | Child run to unlink | Response: `{ "unlinked": true }`. ### `POST /v2/control/delete_run` | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | yes | Run to delete | Response: `{ "deleted": true }`. ### `POST /v2/control/context/snapshot` | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | yes | Session/run identifier | | `include_working_memory` | bool | no | Include working memory state | | `include_goals` | bool | no | Include active goals | Response: `{ "snapshot": { "working_memory": {...}, "attention": {...}, "goals": [...] } }`. ### `POST /v2/control/ingest/stats` | Field | Type | Required | Description | | --- | --- | --- | --- | | `run_id` | string | yes | Session/run identifier | Response: `{ "total_ingested": int, "by_type": {...}, "last_ingest_at": string }`. ### `GET /v2/control/ingest/jobs/:job_id` No request body. Returns: | Field | Type | Description | | --- | --- | --- | | `job_id` | string | Job identifier | | `run_id` | string | Run the job belongs to | | `status` | string | One of `queued`, `running`, `completed`, `failed` | | `done` | bool | True once the job reached a terminal state — poll on this | | `error` | string | Error message when `status == "failed"` | | `created_at` / `started_at` / `finished_at` | string | ISO timestamps for the job lifecycle | ## Managed resources: Projects, Agents, Skills, Prompts Managed Mubit deployments expose a resource model so you can configure agents declaratively instead of re-encoding them in every run. Projects group related agents, each agent owns a versioned system prompt and a set of skills (tools/playbooks), and all of it is versioned with a candidate → active promotion flow. ### Project CRUD | Route | Method | Purpose | Recommended SDK surface | | --- | --- | --- | --- | | `/v2/control/projects` | `POST` | Create a project | `client.advanced.createProject(...)` / `client.advanced.create_project(...)` | | `/v2/control/projects/list` | `POST` | List projects | `client.advanced.listProjects(...)` / `client.advanced.list_projects(...)` | | `/v2/control/projects/get` | `POST` | Get a single project | `client.advanced.getProject(...)` / `client.advanced.get_project(...)` | | `/v2/control/projects/update` | `POST` | Rename / describe | `client.advanced.updateProject(...)` / `client.advanced.update_project(...)` | | `/v2/control/projects/delete` | `POST` | Delete a project | `client.advanced.deleteProject(...)` / `client.advanced.delete_project(...)` | `POST /v2/control/projects` request: | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Project name (shown in the console) | | `description` | string | no | Short description | Response: `{ "project": { "project_id": "proj-...", "name": "...", "description": "...", "agent_ids": [], "created_at": "...", "updated_at": "..." } }`. ### Agent Definition CRUD Agents belong to a project and own a role, description, and active system prompt. | Route | Method | Purpose | | --- | --- | --- | | `/v2/control/projects/agents` | `POST` | Create agent definition | | `/v2/control/projects/agents/list` | `POST` | List agents in a project | | `/v2/control/projects/agents/get` | `POST` | Get a single agent | | `/v2/control/projects/agents/update` | `POST` | Update role / description / prompt | | `/v2/control/projects/agents/delete` | `POST` | Delete an agent | `POST /v2/control/projects/agents` request: | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | Owning project | | `agent_id` | string | yes | Stable agent identifier | | `role` | string | no | Short role description | | `description` | string | no | Longer description | | `system_prompt_content` | string | no | Initial prompt — creates the first active PromptVersion | ### Run history for a project | Route | Method | Purpose | | --- | --- | --- | | `/v2/control/projects/runs` | `POST` | List run history for a project | | `/v2/control/projects/runs/get` | `POST` | Get a specific run history record | Run history records expose `lessons_extracted`, `prompt_changes`, `avg_outcome_score`, `outcome_count`, and `ingest_count` per run — so you can observe how an agent's behavior evolves. ### Prompt versioning + optimization Every agent has at most one `active` PromptVersion and zero or more `candidate` PromptVersions awaiting approval. The control plane can mint candidates automatically via `optimize_prompt`. | Route | Method | Purpose | | --- | --- | --- | | `/v2/control/prompt/set` | `POST` | Write a new version (`activate=true` promotes immediately) | | `/v2/control/prompt/get` | `POST` | Get the current active prompt | | `/v2/control/prompt/versions` | `POST` | List all versions for an agent | | `/v2/control/prompt/activate` | `POST` | Promote a candidate to active | | `/v2/control/prompt/optimize` | `POST` | LLM-powered candidate generation from recent outcomes | | `/v2/control/prompt/diff` | `POST` | Diff two versions (returns unified `diff_text`) | Version statuses: `active`, `candidate`, `retired`, `archived`. Sources: `manual`, `optimization`, `rollback`. `POST /v2/control/prompt/optimize` request: | Field | Type | Required | Description | | --- | --- | --- | --- | | `agent_id` | string | yes | Agent whose prompt is being optimized | | `project_id` | string | yes | Containing project | | `llm_override` | object | no | `{ provider, model, temperature, max_tokens, timeout_ms }`. `timeout_ms` is clamped server-side to `[1000, 600000]` ms (10 min); out-of-range values are silently adjusted | Response: `{ success, candidate, optimization_summary, confidence, activated }`. ### Skill CRUD + optimization Skills are named tools or playbooks attached to a project (or optionally to a specific agent). They version identically to prompts. | Route | Method | Purpose | | --- | --- | --- | | `/v2/control/skills` | `POST` | Create skill | | `/v2/control/skills/list` | `POST` | List skills | | `/v2/control/skills/get` | `POST` | Get a skill | | `/v2/control/skills/update` | `POST` | Update definition | | `/v2/control/skills/delete` | `POST` | Delete a skill | | `/v2/control/skills/versions` | `POST` | List versions | | `/v2/control/skills/activate` | `POST` | Promote a candidate skill version | | `/v2/control/skills/optimize` | `POST` | Generate a candidate skill version from recent outcomes | | `/v2/control/skills/diff` | `POST` | Diff two skill versions | `POST /v2/control/skills` request: | Field | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string | yes | Owning project | | `agent_id` | string | no | If set, attaches the skill to a specific agent | | `name` | string | yes | Skill name | | `description` | string | no | Short description | | `parameters_schema` | string | no | JSON schema describing parameters | | `instructions` | string | no | Instructions/playbook text | | `skill_type` | string | no | `"tool"` (default) or `"playbook"` | ### Control sessions Sessions are long-lived handles the control plane uses to coordinate multi-turn agent work. In typical SDK usage, `run_id` serves the same purpose; sessions are exposed for tools that need explicit lifecycle control. | Route | Method | Purpose | | --- | --- | --- | | `/v2/control/sessions/create` | `POST` | Create a control session | | `/v2/control/sessions/get` | `POST` | Fetch session state (phase, ingest count, last activity) | | `/v2/control/sessions/close` | `POST` | Close a session | ## Platform health endpoints These unauthenticated routes live at the root of each Mubit node (alongside `/v2/core/*` and `/v2/control/*`). Use them for Kubernetes probes, load-balancer checks, and operational monitoring — **not** from application code. | Route | Method | Purpose | Typical use | | --- | --- | --- | --- | | `/livez` | `GET` | Liveness: the process is running and its HTTP stack is responsive. Always returns 200 while the server accepts connections. | Kubernetes `livenessProbe`; triggers pod restart on failure. Keep the threshold forgiving — a busy node should not be killed. | | `/readyz` | `GET` | Readiness: the node is ready to serve real traffic. Returns 503 while background ACL and sparse-index rebuilds are still running (reads would see stale state in that window). | Kubernetes `readinessProbe` and load-balancer health check; a 503 temporarily removes the node from rotation without restarting it. | | `/v2/core/health` | `GET` | Coarse legacy health probe. Unauthenticated. Returns the literal string `"OK"` when the HTTP layer is up. | External checks that only need "is the endpoint reachable". Prefer `/livez` + `/readyz` for K8s. | | `/metrics` | `GET` | Prometheus scrape endpoint — includes `mubit_llm_calls_total`, `mubit_llm_tokens_total`, `mubit_llm_call_duration_seconds`, storage and ingest counters. | Prometheus / Grafana scraping. | ### `GET /livez` **Response:** `200 OK` with body `ok`. Liveness is intentionally cheap. It does not query storage, Redis, or any downstream dependency — that is the job of `/readyz`. A failing `/livez` means the process is wedged and should be restarted. ### `GET /readyz` **Responses:** - **Ready:** `200 OK` ```json { "status": "ready" } ``` - **Not ready:** `503 Service Unavailable` ```json { "status": "not_ready", "reason": "acl_rebuild_in_progress" } ``` The node flips to ready once the initial ACL warmup, sparse-index prewarm, and partitioned sparse prewarm complete. During that window, the process accepts connections (so `/livez` stays 200) but returns 503 from `/readyz` so the load balancer can route around it. Typical `reason` values: `starting`, `acl_rebuild_in_progress`, `sparse_prewarm_in_progress`. Treat `reason` as a diagnostic hint — it is advisory, not API-stable. > **Note:** The platform-api service (which fronts the console and the managed-instance API) exposes its own `/healthz` on port 8080. That is a separate surface from the data-plane `/livez`/`/readyz` probes above, which sit on each instance's port 3000. ## Next steps - Compare the proto surface at [Control gRPC reference](/api-reference/control-grpc). - Review explicit planning routes at [State management endpoints](/api-reference/state-management). --- # Control gRPC API > Current public Mubit ControlService RPC surface for memory, diagnostics, learning loop, coordination, and explicit planning state. Current public Mubit ControlService RPC surface for memory, diagnostics, learning loop, coordination, and explicit planning state. This page summarizes the current public `mubit.v1.ControlService` RPC surface. Use it for generated-client debugging and transport-parity checks. ## Service identity - Package: `mubit.v1` - Service: `ControlService` ## Core memory RPCs | Group | RPCs | | --- | --- | | Ingestion | `Ingest`, `BatchInsert`, `GetIngestJob`, `GetRunIngestStats`, `ArchiveBlock`, `Dereference` | | Retrieval and context | `Query`, `GetContext`, `ListActivity`, `ExportActivity`, `GetRunSnapshot` | | Diagnostics and reflection | `Diagnose`, `Reflect`, `ListLessons`, `DeleteLesson`, `GetMemoryHealth` | ## MAS and learning-loop RPCs | Group | RPCs | | --- | --- | | Agent identity | `RegisterAgent`, `ListAgents`, `AgentHeartbeat`, `AppendActivity` | | Compaction and reinforcement | `Checkpoint`, `RecordOutcome`, `RecordStepOutcome`, `SurfaceStrategies` | | Coordination | `CreateHandoff`, `SubmitFeedback` | | Run management | `DeleteRun`, `LinkRun`, `UnlinkRun` | ## Managed resources: Projects, Agents, Skills, Prompts Managed Mubit deployments version agent configuration server-side so prompt + skill changes can be reviewed, rolled out, and rolled back without touching deployed SDK code. | Group | RPCs | | --- | --- | | Projects | `CreateProject`, `GetProject`, `ListProjects`, `UpdateProject`, `DeleteProject` | | Agent Definitions | `CreateAgentDefinition`, `GetAgentDefinition`, `ListAgentDefinitions`, `UpdateAgentDefinition`, `DeleteAgentDefinition` | | Prompt Lifecycle | `SetPrompt`, `GetPrompt`, `ListPromptVersions`, `ActivatePromptVersion`, `OptimizePrompt`, `GetPromptDiff` | | Skill Lifecycle | `CreateSkill`, `GetSkill`, `ListSkills`, `UpdateSkill`, `DeleteSkill`, `ListSkillVersions`, `ActivateSkillVersion`, `OptimizeSkill`, `GetSkillDiff` | | Run history | `ListRunHistory`, `GetRunHistory` | | Control sessions | `CreateControlSession`, `GetControlSession`, `CloseControlSession` | ### PromptVersion / SkillVersion lifecycle Both prompt and skill versions share the same state machine: - `status` transitions: `candidate` → `active` (via `ActivatePromptVersion` / `ActivateSkillVersion`) → `retired` (when a newer version becomes active) → `archived`. - `source` values: `manual` (written via `SetPrompt` / `CreateSkill`), `optimization` (minted by `OptimizePrompt` / `OptimizeSkill`), `rollback` (restored from a retired version). - Each version carries outcome aggregates: `avg_outcome_score`, `outcome_count`, and `optimization_summary`. `OptimizePrompt` and `OptimizeSkill` accept an optional `LlmOverride` (`provider`, `model`, `temperature`, `max_tokens`, `timeout_ms`) so you can steer which model synthesises the candidate. `GetPromptDiff` / `GetSkillDiff` return a unified `diff_text` comparing any two versions — the same text the console surfaces in its review UI. ## Deprecated planning state RPCs The following RPCs remain wired for backward compatibility but are no longer the recommended path. Use external task/orchestration systems (Linear, Jira, LangGraph, CrewAI) for goals, actions, and decision cycles. See [State management endpoints](/api-reference/state-management) for guidance. | Group | RPCs | Recommendation | | --- | --- | --- | | Goals | `AddGoal`, `UpdateGoal`, `ListGoals`, `GetGoalTree` | **Deprecated** — track goals in your task system | | Actions | `SubmitAction`, `GetActionLog` | **Deprecated** — use LangGraph / CrewAI | | Decision cycles | `RunCycle`, `GetCycleHistory` | **Deprecated** — use your agent orchestration framework | | Variables | `SetVariable`, `GetVariable`, `ListVariables`, `DeleteVariable` | Still supported for run-scoped working memory | | Concepts | `DefineConcept`, `ListConcepts` | Still supported for typed concept definitions | ## Practical parity checks - Helper-first SDKs should map cleanly onto these RPC groups even when they expose more ergonomic names. - `GetContext` is the primary gRPC compaction-safe context assembly surface. - `ListActivity` and `ExportActivity` are the chronological audit/browse surfaces; use them when you need exact ordering and pagination rather than ranked retrieval. - `Diagnose`, `GetMemoryHealth`, `Reflect`, `RecordOutcome`, `RecordStepOutcome`, and `SurfaceStrategies` are the key loop-inspection and reinforcement RPCs. - `RecordStepOutcome` records per-step process rewards with signal, rationale, and directive hint for dense RL within a run. - `Reflect` accepts `step_id`, `last_n_items`, and `include_step_outcomes` for step-wise scoped reflection. - `Ingest`, `Query`, and `GetContext` accept lane fields (`lane`, `lane_filter`) for multi-agent memory isolation within a shared run. - `ArchiveBlock` and `Dereference` are the exact-reference pair: archive an exact artifact once, then recover it later by stable `reference_id`. ## Evidence provenance and telemetry `Query` and `GetContext` now expose exact-reference provenance on surfaced evidence: - `retrieval_mode` - `reference_id` - `referenceable` - `origin_entry_type` `AgentQueryResponse` also returns a `citations` field (`repeated uint32`, field 9) — 0-based indices into `evidence` marking which items grounded `final_answer` (empty when the answer cites no specific evidence). The indices are validated server-side against the evidence length, so every value is a valid index into `evidence`. `GetContext` also returns context-pressure telemetry: - source counts by entry type and retrieval mode - evidence candidates considered - evidence dropped by token budget - exact references surfaced ## Step-level outcomes `RecordStepOutcome` records a per-step process reward signal within a run, complementing the run-level `RecordOutcome`. Use it after each agentic step where you want dense feedback for reflection. | Field | Type | Description | | --- | --- | --- | | `run_id` | string | Session/run identifier | | `step_id` | string | Unique step identifier within the run | | `step_name` | string | Human-readable step label (e.g. `"initial_planning"`) | | `outcome` | string | One of `success`, `failure`, `partial`, `neutral` | | `signal` | float | Reward signal from -1.0 to 1.0 | | `rationale` | string | Why this outcome was assigned | | `directive_hint` | string | Hindsight guidance for future runs (Hindsight-Guided OPD) | | `agent_id` | string | Agent that performed the step | | `user_id` | string | Optional logical user scope | | `metadata_json` | string | Arbitrary JSON metadata | Response returns `step_outcome_id` and `accepted`. Emits `context.step_outcome_recorded` event. ## Run-level outcome attribution `RecordOutcomeRequest` carries two fields for accurate, idempotent reinforcement: | Field | Type | Description | | --- | --- | --- | | `entry_ids` | `repeated string` (field 9) | Additional memory entry IDs that contributed to this outcome (multi-entry attribution). Attribute the outcome to every contributing recalled entry so each one's reinforcement counters update, not just the primary `reference_id` — which is never double-counted if it also appears here. Unknown or out-of-scope IDs are skipped. | | `idempotency_key` | string (field 10) | Optional client-supplied dedup token. When set, the server applies the outcome at most once for the token — a retry with the same token (e.g. after a transport timeout) returns success without re-applying the signal, so reinforcement counts and champion/challenger stats aren't double-counted. Leave empty to opt out. | ## Lane-scoped memory Lanes partition memory within a shared run so each agent sees only relevant entries. | RPC / Field | Description | | --- | --- | | `IngestItem.lane` | Tags ingested items with a named lane | | `AgentQueryRequest.lane_filter` | Retrieves only entries tagged with the specified lane | | `ContextRequest.lane_filter` | Filters context assembly by lane | | `AgentRegisterRequest.shared_memory_lanes` | Declares which lanes an agent participates in | Items without a lane are visible to all queries. Items with a lane are visible only when `lane_filter` matches or is empty. > **Note:** `lane` (MAS memory isolation) is distinct from the core data-plane retrieval lane concept used in direct search routing. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | gRPC behavior differs from helper behavior | You are comparing a helper to the wrong raw RPC | Check whether the helper wraps `Query`, `GetContext`, or another control RPC | | Reflection succeeds but learning feels weak | Outcomes are not being recorded | Pair `Reflect` with `RecordOutcome` | | Context feels incomplete | Wrong mode or budget settings | Validate `ContextRequest` mode, sections, and token budget fields | | Step outcomes recorded but reflection ignores them | `include_step_outcomes` defaults to false | Pass `include_step_outcomes=True` in `ReflectRequest` | | Lane-filtered query returns nothing | Lane name mismatch or items ingested without lane | Verify the `lane` value at ingest time matches `lane_filter` at query time | ## Next steps - Review HTTP route names at [Control HTTP reference](/api-reference/control-http). - Review the helper-first SDK surfaces at [SDK methods](/sdk/sdk-methods). --- # Errors > HTTP status codes, error types, and structured error payloads returned by the Mubit control API. HTTP status codes, error types, and structured error payloads returned by the Mubit control API. Every error response carries a flat JSON body with a single human-readable `error` string: ```json { "error": "ingest request exceeds the 1000-item limit" } ``` There is no nested `type`/`code` envelope and no `request_id` field on the response body. Map programmatically on the HTTP **status code** (below) or, over gRPC, on the canonical [status code](https://grpc.io/docs/guides/status-codes/). The HTTP API is a thin shell over the gRPC service, so every HTTP status is derived from a gRPC code. ## Status codes | Status | gRPC code | Meaning | Retryable? | | --- | --- | --- | --- | | `400` | `InvalidArgument` | Request shape rejected — missing/invalid field, or an input cap exceeded (e.g. >1000 items) | No | | `401` | `Unauthenticated` | Missing or invalid `Authorization` header | No | | `403` | `PermissionDenied` | Key is valid but not scoped to this run/session/resource | No | | `404` | `NotFound` | Session, agent, reference id, or job id does not exist | No | | `409` | `AlreadyExists` | A unique resource (project, agent, skill, …) already exists | No | | `410` | — | Endpoint was removed (legacy core SDM lanes); use `insert`/`search` instead | No | | `429` | `ResourceExhausted` | An upstream dependency (e.g. an LLM provider) is throttling | Yes (with backoff) | | `500` | `Internal` (and unmapped) | Unhandled server error | Yes (with backoff) | | `503` | `Unavailable` / `FailedPrecondition` | Backend temporarily unavailable or a precondition is not yet met | Yes (with backoff) | > **Note:** The runtime does not currently emit `422`, `499`, `502`, or `504`. Business-rule rejections surface as `400` (`InvalidArgument`); overload and unmet preconditions surface as `503`. See [Rate limits](/sdk/rate-limits) for what is and isn't throttled. ## SDK exception mapping The Python SDK raises typed exceptions exported from the top-level `mubit` package (there is no `mubit.errors` submodule): | Exception | Raised for | | --- | --- | | `mubit.AuthError` | `401` — bad, missing, or revoked key | | `mubit.ValidationError` | `400` — wrong request shape or a rejected business rule | | `mubit.AlreadyExistsError` | `409` — duplicate unique resource (subclass of `ValidationError`) | | `mubit.UnsupportedFeatureError` | A call the current server/transport doesn't support | | `mubit.ServerError` | `5xx` — server-side failure (retryable) | | `mubit.TransportError` | Network/transport failure; carries a `.code` (e.g. `UNAVAILABLE`, `DEADLINE_EXCEEDED`) | ```python import mubit client = mubit.Client() try: client.remember(session_id=run_id, agent_id="support-agent", content="…", intent="fact") except mubit.AuthError: raise # fix the API key — don't retry except mubit.ValidationError: raise # fix the call shape — don't retry except (mubit.ServerError, mubit.TransportError): ... # transient — safe to retry (see below) ``` `ServerError` and transient `TransportError`s are the retryable cases; auth, validation, and unsupported-feature errors are not. The SDK already retries the transient ones for you — see [Retries and idempotency](/sdk/retries) for the policy and the manual pattern. --- # Core Direct Lanes and Policy > Policy contract for direct core query routes and safe rollout guidance. Policy contract for direct core query routes and safe rollout guidance. Direct core routes expose low-level retrieval behavior and are intentionally policy-gated. ## Route policy contract Always allowed: - `/v2/core/health` - `/v2/core/auth/*` Policy-gated: - `POST /v2/core/search` requires `MUBIT_CORE_ENABLE_DIRECT_SEARCH` - `/v2/core/pubsub/subscribe`, `/v2/core/pubsub/unsubscribe`, `/v2/core/pubsub/list` require `MUBIT_CORE_ENABLE_DIRECT_PUBSUB` Other external `/v2/core/*` routes are denied by default middleware. ## Core route reference These routes are available at `/v2/core/*`. Routes beyond auth and health require appropriate policy flags or internal access. ### Data operations | Route | Method | Purpose | | --- | --- | --- | | `/v2/core/insert` | `POST` | Insert a single node | | `/v2/core/batch_insert` | `POST` | Batch insert multiple nodes (always allowed) | | `/v2/core/node/:id` | `GET` | Retrieve a node by ID | | `/v2/core/node/:id` | `DELETE` | Delete a node by ID | | `/v2/core/runs/:run_id` | `DELETE` | Delete all nodes in a run | | `/v2/core/search` | `POST` | Semantic vector search (policy-gated) | ### Agent scratchpad memory Per-session key-value scratchpad for agent working memory. | Route | Method | Purpose | | --- | --- | --- | | `/v2/core/memory/:session_id` | `POST` | Write to session scratchpad | | `/v2/core/memory/:session_id` | `GET` | Read session scratchpad | | `/v2/core/memory/:session_id` | `DELETE` | Clear session scratchpad | ### SDM (Sparse Distributed Memory) operations > **Note:** The legacy `POST /v2/core/sdm/write` and `POST /v2/core/sdm/read` endpoints have been removed and now return `HTTP 410 Gone`. SDM writes happen implicitly on `/v2/core/insert`, and SDM reads happen implicitly on `/v2/core/search` — there is no separate SDM surface to call. ### Session management Transactional sessions for atomic multi-operation commits. | Route | Method | Purpose | | --- | --- | --- | | `/v2/core/session/create` | `POST` | Create a new session | | `/v2/core/session/:id/commit` | `POST` | Commit session changes | | `/v2/core/session/:id/drop` | `DELETE` | Drop session without committing | | `/v2/core/session/:id/snapshot` | `POST` | Snapshot current session state | | `/v2/core/session/load` | `POST` | Load a previously snapshotted session | ### Storage maintenance | Route | Method | Purpose | | --- | --- | --- | | `/v2/core/storage/stats` | `GET` | RocksDB storage statistics (size, keys, compaction) (always allowed) | | `/v2/core/storage/compact` | `POST` | Trigger manual compaction (always allowed) | ### ACL (Access Control) | Route | Method | Purpose | | --- | --- | --- | | `/v2/core/acl/grant` | `POST` | Grant permission to a user/agent | | `/v2/core/acl/revoke` | `POST` | Revoke a permission | | `/v2/core/acl/check` | `POST` | Check if a permission is granted | ### PubSub Data-plane subscriptions fire on `Node` insert / update / delete and on session memory writes. Filters are `all`, `node` (specific `node_id`), `semantic` (vector similarity over `query_text` with a `threshold`), or `session` (session scope). | Route | Method | Response | Purpose | | --- | --- | --- | --- | | `/v2/core/pubsub/subscribe` | `POST` | `text/event-stream` (SSE) | Open a server-sent-event stream. First event is `subscribed` with `{subscription_id}`; subsequent events are `node.inserted`, `node.updated`, `node.deleted`, or `memory.added` with flat JSON payloads (see schema below). The subscription lives for the lifetime of the HTTP connection and is cleaned up automatically on disconnect. | | `/v2/core/pubsub/unsubscribe` | `POST` | JSON | Explicitly remove a subscription by ID (primarily an admin/debugging surface — normal clients just close the SSE connection). | | `/v2/core/pubsub/list` | `POST` | JSON | List active subscription IDs for the calling user, plus the server-wide total. | gRPC equivalent: `CoreService.Subscribe(CoreSubscribeRequest) → stream PubSubEvent` on the same data-plane gRPC surface. Request body for `/v2/core/pubsub/subscribe`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `filter_type` | string | yes | `"all"`, `"node"`, `"semantic"`, or `"session"`. | | `node_id` | uint64 | when `filter_type == "node"` | Node to watch. | | `query_text` | string | when `filter_type == "semantic"` | Encoded server-side; events fire when an inserted/updated node's vector exceeds similarity `threshold`. | | `threshold` | float | no | Cosine-similarity floor for semantic filters. Defaults to `0.8`. | Event payload schema (each SSE `data:` line + each gRPC `PubSubEvent` message decode to the same JSON shape): | `type` | Populated fields | | --- | --- | | `subscribed` | `subscription_id` | | `node.inserted` / `node.updated` | `node_id`, `run_id`, `metadata_json`, `created_at`, `updated_at` | | `node.deleted` | `node_id` | | `memory.added` | `session_id`, `entry_json` | `metadata_json` and `entry_json` are JSON-encoded strings on the wire. The official SDK wrappers parse them into native `metadata` / `entry` objects, coerce `uint64` / `int64` fields to numbers, and drop irrelevant proto defaults before yielding the event. Clients speaking the raw HTTP / gRPC surface need to do the same translation themselves. ## Control parity rule The same policy gates apply when `control.query` runs in direct bypass mode. ## Rollout guidance 1. Start with both direct lane flags disabled. 2. Enable one lane at a time outside production. 3. Monitor lane-specific request/error metrics. 4. Keep routed mode fallback active. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Direct lane request denied | Required flag disabled | Use routed retrieval or enable lane explicitly | | Increased production error rate | Lane enabled without guardrails | Roll back lane and re-enable with staged traffic | | Unclear fallback behavior | No route policy decision tree | Define mode fallback rules per feature | ## Next steps - Review integration guardrails at [Data guardrails](/sdk/data-guardrails). - Review retrieval implementation choices at [Retrieve data](/sdk/retrieve-data). --- # State Management Surface > Run-scoped state routes for variables and concepts, plus deprecated planning state (goals / actions / cycles). Run-scoped state routes for variables and concepts, plus deprecated planning state (goals / actions / cycles). Mubit is primarily a memory engine. It also exposes a small set of **run-scoped state routes** (variables, concepts) that are useful when you want to persist orchestrator state next to memory. A broader planning surface (goals, actions, decision cycles) is still wired for backward compatibility but is **deprecated** — most workflows are better served by external task / orchestration systems. > **Note:** For most use cases, start with `remember`, `recall`, `getContext`, `checkpoint`, and the learning-loop APIs. Reach for these state routes only when memory alone isn't enough. ## Supported state surfaces | Domain | Purpose | Status | | --- | --- | --- | | Variables | Small explicit run-scoped state values that memory shouldn't infer | **Supported** | | Concepts | Named planner concepts / controlled vocabulary | **Supported** | ### Variables routes | Route | Method | Purpose | | --- | --- | --- | | `/v2/control/variables/set` | `POST` | Set a run-scoped variable | | `/v2/control/variables/get` | `POST` | Get a variable | | `/v2/control/variables/list` | `POST` | List variables for a run | | `/v2/control/variables/delete` | `POST` | Delete a variable | ### Concepts routes | Route | Method | Purpose | | --- | --- | --- | | `/v2/control/concepts/define` | `POST` | Define a concept | | `/v2/control/concepts/list` | `POST` | List concepts | ## Deprecated planning surfaces > **Warning:** The goals, actions, and decision-cycle RPCs are **deprecated**. They remain wired for existing deployments but are no longer recommended. > > - **Goals** — Track tasks in a dedicated system (Linear, Jira, Asana). Mubit should store the lessons and outcomes, not the task list. > - **Actions / decision cycles** — Delegate to your agent orchestration framework (LangGraph, CrewAI, ADK). Persist only the resulting lessons, outcomes, and traces in Mubit. > > If you're currently using these routes, keep them running — they aren't being turned off — but plan to migrate the planning surface to external tools and keep Mubit focused on experiential memory. | Capability | Routes | Replacement | | --- | --- | --- | | Goals | `/v2/control/goals/add`, `/v2/control/goals/update`, `/v2/control/goals/list`, `/v2/control/goals/tree` | External task tracker (Linear, Jira). Store only the resulting **lessons / rules / mental models** in Mubit. | | Actions | `/v2/control/actions/submit`, `/v2/control/actions/log` | Agent framework execution log (LangGraph, CrewAI). Use [archive + dereference](/api-reference/control-http#core-memory-routes) when you need exact trace fidelity. | | Decision cycles | `/v2/control/cycles/run`, `/v2/control/cycles/history` | Agent framework planning loop. Record the **outcome** of each cycle via `record_outcome` / `record_step_outcome`. | ## Recommended usage guidance - Keep variables and concepts in the same `run_id` as the memory they explain. - Move goals, actions, and decision cycles to your task / orchestration system of record. - Record the learning signal that comes out of those systems (outcomes, lessons, rules) in Mubit so future runs benefit. - Use `checkpoint` when a long run is about to compact or cross a risky boundary. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Planner state exists but the model still forgets things | Only state was stored, not memory | Persist facts, lessons, or rules alongside variables | | Debugging is hard | Actions / cycles are missing | Record planner decisions in your orchestration framework and surface outcomes to Mubit | | Variable list grows unbounded | No TTL discipline | Use `variables/delete` at end-of-run or namespace variables by `run_id` | ## Next steps - Review the main control surface at [Control HTTP reference](/api-reference/control-http). - See memory and the learning loop in action in [Support agent loop](/recipes/support-agent-loop). --- # Data Plane (Memory) > Understand direct-core memory operations, policy boundaries, and when low-level lanes are appropriate. Understand direct-core memory operations, policy boundaries, and when low-level lanes are appropriate. The Mubit data plane exposes low-level memory primitives. It is powerful and intentionally policy-constrained. Use direct-core methods only when you need an explicitly enabled compatibility lane such as direct semantic search. For most app request paths, control-plane methods are safer and easier to operate. Keeping this boundary clear reduces policy and reliability incidents. ## Core route families | Family | Representative routes | | ---------------------- | --------------------------------------------------- | | Direct retrieval | `/v2/core/search` | | Health/auth | `/v2/core/health`, `/v2/core/auth/*` | ## Policy behavior - `/v2/core/search` requires `MUBIT_CORE_ENABLE_DIRECT_SEARCH`. - Other external `/v2/core/*` routes should be treated as compatibility-only or policy-denied, not normal SDK adoption paths. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | ---------------------------------------- | -------------------------- | -------------------------------------------- | | Core calls are denied | Direct lanes disabled | Use control path or enable lane deliberately | | Retrieval behavior diverges unexpectedly | Mixed control/core usage | Define one default retrieval path | | Operational overhead increases | Broad direct-core adoption | Restrict direct-core to targeted features | ## Next steps - See direct-lane policy contract at [Core direct lanes policy](/api-reference/core-direct-lanes). - See how the runtime fits together at [How Mubit works](/sdk/how-mubit-works). --- # Control Plane (State Runtime) > Use Mubit control APIs as the stable integration boundary for memory, context assembly, diagnostics, the learning loop, and coordination. Use Mubit control APIs as the stable integration boundary for memory, context assembly, diagnostics, the learning loop, and coordination. The control plane is the primary public integration boundary for current Mubit. It combines the memory lifecycle, compaction-safe context assembly, diagnostics, learning loop, coordination primitives, and structured run-scoped variables. If your team needs one stable runtime boundary for long-running AI systems, standardize on the control plane first. ## Core workflow model | Workflow | Recommended surfaces | | --- | --- | | Memory write/read | `remember`, `recall`, raw `control.ingest`, raw `control.query` | | Context assembly | `getContext` / `get_context`, `/v2/control/context` | | Learning loop | `reflect`, `recordOutcome` / `record_outcome`, `surfaceStrategies` / `surface_strategies` | | Diagnostics | `memoryHealth` / `memory_health`, `diagnose` | | Coordination | `registerAgent` / `register_agent`, `handoff`, `feedback` | ## Operating guidance - Use one deterministic `run_id` / `session_id` strategy. - Prefer helper-first SDK methods for application code. - Use `checkpoint` before compaction or risky transitions. - Use `getContext` rather than rebuilding long prompts manually. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Stale or weak responses | Context was built manually and inconsistently | Standardize on `getContext()` / `get_context()` | | The system does not improve across attempts | Reflection or outcome recording is missing | Pair `reflect()` with `recordOutcome()` | | Multi-agent behavior drifts | Roles are implicit | Register agents and persist handoffs / feedback | ## Next steps - Review the full route contract at [Control HTTP reference](/api-reference/control-http). - Apply the workflow in [Support agent memory loop](/recipes/support-agent-loop). --- # Support Agent Memory Loop > Build a support-agent loop with context assembly, checkpointing, reflection, and outcome reinforcement. Build a support-agent loop with context assembly, checkpointing, reflection, and outcome reinforcement. This is the default long-running support-agent pattern in current Mubit: 1. store customer facts, traces, and preferences 2. assemble context before each response 3. checkpoint before compaction or risky transitions 4. reflect after the attempt 5. record the outcome so the next similar case gets better guidance 6. archive exact artifacts when later steps need exact reuse instead of only semantic recall ## Minimal implementation example #### Python ```python title="support_agent_memory_loop.py" from mubit import Client import os run_id = "support:acme:ticket-42" client = Client( endpoint=os.getenv("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], run_id=run_id, transport="http", ) client.register_agent( session_id=run_id, agent_id="support-agent", role="support", # `archive_block` must be listed explicitly — archive() is its own scope, # distinct from "lesson"/"fact"/"trace". Agents without it get # PermissionDenied on archive() calls. read_scopes=["fact", "lesson", "rule", "handoff", "feedback", "archive_block"], write_scopes=["fact", "trace", "lesson", "handoff", "feedback", "archive_block"], ) client.remember( session_id=run_id, agent_id="support-agent", content="Customer Taylor prefers concise Friday updates and wants billing fixes summarized in one paragraph.", intent="fact", metadata={"customer": "taylor", "source": "ticket"}, ) context = client.get_context( session_id=run_id, query="Draft the next customer-safe update for Taylor.", mode="full", max_token_budget=700, ) checkpoint = client.checkpoint( session_id=run_id, agent_id="support-agent", label="pre-response-compaction", context_snapshot=context.get("context_block", ""), metadata={"stage": "drafting"}, ) archived = client.archive( session_id=run_id, agent_id="support-agent", content="Original billing diff and exact remediation note for ticket-42", artifact_kind="billing_postmortem", labels=["billing", "exact"], ) exact = client.dereference( session_id=run_id, reference_id=archived["reference_id"], ) reflection = client.reflect(session_id=run_id) # Read the lesson id off the reflect response directly — it's populated # server-side from the persistence call. Avoid a round-trip through # `client.lessons()` which races with the vector store's index propagation # and can return the run as empty for a few seconds after reflect. lesson_id = next( (l.get("lesson_id") for l in (reflection.get("lessons") or []) if l.get("lesson_id")), None, ) if lesson_id: client.record_outcome( session_id=run_id, agent_id="support-agent", reference_id=lesson_id, outcome="success", signal=0.8, rationale="The customer update matched the stored preference and resolved the billing question cleanly.", ) strategies = client.surface_strategies( session_id=run_id, lesson_types=["success", "failure"], max_strategies=3, ) print({ "checkpoint_id": checkpoint.get("checkpoint_id"), "lessons_stored": reflection.get("lessons_stored"), "strategies": len(strategies.get("strategies", [])), "exact_reference_found": exact.get("found"), }) ``` #### Node / TS ```ts title="support_agent_memory_loop.ts" import { Client } from "@mubit-ai/sdk"; const runId = "support:acme:ticket-42"; const client = new Client({ endpoint: process.env.MUBIT_ENDPOINT ?? "https://api.mubit.ai", api_key: process.env.MUBIT_API_KEY, run_id: runId, transport: "http", }); await client.registerAgent({ session_id: runId, agent_id: "support-agent", role: "support", // `archive_block` must be listed explicitly — archive() is its own scope, // distinct from "lesson"/"fact"/"trace". Agents without it get // PermissionDenied on archive() calls. read_scopes: ["fact", "lesson", "rule", "handoff", "feedback", "archive_block"], write_scopes: ["fact", "trace", "lesson", "handoff", "feedback", "archive_block"], }); await client.remember({ session_id: runId, agent_id: "support-agent", content: "Customer Taylor prefers concise Friday updates and wants billing fixes summarized in one paragraph.", intent: "fact", metadata: { customer: "taylor", source: "ticket" }, }); const context = await client.getContext({ session_id: runId, query: "Draft the next customer-safe update for Taylor.", mode: "full", max_token_budget: 700, }); const checkpoint = await client.checkpoint({ session_id: runId, agent_id: "support-agent", label: "pre-response-compaction", context_snapshot: context.context_block ?? "", metadata: { stage: "drafting" }, }); const archived = await client.archive({ session_id: runId, agent_id: "support-agent", content: "Original billing diff and exact remediation note for ticket-42", artifact_kind: "billing_postmortem", labels: ["billing", "exact"], }); const exact = await client.dereference({ session_id: runId, reference_id: archived.reference_id, }); // Read the lesson id off the reflect response directly — it's populated // server-side from the persistence call. Avoid a round-trip through // `client.lessons()` which races with the vector store's index propagation // and can return the run as empty for a few seconds after reflect. const reflection = await client.reflect({ session_id: runId }); const lessonId = (reflection.lessons ?? []).find((l) => l.lesson_id)?.lesson_id; if (lessonId) { await client.recordOutcome({ session_id: runId, agent_id: "support-agent", reference_id: lessonId, outcome: "success", signal: 0.8, rationale: "The customer update matched the stored preference and resolved the billing question cleanly.", }); } const strategies = await client.surfaceStrategies({ session_id: runId, lesson_types: ["success", "failure"], max_strategies: 3, }); console.log({ checkpoint_id: checkpoint.checkpoint_id, lessons_stored: reflection.lessons_stored, strategies: strategies.strategies?.length ?? 0, exact_reference_found: exact.found, }); ``` #### Rust ```rust title="support_agent_memory_loop.rs" use mubit_sdk::{ ArchiveOptions, CheckpointOptions, Client, ClientConfig, DereferenceOptions, GetContextOptions, RecordOutcomeOptions, ReflectOptions, RegisterAgentOptions, RememberOptions, SurfaceStrategiesOptions, }; use serde_json::json; #[tokio::main] async fn main() -> anyhow::Result<()> { let run_id = "support:acme:ticket-42"; // from_env() reads MUBIT_ENDPOINT / MUBIT_API_KEY. let client = Client::new(ClientConfig::from_env().run_id(run_id).transport("http"))?; // `archive_block` must be listed explicitly — archive() is its own scope, // distinct from "lesson"/"fact"/"trace". Agents without it get // PermissionDenied on archive() calls. let mut agent = RegisterAgentOptions::new("support-agent"); agent.run_id = Some(run_id.to_string()); agent.role = "support".to_string(); agent.read_scopes = vec!["fact", "lesson", "rule", "handoff", "feedback", "archive_block"] .into_iter().map(String::from).collect(); agent.write_scopes = vec!["fact", "trace", "lesson", "handoff", "feedback", "archive_block"] .into_iter().map(String::from).collect(); client.register_agent(agent).await?; let mut fact = RememberOptions::new( "Customer Taylor prefers concise Friday updates and wants billing fixes summarized in one paragraph.", ); fact.run_id = Some(run_id.to_string()); fact.agent_id = Some("support-agent".to_string()); fact.intent = Some("fact".to_string()); fact.metadata = Some(json!({"customer": "taylor", "source": "ticket"})); client.remember(fact).await?; let mut ctx = GetContextOptions::default(); ctx.run_id = Some(run_id.to_string()); ctx.query = Some("Draft the next customer-safe update for Taylor.".to_string()); ctx.mode = Some("full".to_string()); ctx.max_token_budget = Some(700); let context = client.get_context(ctx).await?; let context_block = context["context_block"].as_str().unwrap_or_default().to_string(); let mut cp = CheckpointOptions::new(context_block); cp.run_id = Some(run_id.to_string()); cp.agent_id = Some("support-agent".to_string()); cp.label = Some("pre-response-compaction".to_string()); cp.metadata = Some(json!({"stage": "drafting"})); let checkpoint = client.checkpoint(cp).await?; let mut archive = ArchiveOptions::new( "Original billing diff and exact remediation note for ticket-42", "billing_postmortem", ); archive.run_id = Some(run_id.to_string()); archive.agent_id = Some("support-agent".to_string()); archive.labels = vec!["billing".to_string(), "exact".to_string()]; let archived = client.archive(archive).await?; let mut deref = DereferenceOptions::new(archived["reference_id"].as_str().unwrap_or_default()); deref.run_id = Some(run_id.to_string()); let exact = client.dereference(deref).await?; // Read the lesson id off the reflect response directly — it's populated // server-side from the persistence call. Avoid a round-trip through // `client.lessons()` which races with the vector store's index propagation // and can return the run as empty for a few seconds after reflect. let reflection = client .reflect(ReflectOptions { run_id: Some(run_id.to_string()), ..Default::default() }) .await?; let lesson_id = reflection["lessons"] .as_array() .and_then(|lessons| lessons.iter().find_map(|l| l["lesson_id"].as_str())) .map(String::from); if let Some(lesson_id) = lesson_id { let mut outcome = RecordOutcomeOptions::new(lesson_id, "success"); outcome.run_id = Some(run_id.to_string()); outcome.agent_id = Some("support-agent".to_string()); outcome.signal = 0.8; outcome.rationale = "The customer update matched the stored preference and resolved the billing question cleanly.".to_string(); client.record_outcome(outcome).await?; } let mut strat = SurfaceStrategiesOptions::default(); strat.run_id = Some(run_id.to_string()); strat.lesson_types = vec!["success".to_string(), "failure".to_string()]; strat.max_strategies = 3; let strategies = client.surface_strategies(strat).await?; println!( "checkpoint_id={} lessons_stored={} strategies={} exact_reference_found={}", checkpoint["checkpoint_id"], reflection["lessons_stored"], strategies["strategies"].as_array().map(|s| s.len()).unwrap_or(0), exact["found"], ); Ok(()) } ``` ## What matters operationally - `get_context` is the pre-compaction memory surface. - `checkpoint` preserves the critical context snapshot before the LLM window is compacted. - `archive` plus `dereference` is the exact-reference path for artifacts that must come back verbatim later. - `reflect` extracts reusable lessons and rules from the run. - `record_outcome` changes later ranking so good lessons appear earlier next time. > **Note:** A freshly reflected lesson does not always land as a fully trusted, immediately-surfaced entry. Reflected lessons enter long-term memory as **pending** candidates and are promoted to **active** only once their score crosses the accept threshold (default `0.6`); the control stream emits `context.lesson_validation_passed` / `context.lesson_validation_failed` alongside `context.lesson_promoted`. The `record_outcome` calls above are part of how a pending candidate accrues positive evidence and gets promoted, so a brand-new lesson may not be surfaced or trusted on the very next case until enough outcomes accumulate. Set `MUBIT_CONTROL_LESSON_VALIDATION_ENABLED=0` on the control service to restore the legacy behavior where every reflected lesson is active immediately. ## Self-contained runnable example Copy the two files below into an empty directory, fill in `MUBIT_API_KEY`, and `python support_agent.py` will run the full loop end-to-end without any LLM provider setup. The "LLM response" is a deterministic stub so you can see every Mubit call succeed before wiring in a real model. The script is Python; the tabbed example above shows the same loop in TypeScript and Rust. ```bash title=".env" MUBIT_API_KEY=mbt___ MUBIT_ENDPOINT=https://api.mubit.ai ``` ```python title="support_agent.py" """Self-contained support-agent loop against Mubit. Prereqs: pip install mubit-sdk>=0.6.0 python-dotenv Run: python support_agent.py """ from __future__ import annotations import os import uuid from dotenv import load_dotenv from mubit import Client load_dotenv() RUN_ID = f"support:demo:{uuid.uuid4().hex[:8]}" AGENT = "support-agent" def stub_llm(prompt: str, context: str) -> str: """Deterministic stand-in for a real LLM so the example is self-contained.""" return ( "Hi Taylor — here is your Friday billing summary in one paragraph: " "the duplicate charge was refunded, the recurring invoice was corrected, " "and no further action is needed. (context_chars=%d)" % len(context) ) def main() -> None: client = Client(run_id=RUN_ID) # 1. Register the agent and its scopes client.register_agent( session_id=RUN_ID, agent_id=AGENT, role="support", read_scopes=["fact", "lesson", "rule", "handoff"], write_scopes=["fact", "trace", "lesson", "handoff"], ) # 2. Store a durable customer fact client.remember( session_id=RUN_ID, agent_id=AGENT, content="Customer Taylor prefers concise Friday updates; bills should be " "summarized in one paragraph, not a bulleted list.", intent="fact", metadata={"customer": "taylor", "source": "ticket-42"}, ) # 3. Assemble context before the response ctx = client.get_context( session_id=RUN_ID, query="Draft the next customer-safe billing update for Taylor.", mode="full", max_token_budget=700, ) context_block = ctx.get("context_block", "") # 4. Checkpoint before the generation (compaction safety) checkpoint = client.checkpoint( session_id=RUN_ID, agent_id=AGENT, label="pre-response", context_snapshot=context_block, metadata={"stage": "drafting"}, ) # 5. Generate the response (stub in place of a real LLM) response = stub_llm("Draft the billing update.", context_block) # 6. Ingest the response as a trace (kept distinct from the fact) client.remember( session_id=RUN_ID, agent_id=AGENT, content=f"Agent response: {response}", intent="trace", ) # 7. Reflect after the attempt. The lesson id is returned inline on the # response so we don't need a separate `lessons()` call (which can race # with the vector-store index right after a write). reflection = client.reflect(session_id=RUN_ID) lesson_id = next( (l.get("lesson_id") for l in (reflection.get("lessons") or []) if l.get("lesson_id")), None, ) # 8. Reinforce the lesson that drove the right format if lesson_id: client.record_outcome( session_id=RUN_ID, agent_id=AGENT, reference_id=lesson_id, outcome="success", signal=0.8, rationale="Response matched the stored preference for concise paragraph format.", ) # 9. Surface reusable strategies for the next similar ticket strategies = client.surface_strategies( session_id=RUN_ID, lesson_types=["success", "failure"], max_strategies=3, ) print({ "run_id": RUN_ID, "checkpoint_id": checkpoint.get("checkpoint_id"), "lessons_stored": reflection.get("lessons_stored", 0), "strategies_surfaced": len(strategies.get("strategies", [])), "response_preview": response[:80], }) if __name__ == "__main__": main() ``` **Expected output** (values will differ, but the shape should match): ```json { "run_id": "support:demo:a1b2c3d4", "checkpoint_id": "ck_...", "lessons_stored": 1, "strategies_surfaced": 2, "response_preview": "Hi Taylor — here is your Friday billing summary..." } ``` Once this works, swap `stub_llm` for a real provider (`openai.ChatCompletion.create`, `anthropic.Anthropic().messages.create`, etc.) and feed `context_block` as the system message. Nothing else in the loop needs to change. ## Next steps - Coordinate multiple specialists at [Multi-agent shared state](/recipes/multi-agent-shared-state). - Debug weak memory quality with [Control HTTP reference](/api-reference/control-http). --- # Multi-Agent Shared State > Coordinate specialist agents with agent registration, scoped access, handoffs, feedback, and shared context assembly. Coordinate specialist agents with agent registration, scoped access, handoffs, feedback, and shared context assembly. Current Mubit coordination is centered on **registered agents plus persisted coordination artifacts**. Use this pattern when a planner, reviewer, specialist, or resolver must share memory while keeping role boundaries explicit. ## Flow 1. Register each agent with the scopes it should read and write. 2. Persist shared facts, traces, rules, and lessons in one session. 3. Use `handoff()` to transfer a subtask. 4. Use `feedback()` to record the response to that handoff. 5. Use `getContext()` / `get_context()` to retrieve shared coordination context for the next agent. ## Minimal implementation example #### Python ```python title="multi_agent_shared_state.py" from mubit import Client import os run_id = "claims:planner:claim-42" client = Client( endpoint=os.getenv("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], run_id=run_id, transport="http", ) client.register_agent(session_id=run_id, agent_id="planner", role="planner") client.register_agent( session_id=run_id, agent_id="coverage", role="coverage", read_scopes=["fact", "rule", "lesson", "handoff", "feedback"], write_scopes=["fact", "trace", "handoff", "feedback"], ) client.remember( session_id=run_id, agent_id="planner", content="Policy P-778 includes rental reimbursement after collision claims.", intent="fact", metadata={"claim_id": "claim-42"}, ) handoff = client.handoff( session_id=run_id, task_id="claim-42-coverage-review", from_agent_id="planner", to_agent_id="coverage", requested_action="review", content="Confirm whether rental reimbursement applies to this collision claim.", metadata={"claim_id": "claim-42"}, ) feedback = client.feedback( session_id=run_id, handoff_id=handoff["handoff_id"], from_agent_id="coverage", verdict="approve", comments="Coverage confirmed. Rental reimbursement applies for up to 14 days.", metadata={"claim_id": "claim-42"}, ) context = client.get_context( session_id=run_id, query="What did coverage conclude for claim-42?", mode="sections", sections=["handoffs", "feedback", "facts", "active_rules"], ) print({ "handoff_id": handoff.get("handoff_id"), "feedback_id": feedback.get("feedback_id"), "sections": len(context.get("section_summaries", [])), }) ``` #### Node / TS ```ts title="multi_agent_shared_state.ts" import { Client } from "@mubit-ai/sdk"; const runId = "claims:planner:claim-42"; const client = new Client({ endpoint: process.env.MUBIT_ENDPOINT ?? "https://api.mubit.ai", api_key: process.env.MUBIT_API_KEY, run_id: runId, transport: "http", }); await client.registerAgent({ session_id: runId, agent_id: "planner", role: "planner" }); await client.registerAgent({ session_id: runId, agent_id: "coverage", role: "coverage", read_scopes: ["fact", "rule", "lesson", "handoff", "feedback"], write_scopes: ["fact", "trace", "handoff", "feedback"], }); await client.remember({ session_id: runId, agent_id: "planner", content: "Policy P-778 includes rental reimbursement after collision claims.", intent: "fact", metadata: { claim_id: "claim-42" }, }); const handoff = await client.handoff({ session_id: runId, task_id: "claim-42-coverage-review", from_agent_id: "planner", to_agent_id: "coverage", requested_action: "review", content: "Confirm whether rental reimbursement applies to this collision claim.", metadata: { claim_id: "claim-42" }, }); const feedback = await client.feedback({ session_id: runId, handoff_id: handoff.handoff_id, from_agent_id: "coverage", verdict: "approve", comments: "Coverage confirmed. Rental reimbursement applies for up to 14 days.", metadata: { claim_id: "claim-42" }, }); const context = await client.getContext({ session_id: runId, query: "What did coverage conclude for claim-42?", mode: "sections", sections: ["handoffs", "feedback", "facts", "active_rules"], }); console.log({ handoff_id: handoff.handoff_id, feedback_id: feedback.feedback_id, sections: context.section_summaries?.length ?? 0, }); ``` #### Rust ```rust title="multi_agent_shared_state.rs" use mubit_sdk::{ Client, ClientConfig, FeedbackOptions, GetContextOptions, HandoffOptions, RegisterAgentOptions, RememberOptions, }; use serde_json::json; #[tokio::main] async fn main() -> anyhow::Result<()> { let run_id = "claims:planner:claim-42"; // from_env() reads MUBIT_ENDPOINT / MUBIT_API_KEY. let client = Client::new(ClientConfig::from_env().run_id(run_id).transport("http"))?; let mut planner = RegisterAgentOptions::new("planner"); planner.run_id = Some(run_id.to_string()); planner.role = "planner".to_string(); client.register_agent(planner).await?; let mut coverage = RegisterAgentOptions::new("coverage"); coverage.run_id = Some(run_id.to_string()); coverage.role = "coverage".to_string(); coverage.read_scopes = vec!["fact", "rule", "lesson", "handoff", "feedback"] .into_iter().map(String::from).collect(); coverage.write_scopes = vec!["fact", "trace", "handoff", "feedback"] .into_iter().map(String::from).collect(); client.register_agent(coverage).await?; let mut fact = RememberOptions::new( "Policy P-778 includes rental reimbursement after collision claims.", ); fact.run_id = Some(run_id.to_string()); fact.agent_id = Some("planner".to_string()); fact.intent = Some("fact".to_string()); fact.metadata = Some(json!({"claim_id": "claim-42"})); client.remember(fact).await?; let mut handoff_opts = HandoffOptions::new( "claim-42-coverage-review", "planner", "coverage", "Confirm whether rental reimbursement applies to this collision claim.", ); handoff_opts.run_id = Some(run_id.to_string()); handoff_opts.requested_action = "review".to_string(); handoff_opts.metadata = Some(json!({"claim_id": "claim-42"})); let handoff = client.handoff(handoff_opts).await?; let mut feedback_opts = FeedbackOptions::new( handoff["handoff_id"].as_str().unwrap_or_default(), "approve", ); feedback_opts.run_id = Some(run_id.to_string()); feedback_opts.from_agent_id = Some("coverage".to_string()); feedback_opts.comments = "Coverage confirmed. Rental reimbursement applies for up to 14 days.".to_string(); feedback_opts.metadata = Some(json!({"claim_id": "claim-42"})); let feedback = client.feedback(feedback_opts).await?; let mut ctx = GetContextOptions::default(); ctx.run_id = Some(run_id.to_string()); ctx.query = Some("What did coverage conclude for claim-42?".to_string()); ctx.mode = Some("sections".to_string()); ctx.sections = vec!["handoffs", "feedback", "facts", "active_rules"] .into_iter().map(String::from).collect(); let context = client.get_context(ctx).await?; println!( "handoff_id={} feedback_id={} sections={}", handoff["handoff_id"], feedback["feedback_id"], context["section_summaries"].as_array().map(|s| s.len()).unwrap_or(0), ); Ok(()) } ``` ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Specialist sees too much memory | Agent scopes are too broad | Narrow `read_scopes` and `write_scopes` | | Coordination disappears after the turn | Handoff or feedback stayed in app memory only | Persist both with `handoff()` and `feedback()` | | Next agent repeats work | Shared context was not reassembled | Call `getContext()` before each transition | ## Next steps - Add checkpoints before compaction at [Support agent memory loop](/recipes/support-agent-loop). - See the route-level contract at [Control HTTP reference](/api-reference/control-http). --- # Building Event-Driven Agents > Use control events to react to memory changes, checkpoints, outcomes, and coordination artifacts instead of relying only on polling loops. Use control events to react to memory changes, checkpoints, outcomes, and coordination artifacts instead of relying only on polling loops. Polling is acceptable for simple loops, but event-driven coordination is better when planners and workers need to react to new memory, handoffs, feedback, checkpoints, or outcome updates. The event stream is still a low-level control-plane surface. Keep the public integration model helper-first for memory writes and reads, and use the event stream to wake orchestration logic. ## Decision model | Event lane | Surface | Use when | | --- | --- | --- | | Run-scoped control events | `GET /v2/control/events/subscribe` | One workflow should react to ingest, reflection, handoff, feedback, checkpoint, or outcome events | | Memory read/write loop | `remember`, `recall`, `getContext`, `checkpoint` | Most application behavior | ## Minimal implementation example #### Python ```python title="event_driven_agents.py" from mubit import Client import os run_id = "claims:planner:claim-42" client = Client( endpoint=os.getenv("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], run_id=run_id, transport="http", ) stream = client.advanced.subscribe({"run_id": run_id}) client.remember( session_id=run_id, agent_id="planner", content="A new adjuster note arrived for claim-42.", intent="trace", ) client.checkpoint( session_id=run_id, agent_id="planner", label="before-adjuster-review", context_snapshot="Planner stored the adjuster note and is waiting for review.", ) for event in stream: if event: print(event) break ``` #### Node / TS ```ts title="event_driven_agents.ts" import { Client } from "@mubit-ai/sdk"; const runId = "claims:planner:claim-42"; const client = new Client({ endpoint: process.env.MUBIT_ENDPOINT ?? "https://api.mubit.ai", api_key: process.env.MUBIT_API_KEY, run_id: runId, transport: "http", }); const stream = client.advanced.subscribe({ run_id: runId }); await client.remember({ session_id: runId, agent_id: "planner", content: "A new adjuster note arrived for claim-42.", intent: "trace", }); await client.checkpoint({ session_id: runId, agent_id: "planner", label: "before-adjuster-review", context_snapshot: "Planner stored the adjuster note and is waiting for review.", }); for await (const event of stream) { console.log(event); break; } ``` ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Workers still poll constantly | Event stream is not wired to the orchestration loop | Move wakeup logic to the control stream | | Event stream is noisy | Too many memory writes without filtering in the worker | Filter by event type in the consumer | | Recovery is hard after compaction | Events fire but context is not rebuilt | Re-read with `getContext()` after the wakeup | ## Next steps - Add planner state at [Multi-agent shared state](/recipes/multi-agent-shared-state). - Add branch-safe experimentation at [Branching memory](/recipes/branching-memory). --- # Branching Memory (What-If Simulations) > Use checkpoints and core session branches for reversible what-if exploration without polluting the main execution path. Use checkpoints and core session branches for reversible what-if exploration without polluting the main execution path. When one task has multiple candidate strategies, preserve the main path and branch only the temporary exploratory state. Current Mubit supports this with two complementary tools: - `checkpoint()` on the control plane for the durable main-path snapshot - core session create / commit / drop for reversible what-if branches ## Decision model | Requirement | Primitive | | --- | --- | | Preserve the current durable task state before compaction or divergence | `checkpoint()` | | Try a temporary branch against the hosted API and discard it later | `core.create_session`, `core.drop_session` | | Merge a branch back into its parent | `core.commit_session` | | Persist a session to disk on the instance's own filesystem (operator-level) | `core.snapshot_session`, `core.load_session` | > **Note:** `snapshot_session` and `load_session` are disk-persistence operations that take a **server-side filesystem `path`** — they are operator-level operations on the instance itself, not part of the hosted API surface. Reversible in-memory what-if branching against `https://api.mubit.ai` is `create_session` + `drop_session`, with `commit_session` to merge a branch into its parent. ## Minimal implementation example #### Python ```python title="branch_what_if.py" from mubit import Client import os run_id = "claims:planner:claim-42" client = Client( endpoint=os.getenv("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], run_id=run_id, transport="http", ) client.checkpoint( session_id=run_id, label="before-strategy-branch", context_snapshot="Planner has two settlement strategies to compare.", metadata={"stage": "what-if"}, ) base = client.core.create_session({}) branch = client.core.create_session({"parent_session_id": base["session_id"]}) branch_id = branch["session_id"] # Explore the branch, then either merge it into the parent or discard it. # Merge back: client.core.commit_session({"session_id": branch_id, "merge_strategy": "overwrite"}) client.core.drop_session({"session_id": branch_id}) ``` #### Node / TS ```ts title="branch_what_if.ts" import { Client } from "@mubit-ai/sdk"; const runId = "claims:planner:claim-42"; const client = new Client({ endpoint: process.env.MUBIT_ENDPOINT ?? "https://api.mubit.ai", api_key: process.env.MUBIT_API_KEY, run_id: runId, transport: "http", }); await client.checkpoint({ session_id: runId, label: "before-strategy-branch", context_snapshot: "Planner has two settlement strategies to compare.", metadata: { stage: "what-if" }, }); const base = await client.core.createSession({}); const branch = await client.core.createSession({ parent_session_id: base.session_id }); const branchId = branch.session_id; // Explore the branch, then either merge it into the parent or discard it. // Merge back: await client.core.commitSession({ session_id: branchId, merge_strategy: "overwrite" }); await client.core.dropSession({ session_id: branchId }); ``` #### Rust ```rust title="branch_what_if.rs" use mubit_sdk::{CheckpointOptions, Client, ClientConfig}; use serde_json::json; #[tokio::main] async fn main() -> anyhow::Result<()> { let run_id = "claims:planner:claim-42"; // from_env() reads MUBIT_ENDPOINT / MUBIT_API_KEY. let client = Client::new(ClientConfig::from_env().run_id(run_id).transport("http"))?; let mut cp = CheckpointOptions::new("Planner has two settlement strategies to compare."); cp.run_id = Some(run_id.to_string()); cp.label = Some("before-strategy-branch".to_string()); cp.metadata = Some(json!({"stage": "what-if"})); client.checkpoint(cp).await?; // Core session ops are payload-style methods on `client.core`. let base = client.core.create_session(json!({})).await?; let branch = client .core .create_session(json!({"parent_session_id": base["session_id"]})) .await?; let branch_id = branch["session_id"].clone(); // Explore the branch, then either merge it into the parent or discard it. // Merge back: // client.core.commit_session(json!({"session_id": branch_id, "merge_strategy": "overwrite"})).await?; client .core .drop_session(json!({"session_id": branch_id})) .await?; Ok(()) } ``` ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Main path gets overwritten by experimentation | No durable checkpoint before branching | Always checkpoint the primary path first | | Scratch changes vanish too early | Branch was dropped before review | Snapshot or commit only after inspection | | Compaction loses the decision context | Branching happened without checkpointing | Save a checkpoint before diverging | ## Next steps - Apply the durable path in [Sessions and branching](/sdk/sessions-and-branching). - Combine branching with planner state at [Lane-scoped memory](/recipes/lane-scoped-memory). --- # Lane-Scoped Multi-Agent Memory > Isolate agent memory into named lanes so specialists see only their relevant context while sharing a common run. Isolate agent memory into named lanes so specialists see only their relevant context while sharing a common run. When multiple agents share a run, not every agent should see every piece of memory. Lanes partition the memory space so each agent reads only what it needs while still operating within the same session. ## Prerequisites - Mubit client initialized with a valid API key - A multi-agent system where agents have distinct responsibilities ## Flow 1. Register each agent with `shared_memory_lanes` declaring which lanes it participates in. 2. Ingest data with `lane=` to tag items to a specific lane. 3. Query with `lane_filter=` to retrieve only lane-scoped entries. 4. Context assembly respects the same `lane_filter` for token-budgeted prompts (a wire-level field — send it via `client.advanced.context({...})` in Python/JS or `GetContextOptions.lane` in Rust; the typed `get_context()` helper does not forward it). ## Minimal implementation example #### Python ```python title="lane_scoped_memory.py" from mubit import Client import os run_id = "multi-agent:task-42" client = Client( endpoint=os.getenv("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], run_id=run_id, transport="http", ) # Register agents with lane participation client.register_agent( agent_id="planner", role="planner", shared_memory_lanes=["planning", "shared"], write_scopes=["fact", "trace", "handoff"], ) client.register_agent( agent_id="executor", role="executor", shared_memory_lanes=["execution", "shared"], write_scopes=["fact", "trace", "observation"], ) # Planner writes to the planning lane client.remember( content="Task A depends on Task B completion before starting", intent="fact", lane="planning", agent_id="planner", ) # Executor writes to the execution lane client.remember( content="Task B completed in 2.3s with exit code 0", intent="observation", lane="execution", agent_id="executor", ) # Shared data visible to both client.remember( content="Project deadline is March 15", intent="fact", lane="shared", ) # Planner queries only planning context planning_context = client.recall( query="What are the task dependencies?", lane="planning", ) # Returns: Task A depends on Task B — but NOT the execution trace # Executor queries only execution context execution_context = client.recall( query="What tasks have completed?", lane="execution", ) # Returns: Task B completed — but NOT the planning dependencies # Either agent can query shared context shared_context = client.recall( query="What is the deadline?", lane="shared", ) # Query without lane_filter returns everything all_context = client.recall(query="task status", limit=10) ``` #### Node / TS ```js title="lane_scoped_memory.js" import { Client } from "@mubit-ai/sdk"; const runId = "multi-agent:task-42"; const client = new Client({ endpoint: process.env.MUBIT_ENDPOINT ?? "https://api.mubit.ai", apiKey: process.env.MUBIT_API_KEY, runId, transport: "http", }); await client.registerAgent({ agent_id: "planner", role: "planner", shared_memory_lanes: ["planning", "shared"], }); await client.remember({ content: "Task A depends on Task B", intent: "fact", lane: "planning", agent_id: "planner", }); // Query only the planning lane const result = await client.recall({ query: "task dependencies", lane: "planning", }); ``` ## How lanes interact with scopes Lanes and scopes are independent filtering mechanisms that compose: | Mechanism | What it filters | Set at | | --- | --- | --- | | **Lane** (`lane` / `lane_filter`) | Which memory partition an item belongs to | Ingest time / query time | | **Scope** (`read_scopes` / `write_scopes`) | Which entry types an agent can read or write | Agent registration time | | **User isolation** | Which authenticated user owns the data | Authentication time | An agent with `read_scopes=["fact", "lesson"]` and `lane_filter="planning"` sees only facts and lessons tagged with the `planning` lane. > **Note:** `lane` (MAS memory isolation) is distinct from the core data-plane retrieval lane concept (`direct_lane`). They serve different purposes and do not interact. ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Lane-filtered query returns nothing | Lane name mismatch between ingest and query | Verify the exact `lane` string matches at both ends | | Agent sees data from other lanes | Querying without `lane_filter` | An empty `lane_filter` returns all entries including unscoped ones | | Items without lanes are invisible to lane queries | Items ingested without `lane` are unscoped (lane `""`) | Unscoped items are **not** returned by a lane-filtered query — `""` never equals a named lane. Tag the item with that lane, or place shared data on an explicit `shared` lane and query that lane (as the example does). Only an empty `lane_filter` returns unscoped items. | | Context assembly ignores lanes | `lane_filter` not sent on the context request | `lane_filter` is a wire-level field the typed `get_context()` helper does not forward in Python/JS — send it via the raw passthrough (`client.advanced.context({...})`); in Rust set `GetContextOptions.lane` | ## Next steps - Review the full HTTP contract at [Control HTTP reference](/api-reference/control-http#lane-parameters). - See [Multi-Agent Shared State](/recipes/multi-agent-shared-state) for scope-based coordination without lanes. - See [Step-Level Outcomes](/recipes/step-level-outcomes) for per-step reward recording. --- # Step-Level Outcomes and Process Rewards > Record per-step reward signals during agentic runs so reflection produces richer, step-attributed lessons. Record per-step reward signals during agentic runs so reflection produces richer, step-attributed lessons. Run-level outcomes tell you whether the whole run succeeded. Step-level outcomes tell you which specific step caused success or failure. Mubit stores these signals and feeds them into reflection so lessons are attributed to the exact step that mattered. ## Prerequisites - Mubit client initialized with a valid API key - A multi-step agent run where each step produces an observable result ## Flow 1. Execute an agent step (tool call, LLM inference, decision). 2. Record a step outcome with signal, rationale, and optional directive hint. 3. Repeat for each step in the run. 4. Reflect with `include_step_outcomes` set to `true` to produce step-attributed lessons. This is a wire-level field on the reflect request — the typed `reflect()` helpers do not forward it, so use the low-level passthrough (`client.advanced.reflect({...})` in JS — and in Python from SDK v0.12.0 — or `ReflectOptions.include_step_outcomes` in Rust; on Python 0.11.0 and earlier send the field directly over gRPC/HTTP). 5. Use `record_outcome()` at the end for the overall run-level signal, passing the `reference_id` of the lesson, evidence item, or archive block the outcome is about. ## Minimal implementation example #### Python ```python title="step_outcomes.py" from mubit import Client import os run_id = "agent:planner:task-123" client = Client( endpoint=os.getenv("MUBIT_ENDPOINT", "https://api.mubit.ai"), api_key=os.environ["MUBIT_API_KEY"], run_id=run_id, transport="http", ) # Step 1: Planning plan = call_llm("Break down the task into sub-steps") client.record_step_outcome( step_id="step-1-planning", step_name="initial_planning", outcome="success", signal=0.8, rationale="Generated a clear 3-step plan with dependencies identified", directive_hint="Include sub-task dependencies explicitly in plans", ) # Step 2: Tool call tool_result = execute_tool("search_api", query="relevant docs") client.record_step_outcome( step_id="step-2-search", step_name="search_api", outcome="failure", signal=-0.6, rationale="Search returned no results — query was too narrow", directive_hint="Use broader search terms before narrowing", ) # Step 3: Recovery recovery = call_llm("Retry with broader query") client.record_step_outcome( step_id="step-3-recovery", step_name="search_retry", outcome="success", signal=0.9, rationale="Broader query found the target document", ) # Reflect to produce lessons. # # include_step_outcomes is a wire-level field on the reflect request — # the typed reflect() helper does not forward it. From SDK v0.12.0 use the # Python advanced passthrough: # client.advanced.reflect({"run_id": run_id, "include_step_outcomes": True}) # On 0.11.0 and earlier, send the field directly over the transport # (POST /v2/control/reflect), or use the JS/TS advanced passthrough shown # in the next tab. lessons = client.reflect() # Run-level outcome — reference_id is required and points at the lesson, # evidence item, or archive block this outcome is about. client.record_outcome( reference_id="", outcome="success", signal=0.7, rationale="Task completed after one retry", # Optionally attribute the same outcome to every recalled entry that # contributed; the primary reference_id is never double-counted. # entry_ids=["", ""], ) ``` #### Node / TS ```js title="step_outcomes.js" import { Client } from "@mubit-ai/sdk"; const runId = "agent:planner:task-123"; const client = new Client({ endpoint: process.env.MUBIT_ENDPOINT ?? "https://api.mubit.ai", apiKey: process.env.MUBIT_API_KEY, runId, transport: "http", }); // Step 1: Planning await client.recordStepOutcome({ step_id: "step-1-planning", step_name: "initial_planning", outcome: "success", signal: 0.8, rationale: "Generated a clear 3-step plan", directive_hint: "Include sub-task dependencies explicitly", }); // Step 2: Failed tool call await client.recordStepOutcome({ step_id: "step-2-search", step_name: "search_api", outcome: "failure", signal: -0.6, rationale: "Search returned no results — query too narrow", directive_hint: "Use broader search terms before narrowing", }); // Reflect with step outcomes. // The typed reflect() helper does not forward include_step_outcomes, so use // the low-level advanced passthrough, which sends the field over the wire. const lessons = await client.advanced.reflect({ run_id: runId, include_step_outcomes: true, }); ``` ## Field reference | Field | Type | Required | Description | | --- | --- | --- | --- | | `step_id` | string | yes | Unique step identifier within the run | | `step_name` | string | no | Human-readable label for the step | | `outcome` | string | yes | `success`, `failure`, `partial`, or `neutral` | | `signal` | float | no | Reward signal from -1.0 (worst) to 1.0 (best) | | `rationale` | string | no | Explanation of why the outcome was assigned | | `directive_hint` | string | no | Hindsight guidance for future runs | | `agent_id` | string | no | Agent that performed the step | | `metadata_json` | string | no | Arbitrary structured metadata | ## Combining with lane-scoped memory Step outcomes work naturally with lanes. If your multi-agent system uses lane-scoped memory, record step outcomes from each agent and then reflect with both lane and step context: ```python # Agent "planner" records its step outcomes in the run client.record_step_outcome( step_id="plan-v1", step_name="planning", outcome="success", signal=0.9, agent_id="planner", ) # Reflect across all step outcomes. include_step_outcomes is a wire-level # field the typed helper does not forward — use the advanced passthrough # client.advanced.reflect({"run_id": run_id, "include_step_outcomes": True}) # (Python from SDK v0.12.0; JS on all versions), or send it directly over # gRPC/HTTP (POST /v2/control/reflect) on earlier Python versions. client.reflect() ``` ## Failure modes and troubleshooting | Symptom | Root cause | Fix | | --- | --- | --- | | Reflection produces generic lessons despite step outcomes | `include_step_outcomes` not set, or dropped by the typed helper | Send `include_step_outcomes: true` as a wire-level field on the reflect request — `client.advanced.reflect({ run_id, include_step_outcomes: true })` in JS (and in Python from SDK v0.12.0); on Python 0.11.0 and earlier POST it directly over gRPC/HTTP (the typed `reflect()` helper does not forward it) | | Step outcome not accepted | Missing `step_id` or `outcome` | Both fields are required | | Lessons lack step attribution | Step outcomes recorded after reflection | Record step outcomes before calling `reflect()` | | Too many step outcomes dilute signal | Recording outcomes for trivial steps | Only record outcomes for decision-significant steps | ## Next steps - Review the full HTTP contract at [Control HTTP reference](/api-reference/control-http#step-level-outcome-request-shape). - Review the gRPC surface at [Control gRPC reference](/api-reference/control-grpc#step-level-outcomes). - See [Lane-Scoped Multi-Agent Memory](/recipes/lane-scoped-memory) for memory isolation patterns. --- # Temporal Memory Patterns > Use occurrence time, temporal range queries, staleness detection, and budget control for time-aware agent memory. Use occurrence time, temporal range queries, staleness detection, and budget control for time-aware agent memory. Mubit tracks two time dimensions for every memory entry: **ingestion time** (when the system learned it) and **occurrence time** (when the event actually happened). This distinction is critical for agents that reason about historical events. ## Storing events with occurrence time When ingesting facts about past events, set `occurrence_time` to record when the event happened. Without it, the system only knows when the fact was ingested. #### Python ```python import time # Event happened 3 days ago, ingested now. # From SDK v0.12.0: occurrence_time is a typed remember() kwarg — # client.remember(..., occurrence_time=int(time.time()) - 86400 * 3) # On earlier versions, pass it via metadata — the server reads the # `occurrence_time` key (Unix seconds) when ranking by recency. client.remember( content="New CI/CD pipeline reduced deployment time by 60%.", intent="fact", metadata={"occurrence_time": int(time.time()) - 86400 * 3}, ) # Historical event from January 2025 client.remember( content="Server migration to AWS completed with zero downtime.", intent="fact", metadata={"occurrence_time": 1736899200}, # Jan 15 2025 UTC ) ``` #### Node / TS ```js // Event happened 3 days ago, ingested now. // From SDK v0.12.0: occurrence_time (or occurrenceTime) is a typed // remember() option. On earlier versions, pass it via metadata — the // server reads the `occurrence_time` key (Unix seconds). await client.remember({ content: "New CI/CD pipeline reduced deployment time by 60%.", intent: "fact", metadata: { occurrence_time: Math.floor(Date.now() / 1000) - 86400 * 3 }, }); // Historical event from January 2025 await client.remember({ content: "Server migration to AWS completed with zero downtime.", intent: "fact", metadata: { occurrence_time: 1736899200 }, // Jan 15 2025 UTC }); ``` #### Rust ```rust use mubit_sdk::RememberOptions; use std::time::{SystemTime, UNIX_EPOCH}; // Event happened 3 days ago, ingested now. The Rust SDK exposes // `occurrence_time` (Unix seconds) as a first-class field on RememberOptions. let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let mut pipeline = RememberOptions::new("New CI/CD pipeline reduced deployment time by 60%."); pipeline.intent = Some("fact".to_string()); pipeline.occurrence_time = Some(now - 86_400 * 3); client.remember(pipeline).await?; // Historical event from January 2025 let mut migration = RememberOptions::new("Server migration to AWS completed with zero downtime."); migration.intent = Some("fact".to_string()); migration.occurrence_time = Some(1_736_899_200); // Jan 15 2025 UTC client.remember(migration).await?; ``` ## Querying by time range Use `min_timestamp` and `max_timestamp` to filter evidence to a specific time window. The filter checks `occurrence_time` first, falling back to ingestion time. #### Python ```python # "What happened in January 2025?" # From SDK v0.12.0, recall() forwards min_timestamp / max_timestamp # directly. On earlier versions, use the low-level query passthrough: results = client.advanced.query({ "run_id": run_id, "query": "What technical changes were made?", "min_timestamp": 1735689600, # Jan 1 2025 "max_timestamp": 1738367999, # Jan 31 2025 }) for evidence in results["evidence"]: print(f" {evidence['content'][:80]}") ``` #### Node / TS ```js const results = await client.recall({ query: "What technical changes were made?", min_timestamp: 1735689600, max_timestamp: 1738367999, }); results.evidence.forEach(e => console.log(e.content.slice(0, 80))); ``` #### Rust ```rust use mubit_sdk::RecallOptions; // RecallOptions exposes the temporal bounds as first-class fields. let mut recall = RecallOptions::new("What technical changes were made?"); recall.min_timestamp = Some(1_735_689_600); // Jan 1 2025 recall.max_timestamp = Some(1_738_367_999); // Jan 31 2025 let results = client.recall(recall).await?; if let Some(evidence) = results["evidence"].as_array() { for item in evidence { println!(" {}", item["content"].as_str().unwrap_or_default()); } } ``` Without temporal bounds, "What happened last week?" uses natural language temporal intent detection and prioritizes entries by occurrence time in the recency ranking. ## Handling stale facts When a newer fact contradicts an older one, Mubit marks the older entry as stale and deprioritizes it in ranking. The staleness metadata is available in evidence responses. #### Python ```python # Store contradicting facts client.remember( content="The office is in Building A, Floor 3.", intent="fact", metadata={"speaker": "facilities"}, ) # Later... client.remember( content="The office moved to Building B, Floor 7.", intent="fact", metadata={"speaker": "facilities"}, ) # Query — newer fact ranks higher, older is marked stale results = client.recall(query="Where is the office?") for evidence in results["evidence"]: stale = evidence.get("is_stale", False) status = " [STALE]" if stale else "" print(f" {evidence['content'][:60]}{status}") ``` #### Node / TS ```js const results = await client.recall({ query: "Where is the office?" }); results.evidence.forEach(e => { const status = e.is_stale ? " [STALE]" : ""; console.log(` ${e.content.slice(0, 60)}${status}`); }); ``` #### Rust ```rust use mubit_sdk::RecallOptions; // Query — newer fact ranks higher, older is marked stale let results = client.recall(RecallOptions::new("Where is the office?")).await?; if let Some(evidence) = results["evidence"].as_array() { for item in evidence { let stale = item["is_stale"].as_bool().unwrap_or(false); let status = if stale { " [STALE]" } else { "" }; println!(" {}{}", item["content"].as_str().unwrap_or_default(), status); } } ``` > **Note:** Stale entries are still returned for transparency. The ranking penalty ensures they appear below the current fact. Filter them out in your application if you only want current information. ## Budget control for latency-sensitive agents The `budget` parameter controls the depth of retrieval. Use `"low"` for real-time agents and `"high"` for accuracy-critical offline analysis. | Budget | Behavior | Typical latency | | --- | --- | --- | | `"low"` | Fewer candidates, skip deep traversal | < 500ms | | `"mid"` | Standard retrieval (default) | 500ms–2s | | `"high"` | More candidates, deeper graph traversal | 1–5s | #### Python ```python # From SDK v0.12.0, `budget` is a typed recall() kwarg. On earlier # versions, set it through the low-level query passthrough: # Fast retrieval for a real-time chatbot fast = client.advanced.query({ "run_id": run_id, "query": "user question", "budget": "low", }) # Deep retrieval for a research report deep = client.advanced.query({ "run_id": run_id, "query": "comprehensive analysis topic", "budget": "high", }) ``` #### Node / TS ```js // Fast const fast = await client.recall({ query: "user question", budget: "low" }); // Deep const deep = await client.recall({ query: "analysis topic", budget: "high" }); ``` #### Rust ```rust use mubit_sdk::RecallOptions; // `budget` is a first-class RecallOptions field in Rust. // Fast retrieval for a real-time chatbot let mut fast_opts = RecallOptions::new("user question"); fast_opts.budget = Some("low".to_string()); let fast = client.recall(fast_opts).await?; // Deep retrieval for a research report let mut deep_opts = RecallOptions::new("comprehensive analysis topic"); deep_opts.budget = Some("high".to_string()); let deep = client.recall(deep_opts).await?; ``` ## Next steps - See [SDK methods](/sdk/sdk-methods#temporal-and-quality-features) for the full parameter reference. - Use [mental models](/sdk/sdk-methods#mental-models) to store consolidated entity summaries. - Check [staleness detection](/sdk/sdk-methods#staleness-detection) for handling knowledge updates. --- # Prompt Optimization Lifecycle > Capture outcomes → optimize → diff → activate. A human-in-the-loop workflow for evolving agent prompts from real execution data. Capture outcomes → optimize → diff → activate. A human-in-the-loop workflow for evolving agent prompts from real execution data. Prompts drift. An agent that was well-tuned two weeks ago now misses cases, misroutes escalations, or over-hedges. The Mubit control plane ships an **optimization loop** that uses recorded outcomes to propose a better prompt, a diff view to review it, and a one-click approval to activate it — without touching deployed SDK code. This recipe shows the end-to-end flow. Every SDK step below has a **Console equivalent** inline — use the console when you want human-in-the-loop review and the SDK when you want to automate or schedule. Both paths call the same control-plane endpoints and produce identical `PromptVersion` rows. ## The loop at a glance ``` Run agents → Record outcomes → Optimize → Review diff → Activate ↑ │ └────── (next cycle) ────┘ ``` Every step is a single control-plane call. You can wire this into CI, a cron, or trigger it manually from the console's Agent Card. ## 1. Record outcomes while agents run Every interaction that ends with a judgeable result should call `record_outcome` (run-level) or `record_step_outcome` (per-step, for dense feedback). This is the signal the optimizer reads. ```python Python client.record_outcome( session_id=run_id, # falls back to the client's run_id if omitted reference_id=evidence_id, # the specific fact / lesson / archive block the outcome is about outcome="success", # "success" | "failure" | "partial" | "neutral" signal=0.8, # -1.0..1.0 rationale="Customer confirmed the refund was processed correctly", agent_id="triage", ) ``` For multi-step agents, also record per-step signal: ```python Python client.record_step_outcome( run_id=run_id, step_id="2026-04-17T09-12-route", step_name="routing", outcome="partial", signal=0.3, rationale="Routed to billing but should have gone to compliance", directive_hint="Check billing AND compliance scopes before routing", agent_id="triage", ) ``` > **Tip:** The optimizer weighs failures (`signal < 0`) and the `rationale` / `directive_hint` fields heavily. Invest in writing short, specific rationales — they become the material the synthesised candidate is built from. > **Note:** **Console equivalent:** outcomes are recorded from your agent code, not the console — the console reads them back under **Agents → your agent → Runs** (`/app/projects//agents//runs`). Even when you drive optimization entirely from the UI, the `record_outcome` / `record_step_outcome` call in your agent loop is still the signal source. ## 2. Trigger an optimization When you have enough outcomes to form an opinion (empirically: ~10–20 outcomes with at least a few negatives), ask the control plane to propose a candidate. ```python Python resp = client.optimize_prompt( agent_id="triage", project_id=project_id, ) candidate = resp["candidate"] print(resp["optimization_summary"]) # human-readable rationale print(resp["confidence"]) # 0..1 print(resp["activated"]) # False by default — human review first ``` > **Note:** **Steering the synthesis model:** you can override which model writes the candidate via the `llm` field (an `LlmOverride`), but **only over the gRPC transport** — the HTTP optimize endpoint (the SDK's default transport) ignores any override and uses the instance's default optimizer model, exactly like the console. To pass an override, construct a gRPC client and supply `llm`: > > ```python Python client = mubit.Client(transport="grpc") # override is dropped on the default HTTP transport resp = client.optimize_prompt( agent_id="triage", project_id=project_id, llm={ "provider": "anthropic", "model": "claude-sonnet-4-6", "temperature": 0.2, }, ) ``` > > From SDK v0.12.0 the typed `optimize_prompt()` helper does not forward `llm` — pass the override through the raw op instead: `client.advanced.optimize_prompt({"agent_id": ..., "project_id": ..., "llm": {...}})` (still gRPC-only for the override to take effect). On 0.11.0 the top-level pass-through shown above forwards it as-is. The response includes: - `candidate` — a new `PromptVersion` row with `status="candidate"` and `source="optimization"`. - `optimization_summary` — what the optimizer changed and why. - `confidence` — the optimizer's self-reported confidence. - `activated` — whether the candidate was auto-activated (default: `false`). > **Note:** **Console equivalent:** open the agent's **Prompts** tab (`/app/projects//agents//prompts`) and click `Suggest Optimization` on the **Active System Prompt** card. A new row appears in the Version History table with `status: candidate` and `source: optimization`, auto-expanded to show the candidate prompt, and a pending-candidate banner appears at the top of the page. The console uses the instance's default optimizer model — as does the SDK over its default HTTP transport. To pick a different synthesis model, use the gRPC transport with an `llm` override (see above). ## 3. Review the diff Never promote a candidate blind. Fetch the diff against the currently active version: ```python Python active = client.advanced.get_prompt(agent_id="triage") diff = client.advanced.get_prompt_diff( agent_id="triage", version_a_id=active["version"]["version_id"], version_b_id=candidate["version_id"], ) print(diff["diff_text"]) # unified diff format ``` **Console equivalent:** click `Review` on the pending-candidate banner, or `Compare` in the Version History row. That opens `/app/projects//agents//compare/` with the same `diff_text` rendered in a split view, the `optimization_summary` in a muted caption above the diff, and an `Approve & Activate` button at the top. What to check: 1. **Does the summary match the diff?** If the summary says "tightened escalation criteria" but the diff rewrites the tone, the optimizer hallucinated. 2. **Are edits localized?** Small, targeted edits ship safely. A full rewrite needs a canary. 3. **Does the outcome count justify the change?** The optimizer can synthesize a confident-looking candidate from 3 outcomes. Wait for more data. ## 4. Shadow test (optional but recommended) Before activating, run the candidate side-by-side with the active prompt on a known replay set. Use branching for reversibility: ```python Python # Snapshot current run so we can compare before / after checkpoint = client.checkpoint( session_id=run_id, label="pre-candidate-evaluation", context_snapshot="State before evaluating the candidate prompt.", ) # Run replay traffic. Capture outcomes for both branches. # (Your replay harness, not shown.) ``` Or, for a controlled canary, activate the candidate for a fraction of traffic by routing some runs to a duplicated agent with `agent_id="triage-canary"` whose prompt is the candidate. ## 5. Activate the winner Once you're satisfied, promote the candidate: ```python Python client.advanced.activate_prompt_version( agent_id="triage", version_id=candidate["version_id"], ) ``` Activation is atomic — in-flight runs continue with the old prompt; new runs see the new one. The previously active version transitions to `retired` and remains available for rollback. > **Note:** **Console equivalent:** click `Approve & Activate` on the compare page, or `Approve` on the pending-candidate banner in the Prompts tab. The console flips the status badges, retires the prior active version, and returns you to the Prompts tab — no further confirmation step. ## 6. Rollback if something breaks If the new prompt regresses, every prior version is still addressable. List versions, pick one, and reactivate: ```python Python versions = client.advanced.list_prompt_versions(agent_id="triage") prior_active = next( v for v in versions["versions"] if v["status"] == "retired" and v["source"] != "rollback" ) client.advanced.activate_prompt_version( agent_id="triage", version_id=prior_active["version_id"], ) ``` The newly activated version takes `source="rollback"` so your audit log reflects intent. > **Note:** **Console equivalent:** every retired version stays in Version History on the Prompts tab. Click `Compare` on a retired row to confirm the diff, then `Approve & Activate`. The activation is recorded with `source: rollback` just like the SDK path. ## Skill optimization Exactly the same loop works for skills — swap the method names: - `optimize_skill(project_id, skill_id)` — top-level like `optimize_prompt` (typed from SDK v0.12.0); an `llm` override only applies over the gRPC transport, and from v0.12.0 must go through `client.advanced.optimize_skill({...})` (the typed helper does not forward it) - `client.advanced.list_skill_versions(skill_id)` - `client.advanced.get_skill_diff(skill_id, version_a_id, version_b_id)` - `client.advanced.activate_skill_version(skill_id, version_id)` Skills include both `parameters_schema` and `instructions` in the diff, so review both sections of the unified diff. > **Note:** **Console equivalent:** open a project's **Skills** tab → pick a skill (`/app/projects//skills/`). The **Active Definition** card has separate editable fields for Description, Parameters Schema, and Instructions. `Suggest Optimization` creates a candidate; the compare page at `.../compare/` renders a unified diff across all three fields; `Approve & Activate` promotes it. ## Automating the loop A common pattern: run the optimize step nightly, but **never auto-activate**. Post the diff into a Slack channel or create a ticket for a human to approve. ```python Python # Cron: nightly per agent import mubit client = mubit.Client() for agent_id in ("triage", "billing", "escalation"): resp = client.optimize_prompt(agent_id=agent_id, project_id=PROJECT) if resp["confidence"] < 0.6: continue # too speculative; skip candidate = resp["candidate"] active = client.advanced.get_prompt(agent_id=agent_id) diff = client.advanced.get_prompt_diff( agent_id=agent_id, version_a_id=active["version"]["version_id"], version_b_id=candidate["version_id"], ) notify_slack(agent_id, resp["optimization_summary"], diff["diff_text"]) ``` ## Related pages - [Projects, Agents, Skills, Prompts](/sdk/projects-and-agents) — the resource model behind the lifecycle. - [Step-Level Outcomes](/recipes/step-level-outcomes) — dense reward signal that feeds better optimizations. - [Activity & Audit Trail](/sdk/activity-audit) — inspect what outcomes were available when the optimizer ran. --- # Agent Memory Skill — Direct Retrieval > A drop-in skill file that teaches any coding agent to use Mubit as persistent, cross-session memory — with fast direct (non-LLM) retrieval as the default path. A drop-in skill file that teaches any coding agent (Claude Code, Codex, Cursor, or a general-purpose agent) to use Mubit as persistent, cross-session memory — with the **direct, non-LLM retrieval path as the default**. The core insight: your agent is already an LLM, so it rarely needs Mubit's server-side query LLM to interpret memory for it. Direct retrieval returns the same ranked evidence — including cross-session lessons — with zero LLM calls, typically in 30–250 ms instead of the 4–9 s an agent-routed query takes. Live-benchmarked on hosted instances, direct recall is 30×+ faster than agent-routed recall with the same hit rate. ## Install the skill Download the skill file: [`SKILL.md`](https://docs.mubit.ai/skills/mubit-memory/SKILL.md) #### Claude Code ```bash mkdir -p .claude/skills/mubit-memory curl -o .claude/skills/mubit-memory/SKILL.md \ https://docs.mubit.ai/skills/mubit-memory/SKILL.md ``` The file's frontmatter (`name` + `description`) makes it a native Claude Code skill — Claude loads it automatically when a task involves remembering things across sessions. #### Other agents Hand the agent `SKILL.md` as context: a system-prompt include, an `AGENTS.md` reference, or a retrieval doc. The body is plain Markdown and reads correctly without the frontmatter. ```bash curl -o AGENTS-mubit-memory.md \ https://docs.mubit.ai/skills/mubit-memory/SKILL.md ``` The agent needs two values (from the [console](https://console.mubit.ai)): ```bash export MUBIT_ENDPOINT="https://" export MUBIT_API_KEY="mbt_..." ``` ## What the skill teaches ### Choosing a read path | Path | LLM calls | Embedding calls | Lesson overlay | Typical use | |---|---|---|---|---| | `lookup` (keyed) | 0 | **0** | no | exact reads by metadata match — fastest path (~1–60 ms) | | `core search` | 0 | 1 | no | raw semantic top-k, no extras | | **`recall` with `mode="direct_bypass"`, `evidence_only=True`** | **0** | 1–3 | **yes** | **default: ranked evidence + cross-session lessons, no LLM (~30–250 ms)** | | `recall` (default `agent_routed`) | 2 | several | yes | only when you want the server to write `final_answer` (~4–9 s) | | `get_context` | 2 | several | yes | pre-formatted prompt block — convenient, but pays full LLM cost | Rules of thumb: structured reads → `lookup`; "what do I know about X?" → direct-bypass evidence-only recall; never call agent-routed recall inside a loop. If a direct recall misses, rephrase and retry — two direct recalls still cost far less than one agent-routed call. ### The default recall #### Python ```python title="direct_recall.py" from mubit import Client client = Client() # reads MUBIT_ENDPOINT / MUBIT_API_KEY with client.run("sprint-42"): r = client.recall( query="How should I search for an invoice in the billing tool?", mode="direct_bypass", # skip the query-router LLM evidence_only=True, # skip the answer-synthesis LLM limit=8, ) for ev in r.get("evidence", []): print(ev["retrieval_mode"], round(ev["score"], 3), ev["content"][:80]) ``` #### HTTP ```bash curl -s "$MUBIT_ENDPOINT/v2/control/query" \ -H "Authorization: Bearer $MUBIT_API_KEY" -H 'Content-Type: application/json' \ -d '{"run_id":"sprint-42", "query":"How should I search for an invoice in the billing tool?", "mode":"direct_bypass","evidence_only":true,"limit":8}' ``` In this mode `final_answer` is empty by design — the ranked `evidence` array is the product, and cross-session lessons arrive as evidence items with `retrieval_mode: "lesson_overlay"`. The agent does the reasoning itself. > **Note:** `direct_bypass`, `core search`, and `lookup` are gated by the instance's direct-access policy dials (on by default on hosted instances). If an operator has disabled direct search, the fallback that always works is `recall(evidence_only=True)` **without** the mode flag — the router LLM runs, but synthesis is skipped and you still get the evidence array. ### Two write paths | Write path | Latency to accept | Visible after | Lookup-matchable metadata | |---|---|---|---| | `remember()` | ~30 ms (`wait=False`) | ~2–4 s/item, ordered queue | no | | `batch_insert` | ~0.2–1.5 s (synchronous) | immediately | **yes** | Use `remember()` for narrative memory retrieved semantically; use `batch_insert` for structured records fetched back by key with `lookup` — only `batch_insert` metadata becomes lookup match keys. After a burst of `wait=False` writes, ingestion is ordered per run, so waiting on the **last** job id is a complete write barrier. ### Cross-session lessons ```python client.remember( content="Always search billing by invoice ID; full-name search times out.", item_id="lesson-invoice-id", intent="lesson", lesson_type="success", lesson_scope="global", # the cross-session switch: run < session < global lesson_importance="high", ) ``` `lesson_scope="global"` is the guaranteed way to make a memory reachable from a brand-new, unlinked run. `reflect()`-extracted lessons start run-scoped and are promoted by the server over time — so when the agent learns something it needs in the *next* session, it should also write the lesson explicitly. ### Cross-session matching rules A memory written in session A surfaces in session B only if the read matches the write on all five: **scope** (same run, linked run, or lesson scope ≥ session), **`user_id`** (same value on both sides, or unset on both), **`entry_types`** (don't filter unless you know the stored type), **`lane`**, and **identity** (same API key — run ids are namespaced per user). ## Validation The skill was validated live against hosted instances (real embedding service, real LLMs, no mocks): three cold agents given only the skill file completed store → learn → cross-session-recall workflows on the first try, and direct-bypass recall matched agent-routed recall's hit rate (8/8 known-answer queries) at 30×+ lower latency. The full read-path latency ladder in the skill reflects those measurements. ## Related - **[Query patterns](/sdk/query-patterns)** — All recall modes and when to use each. - **[Step-level outcomes](/recipes/step-level-outcomes)** — Feed the learning loop with per-step outcomes. - **[Associative retrieval](/sdk/associative-retrieval)** — How Mubit ranks and overlays evidence. - **[Support agent loop](/recipes/support-agent-loop)** — A complete agent loop end to end. --- # Changelog Overview > User-visible Mubit documentation and integration updates. User-visible Mubit documentation and integration updates. This changelog tracks user-visible Mubit changes — SDK releases, API and runtime behavior, framework integrations, and documentation. > **Note:** Two version lines appear below. **SDK vX.Y.Z** is the Python / JavaScript / Rust package version (the three SDKs are released in lockstep). **Runtime vX.Y.Z** is the hosted service release tag for `api.mubit.ai`. They advance independently. ## 2026-07-16 — SDKs v0.12.0 - **Bi-temporal writes everywhere**: `remember(..., occurrence_time=...)` forwarded by the typed helper in Python and JavaScript (Rust already had it). - **Recall parity with the wire**: `budget`, `rank_by`, `explain`, `min_timestamp`/`max_timestamp`, and `evidence_only` forwarded by the typed `recall` in all three SDKs. - **Typed** `circuit_break`, `record_step_outcome`, `optimize_prompt`, `optimize_skill` in Python/JS. - **Run scoping**: `with client.run(...)` (Python, sync + async), `client.withRun(...)` (JS), `client.run_scope(...)` + `end_with_reflect()` (Rust). - **Receiver-side handoffs**: `receive_handoffs(agent_id=...)` inbox and `handoff(..., await_verdict=True)` verdict polling. - `remember(..., share="run"|"session"|"global")` scope alias (`"org"` is rejected — org scope is promotion-gated); `record_outcome(..., idempotency_key=...)` in all three. - Rust: raw control ops move to `client.advanced()`; the flat delegates are deprecated. - Fixes: Rust `record_step_outcome` was mis-wired to the run-level outcome endpoint; the published JS CommonJS entry (`index.cjs`) was syntactically broken; Python gRPC enum normalization restored. ## 2026-07-15 — SDKs v0.11.0 · Runtime v0.11.0 ### SDKs (Python, JavaScript, Rust) — v0.11.0 - Proto contract sync: the bundled `control.proto` now matches the server contract exactly, adding the `evidence_only` query field (return retrieved evidence without answer synthesis) to the typed gRPC surface. ### Runtime v0.11.0 — continual learning on by default - Seven continual-learning features are now enabled by default on hosted instances: bi-temporal supersession, relevance-gated lesson injection, sleep-time consolidation, workflow induction, guardrail distillation, shadow-gated scope promotion, and write-time lesson reconciliation. Each can be disabled per instance. ## 2026-06-05 — SDKs v0.10.0 — `mubit.learn` loop closure - **Python** — `mubit.learn` outcome attribution and context fetching hardened: lesson injection no longer silently no-ops on slow context fetches (the timeout is now configurable), `feedback()` reliably credits the entries recalled for the current span, and automatic capture never fabricates outcomes. - **JavaScript & Rust** — the learn loop is closed in both SDKs: `feedback()` credits recalled entries and the context-fetch timeout is configurable. (JS/Rust `learn` covers injection + loop closure; automatic trace ingest remains Python-only.) - Hermetic `learn` test suites added for JavaScript and Rust. ## 2026-06-04 — SDKs v0.9.0 · Runtime v0.7.0 ### SDKs (Python, JavaScript, Rust) — v0.9.0 - Contract and proto sync across all three SDKs (continual-learning alignment; gRPC dependency bump). `PrewarmRun` exposed in the typed core gRPC surface. ### Runtime v0.7.0 — retrieval accuracy - Major retrieval-accuracy release: exact reranking is on by default, with improved cross-run fusion and sparse recall. No API changes required to benefit. ## 2026-06-03 — Faster cold starts: PrewarmRun - New `PrewarmRun` RPC and `POST /v2/core/runs/:run_id/prewarm` route warm a run's index ahead of the first query, cutting first-query latency on cold or recently-evicted runs. Exposed across the SDKs. ## 2026-05-25 — SDKs v0.8.0 · Runtime v0.6.3 ### SDKs (Python, JavaScript, Rust) — v0.8.0 - Version bump consolidating the `learned()`, `include_step_outcomes`, and environment-tag work below into a single release across all three packages. ### Retrieval ranking fix - Cross-run global lessons no longer crowd out results specific to the current run, and knowledge-confidence is now factored into answer synthesis. ### Integrations - `@mubit-ai` JavaScript integration packages published at **v0.7.0**. ## 2026-05-22 — Runtime v0.6.2 — Environment tags, knowledge confidence, verified-in-production, `learned()` - **Environment tags** — attach semantic environment tags to lessons and facts in `TYPE:NAME:VERSION` form (e.g. `["lang:python:3.12", "framework:langchain:0.3"]`) so retrieval can prefer evidence from a matching stack. Available as the `env_tags` field on remember/ingest. - **Knowledge confidence** — query evidence now carries a `knowledge_confidence` score that feeds ranking and the synthesis prompt. - **Verified-in-production** — `record_outcome(verified_in_production=True)` marks an outcome as confirmed in production and gives the associated entries a retrieval boost. - **`learned()`** — a convenience helper across all three SDKs for storing a production-tested lesson in one call. Also exposed as the `mubit_learned` MCP tool. ## 2026-05-19 — SDK: step-outcome-aware reflection - The SDK `reflect` surface gained the `include_step_outcomes` option, so reflection can fold per-step outcome signals into lesson extraction. (Over the typed Python helper this remains a wire-level field — pass it through the low-level control op; the JS/Rust clients accept it directly.) ## 2026-04-27 — Console billing + integration updates - **Console (hosted)** — the dashboard gained a usage and credit-balance view. - Python integration packages published at **v0.5.2** / **v0.5.3** for SDK v0.7.0 compatibility. - New example: Google ADK BigQuery agent with Mubit memory. ## 2026-04-24 — Streaming events, resilience & SDKs v0.7.0 ### Real-time event streaming - **Pub/sub streaming** — subscribe to control and core events over SSE or gRPC server-streaming, with a consistent event shape across both transports and automatic cleanup when a subscriber disconnects. - **Reflection fix** — a freshly reflected lesson now returns its `lesson_id` directly, so you no longer have to re-list lessons to correlate it. - **Drift monitoring** can track a registered agent's objective and fold in per-step outcomes. ### Resilience & durability - The service now self-heals from transient infrastructure blips (e.g. a cache or coordinator restart) without manual intervention. - Agent **prompt-version history now persists across service restarts**. Additive — no wire change, no new configuration. ### SDKs (Python, JavaScript, Rust) — v0.7.0 - Normalized pub/sub events: payloads and enum values decode to consistent strings, so a streamed event reads the same shape regardless of transport (`for await … of client.subscribe(...)` in JS; streamed events in Python and Rust). ## 2026-04-23 — Production hardening + new adapters ### Reliability - Stronger availability and durability defaults: readiness/liveness health checks, graceful shutdown that flushes pending writes, configurable CORS and request timeouts, and control-plane retry backoff. ### Integrations - Python `mubit-langchain` and `mubit-llama-index` memory/store adapters published (**integrations-python v0.5.1**). ## 2026-04-21 — SDKs v0.6.0 — Automatic retries + Run Monitor - **Automatic retries** — all three SDKs now retry transient transport failures and `5xx` responses with exponential backoff and jitter, tunable via `MUBIT_RETRY_ATTEMPTS` / `MUBIT_RETRY_BASE_MS` / `MUBIT_RETRY_CAP_MS` / `MUBIT_RETRY_JITTER`. See [Retries and idempotency](/sdk/retries). - **Run Monitor** — a new `GetRunSignal` op surfaces a per-run health signal (e.g. looping or drift) that agents can poll mid-run. ## 2026-04-19 — Grouped option builders + more integrations - SDKs introduce grouped metadata structures and fluent builders for `Remember` and `Archive` options, so optional fields (metadata, hints, lesson attributes, lane, idempotency key) are set without long positional argument lists. - New integrations for Agent Lightning and AutoGen. ## 2026-04-18 — `AlreadyExistsError` for HTTP 409 - The SDKs now raise a dedicated `AlreadyExistsError` (a subclass of `ValidationError`) on `409 Conflict` — e.g. creating a project, agent, or skill whose id already exists — so duplicate-create can be caught distinctly from other validation failures. See [Errors](/api-reference/errors). ## 2026-04-17 — v0.6.0 — Flat client surface (breaking) ### SDKs (Python, JavaScript, Rust) - Every control-plane operation now lives directly on `Client`. Call `client.create_project(...)`, `client.set_prompt(...)`, `client.optimize_prompt(...)` — the `.control.` step is gone. - High-level helpers (`remember`, `recall`, `checkpoint`, `reflect`, `record_outcome`, …) are unchanged and continue to wrap the 10 identically-named raw ops with session resolution and richer defaults. Helper names win over the raw ops at name-collision sites. - Admin and low-level storage ops remain under `client.auth.*` and `client.core.*`. - **Migration:** global find-and-replace `client.control.` → `client.` and re-run tests. Three Core↔Control overlaps (`batch_insert`, `create_session`, `delete_run`) now resolve to the control version at `client.()`; call `client.core.()` explicitly for the core variant. - The Rust SDK also gains typed methods for 34 previously-missing control ops (Projects, Agent Definitions, Skills, Prompts, Sessions, Run History, activity listing/export, `record_step_outcome`, `get_run_ingest_stats`) — Rust is now at parity with Python and JavaScript. All three packages ship at **v0.6.0**. ### Version compatibility When upgrading `mubit-sdk` to `0.6.0`, upgrade the framework integration packages to their matching releases. The Python adapters all declare `mubit-integration-base>=0.5.1`, which in turn pins `mubit-sdk>=0.6.0`, so a mismatched upgrade will fail at install time rather than at runtime. | SDK | Version | Matching integrations | | --- | --- | --- | | `mubit-sdk` (Python) | `0.6.0` | `mubit-integration-base 0.5.1`, `mubit-crewai 0.5.1`, `mubit-langgraph 0.5.1`, `mubit-langchain 0.5.1`, `mubit-llama-index 0.5.1`, `mubit-adk 0.5.1`, `mubit-agno 0.5.1` | | `@mubit-ai/sdk` (JS) | `0.6.0` | `@mubit-ai/langgraph`, `@mubit-ai/ai-sdk`, `@mubit-ai/mcp` | | `mubit-sdk` (Rust crate) | `0.6.0` | — | Recommended upgrade flow: ```bash # Python pip install --upgrade mubit-sdk==0.6.0 \ mubit-integration-base==0.5.1 \ mubit-langgraph==0.5.1 mubit-langchain==0.5.1 \ mubit-crewai==0.5.1 mubit-adk==0.5.1 \ mubit-agno==0.5.1 mubit-llama-index==0.5.1 # JavaScript npm install @mubit-ai/sdk@0.6.0 \ @mubit-ai/langgraph @mubit-ai/ai-sdk @mubit-ai/mcp # Rust cargo add mubit-sdk@0.6.0 ``` ## 2026-04-17 — Managed resources + Project-first console ### New API documentation - **Projects, Agent Definitions, Skills, Prompts** are now documented as first-class managed resources. See [Projects, Agents, Skills, Prompts](/sdk/projects-and-agents). - **Prompt and Skill version lifecycle** — candidate → active promotion, LLM-powered optimization, version diff — are covered end-to-end. - New recipe: [Prompt Optimization Lifecycle](/recipes/prompt-optimization) — record outcomes → optimize → review diff → activate → rollback. - Control HTTP reference expanded with full route tables for `/v2/control/projects/*`, `/v2/control/projects/agents/*`, `/v2/control/skills/*`, `/v2/control/prompt/*`, and `/v2/control/sessions/*`. See [Control HTTP — Managed resources](/api-reference/control-http#managed-resources-projects-agents-skills-prompts). - Control gRPC reference expanded with the matching RPC groups (`CreateProject`/`...`, `SetPrompt`/`OptimizePrompt`/`ActivatePromptVersion`/`GetPromptDiff`, `CreateSkill`/`OptimizeSkill`/`...`, `CreateControlSession`/`...`). See [Control gRPC — Managed resources](/api-reference/control-grpc#managed-resources-projects-agents-skills-prompts). ### Deprecations (no runtime impact) - Goals, Actions, and Decision Cycle routes (`/v2/control/goals/*`, `/v2/control/actions/*`, `/v2/control/cycles/*`) are now formally marked **deprecated** in [State management](/api-reference/state-management). Existing deployments continue to work; new integrations should track goals/actions in external task and orchestration systems (Linear, LangGraph, CrewAI) and feed resulting lessons / outcomes into Mubit. - Variables and Concepts remain supported. ### Console - The user console is now organized around **Projects** as the top-level concept, with Agent Cards, Prompt versioning, Skill versioning, per-project Memory, Sandbox, Logs, and Settings. The SDK surface is unchanged — the new resource model is what the console renders against. ## 2026-04-02 — Security hardening - Stricter API-key validation and safer SDK install behavior (npm `ignore-scripts`). ## 2026-03-25 — Runtime v0.5.2 — Read-path quality - Retrieval and consistency fixes across the read path: a `rank_by` ranking parameter (`relevance` / `freshness` / `balanced`), an `explain` mode that returns per-evidence score components, temporal decay in scoring, a context recency fallback, lessons/health consistency, promotion dedup, and activity filtering — with matching SDK field alignment. All additive. ## 2026-03-23 — v0.5.1 (Hindsight features + scaling) ### New SDK features - **Temporal range queries** — the `min_timestamp` / `max_timestamp` query fields filter evidence by when events occurred, not when they were ingested. They are wire-level fields, not typed `recall()` kwargs — pass them through the low-level query op (`client.advanced.query({..., "min_timestamp": ..., "max_timestamp": ...})`). See [temporal queries](/sdk/sdk-methods#temporal-queries). - **Occurrence time tracking** — the `occurrence_time` field records when an event happened, separate from ingestion time. It travels in item metadata, not as a typed `remember()` kwarg — set it via `remember(metadata={"occurrence_time": ...})` or on the raw ingest item (`client.advanced.ingest({...})`). See [occurrence time](/sdk/sdk-methods#occurrence-time). - **Search budget control** — the `budget` query field (`"low" | "mid" | "high"`) controls the latency/quality tradeoff. It is a wire-level field, not a typed `recall()` kwarg — pass it through the low-level query op (`client.advanced.query({..., "budget": "mid"})`). See [search budget](/sdk/sdk-methods#search-budget). - **Staleness metadata** — Query evidence now includes `is_stale` and `superseded_by` fields. Stale entries are automatically deprioritized in ranking. See [staleness detection](/sdk/sdk-methods#staleness-detection). - **Mental model entry type** — `remember(intent="mental_model")` stores consolidated entity summaries that are prioritized over raw facts in context assembly. See [mental models](/sdk/sdk-methods#mental-models). ### Server improvements - Per-run disk-backed index pools — memory stays bounded regardless of data volume. - LLM telemetry with Prometheus metrics (`mubit_llm_calls_total`, `mubit_llm_tokens_total`, `mubit_llm_call_duration_seconds`). - Connection pooling, write tuning, and client-side LLM rate limiting for 200+ concurrent agent workloads. - Event stream trimming, storage health monitoring, and disk usage metrics. - Sanitized error messages — no internal implementation details exposed to SDK clients. - New `xlarge` instance plan tier (8 CPU, 16 GiB) for high-throughput workloads. ### No breaking changes All new fields are optional with sensible defaults. Existing code continues to work without modification. ## 2026-03-16 — Documentation sync - Added SDK Configuration Reference page covering env vars, transport selection, and endpoint defaults. - Added gRPC Transport Guide explaining when to use gRPC vs HTTP, endpoint configuration, and TLS. - Added Activity & Audit Trail documentation for `listActivity`, `exportActivity`, and `appendActivity`. - Added missing SDK methods to the SDK methods reference: activity, ingest job tracking, run management, context snapshot. - Added missing HTTP routes to the Control HTTP reference: activity, runs, ingest stats, heartbeat, context/snapshot. - Added missing gRPC RPCs to the Control gRPC reference: full variable, concept, action, and cycle RPCs. - Added core route reference to Core Direct Lanes: SDM, scratchpad, sessions, storage, ACL, PubSub. - Added Troubleshooting / FAQ page covering common issues. - Corrected the GitHub repository links on the Framework Integrations page. ## 2026-03-14 — v0.4.1 (JS SDK patch) - Fixed `keepCase` gRPC field casing in the JavaScript SDK to preserve proto field names. ## 2026-03-14 — Integration packages v0.1.0 - Published framework integration packages: - Python: `mubit-crewai`, `mubit-langgraph`, `mubit-langchain`, `mubit-adk` - JavaScript: `@mubit-ai/langgraph`, `@mubit-ai/ai-sdk`, `@mubit-ai/mcp` - TLS enabled on `api.mubit.ai` and `api.dev.mubit.ai`. > **Note:** **Subsequent additions (shipped at 0.5.1 alongside the SDK v0.6.0 release):** `mubit-integration-base` (shared client used by every Python adapter), `mubit-llama-index` (chat-memory + vector store), and `mubit-agno` (MemoryDb + Toolkit). See the [Version compatibility](#version-compatibility) table under v0.6.0. ## 2026-03-13 — v0.4.0 (MAS Features) - Added step-level outcome recording: `RecordStepOutcome` RPC, `/v2/control/step_outcome` HTTP route, `record_step_outcome()` SDK helper. Records per-step process reward signals (signal, rationale, directive hint) for dense RL within a run. - Added lane-scoped memory: `lane` field on `IngestItem`, `lane_filter` on queries and context assembly, `shared_memory_lanes` on agent registration. Enables multi-agent memory isolation within a shared run. - Added step-wise reflection: `step_id`, `last_n_items`, and `include_step_outcomes` fields on `ReflectRequest` for targeted, incremental lesson extraction scoped to recent evidence or a specific step. - Added auto-extraction in `mubit.learn`: heuristic extraction of rules, lessons, preferences, and facts from LLM responses without an extra LLM call (`auto_extract=True` in learn config). - New cookbook: [Step-Level Outcomes and Process Rewards](/recipes/step-level-outcomes). - New cookbook: [Lane-Scoped Multi-Agent Memory](/recipes/lane-scoped-memory). ## 2026-03-10 — SDK: archive, dereference & activity audit - Added `archive` and `dereference` — store a full artifact (long document, transcript, plan) and pull it back verbatim by reference, instead of round-tripping it through memory. - Added the `ListActivity` and `ExportActivity` RPCs (`/v2/control/activity`, `/v2/control/activity/export`) for a chronological, filterable browse and export of a run's memory/activity. These are HTTP/gRPC surfaces, not typed SDK helpers. ## 2026-03-05 — Reflection, lessons & multi-agent auto-capture - **Reflection & lessons** — `reflect()` extracts reusable lessons from a run's evidence, with lesson listing/deletion and the run→session→global promotion ladder. - **Auto-capture for `mubit.learn`** — three interception tiers that capture lessons from existing LLM calls (Anthropic, OpenAI, LiteLLM, Google GenAI) without rewriting agent code. - Control-plane primitives for agent registration, handoffs/feedback, and `checkpoint` for compaction-safe continuity. ## 2026-02-22 — SDKs v0.1.0 — Initial release - First public **Python, JavaScript, and Rust** SDKs, each speaking both gRPC and HTTP with a shared operation contract and `MUBIT_ENDPOINT` / `MUBIT_API_KEY` configuration. - Backed by the core retrieval engine: high-performance (HNSW) semantic search, run-scoped isolation via `run_id`, and durable persistent storage, with the control-plane memory primitives `remember` / `recall` / `get_context`. ## 2026-02-20 — Documentation - Launched the SDK-first documentation site, including a full SDK method catalog that maps every operation across the Python, JavaScript, and Rust method names. - Added the first cookbook guides — [Event-driven agents](/recipes/event-driven-agents) and [Branching memory](/recipes/branching-memory) — plus coverage of linked runs (`include_linked_runs`, `link_run` / `unlink_run`) and control event subscriptions. ## Next steps - Review SDK onboarding at [/](/). - Review API mapping at [Control HTTP reference](/api-reference/control-http).