# How agents work

> Build a Swiftide agent, run its completion and tool-call loop, and read why it stopped.

After this page you can build an agent, run it, continue its conversation, and read why it stopped. The larger example is the [release-notes-agent showcase](/showcases/release-notes-agent/), which drafts release notes from git history. Agents live in `swiftide::agents`, behind the `swiftide-agents` feature flag:

```sh
cargo add swiftide@0.34 --features openai,swiftide-agents
cargo add tokio --features full
cargo add anyhow
```

## A minimal agent

```rust
// Reads OPENAI_API_KEY from the environment.
let openai = OpenAI::builder()
    .default_prompt_model("gpt-5-mini")
    .build()?;

let mut agent = Agent::builder()
    .llm(&openai)
    .system_prompt("You answer questions about Rust crates in two sentences.")
    .limit(10)
    .build()?;

// A failed run, including a tool that fails too often, returns `Err` here.
agent.query("What does the tokio crate do?").await?;
assert!(agent.is_stopped());

match agent.stop_reason() {
    Some(StopReason::RequestedByTool(..)) => println!("a tool stopped the agent"),
    Some(StopReason::NoNewMessages) => println!("no new messages, or the limit was reached"),
    other => println!("stopped: {other:?}"),
}

// A second query appends to the same message history and runs the loop again.
agent.query("And how does it compare to async-std?").await?;
println!("{} messages in the history", agent.history().await?.len());
```

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

`OpenAI::builder()` reads `OPENAI_API_KEY` from the environment.

`llm` takes any `ChatCompletion` that is `Clone` and clones it into the agent. `build` fails without it. `system_prompt` takes anything that converts into a `Prompt`. The agent has no system prompt unless you set one. `limit` caps the completions in one run.

`query` adds a user message and loops until the agent stops. A stopped agent keeps its message history, so the second `query` continues the same conversation. `history` returns every message in the context.

## The agent loop

![Agent loop](/diagrams/diagram-agent-loop.svg)

The agent sends the message history and the spec of every tool to the model. When the completion holds tool calls, the agent runs the tools and records their outputs in the history. Then it asks the model again. Hooks run around every step.

The loop ends when a tool stops the agent, or when the step limit is reached. With `DefaultContext`, it also ends when the last message is an answer without a tool call. `DefaultContext::with_stop_on_assistant(false)` turns that off.

A tool that returns `ToolOutput::FeedbackRequired` stops the agent and leaves its tool call open, without an output. A call to a tool the agent does not have is skipped with a warning and gets no output either.

## State and stop reasons

`agent.state()` returns a `State`: `Pending` before the first run, `Running` during one, and `Stopped` with a `StopReason` after it. `agent.stop_reason()` tells you why it stopped:

- a tool returned `ToolOutput::Stop` (`RequestedByTool`, which holds that tool call)
- an answer without a tool call, the step limit, or the end of a `query_once` or `run_once` step (`NoNewMessages`)
- a pending approval (`FeedbackRequired`)
- a failure (`ToolCallsOverLimit`, `AgentFailed`, `Error`)

For `ToolCallsOverLimit` and `Error`, `query` also returns the error, so the `?` in the snippet returns before the `match`. `StopReason` is marked non-exhaustive, so a `match` on it needs a wildcard arm. [Stopping and structured results](/agents/stopping-and-structured-results/) covers each reason.

## Runs

`query_once` adds the message and runs one step. `run` and `run_once` do the same without a new message, which is how you resume after an approval. A run on an agent whose state is `Running` returns `AgentError::AlreadyRunning`.

Cloning an agent shares its hooks and context, so both clones write to the same message history. Each clone gets its own copy of the tools, so `add_tool` on one does not change the other. The clone copies the state and starts with an empty tool retry counter.

To run an agent as one step of a larger workflow, wrap it in a task node; see [Agents as task nodes](/tasks/agents-as-nodes/).

## A larger builder

The release-notes-agent showcase builds its agent in one function:

```rust title="examples/release-notes-agent/src/agent.rs"
/// Builds the agent. All tools run through `executor`, whose working directory is the repo.
pub fn build(
    llm: &(impl ChatCompletion + Clone + 'static),
    executor: Arc<dyn ToolExecutor>,
    task: &ReleaseTask,
    github_tools: Option<impl ToolBox + 'static>,
) -> Result<Agent> {
    let mut builder = Agent::builder();
    builder
        .llm(llm)
        .context(DefaultContext::from_executor(executor))
        .system_prompt(task.system_prompt(github_tools.is_some()))
        // Our own `stop` tool replaces the default one; call this before `tools`.
        .without_default_stop_tool()
        .tools([
            git_log_since(),
            git_diff(),
            read_file(),
            // Drafts that describe a change twice go back to the model before anyone reviews them.
            OneEntryPerChange::new(ApprovalRequired::new(WriteChangelog::new(&task.notes_file)))
                .into(),
            ReleaseNotes::stop_tool().into(),
        ])
        .limit(STEP_LIMIT);

    if let Some(github_tools) = github_tools {
        builder.add_toolbox(github_tools);
    }
    progress::report_tool_calls(&mut builder);
    progress::stream_draft(&mut builder);

    Ok(builder.build()?)
}
```

