# Resuming

`agent.ask(...)` starts a turn from a new message on the agent's current stream. `agent.resume(...)` does the same job from a **recorded trajectory**: you hand it a list of past events, and it re-enters the agent loop driven by the **last** event in that list. The trigger can be _any_ event (a fresh user message, a tool result, a human reply) so `resume` is the general way to rebuild a conversation from stored state and carry it forward.

Most multi-turn chats do **not** need `resume`

To continue a conversation within a running process, just keep using the reply: call `agent.ask(...)` once, then `reply.ask(...)` for each follow-up. That is the normal continuation path — `resume` is **not** a step you run after `ask`.

|     |     |
| --- | --- |
| ```<br>1<br>2<br>``` | ```<br>reply = await agent.ask("Plan a trip to Kyoto.")<br>reply = await reply.ask("Make it five days.")   # continue — no resume needed<br>``` |

Reach for `resume` only when you **can't** hold onto that reply: the process ended, another worker picks the turn up, or you need to drive the loop from a non-message event such as a tool result. See [How it relates to `ask` / `reply.ask`](https://docs.ag2.ai/latest/docs/beta/resume/#how-resume-works) below.

## How `resume` works

`resume` takes the full trajectory as positional `events`:

```
await agent.resume(*events, ...)
```

The list is split in two:

