Skip to content

What is Swiftide

Swiftide is a Rust library for building LLM (large language model) applications. It has three building blocks: agents for tool loops, tasks for typed hand-offs, and pipelines for data flows. This page shows each block in a real program, so you know which one to start with.

Building blockUse it forEntry pointFeature flag
AgentsA model that calls tools in a loop until it stopsswiftide::agents::Agent::builder()swiftide-agents
TasksA typed graph of steps with fan-out, join and pauseswiftide::tasks::Task::new()swiftide-tasks, also turned on by swiftide-agents
PipelinesStreaming data into storage, and answering questions from itswiftide::indexing::Pipeline, swiftide::query::Pipelinealways compiled; each integration has its own flag

The three blocks share one set of core traits, such as SimplePrompt, ChatCompletion, EmbeddingModel and Tool. The OpenAI client implements the first three, so one client can serve all three blocks. It needs the openai feature flag.

Agent loop

The release-notes-agent showcase reads the git history since the last tag and drafts release notes. This function builds its agent:

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

The agent sends its message history to the model, calls the tools the model asks for, and adds their results to the history. The tools run their commands through the tool executor in the context, here a DefaultContext built from the repository’s executor. progress::report_tool_calls prints each tool call from before_tool and after_tool hooks. progress::stream_draft registers an on_stream hook, which switches the agent to streaming completions. add_toolbox adds optional GitHub tools from an MCP (Model Context Protocol) server, which needs the mcp feature flag.

The loop ends when a tool asks the agent to stop, when a tool needs human feedback (here ApprovalRequired around WriteChangelog), when the model replies without calling a tool, or when it reaches the step limit. Each case sets a stop reason. Reaching the limit is not an error, so check the stop reason after query returns. This agent swaps the default stop tool for its own, which requires the full release notes as structured arguments. limit(STEP_LIMIT) caps the completions per query at 40. Your first agent builds the first step of this agent with one tool and one hook. Read more in Agents overview and Release notes agent.

Task graph

The workspace-architect showcase drafts an ARCHITECTURE.md for a Cargo workspace. Its task first discovers the member crates, then fans out to one analyst per crate:

examples/workspace-architect/src/graph.rs
// One branch per crate, all evaluating the same analyst node with a different input.
task.register_transition(discover, move |members: WorkspaceMembers| {
let mut crates = members.crates.into_iter();
let Some(first) = crates.next() else {
return Transition::error("the workspace has no crates to document");
};
crates
.fold(Transition::fan_out(&analyse, first), |fan_out, member| {
fan_out.and(&analyse, member)
})
.concurrency_model(ConcurrencyModel::Parallel)
.join_with(join.join())
})?;

Source: examples/workspace-architect/src/graph.rs#L92-L105

Every task node has an input type and an output type. The compiler checks that each transition hands the next node the input it expects. The transition after discover starts one branch per crate on the same analyse node and runs the branches with ConcurrencyModel::Parallel. join_with names the join node that collects the results. The analyse node still needs its own transition into the join, task.register_transition(analyse, join.join()), or the task fails validation with a missing-transition error. An empty workspace ends the task with Transition::error.

A run on the swiftide workspace with claude-sonnet-5-5, through an OpenAI-compatible gateway, prints this (trimmed):

[ 0.0s] analyse started swiftide-core
[ 0.0s] analyse started swiftide-langfuse
...
[ 40.5s] analyse finished swiftide-core in 40.5s (15 tool calls)
[ 40.5s] join 10 summaries; 230s of agent work in 41s wall-clock (5.7x)

Your first task builds a smaller task that starts the same way, with a discover node and no fan-out. Read more in How tasks work and Workspace architect.

Indexing pipeline

An indexing pipeline streams nodes from a loader through transformers, a chunker and an embedder into storage. A query pipeline turns a question into an answer from retrieved chunks. The ask-your-codebase showcase indexes a codebase into LanceDB and answers questions about it with this query pipeline:

examples/ask-your-codebase/src/query.rs
let search = SimilaritySingleEmbedding::default().with_top_k(top_k);
let pipeline = query::Pipeline::from_search_strategy(search)
// Rephrase the question into several, to widen what the search can match
.then_transform_query(
GenerateSubquestions::builder()
.client(llm.clone())
.prompt_template(SUBQUESTIONS_PROMPT.into())
.build()?,
)
// Embed with the same local model that embedded the chunks
.then_transform_query(Embed::from_client(embedding::local_model()?))
.then_retrieve(codebase.vector_store()?)
// Answer the original question from the retrieved code, each chunk under its file path
.then_answer(
answers::Simple::builder()
.client(llm)
.prompt_template(ANSWER_PROMPT.into())
.document_template(CHUNK_TEMPLATE)
.build()?,
);
let answered = pipeline.query(question).await?;