Source: [`examples/release-notes-agent/src/agent.rs#L104-L136`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/release-notes-agent/src/agent.rs#L104-L136)

The showcase passes an `OpenAI` client pointed at any OpenAI-compatible endpoint, but the function accepts any provider.

`context` sets where the message history lives and which tool executor runs commands. `DefaultContext::from_executor` keeps the history in memory and uses the executor that `run` creates:

```rust title="examples/release-notes-agent/src/main.rs"
let root = Git::new(&LocalExecutor::new(&cli.repo))
    .top_level()
    .await
    .with_context(|| format!("{} is not a git repository", cli.repo.display()))?;
// Every tool the agent runs, runs with the repository as its working directory.
let executor: Arc<dyn ToolExecutor> = Arc::new(
    LocalExecutor::builder()
        .workdir(&root)
        .default_timeout(Some(Duration::from_mins(1)))
        .build()?,
);
```

Source: [`examples/release-notes-agent/src/main.rs#L93-L103`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/release-notes-agent/src/main.rs#L93-L103)

`Agent::builder()` starts with one tool, `stop`, which the model calls when it is done. The showcase replaces it with a `stop` tool whose arguments are the release notes, so it calls `without_default_stop_tool` before `tools`. The list mixes tools from `#[swiftide::tool]` functions, a tool wrapped in `ApprovalRequired`, and a `StopWithArgs` built from a JavaScript Object Notation (JSON) schema.

`add_toolbox` adds the GitHub tools from a Model Context Protocol (MCP) server when you pass `--mcp` in a repository hosted on GitHub. The MCP toolbox needs the `mcp` feature flag, which the showcase enables. The two `progress` functions take `&mut AgentBuilder` and register hooks, which keeps the builder short. The [hooks page](/agents/hooks/) shows a captured run of this agent with `claude-sonnet-5-5`.

### Agent names

`AgentBuilder::name` labels an agent, and `Agent::name()` reads the label back:

```rust
let agent = Agent::builder()
    .llm(openai)
    .name("release-checker")
    .build()?;
assert_eq!(agent.name(), "release-checker");

// Without `name`, every agent is called "unnamed_agent".
let unnamed = Agent::builder().llm(openai).build()?;
assert_eq!(unnamed.name(), "unnamed_agent");
```

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

The name defaults to `unnamed_agent`. Swiftide only shows it in the agent's `Debug` output, so use it in your own logs or to tell agents apart in a multi-agent setup.

## Agent and application responsibilities

Besides running the loop, the agent:

- renders the system prompt on the first run and appends it to the history, after any messages that `BeforeAll` hooks or an existing history put there
- removes duplicate keys from the JSON arguments of a tool call
- wraps queries, steps, tools and hooks in tracing spans ([Tracing](/observability/tracing/))
- lists toolbox tools when it first starts

You provide the model, the tools, the system prompt, the tool executor, the hooks and, when you want a typed result, the schema of the stop tool.

## Gotchas

- A hook that returns an error is logged with tracing. The agent carries on.
- When a completion holds only calls to tools the agent does not have, no output is recorded. The next `query` returns `AgentError::UnfulfilledToolCalls`.
- Some errors return from `query` without setting a stop reason, for example a message history error or a tool task that fails to join. The agent then stays `Running`, and the next run returns `AgentError::AlreadyRunning`.
- A clone of a `Pending` agent renders the system prompt again on its first run and appends it to the shared history.

## Reference

- [`Agent`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.Agent.html)
- [`AgentBuilder`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.AgentBuilder.html)
- [`DefaultContext`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.DefaultContext.html)
- [`State`](https://docs.rs/swiftide/0.34/swiftide/agents/enum.State.html)
- [`StopReason`](https://docs.rs/swiftide/0.34/swiftide/agents/enum.StopReason.html)
- [`AgentError`](https://docs.rs/swiftide/0.34/swiftide/agents/errors/enum.AgentError.html)
- [`StopWithArgs`](https://docs.rs/swiftide/0.34/swiftide/agents/tools/control/struct.StopWithArgs.html)
- [`ApprovalRequired`](https://docs.rs/swiftide/0.34/swiftide/agents/tools/control/struct.ApprovalRequired.html)
- [`LocalExecutor`](https://docs.rs/swiftide/0.34/swiftide/agents/tools/local_executor/struct.LocalExecutor.html)
- [`ChatCompletion`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.ChatCompletion.html)
- [`OpenAI`](https://docs.rs/swiftide/0.34/swiftide/integrations/openai/type.OpenAI.html)

## Next

[Tools](/agents/tools/)