Agents on AWS Bedrock
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. This page covers what changes once that client drives an agent.
Bedrock sits behind the aws-bedrock feature flag, and agents behind swiftide-agents:
cargo add swiftide@0.34 --features aws-bedrock,swiftide-agentscargo add schemars@1 serde --features serde/derivecargo add tokio --features macros,rt-multi-threadcargo add anyhowThe full example lives in the swiftide repository. To run it, clone the repository and start it from the root:
git clone https://github.com/bosun-ai/swiftide && cd swiftidecargo run -p swiftide-examples --example aws-bedrock-agentThe 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.
#[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
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.
let bedrock = AwsBedrock::builder() .default_prompt_model("global.anthropic.claude-sonnet-4-6") .build()?;Source: examples/aws_bedrock_agent.rs#L61-L63
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
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.
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
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
stoptool. The agent stops withStopReason::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
Section titled “Variations”- To make the model always finish through a tool, keep the
stoptool and say so in the prompt. To remove it, callwithout_default_stop_toolon the builder beforetools, or register your own tool namedstopwithadd_tool. A tool namedstoppassed totoolsis dropped, because the default one is already in the set. - To keep the agent going after a text answer, give it a
DefaultContextwithwith_stop_on_assistant(false). Then only thestoptool or the limit ends the run. - A second
queryon the same agent starts with a fresh loop counter, so it gets six completions again. - Bedrock has its own
Optionsand its ownReasoningEffortwithLow,Medium,HighandMax. They are not the OpenAI types. See AWS Bedrock settings.
Gotchas
Section titled “Gotchas”- The agent needs a model that supports tool use through the Converse API.
#[swiftide::tool]reads the parameter names from the function signature. Renamingrequestchanges the JSON field the model must send.