diff --git a/.gitignore b/.gitignore
index f2fb121..7383644 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,6 @@ next-env.d.ts
# Restate
.restate
restate-data
+
+
+.idea
diff --git a/.misc/animations/README.md b/.misc/animations/README.md
index 0126459..f40eb74 100644
--- a/.misc/animations/README.md
+++ b/.misc/animations/README.md
@@ -19,7 +19,7 @@ fully before touching an animation.
| `programmatic-tool-calls.svg` | `timeline.py` → `programmatic-tool-calls` | docs/tools.md: Programmatic tool calling | QuickJS program, guardrails per call, compact result (`ptc/runtime.ts`) |
| `compaction.svg` | `timeline.py` → `compaction` | docs/architecture.md: Control and history | `COMPACT_AFTER_MESSAGES = 32`, `KEEP_RECENT_MESSAGES = 8` (`session/history.ts`) |
| `schedules.svg` | `timeline.py` → `schedules` | docs/schedules.md: Contract | Timers are delayed `Agent.fire` invocations in Restate (`agent/schedules.ts`, `docs/schedules.md`) |
-| `parallel-tool-calls.svg` | `timeline.py` → `parallel-tool-calls` | README: 1. It calls tools in parallel | Guardrails gate the batch once, calls run concurrently, a failure is a result not a throw (`session/step.ts`, `session/tools.ts`) |
+| `parallel-tool-calls.svg` | `timeline.py` → `parallel-tool-calls` | README: Resilient parallel work within an execution | Calls run concurrently; a retryable readFile operation fails twice and succeeds on its third attempt, while completed results are retained (`session/step.ts`, `tools/sandbox.ts`, `tools-api.ts`) |
| `background-operations.svg` | `timeline.py` → `background-operations` | docs/tools.md: Pending tools | Pending results, the turn waits while its text answer stands, `cancelOperation`, completion as a runtime message (`session/pending.ts`, `session/service.ts`, `tools/operations.ts`) |
Code paths are relative to `packages/libs/core/src`.
diff --git a/.misc/animations/hero.py b/.misc/animations/hero.py
index 529adb7..e61f1d3 100644
--- a/.misc/animations/hero.py
+++ b/.misc/animations/hero.py
@@ -14,7 +14,7 @@
("model", "call 3 tools", "in parallel"),
("tool", "getWeather", "12°C, light rain"),
("tool", "webSearch", "5 results"),
- ("tool", "runCommand", "exit 0"),
+ ("tool", "executeCommand", "exit 0"),
("model", "final answer", ""),
("reply", "published", "to the log"),
]
@@ -129,7 +129,7 @@ def visible_between(name, a, b):
captions = [
("Each model response and tool result is recorded in the turn’s journal.", 0, CRASH),
("The process crashes in the middle of the turn. The journal is safe in Restate.", CRASH, RESTART + 4),
- ("After restart, Restate replays the journal: recorded results are reused, nothing runs twice.", RESTART + 4, 68),
+ ("After restart, Restate replays the journal and reuses the recorded results.", RESTART + 4, 68),
("The turn continues from where it stopped and publishes its answer.", 68, 101),
]
for n, (text, a, b) in enumerate(captions):
diff --git a/.misc/animations/timeline.py b/.misc/animations/timeline.py
index 13d4d16..1834481 100644
--- a/.misc/animations/timeline.py
+++ b/.misc/animations/timeline.py
@@ -333,7 +333,7 @@ def durable_wait():
a.bar(0, 30, 124, "ok", "running", label_on_bar=True)
a.bar(31, 62, 124, "idle", "suspended: no process, only state")
a.chip(42, 142, 124, "tool", "new version v2")
- a.bar(63, 86, 124, "ok", "resumed", label_on_bar=True)
+ a.bar(63, 86, 124, "ok", "v1 resumed", label_on_bar=True)
a.link(63, 196, 140)
a.time_break(54, 24, 256, "")
@@ -347,12 +347,12 @@ def durable_wait():
a.playhead(20, 256)
a.captions(318, [
("A guardrail requires a person to approve the deploy before it runs.", 0, 30),
- ("The turn suspends: no process, only state. New versions can ship while it waits.", 30, 62),
+ ("The turn suspends. New versions can ship; its original deployment must remain available.", 30, 62),
("When the approval arrives a day later, the turn resumes exactly where it waited.", 62, 101),
])
a.write("durable-wait.svg",
"Animation: a guardrail requires approval; the turn suspends and holds no process for about a day, "
- "while a new service version ships; when a person approves, the turn resumes, runs the deploy and replies.")
+ "while a new service version ships; when a person approves, the turn resumes on its original deployment, runs the deploy and replies.")
def sub_agents():
@@ -540,7 +540,7 @@ def schedules():
def parallel_tool_calls():
- a = Animation(1000, 360, 16, 86)
+ a = Animation(1000, 360, 18, 86)
a.lane(20, 64, "Turn", "model steps")
a.lane(94, 54, "Guardrails")
a.lane(158, 128, "Tools")
@@ -554,24 +554,30 @@ def parallel_tool_calls():
a.mark(40, 202, "ok", "✓")
a.bar(26, 52, 226, "tool", "webSearch")
a.mark(52, 238, "ok", "✓")
- a.bar(26, 34, 262, "tool", "runCommand")
+ # readFile retries transport failures up to three attempts; shell commands
+ # deliberately do not. Show success on the third attempt, not infinite retry.
+ a.bar(26, 34, 262, "tool", "readFile")
a.mark(34, 274, "bad", "✕")
- a.axis_label(37, 274, "failed: reported to the model, the others keep running")
+ a.bar(38, 46, 262, "tool", "retry 1")
+ a.mark(46, 274, "bad", "✕")
+ a.bar(52, 64, 262, "tool", "retry 2")
+ a.mark(64, 274, "ok", "✓")
- a.chip(54, 34, 164, "model", "model step", "2 results + 1 error")
- a.link(54, 176, 80)
- a.chip(73, 41, 80, "reply", "reply")
+ a.chip(67, 34, 120, "model", "model step", "3 results")
+ a.link(67, 276, 80, dx=0)
+ a.chip(80, 41, 64, "reply", "reply")
a.playhead(20, 286)
a.captions(330, [
- ("One model response proposes three tool calls, and the guardrails check them as one batch.", 0, 25),
- ("They run concurrently. runCommand fails, and its error does not discard the other results.", 25, 53),
- ("The next model step sees both results and the error, and decides what to do.", 53, 101),
+ ("One model response proposes three tool calls; the guardrails check the batch.", 0, 25),
+ ("Restate retries readFile after transient failures. The other calls keep their results.", 25, 66),
+ ("The retry succeeds. All three results reach the next model step; completed calls are not rerun.", 66, 101),
])
a.write("parallel-tool-calls.svg",
- "Animation: one model step proposes three tool calls; the guardrails allow the batch in one check; "
- "the calls run concurrently, one fails and is reported as an error while the others finish; the "
- "next model step sees both results and the error, and replies.")
+ "Animation: three tool calls run concurrently after a guardrail check. readFile encounters "
+ "two transient failures and Restate retries it, succeeding on the third attempt. "
+ "getWeather and webSearch finish once and keep their results. The next model step "
+ "receives all three successful results and replies.")
def background_operations():
diff --git a/README.md b/README.md
index 1fb1262..506fa87 100644
--- a/README.md
+++ b/README.md
@@ -1,180 +1,169 @@
# A reference agent architecture
-A complete agent, built on [Restate](https://restate.dev). Every feature a
-modern agent needs is here as a small module you can read in one sitting,
-and Restate keeps each turn running through crashes and days-long waits.
+> Productionizing agents is notoriously difficult, because of their long-running, stateful nature.
+> This reference architecture shows how to build **durable, stateful,
+> steerable, concurrent agents and agentic systems**.
+
+The architecture fully runs on Restate and your favorite container platform or
+serverless provider. Restate is a durable runtime for agents that gives you
+all the building blocks you need to build advanced, large-scale agentic systems without
+managing a large infra stack.
+
+https://github.com/user-attachments/assets/f2244c8f-5f4a-4a1f-87bd-39c0502f078f
+
+## Core idea
+
+Each agent execution is a durable async process that is:
+
+- **STEERABLE**: Each execution has a handle that is stable across process
+ restarts and can be used to steer it, interrupt it, or approve an action.
+ Interrupts automatically propagate through subagents.
+- **STATEFUL**: Execution is isolated per agent session and stateful. Transcripts
+ and profile data (memories, instructions) are stored in Restate's
+ embedded KV store.
+- **RECOVERABLE**: Restate automatically keeps a journal per agent execution,
+ to recover it automatically after a failure.
+- **CONCURRENT**: Agents can spawn parallel subagents and tools. Tools execute
+ as durable concurrent tasks within the process and can share resources like
+ sandbox connections. Subagents run as separate durable invocations with their
+ own state and resources.
+- **SCALABLE**: Agents can scale up to thousands of concurrent executions, with
+ protection against concurrency issues and race conditions.
+- **PAUSABLE**: Restate can suspend an invocation waiting on a durable timer,
+ approval signal, or child invocation, then resume it when ready. Suspension
+ releases that invocation's execution on serverless platforms.
+
+**Implementing these characteristics in a production-grade manner is
+challenging and usually requires a lot of infra and coordination logic. In this
+reference architecture, the agent processes rely on Restate to handle this complexity**:
+
+[](docs/images/agent-architecture.svg)
+
+## What this reference includes
+
+This is a runnable TypeScript implementation with an agent runtime, a typed
+client, and an optional demo UI. You can use it as a starting point
+for your own application or take individual patterns into an existing agent.
+The reference focuses on agent execution and session management. User accounts,
+OAuth flows, and evals are not included.
-https://github.com/user-attachments/assets/41ddbac1-09a0-4a70-839a-79b29bd53331
+## Features
-*A real session: the model writes a program whose web searches run at
-once, you steer it and approve its file writes, it hands work to four
-sub-agents, and then the service is killed mid-turn and the turn still
-finishes.*
+| Feature | What you can do |
+| --- |------------------------------------------------------------------------------------------------------------------------------------|
+| **[Queueing, steering, and interruption](docs/turn-runtime.md#steering)** | Queue a new request, add instructions to an ongoing execution, or interrupt it and optionally start a replacement. |
+| **[Parallel tools](docs/turn-runtime.md#agent-loop-iterations)** | Run independent tool calls concurrently and return their results or errors to the model. |
+| **[Background operations](docs/tools.md#pending-tools)** | Continue model steps while tool-requested timers or approvals are pending; wait for or cancel them later. |
+| **[Subagents](docs/tools.md#sub-agents)** | Delegate work to persistent agents with their own history, memory, and sandbox, then send follow-up tasks to the same agents. |
+| **[Guardrails](docs/turn-runtime.md#guardrails)** | Define guardrails that guard against dangerous or unwanted tool behavior. |
+| **[Human approval](docs/protocol.md#context-and-approvals)** | Wait durably for a decision. The controller continues accepting steering and interruption; a guardrail wait holds its current proposal. |
+| **[Programmatic tool calling](docs/tools.md#programmatic-tool-calling-ptc)** | Let the model write JavaScript that combines tool calls and returns a result without putting all intermediate data in its context. |
+| **[Tool search](docs/tools.md#turn-local-tool-search)** | Find tools from MCP servers and Restate services, loading their schemas into model context only when needed. |
+| **[Memory](docs/architecture.md#context-and-delegation)** | Store information across turns, search memory descriptions, and retrieve the entries relevant to the task. |
+| **[Compaction](docs/architecture.md#control-and-history)** | Summarize older conversation history in the background and compact long-running turns, while keeping recent context verbatim. |
+| **[Schedules](docs/schedules.md)** | Schedule one-off or recurring messages, with a policy to queue, steer, or interrupt when the agent is busy. |
+| **[Sandboxes](docs/sandboxes.md)** | Read and write files and run commands in a local workspace or Modal sandbox, keeping files between turns. |
+| **[Client updates](docs/protocol.md#history-and-notifications)** | Follow a running agent, reconnect after a connection failure, and read new history from an offset. |
+| **[Extensible tools](docs/tools.md)** | Add built-in tools, connect MCP servers, or expose Restate handlers as tools. |
+| **[Output recovery](docs/turn-runtime.md#model-output-budgets-and-recovery)** | Retry a truncated model response at most once with a larger output budget, if it is below the runtime's cap. |
-[Features](#features) · [One turn, start to finish](#one-turn-start-to-finish) ·
-[How a turn works](#how-a-turn-works) · [Quickstart](#quickstart) ·
-[Documentation](docs/README.md)
+## Architecture
-## Features
+Each agent has two Restate Virtual Objects (stateful entities addressed by key) keyed by the same `agentId`:
-
-
-
Parallel tool calls Every call in a model response runs at once; a failure goes back to the model.
-
Background operations Timers and approvals run on while the model works. It can wait or cancel.
-
-
-
Steering and interrupts Redirect or stop a turn while it runs, without losing finished work.
-
Sub-agents Delegate tasks to agents with their own history, sandbox and memory.
-
-
-
Guardrails Plain-language rules. A policy model allows, blocks or asks a person.
-
Human approval A turn can wait days for a decision, holding no process.
-
-
-
Programmatic tool calls The model writes a small program; only its result enters the context.
-
Compaction Long conversations and long turns are summarized, recent steps kept verbatim.
-
-
-
Schedules Messages to the agent later, once or on a recurrence. No cron.
-
Memory A searchable index, so context does not grow with the memories.
-
-
-
Tool search MCP and discovered tools load on demand, keeping large catalogs out of context.
-
Sandboxes A local directory or Modal sandbox for files and commands.
-
-
-
Extensible tools One module per tool, plus MCP servers and Restate handlers.
-
Output recovery A truncated response gets one retry with a bigger output budget.
-
-
-
-## One turn, start to finish
-
-Here is one turn, from the first message to the answer, and what Restate
-does for it along the way.
-
-### 1. It calls tools in parallel
-
-The model asks for three tools, the guardrails check them once, and they
-run together. Each result is recorded as it lands, and a call that fails
-goes back to the model as an error.
-
-
-
-### 2. You steer it while it works
-
-A new message does not have to wait for the turn to end. A steer reaches the
-next model step without cancelling tools in flight, and an interrupt stops
-the turn with a summary of what it did.
-
-
-
-### 3. It waits a day for your approval
-
-When a guardrail needs a person, the turn suspends: no process, only stored
-state. It can wait a day, through new versions of the service, and resumes
-where it stopped.
-
-
-
-### 4. It survives a crash
-
-Every model call and tool result is in the turn's journal. If the process
-dies, Restate replays the journal: nothing is asked or run twice, and the
-turn finishes.
-
-
-
-## Why Restate
-
-Every feature above gets the same guarantees, because they come from Restate
-rather than from each feature:
-
-- **A crash resumes the turn.** Restate replays the turn's journal. Recorded
- model responses and tool results are reused, so the model is never asked
- to repeat a decision and completed tool calls are not re-run.
-- **New versions don't break running turns.** Restate keeps each turn on
- the service version it started on; new turns use the new version.
-- **Waiting is free.** A turn can wait hours or days for an approval, a timer
- or a sub-agent. While it waits it holds no process, only stored state.
-- **Always responsive.** `ask`, `steer` and `interrupt` return right away,
- even while a turn runs for minutes.
-- **Nothing else to operate.** State, messages, timers and notifications all
- live in Restate: no database, queue or scheduler of the agent's own. Each
- agent's state has a single owner, so there are no locks. The service is
- stateless: it scales out, or builds into a single bundle for serverless.
-
-**What replay does not do:** make external side effects exactly-once. A crash
-between an MCP call completing and its result being recorded can repeat that
-call.
-
-## How a turn works
-
-Each agent is two Restate Virtual Objects with the same key:
-
-- **`Agent`**, the controller. It decides what happens to each incoming
- message and owns the profile: instructions, guardrails, memories, tool
- grants, approvals, schedules and child agents. Its handlers are short, so
- it always answers.
-- **`AgentSession`**, the turn. One `doTurn` invocation is one turn. It owns
- the append-only conversation log and runs the model/tool loop.
-
-```mermaid
-sequenceDiagram
- autonumber
- participant C as Client
- participant A as Agent (controller)
- participant S as AgentSession (turn)
- participant M as Model, guardrails, tools
-
- C->>A: ask("Weather in Berlin?")
- A-)S: doTurn(profile snapshot), one way
- A-->>C: started, turnId
- S->>S: append to log, build context
-
- S->>M: model step
- M-->>S: proposal: call 3 tools
- S->>M: guardrail check
- par tool calls run concurrently
- S->>M: getWeather
- and
- S->>M: webSearch
- and
- S->>M: runCommand
- end
-
- C->>A: steer("Use Fahrenheit")
- A-)S: steering signal, addressed to turnId
- Note over S: A crash anywhere here is harmless. Restate replays the journal and reuses every recorded result.
-
- S->>M: model step, with tool results and steering
- M-->>S: final answer
- S-)A: onTurnEnd(outcome)
- Note over A: Retire the turn. Start the next one if messages were queued.
-
- C->>A: watch(afterRevision), long poll
- A-->>C: history changed
- C->>S: history(fromSequence)
- S-->>C: new entries
-```
+- **[`Agent`](packages/libs/core/src/agent/service.ts)** handles incoming messages
+ and tracks the active turn, queued input, profile, memories, approvals, and schedules.
+- **[`AgentSession`](packages/libs/core/src/session/service.ts)** stores the
+ conversation and runs the model/tool loop. Each `doTurn` invocation handles
+ one turn, which may incorporate several queued messages or steering updates.
+
+
+
+This architecture makes the following advanced features possible:
+
+### Steer and interrupt ongoing agent executions
+
+When an `Agent` starts a turn, it gets the `turnId` back, with which it can:
+- **Steer the turn**: adds an instruction to the context for the next LLM call.
+ Tools already running can finish.
+- **Interrupt the turn**: stops unfinished work, including subagent tasks, and asks
+ the model to summarize what it completed.
+
+Restate provides durable signal delivery. The reference implements the routing,
+turn state machine, and cleanup rules on top of it.
+
+
+
+See [steering](docs/turn-runtime.md#steering) and
+[interruption](docs/turn-runtime.md#interruption-and-stopping).
+
+### Fine-grained recovery within the agent loop
+
+Each step in an agent loop is recorded in Restate's journal: LLM calls, guardrail
+checks, tool calls, state updates, approvals,...
+After a failure or a long wait, the agent process can recover to the exact step
+where it left off, by replaying the journal.
+
+Restate only adds a few milliseconds of overhead to persist a journal entry,
+making fine-grained recovery feasible.
+
+
+
+### Resilient parallel work within an execution
+
+Tools run concurrently in the same process and share resources such as sandbox
+connections. Each tool's durable steps are recorded independently, so recovery
+can reuse completed work across the batch. The runtime handles deterministic
+replay during recovery.
+
+
+
+See [parallel tools](docs/turn-runtime.md#agent-loop-iterations)
+and [background operations](docs/tools.md#pending-tools).
+
+### Scaling thousands of stateful agents
+
+Agent IDs run in parallel across service instances, while each session runs one
+turn at a time. Restate coordinates state access, routes calls, and recovers
+work after process failures, without the need for locks or coordination.
+
+See [state ownership](docs/architecture.md#state-ownership).
+
+### Consistent per-session memory and background compaction
+
+This architecture uses Restate's embedded KV store for both conversation state
+and context. Restate gives each agent session its own isolated store, and
+ensures that only a single process can write to it at a time.
+
+This architecture also implements:
+- **Selective loading:** history is stored in chunks, and memories are retrieved
+ on demand. Clients read only the history they need to update the UI,
+ and the model sees only relevant memories.
+- **Compaction background jobs:** Older messages are compacted in the background,
+ to avoid exceeding the LLM's context limit. The model sees the latest messages
+ verbatim, incl. the last summary. The full chat transcript is retained so the
+ UI client can retrieve it, when needed.
+
+See [history and context](docs/architecture.md#control-and-history).
+
+### Waiting durably for approval
+
+When a guardrail needs a person, the turn suspends: no process, only stored state. It can wait weeks, through new versions of the service, and resumes where it stopped.
+
+
+
+See [approvals](docs/protocol.md#context-and-approvals).
+
+### Subscribing to session updates and reconnecting later
-The controller keeps track of the currently executing turn. `steer`,
-`interrupt` and approval decisions reach the turn as durable signals
-addressed to its invocation ID, the `turnId`, so they never land in the
-wrong turn.
+The reference UI uses a [typed client](packages/libs/client/src/index.ts) to read session data and follow
+changes to history, approvals, configuration, and schedules. The UI
+uses HTTP long-polling and revision tags to fetch only what changed, while
+transcript sequence numbers let it catch up after disconnects.
-`doTurn` runs the model calls and tools itself, as in-process function calls.
-Each result is appended to the turn's journal over one open, low-latency
-stream to Restate. That append is the only persistence a step needs.
+See [session updates](docs/protocol.md#history-and-notifications).
## Further reading
diff --git a/docs/README.md b/docs/README.md
index 1346e8e..3a41f57 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -6,7 +6,9 @@ request path begins with an agent ID, without an account or login prerequisite.
## Reading path
1. [Run a conversation](../README.md).
-2. [Architecture](architecture.md): controller, turn, state owners, notifications.
+2. [Architecture](architecture.md): state owners and an
+ [end-to-end communication diagram](architecture.md#one-request) covering
+ the controller, turn, tools, steering and client updates.
3. [Protocol](protocol.md): ask, steer, interrupt, history, approvals and clients.
4. [Turn runtime](turn-runtime.md): steps, policy, pending work and recovery.
5. [Tools](tools.md): built-ins, PTC, dynamic Restate handlers and MCP.
diff --git a/docs/architecture.md b/docs/architecture.md
index c988c38..376608e 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -34,20 +34,82 @@ its own session waits for that child's outcome without holding the parent lock.
```mermaid
sequenceDiagram
+ autonumber
participant C as Client
- participant A as Agent
- participant S as AgentSession
- participant M as Model and tools
- C->>A: ask(message)
- A->>S: send doTurn(snapshot)
- A-->>C: started + turnId
- S->>M: model / policy / tool steps
- C->>A: steer or interrupt
- A-->>S: durable control signal
+ participant A as Agent (controller)
+ participant S as AgentSession (turn)
+ participant M as Model and guardrails
+ participant T as Tools
+
+ C->>A: ask("Compare weather in Berlin, Paris and Rome")
+ A-)S: doTurn(profile snapshot and input), one way
+ A-->>C: started, turnId
+ S->>S: append input to history, build context
+
+ S->>M: model call
+ M-->>S: proposal: three tool calls
+ opt guardrails configured
+ S->>M: evaluate proposed tool batch
+ M-->>S: allow
+ end
+ Note over S,T: Allowed foreground calls run concurrently inside the turn
+ par Berlin
+ S->>T: getWeather(Berlin)
+ T-->>S: result
+ and Paris
+ S->>T: getWeather(Paris)
+ T-->>S: result
+ and Rome
+ S->>T: getWeather(Rome)
+ T-->>S: result
+ and new input while tools run
+ C->>A: steer("Use Fahrenheit")
+ A-)S: durable steering signal addressed to turnId
+ A-->>C: accepted
+ end
+ Note over S,T: Restate journals durable operations; recovery reuses recorded results
+
+ S->>S: retain tool results, consume steering
+ S->>M: next model call with results and steering
+ M-->>S: proposed final answer
+ opt guardrails configured
+ S->>M: evaluate proposed answer
+ M-->>S: allow
+ end
S->>A: onTurnEnd(outcome)
+ Note over A: Reconcile late input, retire matching turn, dispatch queued work
A-->>S: reconciled outcome
+ S->>S: append final outcome to history
+ S-)A: publish(history)
+
+ C->>A: watch(afterRevision), long poll
+ A-->>C: changed topic revisions
+ C->>S: history(fromSequence)
+ S-->>C: new entries and next sequence
```
+The diagram shows a successful tool batch and final answer. Model/tool steps
+can repeat, and a guardrail can instead block a proposal or wait for approval.
+`getWeather` is a synthetic demo tool. The client can watch throughout the
+turn; history appends publish changes as work progresses, not just at the end.
+
+These arrows describe logical calls through Restate. `doTurn` is dispatched
+one way so the controller can return immediately. Steering, interruption and
+approval decisions use durable signals addressed to that invocation's
+`turnId`. Normal completion uses a request/response call to `onTurnEnd` so the
+session can record the controller's reconciled outcome. A queued successor
+cannot execute until the current exclusive `doTurn` finishes.
+
+The model and tool lanes represent work owned by the turn, not additional
+Virtual Objects. Built-in tools run as in-process functions; model requests,
+MCP calls and sandbox operations use journaled effects. Discovered Restate
+tools use durable RPCs. Recovery reuses recorded results, but an external
+effect that completed before its result was recorded may run again; remote
+side effects still need provider-level idempotency.
+
+For exact message shapes, see the [protocol](protocol.md). For the loop's
+approval, pending-work and cancellation paths, see the [turn runtime](turn-runtime.md).
+
## Control and history

diff --git a/docs/images/agent-architecture.png b/docs/images/agent-architecture.png
new file mode 100644
index 0000000..91fb9db
Binary files /dev/null and b/docs/images/agent-architecture.png differ
diff --git a/docs/images/agent-architecture.svg b/docs/images/agent-architecture.svg
new file mode 100644
index 0000000..15c2979
--- /dev/null
+++ b/docs/images/agent-architecture.svg
@@ -0,0 +1,130 @@
+
diff --git a/docs/images/agent-objects.svg b/docs/images/agent-objects.svg
new file mode 100644
index 0000000..a7ae9d9
--- /dev/null
+++ b/docs/images/agent-objects.svg
@@ -0,0 +1,21 @@
+
diff --git a/docs/images/agent-responsive.svg b/docs/images/agent-responsive.svg
new file mode 100644
index 0000000..dd88c9e
--- /dev/null
+++ b/docs/images/agent-responsive.svg
@@ -0,0 +1,105 @@
+
diff --git a/docs/images/agent-step-recovery.svg b/docs/images/agent-step-recovery.svg
new file mode 100644
index 0000000..33dc030
--- /dev/null
+++ b/docs/images/agent-step-recovery.svg
@@ -0,0 +1,48 @@
+
diff --git a/docs/images/agent-structure.png b/docs/images/agent-structure.png
new file mode 100644
index 0000000..43c9c67
Binary files /dev/null and b/docs/images/agent-structure.png differ
diff --git a/docs/images/agent-structure.svg b/docs/images/agent-structure.svg
new file mode 100644
index 0000000..f06e679
--- /dev/null
+++ b/docs/images/agent-structure.svg
@@ -0,0 +1,60 @@
+
diff --git a/docs/images/durable-turn.svg b/docs/images/durable-turn.svg
index a67b021..d69f069 100644
--- a/docs/images/durable-turn.svg
+++ b/docs/images/durable-turn.svg
@@ -118,7 +118,7 @@ g { transform-box: fill-box; transform-origin: 50% 50%; }
5 · tool
-runCommand
+executeCommandexit 0
@@ -142,6 +142,6 @@ g { transform-box: fill-box; transform-origin: 50% 50%; }
✓ reused1Each model response and tool result is recorded in the turn’s journal.2The process crashes in the middle of the turn. The journal is safe in Restate.
-3After restart, Restate replays the journal: recorded results are reused, nothing runs twice.
+3After restart, Restate replays the journal and reuses the recorded results.4The turn continues from where it stopped and publishes its answer.
diff --git a/docs/images/durable-wait.svg b/docs/images/durable-wait.svg
index 9a4c20a..6b54866 100644
--- a/docs/images/durable-wait.svg
+++ b/docs/images/durable-wait.svg
@@ -1,4 +1,4 @@
-