# What is Swiftide

> Swiftide is a Rust library with three building blocks, agents, tasks, and indexing and query pipelines, and this page shows each one in a real program and when to pick it.

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 blocks

| Building block | Use it for                                                   | Entry point                                                 | Feature flag                                          |
| -------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | ----------------------------------------------------- |
| Agents         | A model that calls tools in a loop until it stops            | `swiftide::agents::Agent::builder()`                        | `swiftide-agents`                                     |
| Tasks          | A typed graph of steps with fan-out, join and pause          | `swiftide::tasks::Task::new()`                              | `swiftide-tasks`, also turned on by `swiftide-agents` |
| Pipelines      | Streaming data into storage, and answering questions from it | `swiftide::indexing::Pipeline`, `swiftide::query::Pipeline` | always 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.

### Agents

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

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

```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 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](/start/your-first-agent/) builds the first step of this agent with one tool and one hook. Read more in [Agents overview](/agents/overview/) and [Release notes agent](/showcases/release-notes-agent/).

### Tasks

![Task graph](/diagrams/diagram-task-graph.svg)

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:

```rust title="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`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/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):

```text
[   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](/start/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](/tasks/overview/) and [Workspace architect](/showcases/workspace-architect/).

### Pipelines

![Indexing pipeline](/diagrams/diagram-pipeline.svg)

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:

```rust title="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`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/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](/start/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](/indexing/overview/), [How querying works](/querying/overview/), [Ask your codebase](/showcases/ask-your-codebase/) and [Docs Q\&A](/showcases/docs-qa/).

## Composition

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:

```rust title="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`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/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.

## Block selection

| You need                                                                                    | Use                                       |
| ------------------------------------------------------------------------------------------- | ----------------------------------------- |
| A model that decides which tools to call and when to stop                                   | an agent                                  |
| Known steps with typed inputs and outputs, parallel branches, or a pause for human approval | a task                                    |
| Documents or code loaded, chunked, embedded and stored, then searched to answer questions   | an indexing pipeline and a query pipeline |
| Several agents in a fixed order, or one agent per item in parallel                          | a 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.

## Project status

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](/reference/upgrading/), and check the [Changelog](/reference/changelog/) for each release.

These docs describe swiftide 0.34.

Swiftide is MIT licensed and part of the [bosun.ai](https://bosun.ai) project.

## Reference

- [`swiftide`](https://docs.rs/swiftide/0.34/swiftide/index.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)
- [`TaskAgent`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.TaskAgent.html)
- [`Task`](https://docs.rs/swiftide/0.34/swiftide/tasks/struct.Task.html)
- [`TaskNode`](https://docs.rs/swiftide/0.34/swiftide/tasks/trait.TaskNode.html)
- [`Transition`](https://docs.rs/swiftide/0.34/swiftide/tasks/struct.Transition.html)
- [`ConcurrencyModel`](https://docs.rs/swiftide/0.34/swiftide/tasks/enum.ConcurrencyModel.html)
- [`indexing::Pipeline`](https://docs.rs/swiftide/0.34/swiftide/indexing/struct.Pipeline.html)
- [`query::Pipeline`](https://docs.rs/swiftide/0.34/swiftide/query/struct.Pipeline.html)
- [`SimilaritySingleEmbedding`](https://docs.rs/swiftide/0.34/swiftide/query/search_strategies/struct.SimilaritySingleEmbedding.html)
- [`GenerateSubquestions`](https://docs.rs/swiftide/0.34/swiftide/query/query_transformers/struct.GenerateSubquestions.html)
- [`Embed`](https://docs.rs/swiftide/0.34/swiftide/query/query_transformers/struct.Embed.html)
- [`answers::Simple`](https://docs.rs/swiftide/0.34/swiftide/query/answers/struct.Simple.html)
- [`Prompt`](https://docs.rs/swiftide/0.34/swiftide/prompt/struct.Prompt.html)
- [`SimplePrompt`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.SimplePrompt.html)
- [`ChatCompletion`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.ChatCompletion.html)
- [`EmbeddingModel`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.EmbeddingModel.html)
- [`Tool`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.Tool.html)
- [`ToolExecutor`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.ToolExecutor.html)
- [Feature flags](/reference/feature-flags/)

## Next

[Installation](/start/installation/)