Swiftide Documentation

repository·master·Indexed 20 days ago

https://github.com/bosun-ai/swiftide

A 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.

Tokens
71K
Snippets
221
Records
270
Agent score
71%

What's inside Swiftide

  1. Project status and API stability

    master
    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.
  2. Orchestrate workflows with Typed Task Graphs

    master

    Tasks are the orchestration layer in Swiftide, represented as a typed graph of TaskNode steps. Each node defines its input, output, and error types, ensuring type safety during transitions.

    Key concepts:

    • Task<I, O>: A graph where I is the initial input type and O is 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 be Completed(output) or Paused.

    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"),
    }
  3. Explore Swiftide usage examples

    master

    The swiftide-examples directory 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).
  4. Configure system prompts for Swiftide agents

    master

    Swiftide 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.

  5. Use the compress_code_outline prompt for RAG context optimization

    master

    The compress_code_outline.prompt.md is 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"] }}

  6. Quick Start with Swiftide

    master

    To start building with Swiftide, add the core dependencies and the agent harness with an LLM integration (e.g., OpenAI). You will also need anyhow for error handling and tokio for 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-thread

    If using OpenAI, ensure you set the OPENAI_API_KEY environment 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-thread
  7. Build an autonomous agent with the Agent Harness

    master

    The 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(())
    }
  8. Build streaming RAG pipelines

    master

    Swiftide 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 FileLoader and Qdrant:

    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(())
    }
  9. Use the swiftide-query module

    master

    The swiftide-query crate 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.
  10. Use ApprovalRequired for sensitive tools

    master

    If you have a tool that should not be executed without human intervention, wrap it in ApprovalRequired.

    When the LLM attempts to call an ApprovalRequired tool, the agent will stop and enter a Stopped(StopReason::FeedbackRequired) state. You must then provide feedback (approved or refused) via the agent's context before the agent can continue.

    Workflow:

    1. Wrap tool: let approval_tool = ApprovalRequired(my_tool.boxed());
    2. Run agent: agent.query_once(prompt).await;
    3. Check state: Ensure state is FeedbackRequired.
    4. Provide feedback: agent.context.feedback_received(&tool_call, &ToolFeedback::approved()).await;
    5. 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();
  11. How typed task graphs work in Swiftide

    master

    Swiftide tasks are built using a directed graph of TaskNode implementations. Each node in the graph receives a typed input, produces a typed output, and determines the next step in execution via a Transition.

    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?;
  12. How LanceDB schema fields are named

    master

    LanceDB field names are normalized to ensure compatibility. The normalize_field_name function converts strings to lowercase and replaces any non-alphanumeric characters with underscores.

    • Vector fields: Named vector_<normalized_name>. For example, an EmbeddedField named MyField becomes vector_myfield.
    • Metadata fields: Named using the normalized version of the provided string.
    • Chunk field: Always named "chunk".
    • ID field: Always named "id".