--- title: "Context" description: "How data flows between workflow stages" --- Every workflow run has a **context** — a shared key-value store that carries data between stages. When an agent writes code, a command captures test output, or a human makes a selection, the results are recorded as context updates. Downstream nodes can read these values to make decisions, build prompts, and route execution. ## How context flows The context starts empty at the beginning of a run. As each stage completes, its outcome includes a set of **context updates** — key-value pairs that are merged into the shared context. Later stages see all updates from earlier stages. ``` Start → Plan → Implement → Test → Exit │ │ │ │ │ └─ sets command.output │ └─ sets response.implement, last_response └─ sets response.plan, last_response ``` Context is thread-safe and shared across the entire run. Parallel branches receive an isolated **deep copy** of the context at the point of fan-out, so branches can't interfere with each other. The parallel handler gathers their results under `parallel.results`. ## How agents access context Agents do not have a tool to query the context store directly. Instead, context is made available through two mechanisms: - **Preamble injection** — When a node starts, Fabro assembles a preamble from the current context and prepends it to the node's prompt. The [fidelity setting](#fidelity-controlling-agent-context) controls how detailed this preamble is. - **Context updates** — Agents can write to the context by including a `context_updates` object in a JSON response. See [Transitions](/workflows/transitions#agent-transitions) for the response format. ## Keys set by handlers Each handler type writes specific keys into the context after execution: ### Agent and prompt nodes | Key | Value | |---|---| | `last_stage` | The node ID of the stage that just completed | | `last_response` | Truncated LLM response (first 200 characters) | | `response.{node_id}` | Full LLM response text | Agents can also emit arbitrary context updates by including a JSON object with a `context_updates` field in their response. See [Transitions](/workflows/transitions#agent-transitions). The `review_target` key has an optional typed convention for human review workflows. A human gate with `review_target=true` reads this exact flat key and presents its document URL as the primary question link. See [Review targets](/workflows/human-in-the-loop#review-targets). ### Command nodes | Key | Value | |---|---| | `command.output` | The command's ordered output stream. Durable context stores this as a `blob://sha256/...` ref after the command completes; downstream prompts resolve it back to text. | ### Human gates | Key | Value | |---|---| | `human.gate.selected` | The accelerator key (e.g. `"A"`) or `"freeform"` | | `human.gate.label` | The full label of the selected edge | | `human.gate.text` | The user's freeform text (if applicable) | | `human.gate..question` | The question text for a specific human gate node | | `human.gate..answer` | The answer text for a specific human gate node | | `human.gate..label` | The selected label for a specific human gate node, when applicable | ### Parallel fan-out and fan-in | Key | Value | |---|---| | `parallel.results` | Ordered branch results. Each entry contains `id`, `index`, optional `item_label`, `status`, and the branch's isolated `context_updates`. Legacy results may omit `index`. | | `parallel.branch_count` | Number of branches dispatched. For `for_each`, this is the runtime array length. | Branch updates remain nested inside `parallel.results`; they are not merged into top-level context. Prompted fan-in nodes can synthesize the complete result array, while promptless fan-in nodes act as barriers. ### Runtime arrays with `for_each` A parallel node can read a flat context key and run one agent or prompt target per array item: ```dot batch [shape=component, for_each="context.candidates"] batch -> reviewer ``` `context.candidates` first checks the exact key and then falls back to `candidates`. The source must be a JSON array, either inline or stored behind a Fabro-managed `blob://` or `file://` reference. General nested lookup such as `output.scan.candidates` is not supported; have the producing node write the array to a flat context key such as `candidates`. Each item receives a separate context fork, while `(id, index)` identifies its result. The item itself is not copied into `parallel.results`. ### Engine-managed keys The engine sets several keys automatically. These are prefixed with `internal.` and are excluded from preambles: | Key | Value | |---|---| | `internal.run_id` | Unique identifier for this run | | `internal.work_dir` | Working directory path | | `internal.fidelity` | The resolved fidelity mode for the current node | | `internal.thread_id` | Thread ID for shared-conversation nodes (or null) | | `internal.node_visit_count` | How many times the current node has been visited | | `internal.retry_count.{node_id}` | Number of retry attempts used by a node | | `outcome` | Status of the last completed stage (`succeeded`, `failed`, etc.) | | `failure_class` | Classification of the last failure (if any) | | `failure_signature` | Deduplication signature for the last failure | | `preferred_label` | Label selected by a human gate or agent routing directive | | `current_node` | ID of the node currently executing | | `graph.goal` | The workflow's goal attribute | | `graph.{attr}` | All graph-level attributes, mirrored into context | ## Using context in conditions Edge [conditions](/workflows/transitions#conditions) can read context values to route execution: ```dot gate -> deploy [condition="outcome=succeeded && context.tests_passed=true"] gate -> fix [condition="outcome=failed"] ``` The `context.` prefix is optional — `tests_passed=true` and `context.tests_passed=true` are equivalent. Engine-managed keys like `internal.node_visit_count` work in conditions too. This is useful for [fixed-count loops](/tutorials/branch-loop#fixed-count-loops): ```dot gate -> exit [condition="context.internal.node_visit_count >= 5"] gate -> improve ``` ## Fidelity: Controlling agent context When a new agent or prompt node starts, Fabro assembles a **preamble** — a summary of what happened in prior stages. The **fidelity** setting controls how detailed this preamble is. | Fidelity | Behavior | |---|---| | `full` | No preamble. The agent continues in the same conversation thread, seeing complete prior context. | | `compact` | Nested-bullet summary with handler-specific details (default). | | `summary:high` | Detailed per-stage Markdown report. | | `summary:medium` | Moderate detail with outcomes and notable findings (~1500 token target). | | `summary:low` | Brief summary with just outcomes per stage (~600 token target). | | `truncate` | Minimal — only the goal and run ID. | ### Setting fidelity Fidelity can be set at three levels. The first match wins: 1. **Edge attribute** — Set `fidelity` on an edge to control the transition into a specific node: ```dot plan -> implement [fidelity="full"] ``` 2. **Node attribute** — Set `fidelity` on a node to control all transitions into it: ```dot implement [fidelity="full"] ``` 3. **Graph default** — Set `default_fidelity` on the graph for a run-wide default: ```dot digraph Example { graph [default_fidelity="summary:medium"] } ``` If none of these are set, fidelity defaults to `compact`. ### Parallel branch fidelity The first node in each parallel branch uses this precedence: 1. `fidelity` on the fork-to-branch edge 2. `fidelity` on the branch node 3. Otherwise, inherit the fork's preamble unchanged Fabro renders any branch-specific preambles before fan-out from the fork's context snapshot, then places them into the isolated branch contexts. An explicit branch-level `full` degrades to `summary:high` because concurrent branches cannot share conversation sessions. `thread_id` on a branch node or fork-to-branch edge is inert. ### Full fidelity and threads `full` fidelity is typically used with `thread_id` to create a shared conversation across multiple nodes. Nodes with the same `thread_id` share a single LLM session, preserving full context continuity: ```dot subgraph cluster_impl { node [fidelity="full", thread_id="impl"] plan [label="Plan"] implement [label="Implement"] review [label="Review"] } ``` In this example, the implement node sees the plan node's entire conversation, and the review node sees both. ### Fidelity on resume When a run is resumed from a checkpoint, the first node after resume degrades `full` fidelity to `summary:high`. This prevents the resumed node from expecting a conversation thread that no longer exists in memory. ## Preamble construction For non-`full` fidelity modes, Fabro builds a preamble from runtime data and prepends it to the node's prompt. Given a plan-test-implement workflow where the plan and test stages have completed, the `implement` node would receive a preamble like this prepended to its prompt: ```markdown Goal: Add a /health endpoint to the API server ## Completed stages - **plan**: success - Model: claude-sonnet-4-5, 12.4k tokens in / 3.2k out - Files: docs/plan.md - **test**: success - Script: `cargo test 2>&1 || true` - Output: ``` running 42 tests ... 41 passed, 1 failed ``` ## Context - tests_passed: false ``` The preamble includes: - The workflow goal - A summary of completed stages (format depends on fidelity level) - Handler-specific details: - **Command nodes** — the script that ran and the ordered output - **Agent/prompt nodes** — the model used, token counts, and files touched - Non-internal context values that weren't already rendered inline Internal keys (prefixed with `internal.`, `current`, `graph.`, `thread.`, `response.`) are excluded from preambles to avoid noise. ## Artifact offloading When a stage produces a large output (over 100KB of serialized JSON), Fabro stores the serialized bytes in a global content-addressed blob store and replaces the context value with a durable blob ref. Command output is always finalized into a blob ref after command completion, even when it is small or empty: ``` response.plan → blob://sha256/2cf24dba5fb0... command.output → blob://sha256/a4f3c1d9c2e1... ``` Checkpoints and checkpoint-completed events persist these `blob://` refs, not host-specific file paths. Before Fabro builds a preamble or starts the next stage, it resolves any blob refs into execution-local files so handlers and agents still see normal `file://` references: - Local execution materializes blobs under `{run_dir}/runtime/blobs/{blob_id}.json` - Remote sandboxes materialize blobs under `{working_directory}/.fabro/blobs/{blob_id}.json` These materialized `file://` paths are runtime-only. They are not written back into durable context snapshots. This keeps the durable context lean and portable while still giving downstream stages a filesystem path they can read. ## Context compaction During long-running agent sessions, the conversation history can grow large enough to exceed the LLM's context window. **Context compaction** automatically summarizes older turns and replaces them with a structured summary, keeping the session running without manual intervention. Compaction is always enabled and runs with hardcoded defaults — there are no user-facing configuration options. ### When it triggers After every assistant turn, Fabro estimates the total token usage of the system prompt and conversation history (using a rough heuristic of 1 token per 4 characters). If the estimate exceeds **80%** of the model's context window, compaction runs. ### How it works 1. **Split history** — The conversation is divided into older turns (to be summarized) and the most recent **6 turns** (preserved verbatim). 2. **Render old turns** — Older turns are serialized to a human-readable text format. Tool call arguments and tool results are truncated to 500 characters each. 3. **Summarize via LLM** — Fabro makes a non-streaming LLM call (using the same model and provider as the agent session) with a structured summarization prompt. The summary uses these sections: - **Goal** — what the user asked for - **Progress** — what was accomplished, with file paths and key decisions - **Key Decisions** — important choices and their rationale - **Failed Approaches** — what was tried and didn't work - **Open Issues** — remaining bugs, edge cases, or TODOs - **Next Steps** — what should happen next - **File Operations** — if the agent has tracked file modifications, this section is copied verbatim into the summary 4. **Replace history** — All old turns are removed and replaced with a single `[Context Summary]` system message containing the structured summary. The preserved recent turns remain unchanged. ### Failure behavior Compaction failures are **non-fatal**. If the summarization LLM call fails, Fabro emits an `Agent.Error` event and the session continues with the original uncompacted history. The next assistant turn will trigger another compaction attempt. ### Events Compaction emits three events to the [event stream](/execution/observability#event-stream): | Event | When | Key fields | |---|---|---| | `Agent.Warning` (kind: `context_window`) | Token estimate exceeds 80% of context window | `kind`, `message`, `details` (contains `estimated_tokens`, `context_window_size`, `usage_percent`) | | `Agent.CompactionStarted` | Compaction begins | `estimated_tokens`, `context_window_size` | | `Agent.CompactionCompleted` | Summary generated and history replaced | `original_turn_count`, `preserved_turn_count`, `summary_token_estimate`, `tracked_file_count` |