Skip to content

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

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

swiftide 0.34rustMIT

Get started
Add swiftide to a project
cargo add swiftide --features swiftide-agents,openai

API reference · GitHub · Discord · llms.txt · For coding agents

release-notes-agent --since v0.33.0 --version 0.34.0
→ 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
examples/release-notes-agent/src/agent.rs
8 collapsed lines
/// 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))
2 collapsed lines
.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);
9 collapsed lines
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

Release notes agent

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

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

Agents guide →

examples/workspace-architect/src/graph.rs
54 collapsed lines
/// 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())
})?;
15 collapsed lines
// 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

Workspace architect

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

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

Tasks guide →

examples/ask-your-codebase/src/index.rs
let stats = indexing::Pipeline::from_loader(
FileLoader::new(codebase.root()).with_extensions(&languages.extensions()),
)
5 collapsed lines
.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)?)
8 collapsed lines
// 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

Ask your codebase

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

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

Pipelines guide →

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, a task graph, a pipeline

Integrations