Prompt system
How the stable prefix and variable tail are budgeted and cached.
Atomic Agent is a local-first AI agent that does real work on your machine — browsing, editing files, running shell commands, remembering things across sessions. This page explains what happens between the moment you send a message and the moment the agent replies.
The short version: one model inference produces one step. A step is a batch of tool calls. The agent runs them, compresses the results, updates its state, and asks the model again. That repeats until the agent decides it’s done.
Most of the cost of running an agent is the model. Atomic Agent is built around keeping each model call small and cheap so that long, multi-step sessions stay affordable on a quantized local model.
It does this with a few deliberate choices:
The result is a tight, observable loop you can trace, replay, and reason about.
flowchart TD
User["User message<br/>or scheduled task"] --> Prompt["Build prompt<br/>(stable prefix + bounded tail)"]
Prompt --> Infer["One LLM inference<br/>(GBNF grammar-constrained)"]
Infer --> Calls["JSON array of<br/>1..N tool calls"]
Calls --> Exec["Execute batch<br/>(parallel reads, approval gates)"]
Exec --> Compress["Compress results"]
Compress --> State["Update durable state<br/>(session, memory, world)"]
State --> Terminal{"reply or finish?"}
Terminal -->|no| Prompt
Terminal -->|reply| Reply["Turn ends,<br/>session pending"]
Terminal -->|finish| Done["Session completed"]
State -.->|async, never awaited| Reflect["Memory reflection<br/>(learns from the turn)"]
Two terms recur:
When you send a message, the runtime calls AgentLoop.runTurn(). Here is what it does, in order.
ToolLoopTracker is created to watch for the agent getting stuck.The loop runs from step 0 up to agent.maxSteps (default 25). Each step:
/props if a model-profile manager is wired (this is how a mid-turn model swap is detected).executeStep, which builds the prompt, runs one inference, parses the tool calls, and executes them.reply, the turn ends (session left pending — the user can continue).finish, the session is completed.When the loop exits, the session status is set (completed, pending, stalled, cancelled, or failed), the turn count is incremented, and a lesson-lifecycle hook records whether the turn succeeded or failed.
Finally, memory reflection fires — asynchronously, never awaited. It runs in the background on a separate llama-server slot so it never disturbs the main agent’s KV-cache or blocks your reply.
executeStep is where the model meets the tools. The pipeline:
flowchart TD
Build["buildPrompt()<br/>stable prefix + tail"] --> Slot["acquire KV-cache slot<br/>(prefix hash)"]
Slot --> Complete["LLM completion<br/>(stream or unary)"]
Complete --> Reason["extract reasoning<br/>(server channel or inline think)"]
Reason --> Parse["parse tool calls<br/>(grammar or native_tools)"]
Parse --> Validate["validate batch"]
Validate -->|ok| Run["executeBatch()"]
Validate -->|parse failed| Repair["one-shot repair completion"]
Repair --> Run
Run --> Effects["apply state effects<br/>(world snapshot, rare-tool autoload)"]
Effects --> Record["append transcript turns"]
A few details worth knowing:
<think> blocks that the parser strips out.Once the calls are parsed and validated, executeBatch runs them in three phases, grouped by resource class (pure_read, fs_write, browser, terminal, and so on).
pure_read tools fan out in parallel; everything else serializes in batch order to keep writes deterministic.reply or finish, that call runs solo, last, after every other call settles — even if an earlier tool failed. This preserves the model’s intent: “do the work, then close the turn.”Results are collected back in batch-index order, then each one is compressed.
Every tool result is passed through compressToolResult() before it re-enters the conversation. The compressor:
error:, failed:, traceback, exception:) and surfaces the first match.[truncated] if it overflows.The full output is never sent to the model — only this compact summary. This is what keeps each step under budget even when a shell command spews thousands of lines.
Local models can get stuck repeating the same call. ToolLoopTracker watches for this within the turn (it has no memory of prior turns):
web.search, web.fetch, browser.*) are watched for a spreading set of distinct args; crossing the escalation threshold jumps straight to the breaker.Thresholds are tunable via config / env:
| Knob | Env var |
|---|---|
| Warning threshold | ATOMIC_AGENT_LOOP_WARNING_THRESHOLD |
| Critical threshold | ATOMIC_AGENT_LOOP_CRITICAL_THRESHOLD |
| Breaker veto streak | ATOMIC_AGENT_LOOP_BREAKER_VETO_STREAK |
| History window size | ATOMIC_AGENT_LOOP_HISTORY_SIZE |
| Max parallel tool calls | ATOMIC_AGENT_MAX_PARALLEL_TOOL_CALLS |
The prompt is split into two zones:
Everything durable lives outside the prompt entirely:
sessions.sqlite.memory.sqlite.tasks.sqlite.The prompt only ever sees compact pointers into memory — never the whole store.
Every entry point — atomic-agent run, atomic-agent serve, atomic-agent tui, and the Tauri sidecar — constructs the runtime exactly once through createAgentRuntime(). That factory probes llama-server, builds the LLM provider registry and tool registry, opens the memory stores, wires the AgentLoop, and returns an AgentRuntime handle.
Turn serialization is the runtime’s job, via the TurnController:
graph TD
Entry["run / serve / tui / sidecar"] --> Factory["createAgentRuntime()"]
Factory --> Llama["probe llama-server"]
Factory --> Tools["ToolRegistry"]
Factory --> Memory["Memory stores (SQLite)"]
Factory --> Loop["AgentLoop"]
Factory --> Turn["TurnController"]
Turn -->|per-session FIFO| Loop
Loop -->|runTurn| Step["executeStep → executeBatch"]
Step --> Tools
Step --> Memory
Step -.->|events| Trace["TraceRecorder (NDJSON)"]
The loop is easiest to watch with tracing on. Run a turn, then replay or inspect it:
# Run an interactive sessionatomic-agent run --cwd . --max-steps 25
# List recorded traces, then inspect one step-by-stepatomic-agent trace listatomic-agent trace show <sessionId>
# Re-run the prompts against llama-server to check for driftatomic-agent trace replay <sessionId>Prompt system
How the stable prefix and variable tail are budgeted and cached.
Memory
Profile facts, notes, lessons, and procedures — and how recall avoids prompt bloat.
Tools
The eight tool families, resource classes, and the approval gate.
Configuration
Loop thresholds, parallel limits, and the agent.* config block.