Swiftide Documentation
repository·master·Indexed 20 days ago
https://github.com/bosun-ai/swiftideA Rust-based framework for building AI agents, RAG (Retrieval-Augmented Generation) pipelines, and typed task graphs. Swiftide provides high-performance primitives for agentic workflows, structured outputs, and complex retrieval systems, featuring an Agent Harness for lifecycle management, tool invocation, and integration with LLMs like OpenAI and AWS Bedrock.
What's inside Swiftide
- Swiftide is currently in a pre-1.0 state. Developers should be aware that APIs may change as the agent harness and task graph APIs stabilize for production use. For the most reliable and up-to-date technical details, always refer to the current API documentation and the provided examples.
Orchestrate workflows with Typed Task Graphs
masterTasks are the orchestration layer in Swiftide, represented as a typed graph of
TaskNodesteps. Each node defines its input, output, and error types, ensuring type safety during transitions.Key concepts:
Task<I, O>: A graph whereIis the initial input type andOis the final output type.register_node: Adds a step (e.g., a prompt, an agent, or a function) to the graph.register_transition: Defines how data flows from one node to another based on logic or specific types.TaskRunOutcome: The result of running a task, which can beCompleted(output)orPaused.
Example of a task graph workflow:
use swiftide::{ prompt::Prompt, tasks::{Task, TaskRunOutcome, Transition}, traits::SimplePrompt, }; use std::sync::Arc; // ... setup openai and agents ... let prompt_model: Arc<dyn SimplePrompt> = Arc::new(openai.clone()); let briefing_agent = BriefingAgent::new(agent); let mut task: Task<Prompt, String> = Task::new(); let brief = task.register_node(prompt_model.clone()); let decide = task.register_node(briefing_agent); let render = task.register_node(prompt_model); task.starts_with(brief); task.register_transition(brief, move |short_brief| { decide.transitions_with(short_brief) })?; task.register_transition(decide, move |decision: BriefingDecision| { Transition::next( &render, Prompt::from("Write a hand-off note for {{audience}}: {{summary}}") .with_context_value("audience", decision.audience) .with_context_value("summary", decision.summary), ) })?; task.register_transition(render, task.transitions_to_finish())?; match task.run(Prompt::from("Summarize the rollout plan")).await? { TaskRunOutcome::Completed(note) => println!("{note}"), TaskRunOutcome::Paused => println!("Task paused"), }Explore Swiftide usage examples
masterThe
swiftide-examplesdirectory contains various implementation patterns for building with Swiftide. Use these examples to understand specific capabilities:- Agents: Basic agent setup (
hello_agents.rs), streaming responses (streaming_agents.rs), human-in-the-loop workflows (agents_with_human_in_the_loop.rs), and using MCP tools (agents_mcp_tools.rs). - Structured Outputs & Control: Implementing custom schemas for stopping agents with arguments (
stop_with_args_custom_schema.rs) or handling agent failures with custom schemas (agent_can_fail_custom_schema.rs). - Tasks: Standard task execution (
tasks.rs) and fanout patterns (tasks_fanout.rs). - RAG & Retrieval: Indexing a codebase (
index_codebase.rs), building query pipelines (query_pipeline.rs), and implementing hybrid search (hybrid_search.rs). - Providers & Observability: Working with the Responses API (
responses_api.rs,responses_api_reasoning.rs), AWS Bedrock agents (aws_bedrock_agent.rs), and Langfuse integration (langfuse.rs).
- Agents: Basic agent setup (
Configure system prompts for Swiftide agents
masterSwiftide agents use a templated system prompt to define their behavior, role, and operational constraints. When building or configuring an agent, you can customize the following sections to control how the agent processes tasks:
- Role: Defines the specific identity or persona of the agent (e.g., "You are a Rust expert").
- Guidelines: Provides soft rules and best practices to help the agent complete tasks effectively.
- Constraints: Defines hard limitations and mandatory behaviors (e.g., "Think step by step", "Do not make up assumptions").
- Response Format: Instructs the agent on how to structure its output, such as requiring chain-of-thought reasoning before tool calls.
The template uses Jinja-style placeholders (
{{role}},{{guidelines}},{{constraints}},{{additional}}) which should be populated during the agent initialization process.Use the compress_code_outline prompt for RAG context optimization
masterThe
compress_code_outline.prompt.mdis a specialized prompt template designed to optimize Retrieval-Augmented Generation (RAG) context. It instructs an LLM to filter a large file outline down to only the lines necessary to understand a specific code chunk.This process helps reduce token usage and noise by removing irrelevant information while ensuring that essential definitions and imports used within the code chunk are preserved.
Prompt Logic and Constraints
When using this prompt, the LLM is bound by these rules:
- Strict Context Adherence: Use only lines from the provided outline; do not hallucinate or add information.
- Relevance: Select the most appropriate lines for the specific code chunk.
- Dependency Preservation: Must include definitions or imports required by the code chunk.
- No Redundancy: Do not repeat the code chunk itself (it is appended later) and do not include lines that are already present in the code chunk.
Template Variables
The prompt expects two primary variables to be injected:
{{ node.chunk }}: The actual snippet of code being analyzed.{{ node.metadata["Outline"] }}: The full structural outline of the file containing the code chunk.
# Filtering Code Outline Your task is to filter the given file outline to the code chunk provided. The goal is to provide a context that is still contains the lines needed for understanding the code in the chunk whilst leaving out any irrelevant information. ## Constraints - Only use lines from the provided context, do not add any additional information - Ensure that the selection you make is the most appropriate for the code chunk - Make sure you include any definitions or imports that are used in the code chunk - You do not need to repeat the code chunk in your response, it will be appended directly after your response. - Do not use lines that are present in the code chunk ## Code{{ node.chunk }}
## Outline{{ node.metadata["Outline"] }}
Quick Start with Swiftide
masterTo start building with Swiftide, add the core dependencies and the agent harness with an LLM integration (e.g., OpenAI). You will also need
anyhowfor error handling andtokiofor the async runtime.# Add core dependencies with agent and openai features cargo add swiftide --features swiftide-agents,openai # Add supporting crates cargo add anyhow cargo add tokio --features macros,rt-multi-threadIf using OpenAI, ensure you set the
OPENAI_API_KEYenvironment variable:export OPENAI_API_KEY=...# Add core dependencies with agent and openai features cargo add swiftide --features swiftide-agents,openai # Add supporting crates cargo add anyhow cargo add tokio --features macros,rt-multi-threadBuild an autonomous agent with the Agent Harness
masterThe Agent Harness manages message history, LLM calls, tool invocation, and lifecycle hooks. You can define tools using the
#[swiftide::tool]macro, which automatically handles descriptions and parameter metadata.Key components:
Agent::builder(): Used to configure the agent with an LLM, tools, and lifecycle hooks.AgentContext: An abstraction over message history and tool access.ToolExecutor: Manages how tools are executed (local by default).
Example of a simple agent with a custom tool:
use anyhow::Result; use swiftide::{ agents, chat_completion::{ToolOutput, errors::ToolError}, traits::AgentContext, }; #[swiftide::tool( description = "Looks up a Swiftide concept", param(name = "concept", description = "Concept to explain") )] async fn explain_concept( _context: &dyn AgentContext, concept: &str, ) -> Result<ToolOutput, ToolError> { let explanation = match concept { "tasks" => "Tasks compose typed nodes into explicit workflows.", "agents" => "Agents run LLM completions, tools, hooks, and stop conditions.", "pipelines" => "Pipelines stream data through indexing and retrieval steps.", _ => "Swiftide composes agents, task graphs, tools, and RAG pipelines,", }; Ok(explanation.into()) } #[tokio::main] async fn main() -> Result<()> { let openai = swiftide::integrations::openai::OpenAI::builder() .default_prompt_model("gpt-4o-mini") .build()?; agents::Agent::builder() .llm(&openai) .tools([explain_concept()]) .on_new_message(|_, message| { println!("{message}"); Box::pin(async { Ok(()) }) }) .limit(8) .build()? .query("Explain Swiftide tasks and agents in one paragraph.") .await?; Ok(()) }Build streaming RAG pipelines
masterSwiftide provides composable, streaming pipelines for RAG (Retrieval-Augmented Generation). You can chain together loaders, transformers, embedders, and storage backends to create an indexing pipeline, and then use query pipelines for retrieval.
Example of an indexing pipeline using
FileLoaderandQdrant:use swiftide::{ indexing::{self, loaders::FileLoader, transformers::{ChunkCode, Embed, MetadataQACode}}, integrations::qdrant::Qdrant, }; async fn index(openai: swiftide::integrations::openai::OpenAI) -> anyhow::Result<()> { let qdrant = Qdrant::builder() .collection_name("swiftide-code") .vector_size(1536) .batch_size(50) .build()?; indexing::Pipeline::from_loader(FileLoader::new(".").with_extensions(&["rs"])) .with_default_llm_client(openai.clone()) .then_chunk(ChunkCode::try_for_language_and_chunk_size("rust", 10..2048)?) .then(MetadataQACode::default()) .then_in_batch(Embed::new(openai)) .then_store_with(qdrant) .run() .await?; Ok(()) }Use the swiftide-query module
masterThe
swiftide-querycrate provides the core querying capabilities for Swiftide. It is organized into several submodules that handle different stages of the query lifecycle, including query construction, transformation, and evaluation.Key components include:
query: The primary module for defining and executing queries.query_transformers: Tools for modifying or augmenting queries before execution.response_transformers: Tools for processing and transforming query results.answers: Logic for handling query responses or answers.evaluators: Components used to evaluate query results or criteria.
Use ApprovalRequired for sensitive tools
masterIf you have a tool that should not be executed without human intervention, wrap it in
ApprovalRequired.When the LLM attempts to call an
ApprovalRequiredtool, the agent will stop and enter aStopped(StopReason::FeedbackRequired)state. You must then provide feedback (approved or refused) via the agent's context before the agent can continue.Workflow:
- Wrap tool:
let approval_tool = ApprovalRequired(my_tool.boxed()); - Run agent:
agent.query_once(prompt).await; - Check state: Ensure state is
FeedbackRequired. - Provide feedback:
agent.context.feedback_received(&tool_call, &ToolFeedback::approved()).await; - Resume:
agent.run_once().await;
// 1. Wrap the tool let approval_tool = ApprovalRequired(mock_tool.boxed()); // 2. Build agent with the tool let mut agent = Agent::builder() .tools([approval_tool]) .llm(&mock_llm) .build() .unwrap(); // 3. Trigger the requirement agent.query_once("Execute sensitive task").await.unwrap(); // 4. Provide feedback via context agent.context .feedback_received(&tool_call, &ToolFeedback::approved()) .await .unwrap(); // 5. Continue execution agent.run_once().await.unwrap();- Wrap tool:
How typed task graphs work in Swiftide
masterSwiftide tasks are built using a directed graph of
TaskNodeimplementations. Each node in the graph receives a typed input, produces a typed output, and determines the next step in execution via aTransition.Key components include:
Task: The primary orchestrator used to define and run the execution graph.TaskNode: An abstraction representing a single unit of work in the graph.Transition: A mechanism that describes how execution should continue after a node completes. Transitions allow for linear flows, branching (fan-out), and synchronization (joins).NodeId: A handle used to reference specific nodes within a task to register transitions or manage joins.
// A small linear task example let mut task = Task::<i32, i32>::new(); let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) }); let finish = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input * 2) }); task.starts_with(start); task.register_transition(start, move |value| finish.transitions_with(value))?; task.register_transition(finish, task.transitions_to_finish())?; let result = task.run(2).await?;How LanceDB schema fields are named
masterLanceDB field names are normalized to ensure compatibility. The
normalize_field_namefunction converts strings to lowercase and replaces any non-alphanumeric characters with underscores.- Vector fields: Named
vector_<normalized_name>. For example, anEmbeddedFieldnamedMyFieldbecomesvector_myfield. - Metadata fields: Named using the normalized version of the provided string.
- Chunk field: Always named
"chunk". - ID field: Always named
"id".
- Vector fields: Named