# Agents on AWS Bedrock

> Run a Swiftide agent with two tools on a Claude model hosted by Amazon Web Services (AWS) Bedrock, and read why it stopped.

The agent on this page runs on a Claude model hosted by Amazon Web Services (AWS) Bedrock, calls two tools and then stops on its own. Client setup, credentials, region and runtime requirements are on [AWS Bedrock](/integrations/aws-bedrock/). This page covers what changes once that client drives an agent.

## Setup

Bedrock sits behind the `aws-bedrock` feature flag, and agents behind `swiftide-agents`:

```sh
cargo add swiftide@0.34 --features aws-bedrock,swiftide-agents
cargo add schemars@1 serde --features serde/derive
cargo add tokio --features macros,rt-multi-thread
cargo add anyhow
```

The full example lives in the swiftide repository. To run it, clone the repository and start it from the root:

```sh
git clone https://github.com/bosun-ai/swiftide && cd swiftide
cargo run -p swiftide-examples --example aws-bedrock-agent
```

The `swiftide-examples` package turns on the `all` feature flag. Its first build compiles every integration, including LanceDB, FastEmbed, Kafka and Fluvio. Those need native build tools such as `protoc` and `cmake`. If you only want Bedrock, copy the snippets below into your own crate instead.

## Tools

```rust title="examples/aws_bedrock_agent.rs"
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct FormatTimestampRequest {
    /// Prefix to prepend to the timestamp.
    prefix: String,
    /// Timestamp to format.
    timestamp: String,
}

#[swiftide::tool(description = "Get the current UTC date and time in RFC3339 format")]
async fn current_utc_time(context: &dyn AgentContext) -> Result<ToolOutput, ToolError> {
    let command_output = context
        .executor()
        .exec_cmd(&Command::shell("date -u +\"%Y-%m-%dT%H:%M:%SZ\""))
        .await?;

    Ok(command_output.into())
}

#[swiftide::tool(
    description = "Format a timestamp with a caller-provided prefix",
    param(name = "request", description = "Timestamp formatting input")
)]
async fn format_timestamp(
    _context: &dyn AgentContext,
    request: FormatTimestampRequest,
) -> Result<ToolOutput, ToolError> {
    Ok(ToolOutput::text(format!(
        "{}{}",
        request.prefix, request.timestamp
    )))
}
```

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

The tools import `swiftide::chat_completion::{ToolOutput, errors::ToolError}` and `swiftide::traits::{AgentContext, Command}`. The client comes from `swiftide::integrations::aws_bedrock_v2::AwsBedrock`.

`#[swiftide::tool]` turns each async function into a tool. It also generates a function with the same name that returns a `Box<dyn Tool>`. The `description` attribute becomes the tool description the model reads.

`current_utc_time` runs a shell command through the tool executor of the agent's context. The default context uses a `LocalExecutor`, so `date` runs on your machine. `exec_cmd` returns a `CommandOutput`, which keeps stdout and stderr as separate chunks since 0.33. `.into()` turns it into `ToolOutput::Text`, with all output as text in the order it arrived.

`format_timestamp` takes one parameter, `request`, of type `FormatTimestampRequest`. The model sends the arguments as a JavaScript Object Notation (JSON) object with a `request` field. That field holds `prefix` and `timestamp`. The struct derives `JsonSchema`, so its doc comments become the field descriptions in the schema. A struct used as a tool parameter needs `Serialize`, `Deserialize`, `JsonSchema` and `Debug`.

Bedrock receives every tool spec with strict schema validation turned on. That comes from the `tool_strict` option, which is `true` when you leave it unset. To turn it off, pass `Options::builder().tool_strict(false).build()?` to `AwsBedrockBuilder::default_options`.

## Agent

```rust title="examples/aws_bedrock_agent.rs"
let bedrock = AwsBedrock::builder()
    .default_prompt_model("global.anthropic.claude-sonnet-4-6")
    .build()?;
```