Source: examples/ask-your-codebase/src/query.rs#L36-L58

Query transformers rewrite and embed the question, the retriever searches the LanceDB table with the search strategy, and the answer generator writes the answer from the retrieved chunks. Your first pipeline runs this showcase and walks through each step, along with the indexing pipeline that fills the table. The showcase needs the lancedb, fastembed, tree-sitter and openai feature flags.

The docs-qa showcase adds hybrid search in Qdrant (the qdrant feature flag), reranking and evaluation. Read more in Indexing overview, How querying works, Ask your codebase and Docs Q&A.

The blocks nest. In workspace-architect, every fan-out branch runs an agent. CrateAnalyst implements TaskNode with a WorkspaceCrate in and a CrateSummary out. Each evaluation builds a fresh agent for one crate:

examples/workspace-architect/src/analyst.rs
/// How often an agent is told to fix a missing or malformed report before the crate fails.
const MAX_CORRECTIONS: usize = 2;
#[async_trait]
impl<L: ChatCompletion + Clone + 'static> TaskNode for CrateAnalyst<L> {
type Input = WorkspaceCrate;
type Output = CrateSummary;
type Error = AnalystError;
async fn evaluate(
&self,
_node_id: &DynNodeId<Self>,
member: &WorkspaceCrate,
) -> Result<CrateSummary, AnalystError> {
self.analyse(member).await.map_err(|cause| AnalystError {
crate_name: member.name.clone(),
cause,
})
}
}
impl<L: ChatCompletion + Clone + 'static> CrateAnalyst<L> {
async fn analyse(&self, member: &WorkspaceCrate) -> Result<CrateSummary, AnalysisFailure> {
let analysis = self.progress.analysis(&member.name);
let mut agent = self.agent_for(member, analysis.clone())?;
agent.query(brief(member)).await?;
// Models occasionally answer in prose or nest the `stop` arguments; say what went wrong.
let mut corrections = 0;
let report = loop {
match submitted_report(&agent) {
Ok(report) => break report,
Err(problem) if corrections < MAX_CORRECTIONS => {
corrections += 1;
analysis.corrected(&problem);
agent.query(problem.correction()).await?;
}
Err(problem) => return Err(problem.into()),
}
};
analysis.finished(&report);
Ok(report.reconciled_with(member))
}
}
/// The report the agent passed to `stop`, checked against the `CrateReport` schema.
fn submitted_report(agent: &Agent) -> Result<CrateReport, ReportProblem> {
match agent.stop_reason() {
Some(StopReason::RequestedByTool(_, Some(arguments))) => {
Ok(CrateReport::deserialize(arguments)?)
}
other => Err(ReportProblem::Missing {
stop_reason: other.cloned(),
}),
}
}

Source: examples/workspace-architect/src/analyst.rs#L138-L194

Two built-in node types need no impl of your own. Arc<dyn SimplePrompt> takes a Prompt and returns a String; the showcase’s render step is one. TaskAgent wraps an Agent as a node that takes a Prompt and returns (). To pass an answer to the next node, write your own TaskNode. TaskAgent also locks the agent, so branches that share one TaskAgent run one at a time. For parallel work, build one agent per branch as CrateAnalyst does.

A task node or a tool is plain async Rust, so its body can run an indexing pipeline or a query pipeline.

You needUse
A model that decides which tools to call and when to stopan agent
Known steps with typed inputs and outputs, parallel branches, or a pause for human approvala task
Documents or code loaded, chunked, embedded and stored, then searched to answer questionsan indexing pipeline and a query pipeline
Several agents in a fixed order, or one agent per item in parallela task with agent nodes

Start with an agent when the model should choose the steps. Move to a task when you know the steps and want the compiler to check the hand-offs between them.

Swiftide is pre-1.0. APIs change while the agent and task APIs settle, and breaking changes land in minor releases. Upgrade one minor version at a time with Upgrading, and check the Changelog for each release.

These docs describe swiftide 0.34.

Swiftide is MIT licensed and part of the bosun.ai project.

Installation