Upgrading
By the end of this page your code compiles against swiftide 0.34. Breaking changes land in minor releases, so upgrade one minor version at a time. Sections run newest first. Start at the section after your current release and work up. Releases without a section, such as 0.25 to 0.26 and 0.28 to 0.30, need no code changes.
cargo add swiftide@0.34Pipeline statistics
Section titled “Pipeline statistics”let stats = indexing::Pipeline::from_stream(vec![TextNode::new("hello")]) .then_store_with(MemoryStorage::default()) .run() .await?;println!("{} nodes stored", stats.nodes_stored);Source: snippets/src/reference/upgrading.rs#L27-L31
indexing::Pipeline::run returns Result<PipelineStats> instead of Result<()>. pipeline.run().await?; keeps compiling. A function that returns pipeline.run().await as its Result<()> tail expression needs pipeline.run().await?; Ok(()).
stats_collector() returns &Arc<StatsCollector>, shared with the running pipeline. Clone the Arc before run to watch the pipeline while it runs. Before, cloning the collector made a copy that never changed. Each branch of split_by gets its own collector, seeded with the statistics so far. merge keeps the collector of the pipeline it is called on, so the merged run reports both branches. See Metrics and statistics.
Search strategy setters
Section titled “Search strategy setters”let similarity = SimilaritySingleEmbedding::default().with_top_k(20);let hybrid = HybridSearch::<()>::default().with_top_k(20).with_top_n(5);Source: snippets/src/reference/upgrading.rs#L39-L40
with_top_k on SimilaritySingleEmbedding, and with_top_k, with_top_n, with_dense_vector_field and with_sparse_vector_field on HybridSearch, take self and return Self. Replace let mut s = Strategy::default(); s.with_top_k(n); with let s = Strategy::default().with_top_k(n);, and drop .to_owned() after a setter chain.
SimilaritySingleEmbedding::default() exists only for the strategy without a filter, so it no longer needs a type annotation. Build a filtered strategy with from_filter or with_filter. HybridSearch::default() still needs the annotation on stores that implement the retriever for more than one filter type. SearchStrategy no longer requires Default.
Swiftide’s HTTP clients no longer need a crypto provider from your application. Each one uses a process-wide rustls provider if you installed one, and otherwise its own. You can remove the rustls dependency and the install_default call from main, or keep them to choose the provider. See TLS.
OpenAI-compatible endpoints
Section titled “OpenAI-compatible endpoints”OpenAI::builder().api_base(url).api_key(key) points the OpenAI client at another endpoint without async-openai’s OpenAIConfig. .client(..) still takes a full client and replaces both; whichever is called last wins. See OpenAI-compatible providers.
Rust version and dependencies
Section titled “Rust version and dependencies”Swiftide now needs Rust 1.97 or newer. ReasoningItem.id is now an Option<String>. Public types from SQLx, text-splitter, LanceDB and DuckDB follow the new versions of those crates. If you pass these crates’ types in yourself, bump them in your Cargo.toml: async-openai 0.41 for OpenAI::builder().client(..), rmcp 2.1 for McpToolbox::try_from_transport, and redis 1.0.
TLS crypto provider
Section titled “TLS crypto provider”In 0.33, Swiftide does not pick a rustls crypto provider. Install one at the start of main, before you build the first model client, or the client build panics. See Crypto provider panic. Since 0.34 the clients no longer need it; see TLS.
System prompts
Section titled “System prompts”let mut agent = Agent::builder() .llm(&openai) .system_prompt("You review pull requests for {{ project }}.") .before_all(|agent: &mut Agent| { Box::pin(async move { if let Some(prompt) = agent.system_prompt_mut() { prompt.insert_context_value("project", "swiftide"); } Ok(()) }) }) .build()?;Source: snippets/src/reference/upgrading.rs#L48-L59
SystemPrompt and its builder are gone, with the role, guidelines and constraints fields. The agent’s system prompt is now a Prompt. AgentBuilder::system_prompt takes anything that converts into a Prompt, such as a &str.
An agent without .system_prompt(..) sends no system prompt. Before 0.33 it sent a default one.
The before_all hook now runs before the agent renders the system prompt. The hook in the snippet uses system_prompt_mut to fill the project value at run time. See System prompts.
Hooks and the agent
Section titled “Hooks and the agent”let mut agent = Agent::builder() .llm(&openai) .after_each(|agent: &mut Agent| { Box::pin(async move { if agent.history().await?.len() > 40 { agent.stop("history too long").await; } Ok(()) }) }) .build()?;Source: snippets/src/reference/upgrading.rs#L68-L78
Most hooks now receive &mut Agent instead of &Agent. A hook can call methods that take &mut self, such as stop. The changed hooks are before_all, on_start, before_completion, after_completion, before_tool, after_tool, after_each and on_stop. on_new_message and on_stream still receive &Agent. See Hooks.
Custom contexts
Section titled “Custom contexts”AgentContext has a new required method, replace_history, which replaces the whole message history. Implement it if you have your own context.
Command output
Section titled “Command output”let cmd = Command::shell("cargo test").with_current_dir("crates/core");match context.executor().exec_cmd(&cmd).await { Ok(output) => { let stdout: Vec<u8> = output.stdout().flat_map(|b| b.iter().copied()).collect(); println!("{}", String::from_utf8_lossy(&stdout)); } Err(CommandError::NonZeroExit(output)) => { for chunk in output.stderr() { eprintln!("{}", String::from_utf8_lossy(chunk)); } } Err(other) => return Err(other.into()),}Source: snippets/src/reference/upgrading.rs#L87-L99
CommandOutput.output is gone, and so is as_ref() on CommandOutput. It keeps stdout and stderr chunks in the order they arrived. Read them with stdout() and stderr(), or get one string with to_string_lossy() or Display. CommandOutput::from now takes bytes, through Into<Bytes>.
AgentContext::exec_cmd has been deprecated since 0.27. Call context.executor().exec_cmd(&cmd) instead.
Tasks crate
Section titled “Tasks crate”let mut task = Task::<i32, i32>::new();let double = task.register_node_fn(|n: &i32| Ok::<_, NodeError>(n * 2));task.starts_with(double);task.register_transition(double, task.transitions_to_finish())?;
match task.run(21).await? { TaskRunOutcome::Completed(answer) => println!("{answer}"), TaskRunOutcome::Paused => println!("waiting to resume"),}Source: snippets/src/reference/upgrading.rs#L107-L115
Task graphs moved to the swiftide-tasks crate, available as swiftide::tasks. Its own swiftide-tasks feature flag lets you use tasks without agents.
swiftide::agents::tasks now re-exports swiftide::tasks, but the nested modules are gone. Import from the root: tasks::task::Task becomes tasks::Task, and tasks::closures::SyncFn becomes tasks::SyncFn. TaskAgent moved from tasks::impls to swiftide::agents::TaskAgent.
Task::run and Task::resume return a TaskRunOutcome: Completed with the output, or Paused. transitions_to_finish() replaces transitions_to_done(). current_nodes() replaces current_node(). done(), current_input(), current_output(), node_at() and node_at_index() are gone.
TransitionPayload and NextNode are no longer public. In a transition closure, return Transition::next(&node, value) or node.transitions_with(value) to move on. Return Transition::pause() to pause and Transition::error(err) to fail.
A fan-out ends in a join: Transition::fan_out(..) with join_with(..), and the join node receives a JoinInput<T>. See Fan-out and join.
Tool schemas
Section titled “Tool schemas”ToolSpecBuilder::build now validates the parameter schema against the strict tool-schema contract and returns a ToolSpecBuildError. A schema that built in 0.32 can fail. See Rejected tool schemas.
The #[tool] macro reads the tool and argument descriptions from the doc comment and its # Arguments list, as in the 0.32 snippet.
AWS Bedrock
Section titled “AWS Bedrock”The Amazon Web Services (AWS) Bedrock integration is now integrations::aws_bedrock_v2. Build it with AwsBedrock::builder().default_prompt_model("model-id"). The model family builders, such as build_anthropic_family, are gone. See AWS Bedrock.
Chat messages
Section titled “Chat messages”ChatMessage has two new variants, UserWithParts for images and other content parts, and Reasoning. A match without a wildcard arm needs arms for both. ChatCompletionRequest now has a lifetime parameter. See Images and multimodal input.
Tool arguments
Section titled “Tool arguments”/// An inclusive range of lines.#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]pub struct LineRange { /// First line, starting at 1 pub start: u32, /// Last line, inclusive pub end: u32,}
/// Reads line ranges from files in the workspace.////// # Arguments/// - `paths`: Files to read, relative to the workspace root./// - `range`: Optional line range to limit the output.#[swiftide::tool]async fn read_lines( context: &dyn AgentContext, paths: Vec<String>, range: Option<LineRange>,) -> Result<ToolOutput, ToolError> { let _ = (context, range); Ok(ToolOutput::text(paths.join("\n")))}Source: snippets/src/reference/upgrading.rs#L121-L143
ToolSpec.parameters and ParamSpec are gone. A hand-written tool sets parameters_schema, an Option<schemars::Schema>, instead. The #[tool] macro derives a JSON Schema from the Rust types of the arguments. A struct argument needs schemars::JsonSchema next to the serde derives, so add schemars 1.x to your dependencies. See Tools.
Working directories
Section titled “Working directories”let repo = executor.scoped("repo");let output = repo.exec_cmd(&Command::shell("git status --short")).await?;println!("{output}");Source: snippets/src/reference/upgrading.rs#L159-L161
Its variants are now structs, so patterns like Command::Shell(script) no longer compile. Build commands with Command::shell, Command::read_file and Command::write_file, and set a directory with with_current_dir. scoped on ExecutorExt wraps a tool executor in a ScopedExecutor. It joins a relative working directory onto the scope and leaves an absolute one unchanged. A custom tool executor reads the directory with current_dir_path(). See Tool executors.
MCP tool names
Section titled “MCP tool names”Model Context Protocol (MCP) tools are now named server:tool. The prefix is the name the server reports in its handshake, or mcp when it reports none. The name you set with McpToolbox::with_name does not change it. MCP needs the mcp feature flag. See MCP.
Generic nodes
Section titled “Generic nodes”#[derive(Clone)]pub struct Uppercase;
#[async_trait]impl Transformer for Uppercase { type Input = String; type Output = String;
async fn transform_node(&self, mut node: TextNode) -> anyhow::Result<TextNode> { node.chunk = node.chunk.to_uppercase(); Ok(node) }}Source: snippets/src/reference/upgrading.rs#L167-L179
Indexing pipelines are generic over the content of a node. Node<T> replaces Node, and TextNode is the alias for Node<String>. The transformer, batch transformer, chunker and persist traits declare type Input and type Output. For text, set both to String. A loader declares only Output, and a node cache only Input.
Tool invocation
Section titled “Tool invocation”#[derive(Clone)]pub struct Echo;
#[async_trait]impl Tool for Echo { async fn invoke( &self, _context: &dyn AgentContext, tool_call: &ToolCall, ) -> Result<ToolOutput, ToolError> { Ok(ToolOutput::text(tool_call.args().unwrap_or_default())) }
fn name(&self) -> Cow<'_, str> { Cow::Borrowed("echo") }
fn tool_spec(&self) -> ToolSpec { ToolSpec::builder() .name("echo") .description("Repeats its arguments") .build() .expect("valid tool spec") }}Source: snippets/src/reference/upgrading.rs#L183-L207
Tool::invoke receives the whole ToolCall instead of raw_args: Option<&str>. The raw arguments are tool_call.args().
Context and message history
Section titled “Context and message history”let mut context = DefaultContext::default();context.with_existing_messages(saved).await?;
let mut agent = Agent::builder().llm(&openai).context(context).build()?;agent.query("Continue where we left off").await?;Source: snippets/src/reference/upgrading.rs#L290-L294
To store messages elsewhere, implement MessageHistory and pass it to DefaultContext::with_message_history. Loading saved messages is now the async with_existing_messages. Redis implements MessageHistory behind the redis feature flag. See Context and message history.
Context methods
Section titled “Context methods”/// 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
The context methods that read or change messages now return a Result. Add a ? where you call them, so context.history().await becomes context.history().await?. This applies to next_completion, current_new_messages, add_message, add_messages, history and redrive.
A custom context needs three new methods. executor returns the tool executor that runs commands for the agent. feedback_received stores the feedback for a tool call, such as a human approval. has_received_feedback returns that feedback when the agent calls the tool again. The snippet wraps a DefaultContext and passes each call through to it. It targets 0.34, so it also has replace_history, which arrived in 0.33.
Tool output
Section titled “Tool output”match output { ToolOutput::Text(text) => text.clone(), ToolOutput::FeedbackRequired(payload) => format!("waiting for approval: {payload:?}"), ToolOutput::Fail(error) => format!("tool failed: {error}"), other => format!("agent stops: {other:?}"),}Source: snippets/src/reference/upgrading.rs#L278-L283
ToolOutput has a new FeedbackRequired variant. A tool returns it when it needs feedback before it runs. The agent then stops with the new StopReason::FeedbackRequired. Both enums are marked non-exhaustive, so your match already has a wildcard arm. That arm now catches FeedbackRequired, which compiles but can hide a paused agent. Add an arm for it. To ask for approval before a tool runs, wrap the tool in ApprovalRequired. See Human in the loop.
Model errors
Section titled “Model errors”#[derive(Debug, Clone)]pub struct CannedModel { pub overloaded: bool,}
#[async_trait]impl SimplePrompt for CannedModel { async fn prompt(&self, prompt: Prompt) -> Result<String, LanguageModelError> { if self.overloaded { return Err(LanguageModelError::transient("model is overloaded")); } // An `anyhow::Error` converts into `LanguageModelError::PermanentError`. let rendered = prompt.render()?; Ok(format!("You asked: {rendered}")) }}Source: snippets/src/reference/upgrading.rs#L300-L315
The model traits now return a LanguageModelError. This breaks every model client you wrote yourself. SimplePrompt::prompt returns Result<String, LanguageModelError> instead of anyhow::Result<String>. EmbeddingModel::embed and SparseEmbeddingModel::sparse_embed changed the same way. ChatCompletion::complete returns LanguageModelError in place of ChatCompletionError, which is gone.
Pick the variant that tells callers what to do next. LanguageModelError::transient marks an error worth a retry, such as a rate limit. permanent marks one that fails again on retry. context_length_exceeded marks a request too long for the model. The ? operator still works on an anyhow::Error, which becomes a PermanentError. See Errors, backoff, usage and tokens.
Templates
Section titled “Templates”let prompt = Prompt::from("Summarize {{ title }} in one sentence.") .with_context_value("title", "the changelog");let rendered = prompt.render()?;Source: snippets/src/reference/upgrading.rs#L321-L323
Template is gone. Prompt holds either a one-off template string or the name of a compiled template. Use Prompt::from for a string and Prompt::from_compiled_template for a name. render is no longer async. See Prompts and templates.
Stopping an agent
Section titled “Stopping an agent”stop is now async and takes a stop reason, so agent.stop() becomes agent.stop("done").await. A &str converts into StopReason::Other.
Reference
Section titled “Reference”PromptAgentAgentBuilderhooksStopReasonAgentContextCommandOutputCommandErrorCommandExecutorExtScopedExecutorTaskTaskRunOutcomeTransitionJoinInputTaskAgentAwsBedrockChatMessageLanguageModelErrorSimplePromptEmbeddingModelChatCompletionToolToolCallToolSpecNodeTextNodeTransformerMessageHistoryDefaultContextToolOutputToolFeedbackApprovalRequired
Next: Troubleshooting and FAQ