Skip to main content
MCP tools can return responses far larger than an LLM’s context window. A single Kubernetes workload listing or log export can be tens of thousands of tokens. Without intervention, this fills the context and degrades reasoning quality. Scratchpad solves this by intercepting large tool outputs and storing them on disk. The LLM gets a summary and eight read-only exploration tools (head, slice, grep, schema, item_schema, get_in, iterate_over, read) to selectively pull in only the data it needs. Scratchpad works in both single-agent and orchestration modes.

Configuration

Configure at [agent.scratchpad] (applies to the single agent, or provides defaults for orchestration workers) and optionally override per worker at [orchestration.worker.<name>.scratchpad]. Set a top-level memory_dir for persistence. See [agent.scratchpad] in the configuration reference for the full field table, and [mcp.servers.<name>.scratchpad] for per-tool interception thresholds.

Storage locations

  • Single-agent: {memory_dir}/scratchpad/
  • Orchestration: {memory_dir}/{run_id}/iteration-{n}/scratchpad/ (legacy [orchestration.artifacts].memory_dir still works as a fallback)

Per-tool interception thresholds

Per-tool interception thresholds are configured at [mcp.servers.<server>.scratchpad]. Keys are tool-name patterns (default threshold 5_120 if omitted) that are matched against that server’s tool names:
When multiple patterns match the same tool, the longest (most specific) pattern wins; on length ties the smallest threshold wins. Keys use the same syntax as mcp_filter, described in Tool-Name Patterns. A key outside that syntax stops the config from loading.

Token counting

Token counting uses real tokenization, not byte/character heuristics, so min_tokens and the context budget reflect actual model token cost. Dispatch is provider-aware:
  • OpenAI: tiktoken-rs, using o200k_base for GPT-5/4o/o-series models and cl100k_base for older models.
  • Gemini: an embedded Gemma 3 SentencePiece model (exact, fully local).
  • Anthropic / Bedrock-Claude: a calibrated cl100k_base × 1.1 approximation, since Claude ships no public tokenizer.
  • Everything else falls back to o200k_base.

Per-call extraction limit

max_extraction_tokens (default 10_000): every exploration tool checks the size of its result before returning. If a single call would exceed this cap (or the cumulative context budget), the tool returns a structured JSON error like {"error": "head_too_large", "estimated_tokens": ..., "suggestions": [...]} instead of the content. The LLM sees this as a successful tool result and retries with smaller params — each retry consumes a turn, which is why turn_depth_bonus exists. When an iterate_over result is too large, the iterate_over_too_large error leads with a ready-to-run paginated retry over a narrower window that halves the current window, ahead of suggestions to request fewer fields, use get_in, or use grep.

Paginate iterate_over with offset and limit

iterate_over can project the requested fields over a window of a large array instead of the whole array, using two optional arguments. This lets a call that would otherwise exceed max_extraction_tokens succeed over a slice. Windowed rows keep their absolute array position in the _index field. That position is offset plus the row’s position in the window, so paths that get_in resolves stay valid no matter which window produced the row. An offset past the end of the array returns an empty result [] rather than an error, so the agent can correct course. Each exploration tool call ends its result with a one-line footer that summarizes what it returned. When you paginate, that footer reports the window as a half-open interval over the total, with the start index inclusive and the end index exclusive. For example, the footer output iterate_over: $.results (items [3..7) of 120 total, fields: [id,title]) covers the items at index 3, 4, 5, and 6. With no offset or limit, the footer reports a full scan instead, for example 2 items. get_in and item_schema take the same offset and limit arguments. get_in paginates a large string value by line, and item_schema windows a large array by item.

Budget and usage reporting

Each agent (single-agent or orchestration worker) gets a fresh context budget scoped to that agent’s effective LLM’s context_window. Workers never share an “orchestrator-level” budget. LLM-reported per-turn token counts feed back into the budget as ground truth, so remaining budget reflects actual context pressure. A per-agent aura.scratchpad_usage SSE event (with agent_id, tokens_intercepted, tokens_extracted) is emitted when the agent finishes — the same event name fires for both single-agent and worker contexts (it lives in the base aura.* namespace, not aura.orchestrator.*). See Streaming API Guide for the full event reference.

Result artifacts and read_artifact

In orchestration, large task results are saved to artifact files under {memory_dir}/{run_id}/artifacts/. The scratchpad read tools resolve files anywhere under a per-agent read root, not just the scratchpad subdir. For orchestration workers the read root is the session dir, so result artifacts are explorable in place without copying. When a worker reads an artifact back with read_artifact, the same budget rules apply: an artifact that fits is inlined and recorded against the budget; one that exceeds the limit comes back as a scratchpad pointer that the worker explores in place with the read tools (head, grep, slice, …) — never copied into the scratchpad. The coordinator has no scratchpad, so its read_artifact always returns inline content. If a worker asks read_artifact for a filename that doesn’t exist in the current run, the response has found: false and lists the run’s artifact filenames in available, so the worker can retry with a correct name. The list holds up to 40 names. When the run has more, available_omitted gives the number left out.

Hand Content to Workers with write_artifact

Workers don’t see the conversation history, so the coordinator must pass them everything a task needs. When that content is too large or too exact to restate in a task description, such as a runbook draft or a set of literal values, the coordinator saves it as an artifact and attaches it to the task:
  1. The coordinator calls write_artifact with a filename and the full content. AURA saves the file to {memory_dir}/{run_id}/artifacts/ and returns the stored filename.
  2. The coordinator lists that filename in the task’s artifacts field when it calls create_plan. A task can also list existing artifacts from the current run, such as a worker’s result or a tool-output file.
  3. The worker’s context gets an INPUT ARTIFACTS section that tells it to load each listed file with read_artifact before it starts.
AURA renames each file the coordinator writes to coordinator-{name}.{ext}, so runbook-draft.md is stored as coordinator-runbook-draft.md. A name without a valid extension gets .txt. Because of the prefix, the coordinator can’t overwrite worker results or tool-output artifacts, and every write stays inside the run’s artifacts directory. Writing the same name again replaces the file, and the response reports replaced: true. Artifacts need execution persistence, which is on when memory_dir is set. Without it, write_artifact returns written: false with an error, and create_plan rejects any plan whose tasks list artifacts. create_plan also rejects a plan that lists a file that doesn’t exist in the current run. The rejection names the missing files and the artifacts that are available, so the coordinator can correct the plan.