# Context and message history

> Store an agent's message history in memory or Redis, resume it in a new process, and rewrite it while the agent runs.

An agent's message history can outlive the process and be rewritten while the agent runs. The `agents_resume.rs` example saves the history of one agent as JSON (JavaScript Object Notation) and loads it into a second one:

```rust
// Let's store the messages in a database, retrieve them back, and start a new agent
let stored_history = serde_json::to_string(&first_agent.history().await?)?;
let retrieved_history: Vec<_> = serde_json::from_str(&stored_history)?;

let restored_context = DefaultContext::default()
    .with_existing_messages(retrieved_history)
    .await?
    .to_owned();
```

Source: [`examples/agents_resume.rs#L23-L30`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/agents_resume.rs#L23-L30)

```rust
let mut second_agent = agents::Agent::builder()
    .llm(&openai)
    .context(restored_context)
    // We'll use the one from the first agent, alternatively we could also pop it from the
    // previous history and add a new one here
    .no_system_prompt()
    .build()?;

second_agent.query("What did you say?").await?;
```

Source: [`examples/agents_resume.rs#L34-L42`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/agents_resume.rs#L34-L42)

`Agent::history()` returns every `ChatMessage` the context holds. `ChatMessage` implements `Serialize` and `Deserialize`, so `serde_json` turns the history into a string you can store anywhere. `DefaultContext::with_existing_messages` loads the messages back into a fresh context. The second agent gets that context through the builder's `context` method and continues the conversation with `query`. The next completion sends the whole restored history to the model.

The first agent sets no system prompt, so `no_system_prompt()` changes nothing in this example. [Prompt removal](/agents/system-prompts/#prompt-removal) explains when it matters.

`run` without a new message only continues when the history ends in a user message, a tool output or a pending tool call. When it ends in a plain assistant answer, as it does here, `run` stops at once with `StopReason::NoNewMessages`.

## Default context

The context stores the message history, decides when the next completion runs and what it contains, holds the tool executor and records feedback for tools that wait on a person. Every agent built without `.context(..)` gets a `DefaultContext`:

- the message history is an in-memory `Mutex<Vec<ChatMessage>>`;
- tools run commands through a `LocalExecutor` in the current directory;
- the agent stops when the last message is an assistant answer and no tool feedback is pending.

`DefaultContext::from_executor` keeps those defaults and swaps the tool executor. It takes a value that converts into `Arc<dyn ToolExecutor>`. [Tool executors](/agents/executors/) covers the conversion and the tool executors themselves.

`with_stop_on_assistant(false)` turns off the last rule. A text answer then no longer ends the run, and the agent asks the model again. Only a tool that stops the agent, the builder's `limit`, or a model reply with no content and no tool calls ends the loop. An agent has no limit by default, so set one ([Stop reasons](/agents/stopping-and-structured-results/#stop-reasons)).

A clone of a `DefaultContext` shares its history and counters.

## Message history

Where the messages live is the job of the `MessageHistory` trait. It has three required methods:

- `history` returns all messages in order;
- `push_owned` appends one message;
- `overwrite` replaces all messages.

The trait requires `Send + Sync + Debug` and is declared with `#[async_trait]`, so annotate your impl the same way. `push`, `extend` and `extend_owned` have default implementations on top of `push_owned`. Implement the trait for your database and pass the value to `DefaultContext::with_message_history`. The backend only stores the list. Each completion sends the full history, cut at the latest summary, and the context's counters only decide whether a new completion runs.

### Custom backend

This backend keeps one conversation in a JSON file. It uses the `async-trait` crate for the `#[async_trait]` attribute:

```rust
/// Keeps the message history of one conversation in a JSON file.
#[derive(Debug)]
struct JsonFileHistory {
    path: PathBuf,
    // Serializes read-modify-write cycles on the file.
    lock: Mutex<()>,
}

impl JsonFileHistory {
    async fn read(&self) -> anyhow::Result<Vec<ChatMessage>> {
        match tokio::fs::read_to_string(&self.path).await {
            Ok(json) => Ok(serde_json::from_str(&json)?),
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
            Err(err) => Err(err.into()),
        }
    }

    async fn write(&self, messages: &[ChatMessage]) -> anyhow::Result<()> {
        tokio::fs::write(&self.path, serde_json::to_vec(messages)?).await?;
        Ok(())
    }
}

#[async_trait]
impl MessageHistory for JsonFileHistory {
    async fn history(&self) -> anyhow::Result<Vec<ChatMessage>> {
        let _guard = self.lock.lock().await;
        self.read().await
    }

    async fn push_owned(&self, item: ChatMessage) -> anyhow::Result<()> {
        let _guard = self.lock.lock().await;
        let mut messages = self.read().await?;
        messages.push(item);
        self.write(&messages).await
    }

    async fn overwrite(&self, items: Vec<ChatMessage>) -> anyhow::Result<()> {
        let _guard = self.lock.lock().await;
        self.write(&items).await
    }
}
```

Source: [`snippets/src/agents/context_and_history.rs#L71-L112`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/agents/context_and_history.rs#L71-L112)

```rust
let mut context = DefaultContext::default();
context.with_message_history(JsonFileHistory {
    path: "session-42.json".into(),
    lock: Mutex::new(()),
});

let mut agent = Agent::builder().llm(&openai).context(context).build()?;
```

Source: [`snippets/src/agents/context_and_history.rs#L118-L124`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/agents/context_and_history.rs#L118-L124)

Every method takes `&self`, so the backend guards its own state; here a `tokio::sync::Mutex` keeps two writes from interleaving. `history` returns an empty list for a file that does not exist yet. `with_message_history` takes the value by ownership and stores it behind an `Arc`.

## Redis storage

The `redis` feature flag adds a `Redis` integration that implements `MessageHistory`:

```sh
cargo add swiftide@0.34 --features swiftide-agents,openai,redis
```

```rust
let redis = Redis::try_from_url("redis://localhost:6379", "session-42")?;

let mut context = DefaultContext::default();
context.with_message_history(redis);

let mut agent = Agent::builder()
    .llm(&openai)
    .context(context)
    .system_prompt("You help maintain the swiftide repository.")
    .build()?;

agent.query("Which crates changed this week?").await?;
```

Source: [`snippets/src/agents/context_and_history.rs#L17-L28`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/agents/context_and_history.rs#L17-L28)

The messages go into a Redis list under `<prefix>:message_history`, here `session-42:message_history`, one JSON string per message. Use one prefix per conversation. A new process that builds a context with the same prefix sees the whole conversation again. Build that agent without `.system_prompt(..)`, or with `no_system_prompt()`, as in the first example. Otherwise its first run pushes another system message onto the list.

The connection opens on first use. If Redis is unreachable, the agent returns `AgentError::MessageHistoryError`.

## Approvals across restarts

Approvals are not part of the message history. They live in the context's memory. When the agent stops with `StopReason::FeedbackRequired`, store the `ToolCall` it carries; the person's `ToolFeedback` comes later. Store each decision as a `(ToolCall, ToolFeedback)` pair, since `serde_json` cannot use a struct as a map key. Both types implement `Serialize` and `Deserialize`. Collect the pairs into a `HashMap` for `DefaultContext::with_tool_feedback` in the new process. [Human in the loop](/agents/human-in-the-loop/) covers approvals.

```rust
// First process: the agent waits for approval; store the person's decision with its call.
if let Some(StopReason::FeedbackRequired { tool_call, .. }) = agent.stop_reason() {
    let decisions = vec![(tool_call.clone(), ToolFeedback::approved())];
    std::fs::write("approvals.json", serde_json::to_string(&decisions)?)?;
}

// Second process: load the decisions next to the restored message history, then resume.
let decisions: Vec<(ToolCall, ToolFeedback)> =
    serde_json::from_str(&std::fs::read_to_string("approvals.json")?)?;

let mut context = DefaultContext::default();
context.with_message_history(Redis::try_from_url("redis://localhost:6379", "session-42")?);
context.with_tool_feedback(decisions.into_iter().collect::<HashMap<_, _>>());

let mut resumed = Agent::builder()
    .llm(&openai)
    .context(context)
    .no_system_prompt()
    .build()?;
resumed.run().await?;
```

Source: [`snippets/src/agents/context_and_history.rs#L133-L152`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/agents/context_and_history.rs#L133-L152)

`with_tool_feedback` accepts anything that converts into a `HashMap<ToolCall, ToolFeedback>` and adds the entries to the feedback the context already holds. The restored history still ends in the pending tool call, so `run` calls the tool again, and the tool now finds its approval.

## History rewrites

Since 0.33, `AgentContext::replace_history` swaps the whole history while the agent runs. It takes `&self`, so any hook can call it through `agent.context()`. This `after_each` hook shortens old tool outputs once the history passes `MAX_MESSAGES`:

```rust
const MAX_MESSAGES: usize = 40;
const KEEP_RECENT: usize = 10;

let mut agent = Agent::builder()
    .llm(&openai)
    .after_each(|agent: &mut Agent| {
        Box::pin(async move {
            let history = agent.history().await?;
            if history.len() <= MAX_MESSAGES {
                return Ok(());
            }

            let keep_from = history.len() - KEEP_RECENT;
            let trimmed = history
                .into_iter()
                .enumerate()
                .map(|(index, message)| match message {
                    ChatMessage::ToolOutput(call, ToolOutput::Text(_)) if index < keep_from => {
                        ChatMessage::ToolOutput(call, ToolOutput::text("(output removed)"))
                    }
                    other => other,
                })
                .collect();

            agent.context().replace_history(trimmed).await?;
            Ok(())
        })
    })
    .build()?;
```

Source: [`snippets/src/agents/context_and_history.rs#L36-L64`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/agents/context_and_history.rs#L36-L64)

The hook only replaces the text of older tool outputs. Each tool call keeps its output, which the providers require. `DefaultContext` writes the new list through `overwrite` and resets its completion counters. The next completion then runs even if no new message was added since the last one.

## Summaries

A `ChatMessage::Summary` shortens what the model sees without rewriting storage. When the history holds one, `DefaultContext` sends the system messages plus everything from the latest summary on. Add one with `ChatMessage::new_summary` through `agent.context().add_message(..)`. OpenAI-compatible providers send a summary as an assistant message. `ContextCompression` in agents-contrib writes these summaries for you; see [Contrib](/agents/contrib/).

## Custom contexts

For behavior the message history cannot express, implement `AgentContext` yourself. Wrap a `DefaultContext` and forward the calls you do not change. The [upgrading guide](/reference/upgrading/) has a context that counts added messages:

```rust
/// Counts the messages an agent adds and delegates everything else.
#[derive(Clone)]
pub struct CountingContext {
    inner: DefaultContext,
    added: Arc<std::sync::atomic::AtomicUsize>,
}

#[async_trait]
impl AgentContext for CountingContext {
    async fn next_completion(&self) -> anyhow::Result<Option<Vec<ChatMessage>>> {
        self.inner.next_completion().await
    }

    async fn current_new_messages(&self) -> anyhow::Result<Vec<ChatMessage>> {
        self.inner.current_new_messages().await
    }

    async fn add_messages(&self, items: Vec<ChatMessage>) -> anyhow::Result<()> {
        self.added
            .fetch_add(items.len(), std::sync::atomic::Ordering::Relaxed);
        self.inner.add_messages(items).await
    }

    async fn add_message(&self, item: ChatMessage) -> anyhow::Result<()> {
        self.added
            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        self.inner.add_message(item).await
    }

    #[allow(deprecated)]
    async fn exec_cmd(&self, cmd: &Command) -> Result<CommandOutput, CommandError> {
        self.inner.exec_cmd(cmd).await
    }

    fn executor(&self) -> &Arc<dyn ToolExecutor> {
        self.inner.executor()
    }

    async fn history(&self) -> anyhow::Result<Vec<ChatMessage>> {
        self.inner.history().await
    }

    async fn replace_history(&self, items: Vec<ChatMessage>) -> anyhow::Result<()> {
        self.inner.replace_history(items).await
    }

    async fn redrive(&self) -> anyhow::Result<()> {
        self.inner.redrive().await
    }

    async fn has_received_feedback(&self, tool_call: &ToolCall) -> Option<ToolFeedback> {
        self.inner.has_received_feedback(tool_call).await
    }

    async fn feedback_received(
        &self,
        tool_call: &ToolCall,
        feedback: &ToolFeedback,
    ) -> anyhow::Result<()> {
        self.inner.feedback_received(tool_call, feedback).await
    }
}
```

Source: [`snippets/src/reference/upgrading.rs#L211-L272`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/reference/upgrading.rs#L211-L272)

## Gotchas

- `with_existing_messages` calls `overwrite` on the backend. With Redis it deletes the list under the key and writes the given messages.
- `with_existing_messages` and `with_message_history` return `&mut DefaultContext`. Use a `let mut` binding, or end the chain with `.to_owned()` as the example does.
- A builder-made `Redis` uses the key `message_history` unless you set `message_history_key(..)` on the builder or call `with_message_history_key` afterwards. Agents that share that key also share one history.
- Messages added through `agent.context()` do not trigger `on_new_message` hooks. Only `Agent::add_message` runs them.

## Reference

- [`DefaultContext`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.DefaultContext.html)
- [`DefaultContext::with_existing_messages`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.DefaultContext.html#method.with_existing_messages)
- [`DefaultContext::with_message_history`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.DefaultContext.html#method.with_message_history)
- [`DefaultContext::with_tool_feedback`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.DefaultContext.html#method.with_tool_feedback)
- [`AgentContext`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.AgentContext.html)
- [`AgentContext::replace_history`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.AgentContext.html#tymethod.replace_history)
- [`MessageHistory`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.MessageHistory.html)
- [`Redis`](https://docs.rs/swiftide/0.34/swiftide/integrations/redis/struct.Redis.html)
- [`Redis::try_from_url`](https://docs.rs/swiftide/0.34/swiftide/integrations/redis/struct.Redis.html#method.try_from_url)
- [`ChatMessage`](https://docs.rs/swiftide/0.34/swiftide/chat_completion/enum.ChatMessage.html)
- [`ChatMessage::new_summary`](https://docs.rs/swiftide/0.34/swiftide/chat_completion/enum.ChatMessage.html#method.new_summary)
- [`ToolFeedback`](https://docs.rs/swiftide/0.34/swiftide/traits/enum.ToolFeedback.html)
- [`AgentBuilder::context`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.AgentBuilder.html#method.context)
- [`AgentBuilder::no_system_prompt`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.AgentBuilder.html#method.no_system_prompt)
- [`Agent::history`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.Agent.html#method.history)

## Next

[Tool executors](/agents/executors/)