# LLM applications in Rust: swift to build, built to ship.

> Swiftide is a Rust library for building LLM applications: agents, typed task graphs and streaming RAG pipelines that ship with retries, streaming, human approval, caching and tracing built in.

An agent harness, typed task graphs and streaming RAG pipelines that work together, with retries, streaming, human approval, caching and tracing already there.

[Get started](/start/what-is-swiftide/)

```sh title="Add swiftide to a project" wrap
cargo add swiftide --features swiftide-agents,openai
```

[API reference](https://docs.rs/swiftide/0.34/swiftide/) · [GitHub](https://github.com/bosun-ai/swiftide) · [Discord](https://discord.gg/3jjXYen9UY) · [llms.txt](/llms.txt) · [For coding agents](/start/for-coding-agents/)

```text
→ git_log_since v0.33.0
git_log_since ✓ 346 lines
→ git_diff swiftide-integrations/Cargo.toml 3b1ad22e^..3b1ad22e
→ git_diff …/src/chat_completion/chat_completion_response.rs 90bcadc7^..90bcadc7
→ git_diff swiftide-agents-contrib/Cargo.toml 71fa4587^..71fa4587
git_diff ✓ 15 lines
git_diff ✓ 72 lines
git_diff ✓ 75 lines
→ write_changelog <32 lines>
write_changelog ⏸ waiting for approval
Write these release notes? [y/N] y
write_changelog ✓ wrote RELEASE_NOTES.md
→ stop (version 0.34.0: highlights 2, breaking 1, fixes 1)
stop ■ done
```

## Agents: [Release notes agent](/showcases/release-notes-agent/)

Reads git, drafts the notes, and writes them only after you say yes.

```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)

```text
→ stop (version 0.34.0: highlights 2, breaking 1, fixes 1)
```

[Agents guide →](/agents/overview/)

## Tasks: [Workspace architect](/showcases/workspace-architect/)

One agent per crate, run in parallel and joined into ARCHITECTURE.md.

```rust title="examples/workspace-architect/src/graph.rs"
/// Builds the task: `WorkspaceRoot` in, the written ARCHITECTURE.md out.
pub fn architect_task<L>(
    llm: &L,
    settings: ArchitectSettings,
    progress: &Progress,
) -> Result<Task<WorkspaceRoot, WrittenDocument>, TaskError>
where
    L: ChatCompletion + SimplePrompt + Clone + 'static,
{
    let mut task = Task::new();

    let discover = task.register_node_fn({
        let progress = progress.clone();
        move |root: &WorkspaceRoot| {
            let members = workspace::discover(root, settings.selection)?;
            progress.step("discover", format_args!("{members} in {root}"));
            Ok::<_, workspace::DiscoveryError>(members)
        }
    });

    let analyse = task.register_node(CrateAnalyst::new(llm.clone(), progress.clone()));

    let join = task.register_node_fn({
        let progress = progress.clone();
        move |summaries: &JoinInput<CrateSummary>| {
            progress.joined(summaries.len());
            architecture_prompt(summaries)
        }
    });

    // `Arc<dyn SimplePrompt>` is a task node out of the box: `Prompt` in, `String` out.
    let render = task.register_node(Arc::new(llm.clone()) as Arc<dyn SimplePrompt>);

    let review = task.register_node(ApprovalGate::new(settings.pre_approved, progress.clone()));

    let write = task.register_node_async_fn({
        let output = settings.output;
        let progress = progress.clone();
        move |draft: &ArchitectureDraft| {
            let output = output.clone();
            let progress = progress.clone();
            Box::pin(async move {
                let written = draft.write_to(&output).await?;
                progress.step(
                    "write",
                    format_args!("{} ({} bytes)", written.path.display(), written.bytes),
                );
                Ok::<_, std::io::Error>(written)
            })
        }
    });

    task.starts_with(discover);

    // 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())
    })?;

    // Each branch hands its `CrateSummary` to the join; the join runs once all have arrived.
    task.register_transition(analyse, join.join())?;
    task.register_transition(join, {
        let progress = progress.clone();
        move |prompt| {
            progress.step("render", "drafting ARCHITECTURE.md");
            render.transitions_with(prompt)
        }
    })?;

    task.register_transition(render, move |markdown: String| {
        review.transitions_with(ArchitectureDraft::from_model_output(&markdown))
    })?;

    task.register_transition(review, move |decision| match decision {
        Review::Approved(draft) => write.transitions_with(draft).into(),
        Review::AwaitingApproval => Transition::pause(),
    })?;

    task.register_transition(write, task.transitions_to_finish())?;

    Ok(task)
}
```

Source: [`examples/workspace-architect/src/graph.rs#L35-L134`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/workspace-architect/src/graph.rs#L35-L134)

