mi4afa Probing
The ag2.extensions.mi4afa module attributes failed multi-agent runs to their decisive step and responsible agent by reading an open-weight model's internal activations. It is the white-box counterpart of the core failure_attribution scorer: instead of asking a judge model which step went wrong, it trains a small linear probe on labelled failures and reads the answer out of the model's hidden states.
It targets the setting of the Who&When benchmark, where judge models asked to name the failing step usually answer wrongly, yet the correct step is often linearly decodable from a model's activations while it reads the transcript.
Installation#
The extension needs PyTorch, Hugging Face Transformers and safetensors, plus an open-weight chat model (for example Qwen/Qwen2.5-0.5B-Instruct). A GPU is recommended for anything beyond small models.
Without PyTorch installed, the data types (Conversation, Turn, PromptTemplate, conversation_from_trace, subagent_names) still import; the probing classes raise an ImportError with the install command.
Quick start#
Train on labelled failures, select a layer on a validation split, and score held-out data. Records in the Who&When format — question, ground_truth, a history of {"name", "content"} steps, mistake_step and mistake_agent — load directly:
attribute returns the core Attribution model. The probe localizes a failure but does not classify it, so error_mode is "other".
Use it as an eval scorer#
probe_failure_attribution wraps a fitted attributor as an ag2.eval scorer. Each run's trace is converted into a conversation, the probe blames one turn, and the scorer emits a Feedback whose value is the responsible agent — so value_counts gives the blame distribution — and whose detail is the Attribution with decisive_step pointing into trace.events, as in the core scorer:
The model forward pass runs in a worker thread, so the event loop stays responsive.
A run that succeeded is blamed too
The probe ranks the turns of every conversation it reads, so it names a decisive step and a responsible agent even for a run that succeeded. Each successful run you score adds an agent to value_counts as if that agent had failed. Score only runs that failed, or read the blame next to a correctness scorer and keep the runs it marks as wrong.
Trace conversion is best effort
A trace records one agent's stream and its events carry no speaker. conversation_from_trace turns the traced agent's text responses and tool calls into turns by agent_name, and tool results and errors into turns by tool:<name>.
Sub-agents need a mapping. A trace rebuilt from spans — what run_agent and the OpenTelemetry trace sources produce — holds no sub-task events, so a sub-agent's work appears only as its delegation tool's result. Pass subagents={tool name: agent name} to make those results turns by the sub-agent; subagent_names(planner) builds the mapping from the delegation tools in planner.tools, including custom tool names. Without it, a delegated result is blamed on tool:task_<agent> and the sub-agent is never named. A trace you build yourself from an agent's stream also holds TaskCompleted / TaskFailed events, which become turns by the sub-agent that ran them.
Only results are mapped. A delegation tool returns a result after running its sub-agent, and a sub-agent that fails comes back as a Sub-task '<name>' failed: ... result, which is the sub-agent's turn. A delegation tool's error is raised outside the sub-agent — for example, when the caller's arguments fail validation and the sub-agent never starts — so it stays a tool:<name> turn, next to the caller's call that caused it. subagent_names also leaves out delegation tools with their own middleware, because middleware such as approval_required can return a result, like a denial, without starting the sub-agent.
The sub-agent's own steps are not in its parent's trace, so its result stands for its whole sub-task. subagent_names does not see tools passed to ask(...) rather than set on the agent; map those yourself. A background_agent_tool returns a task id, not the sub-agent's answer, so it is not mapped. For full control, build Conversation objects yourself and call attributor.attribute(...).
How it works#
- Prompt. Each conversation is rendered as a judge prompt (task, expected answer, then one numbered line per turn) with the model's chat template.
PromptTemplateholds the wording. - Activations.
ActivationExtractorruns the model's decoder once per conversation and reads, at the token holding each turn's last character, every site:resid_preat layer l — the residual stream entering layer l,mlp_inat layer l — the normalized post-attention residual entering layer l's MLP,resid_final— the final hidden state after the last norm.
- Probe. A standardized, class-weighted
LogisticProbescores each turn as "is this the mistake?". The highest-scoring turn is the decisive step, and its speaker the responsible agent. - Site selection.
fittrains one probe per site on part of the training data and keeps the site with the best validation step accuracy, then refits on all of it. Test data never influences the choice.
A probe needs at least two training turns, and at least one that is not the mistake, so training on one-turn conversations alone raises a ValueError, as do activations that are not finite. A probe whose score for some turn is NaN or infinite raises instead of naming a step, so the scorer reports an error rather than blaming an agent.
A conversation whose prompt is longer than the model's context, as declared by its max_position_embeddings, is refused with a ValueError instead of being read at positions the model was not trained on. Leave such conversations out before calling fit.
Turn tokens are located from the tokenizer's character offsets, so a fast tokenizer is required. Chat templates that embed the current date (Llama 3) change the prompt from day to day; pin them for reproducible activations:
Reuse activations#
Extraction dominates the cost. Extract once and pass the activations to fit, evaluate and sweep_sites, which accept them in place of conversations:
sweep_sites scores every site on the test set, which shows where the information lives but is optimistic if used to pick a site — use fit + evaluate for an unbiased number.
Save and load#
The probe is stored as probe.safetensors (no pickle) next to probe.json, which records the model id and architecture, site, prompt and chat-template variables. load restores the same prompt settings. It refuses a model whose architecture differs from the probe's model, and warns when the same architecture was loaded from another name or path, such as a local copy of the Hub model for an offline machine. The check reads the model's config, so two models that share an architecture but not their weights are not told apart.
Reference#
| Name | Purpose |
|---|---|
Conversation, Turn | A multi-agent conversation, optionally labelled with mistake_step / mistake_agent; Conversation.from_who_and_when(record) |
PromptTemplate, build_prompt | Judge-prompt wording and tokenization with per-turn token positions |
ActivationExtractor, ActivationSite | Read per-turn activations at chosen sites of a Hugging Face causal LM |
LogisticProbe | The standardized logistic probe |
ProbeAttributor | fit / evaluate / sweep_sites / attribute / save / load |
conversation_from_trace | Convert an ag2.eval Trace into a Conversation |
subagent_names | Map an agent's delegation tools to the sub-agents they run, for subagents= |
probe_failure_attribution | The ag2.eval scorer |
Testing#
The bundled unit tests build a tiny random Llama and an in-memory tokenizer, so they run offline on CPU without downloading any model.
Maintainer: @wuliang211 (Liang Wu). Based on research by Wendy Zheng.