- **All events except the last** seed the stream's history — the conversation state the model will see.
- **The last event** is the **trigger**: the event that drives the next LLM call. It can be any [event](https://docs.ag2.ai/latest/docs/beta/advanced/stream/) — typically a `ModelRequest` (a new user message) or a `ToolResultsEvent` (a tool result the model should react to).

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>``` | ```<br>*history, trigger = events<br># history  -> replaces the stream's history (the prefix the model sees)<br># trigger  -> drives the next LLM call (any BaseEvent)<br>``` |

Everything after that is identical to `ask`: the agent calls the model, may issue tool calls, and returns an `AgentReply`. The `resume` signature mirrors `ask` exactly — `stream`, `dependencies`, `variables`, `prompt`, `config`, `tools`, `middleware`, `observers`, `response_schema`, and `hitl_hook` all behave the same way.

How it relates to `ask` / `reply.ask`

[`reply.ask(...)`](https://docs.ag2.ai/latest/docs/beta/agents/) continues the **same live stream** the turn ran on — you keep the conversation going as long as you still hold the `AgentReply` object in the running process. That stream may itself be durable (a `RedisStream`, say); what `reply.ask` needs is the in-process handle, not where the history happens to be stored. `resume` is for when you no longer have that handle — a process restart, a worker on another machine, a turn rebuilt from a store — so you supply the events yourself and drive the next one.

## Continue a stored conversation

The most common use is multi-turn that outlives the process: persist a conversation, load it back later, and continue with a new user message as the trigger.

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>``` | ```<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>from autogen.beta.events import ModelRequest, TextInput<br>agent = Agent(<br>    "assistant",<br>    prompt="You are a helpful travel assistant.",<br>    config=OpenAIConfig("gpt-4o-mini"),<br>)<br># A conversation you stored earlier and just loaded back from your database.<br>past_events = load_conversation("thread-42")<br># Drive the next turn with a fresh user message as the trigger.<br>trigger = ModelRequest([TextInput("And what about getting there by train?")])<br>reply = await agent.resume(*past_events, trigger)<br>print(reply.body)<br>``` |

This is `reply.ask("...")`, except you drive it from events you reload yourself rather than from a live `AgentReply` handle held in the running process.

## Resume from a tool result

A turn can also stop **mid-loop** — the model asked to run a tool whose result is produced elsewhere: a webhook, a queue worker, a long-running job, or a human approving a request. You record the tool call, run the work separately, then hand the result back as the trigger. The model reacts to the result without the tool being re-executed.

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br>28<br>29<br>``` | ```<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>from autogen.beta.events import (<br>    ModelMessage,<br>    ModelRequest,<br>    ModelResponse,<br>    TextInput,<br>    ToolCallEvent,<br>    ToolCallsEvent,<br>    ToolResultEvent,<br>    ToolResultsEvent,<br>)<br>agent = Agent("support", prompt="Answer using the tool result.", config=OpenAIConfig("gpt-4o-mini"))<br># The trajectory recorded earlier: the user asked, and the model responded<br># by requesting a tool call (whose result is not yet known).<br>call = ToolCallEvent(name="lookup_order", arguments='{"id": "A-1001"}', id="call-1")<br>history = [<br>    ModelRequest([TextInput("Where is order A-1001?")]),<br>    ModelResponse(message=ModelMessage(""), tool_calls=ToolCallsEvent([call])),<br>]<br># The tool result, produced out of band, becomes the trigger.<br>trigger = ToolResultsEvent([ToolResultEvent.from_call(call, "Shipped, arriving Tuesday.")])<br># Re-enter the loop: the model sees the history + result and writes the answer.<br>reply = await agent.resume(*history, trigger)<br>print(reply.body)  # -> grounded answer using "Shipped, arriving Tuesday."<br>``` |

`ToolResultEvent.from_call(call, result)` pairs the result with the original call so the model can match it to its request. Because `resume` re-enters the **live** loop, the model is free to react by calling more tools — those continuation calls execute normally; only the trigger event is replayed.

## Capture, persist, resume

A trajectory is just a list of events, so you can store it anywhere and pass it back to `resume` later — even from another process. Pull the events from a live stream with `await stream.history.get_events()`, or build them yourself, as long as the prefix ends at the point you want to continue from.

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>``` | ```<br># --- First process: capture the trajectory and persist it ---<br>events = list(await reply.context.stream.history.get_events())<br>save_to_store(stream_id="thread-42", events=events)<br># --- Later / another process: load it back and drive the next event ---<br>events = load_from_store(stream_id="thread-42")<br>reply = await agent.resume(*events, ModelRequest([TextInput("Carry on.")]))<br>print(reply.body)<br>``` |

Reading back the event log

`resume` reads only the events you pass in — it does not load history from a store on its own. The [event log](https://docs.ag2.ai/latest/docs/beta/advanced/knowledge_store/) written by `KnowledgeConfig(write_event_log=True)` is one place these trajectories can come from: it persists each turn to `/log/{stream_id}.jsonl`, and `EventLogWriter(store).load(stream_id)` reads it back as typed events ready to pass straight to `resume`.

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>``` | ```<br>from autogen.beta.knowledge import EventLogWriter<br>events = await EventLogWriter(store).load(stream_id)<br>reply = await agent.resume(*events, ModelRequest([TextInput("Carry on.")]))<br>``` |

## The stream is replaced, not appended

`resume` **replaces** the target stream's history with the seeded prefix. If you omit `stream`, a fresh `MemoryStream` is created. If you pass an existing stream, any history it already held is discarded in favour of the trajectory you provide.

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>``` | ```<br>from autogen.beta.stream import MemoryStream<br>stream = MemoryStream()<br>await stream.history.replace([ModelRequest([TextInput("stale prior turn")])])<br># The stale turn is dropped; only the events you pass remain.<br>reply = await agent.resume(*history, trigger, stream=stream)<br>``` |

This makes the recorded trajectory the single source of truth for the resumed turn — the events you pass are exactly what the model sees.

Durable-backed streams are overwritten in place

The replacement is applied to the stream's **storage**, not just an in-memory copy. For a persistent stream such as `RedisStream`, seeding the prefix overwrites the durable history stored under that stream id — the backing store clears the key and rewrites it. Resume into a fresh stream, or use a new stream id, when you need to keep the original conversation's stored history intact.

## Caveats — resuming isn't always possible

`resume` replays provider-native events back to the model, and some providers attach opaque, **required** metadata to those events. Gemini 3.x, for example, binds a _thought signature_ to each function call and rejects a replayed call that arrives without it:

```
400 INVALID_ARGUMENT — Function call is missing a thought_signature in functionCall parts
```

OpenAI reasoning items and Anthropic thinking signatures carry similar provider-specific state. As long as you resume from the **original** events — the ones the agent emitted, written verbatim by the [event log](https://docs.ag2.ai/latest/docs/beta/resume/#capture-persist-resume) — this metadata travels with them and `resume` round-trips cleanly. The risk appears when a trajectory is rebuilt from a lossy form: a hand-rolled `{name, args, result}` record, or a trace store that keeps only the fields it understands. The required metadata is silently dropped, and the provider rejects the resumed turn.

Warning

Resuming is not guaranteed for every provider and every trajectory. Preserve the events exactly as the agent emitted them; do not assume a trajectory reconstructed from a reduced or normalized form can be resumed.