```text
[  40.5s] join     10 summaries; 230s of agent work in 41s wall-clock (5.7x)
```

[Tasks guide →](/tasks/overview/)

## Pipelines: [Ask your codebase](/showcases/ask-your-codebase/)

Indexes a codebase as a stream; unchanged files hit the cache.

```rust title="examples/ask-your-codebase/src/index.rs"
let stats = indexing::Pipeline::from_loader(
    FileLoader::new(codebase.root()).with_extensions(&languages.extensions()),
)
.filter(test_code::not_test_files(codebase.clone()))
// Drop unit tests before the cache, which keys on path and content. Changing the content
// after it would make the cache miss on every run.
.then(test_code::without_unit_tests)
.then(files_found.count())
// Skip files whose path and content are unchanged since the last successful run
.filter_cached(codebase.node_cache()?)
.then(files_changed.count())
.then(record_relative_path(codebase.clone()))
// Split along syntax boundaries (functions, impls, ...) with tree-sitter
.then_chunk(ChunkByLanguage::try_new(languages, CHUNK_SIZE)?)
// Ask the LLM which questions each chunk answers; stored as metadata and embedded
.then(
    MetadataQACode::from_client(llm)
        .prompt_template(QUESTIONS_PROMPT)
        .num_questions(QUESTIONS_PER_CHUNK)
        .concurrency(concurrent_requests)
        .build()?,
)
// Embed chunk + metadata locally, in batches
.then_in_batch(Embed::new(embedding::local_model()?).with_batch_size(embedding::BATCH_SIZE))
.then_store_with(codebase.vector_store()?)
.run()
.await?;
```

Source: [`examples/ask-your-codebase/src/index.rs#L89-L115`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/ask-your-codebase/src/index.rs#L89-L115)

```text
Indexed 0 chunks from 0 new or changed files (14 files found, 14 unchanged) in 0.6s
```

[Pipelines guide →](/indexing/overview/)

## The same parts, three places

The same client, executor or store works in an agent, a task graph and a pipeline, so you wire a value once.

- **Values**\
  an `OpenAI` client, a `LocalExecutor`, a `Qdrant` store
- **Traits**\
  `ChatCompletion`, `SimplePrompt`, `ToolExecutor`, `Persist`, `Retrieve`
- **Runtimes**\
  [an agent](/agents/overview/), [a task graph](/tasks/overview/), [a pipeline](/indexing/overview/)

## Integrations

- [openai](/integrations/openai/)
- [anthropic](/integrations/anthropic/)
- [aws-bedrock](/integrations/aws-bedrock/)
- [ollama](/integrations/openai-compatible/)
- [fastembed](/integrations/fastembed/)
- [qdrant](/integrations/qdrant/)
- [lancedb](/integrations/lancedb/)
- [pgvector](/integrations/pgvector/)
- [redis](/indexing/caching/)
- [tree-sitter](/integrations/tree-sitter/)
- [mcp](/agents/mcp/)
- [All 24 integrations](/integrations/overview/)