Source: [`examples/aws_bedrock_agent.rs#L61-L63`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/aws_bedrock_agent.rs#L61-L63)

```rust title="examples/aws_bedrock_agent.rs"
let mut agent = agents::Agent::builder()
    .llm(&bedrock)
    .tools(vec![current_utc_time(), format_timestamp()])
    .on_new_message(|_, msg| {
        let rendered = msg.to_string();
        Box::pin(async move {
            println!("{rendered}");
            Ok(())
        })
    })
    .limit(6)
    .build()?;
```

Source: [`examples/aws_bedrock_agent.rs#L67-L78`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/aws_bedrock_agent.rs#L67-L78)

`llm(&bedrock)` gives the agent the Bedrock client. `tools` registers both tools next to the default `stop` tool.

The `on_new_message` hook runs before each message enters the message history. It receives the agent and the message. The future it returns may not borrow the message, so the example renders the message to a string first. The `Display` form of `ChatMessage` shows the role, the text and any tool calls.

`limit(6)` caps the agent at six completions. Turns that only run pending tool calls do not count. At the cap, Swiftide logs "Agent loop limit reached" and stops the agent with `StopReason::NoNewMessages`.

## Run

```rust title="examples/aws_bedrock_agent.rs"
agent
    .query(
        "Call current_utc_time once. Then call format_timestamp with prefix \"UTC now: \" and \
         that timestamp. After that, report the formatted result and stop.",
    )
    .await?;
```

Source: [`examples/aws_bedrock_agent.rs#L82-L87`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/aws_bedrock_agent.rs#L82-L87)

`query` adds the prompt as a user message and runs the loop. The prompt asks the model to call `current_utc_time`, then `format_timestamp`, then report the result and stop. The model decides how it finishes, and the two ways lead to different stop reasons:

- It answers in text without a tool call. The default context then has no new messages to send, and the agent stops with `StopReason::NoNewMessages`.
- It calls the default `stop` tool. The agent stops with `StopReason::RequestedByTool`, which carries the tool call.

After `query` returns, `agent.stop_reason()` tells you which one happened. Branch on it when your code needs to know whether the model finished or the loop ran out.

## Variations

- To make the model always finish through a tool, keep the `stop` tool and say so in the prompt. To remove it, call `without_default_stop_tool` on the builder before `tools`, or register your own tool named `stop` with `add_tool`. A tool named `stop` passed to `tools` is dropped, because the default one is already in the set.
- To keep the agent going after a text answer, give it a `DefaultContext` with `with_stop_on_assistant(false)`. Then only the `stop` tool or the limit ends the run.
- A second `query` on the same agent starts with a fresh loop counter, so it gets six completions again.
- Bedrock has its own `Options` and its own `ReasoningEffort` with `Low`, `Medium`, `High` and `Max`. They are not the OpenAI types. See [AWS Bedrock settings](/models/overview/#aws-bedrock-settings).

## Gotchas

- The agent needs a model that supports tool use through the Converse API.
- `#[swiftide::tool]` reads the parameter names from the function signature. Renaming `request` changes the JSON field the model must send.

## Reference

- [AWS Bedrock integration](/integrations/aws-bedrock/)
- [`AwsBedrock`](https://docs.rs/swiftide/0.34/swiftide/integrations/aws_bedrock_v2/struct.AwsBedrock.html)
- [`AwsBedrockBuilder`](https://docs.rs/swiftide/0.34/swiftide/integrations/aws_bedrock_v2/struct.AwsBedrockBuilder.html)
- [`Options` (AWS Bedrock)](https://docs.rs/swiftide/0.34/swiftide/integrations/aws_bedrock_v2/struct.Options.html)
- [`ReasoningEffort` (AWS Bedrock)](https://docs.rs/swiftide/0.34/swiftide/integrations/aws_bedrock_v2/enum.ReasoningEffort.html)
- [`Agent`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.Agent.html)
- [`AgentBuilder`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.AgentBuilder.html)
- [`StopReason`](https://docs.rs/swiftide/0.34/swiftide/agents/enum.StopReason.html)
- [`DefaultContext`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.DefaultContext.html)
- [`Stop`](https://docs.rs/swiftide/0.34/swiftide/agents/tools/control/struct.Stop.html)
- [`LocalExecutor`](https://docs.rs/swiftide/0.34/swiftide/agents/tools/local_executor/struct.LocalExecutor.html)
- [`#[swiftide::tool]`](https://docs.rs/swiftide/0.34/swiftide/attr.tool.html)
- [`AgentContext`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.AgentContext.html)
- [`Command`](https://docs.rs/swiftide/0.34/swiftide/traits/enum.Command.html)
- [`CommandOutput`](https://docs.rs/swiftide/0.34/swiftide/traits/struct.CommandOutput.html)
- [`ToolOutput`](https://docs.rs/swiftide/0.34/swiftide/chat_completion/enum.ToolOutput.html)
- [`ChatMessage`](https://docs.rs/swiftide/0.34/swiftide/chat_completion/enum.ChatMessage.html)

## Next

[Structured prompts](/cookbook/structured-prompts/)