Orchestrating Multi-Agent Workflows with Personas

In the previous chapters, you’ve built a foundational Kanban board, integrated Git worktrees for isolated task contexts, and even enabled a single AI agent to perform basic tasks. This chapter marks a significant step forward: orchestrating multiple AI agents to collaborate on a single task, each with a distinct persona.

This milestone is critical because real-world development often involves multiple roles and handoffs. By simulating this with AI agents, we move beyond simple task automation towards a more intelligent, autonomous development assistant. By the end of this chapter, your Kanbots application will be able to initiate and manage sequential workflows, demonstrating how different AI “personalities” can contribute to a larger goal. You’ll verify the workflow by observing agents making distinct, persona-aligned changes in a Git worktree, ultimately completing a small feature or refactoring task.

Project Overview

The Kanbots project aims to build a local-first, cross-platform desktop Kanban application. Its core innovation is the ability to attach AI agents to individual Kanban cards, using Git worktrees to provide isolated, version-controlled environments for agent execution. This allows for automated development workflows, where AI agents can generate, review, and refactor code directly within your local repository.

In this chapter, we specifically implement the multi-agent orchestration layer, enabling a “Developer” agent to write code and a “Reviewer” agent to critique it sequentially. This forms the basis for more complex, persona-driven automation within your development process.

Tech Stack Deep Dive

To achieve multi-agent orchestration, we’ll leverage the following components:

  • Rust (Tauri Backend): The primary orchestrator. Rust’s strong type system, performance, and robust concurrency features make it ideal for managing Git worktrees, communicating with AI APIs, and coordinating agent steps. We’ll extend our existing Tauri backend with new command handlers.
  • Svelte (Tauri Frontend): Provides the interactive user interface. Svelte’s reactivity and simplicity allow us to quickly build UI components for defining workflows and displaying real-time agent progress.
  • Git: Essential for providing isolated contexts via worktrees and for tracking each agent’s distinct contributions through commits.
  • AI Agent APIs (e.g., Claude Code, Codex): While we’ll use a simulated output for this chapter to focus on orchestration, the design accounts for integrating with real large language model (LLM) APIs. These APIs are the “brains” of our agents, interpreting instructions and generating code or reviews.

Milestone: Persona-Based Multi-Agent Workflow

This chapter focuses on delivering the capability for a Kanban card to execute a predefined, sequential workflow involving two distinct AI personas.

The key steps we’ll implement are:

  1. Enhance Agent Structure: Modify the Rust Agent struct to include an optional persona field.
  2. Define Workflow Steps: Introduce a new Rust struct, AgentWorkflowStep, to describe individual actions within a multi-agent workflow.
  3. Implement Workflow Command: Create a Tauri command in Rust that iterates through workflow steps, invokes agents with specific instructions and personas, and manages Git commits between steps.
  4. Integrate Frontend UI: Update the Svelte frontend to allow users to define a simple two-step workflow, assign agents, and trigger the Rust command.
  5. Real-time Feedback: Implement event emission from Rust to Svelte to provide live updates on agent progress.

By the end of this milestone, you will have a functional system where clicking a button in the UI triggers a sequence of automated actions by distinct AI personas, all tracked within a Git worktree.

Architecture: Orchestrating Collaborative AI

Moving from a single agent to multiple agents introduces new challenges and opportunities. We need a way to define agents, assign them roles (personas), and control the flow of execution between them.

The Concept of Personas

What are personas? In the context of AI agents, a persona is a defined role or identity that guides the agent’s behavior, tone, and focus. Instead of a generic AI, an agent with a “Developer” persona will prioritize code generation, best practices, and implementation details. A “Reviewer” persona, conversely, will focus on code quality, security, performance, and adherence to standards.

Why do they exist? Personas help constrain the AI’s output, making it more predictable and relevant to a specific part of a task. It’s about providing clear context and expectations, much like assigning roles to human team members.

What problem do they solve? Without personas, a single agent might try to do everything, leading to unfocused or contradictory outputs. By splitting responsibilities, we leverage the AI’s capabilities more effectively and enable complex workflows.

Workflow Design: Sequential Collaboration

For our first multi-agent workflow, we’ll implement a sequential pattern:

  1. Developer Agent: Generates initial code based on a task description.
  2. Reviewer Agent: Reviews the generated code, suggests improvements, and refactors it.

This sequential flow is simpler to manage than parallel execution initially and effectively demonstrates the core concept of agents building upon each other’s work.

System Architecture Extension

To support this, we’ll extend our existing architecture:

  • Agent Configuration: Each agent instance will now include a persona field.
  • Workflow Definition: A Kanban card will store a definition of a workflow, which is a sequence of agent tasks, each potentially assigned a specific persona. For this chapter, we’ll hardcode a simple workflow in the frontend for demonstration.
  • Rust Orchestrator: The Rust backend will gain new logic to:
    • Parse the workflow definition.
    • Execute agents sequentially, passing relevant context (e.g., file paths, previous agent’s output) from one agent to the next.
    • Manage Git operations (staging, committing) between agent steps to track distinct contributions.
    • Update the Svelte frontend with real-time progress and outputs.
  • Svelte UI: We’ll add UI elements to define a simple workflow for a card and trigger its execution.

Here’s a high-level view of the multi-agent workflow:

flowchart TD UI[Kanban Card UI] -->|Start Workflow| RustBackend[Rust Orchestrator] subgraph Agent_Workflow["Agent Workflow Execution"] RustBackend --> DevAgent[Developer Agent] DevAgent -->|Generates Code| GitWorktree[Git Worktree] GitWorktree -->|Commits Code| RustBackend RustBackend --> ReviewAgent[Reviewer Agent] ReviewAgent -->|Reviews Code| GitWorktree GitWorktree -->|Commits Review| RustBackend end RustBackend -->|Update Status| UI

Data Flow

  1. UI Action: The user clicks “Start Workflow” on a card, passing the card’s ID, its associated worktree path, a list of workflowSteps, and the available aiAgents to the Rust backend.
  2. Rust Orchestration (execute_agent_workflow):
    • Switches to the card’s dedicated Git worktree.
    • Loop through workflowSteps:
      • For each step, it retrieves the specified Agent configuration.
      • It constructs a prompt for the AI, prepending the agent’s persona (or an override from the workflow step) and including context (like previous agent’s output).
      • Makes a simulated (or actual) call to the AI API.
      • File I/O: If an output_path is specified, the agent’s output is written to that file within the worktree.
      • Git Operations: git add . and git commit -m "..." are executed to record the agent’s contribution.
      • Context Update: Relevant output from the current agent is stored in a HashMap to be passed to the next agent in the sequence.
      • UI Feedback: Emits agent_progress events to the Svelte frontend.
  3. UI Update: The Svelte frontend listens for agent_progress and agent_workflow_completed events, updating the workflowStatus display in real-time.

Step-by-Step Implementation

We’ll start by modifying our Rust backend to support personas and then build the workflow execution logic.

1. Update Agent Structure (Rust Backend)

First, let’s enhance our Agent struct in Rust to include a persona field. This allows us to define the role for each AI interaction.

Open src-tauri/src/main.rs and locate your Agent struct (or create one if you haven’t already, based on previous chapters’ single-agent integration).

// src-tauri/src/main.rs

// ... (other imports and structs)

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct Agent {
    pub id: String,
    pub name: String,
    pub api_key: String, // Production note: This should be handled securely
    pub model: String,
    pub persona: Option<String>, // New field for agent persona
    // Add other agent-specific configuration fields as needed
}

// ... (existing Tauri commands)

Explanation:

  • We’ve added persona: Option<String>. Using Option allows us to define agents without a specific persona if needed, but for our workflow, it will be essential. This persona string will be injected directly into the AI prompt to guide its behavior.

2. Define Workflow Step and Command (Rust Backend)

Next, we need a way to define a sequence of agent actions and a new Tauri command to trigger this workflow.

Still in src-tauri/src/main.rs:

// src-tauri/src/main.rs

use std::collections::HashMap;
use std::path::PathBuf;
use tokio::process::Command; // For async process execution
use tauri::{AppHandle, Manager}; // For emitting events to the frontend
use tokio::fs; // For async file operations
use std::time::Duration; // For potential delays or timeouts (optional)

// ... (existing structs and Agent definition)

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct AgentWorkflowStep {
    pub agent_id: String, // ID of the agent to use for this step
    pub persona_override: Option<String>, // Optional: override agent's default persona for this step
    pub instruction: String, // Specific instruction for this step
    pub output_path: Option<String>, // Where the agent should write its output (e.g., "src/feature.rs")
    pub commit_message: String, // Git commit message for this step
}

/// Orchestrates a multi-agent workflow for a specific card.
#[tauri::command]
async fn execute_agent_workflow(
    app_handle: AppHandle,
    card_id: String,
    worktree_path: String,
    workflow_steps: Vec<AgentWorkflowStep>,
    ai_agents: Vec<Agent>, // Pass available agents to the command
) -> Result<String, String> {
    let card_repo_path = PathBuf::from(&worktree_path);

    // Ensure the worktree exists and switch to it
    if !card_repo_path.exists() {
        return Err(format!("Worktree path does not exist: {}", worktree_path));
    }
    // We assume the worktree is already initialized from previous steps (Chapter 2).
    // For production, you'd add more robust checks/initialization here to handle
    // cases where a worktree might be deleted or corrupted.

    // Store agent map for easy lookup
    let agent_map: HashMap<String, Agent> = ai_agents.into_iter().map(|a| (a.id.clone(), a)).collect();

    // Context to pass between agents. This can store file contents, previous outputs, etc.
    let mut current_context: HashMap<String, String> = HashMap::new();

    for (i, step) in workflow_steps.iter().enumerate() {
        let agent = agent_map.get(&step.agent_id)
            .ok_or_else(|| format!("Agent with ID {} not found for step {}", step.agent_id, i))?;

        let actual_persona = step.persona_override.clone().or_else(|| agent.persona.clone());
        let persona_prefix = actual_persona.as_ref().map(|p| format!("You are a {}. ", p)).unwrap_or_default();

        // Construct the prompt for the AI, including context from previous steps
        let mut prompt = format!("{}{}", persona_prefix, step.instruction);

        // Append relevant context from previous agent's work
        if let Some(prev_output) = current_context.get("last_output") {
            prompt = format!("{} Previous output/summary: {}", prompt, prev_output);
        }
        if let Some(prev_output_file_path) = current_context.get("last_output_file") {
            // In a real scenario, you'd read the file content here
            // and append it to the prompt if relevant for the current agent.
            // For now, we'll just mention the file.
            prompt = format!("{} Also consider the file generated at: {}", prompt, prev_output_file_path);
        }

        // ⚡ Quick Note: For real code review, you'd read the actual file content from `prev_output_file_path`
        // and include it in the prompt for the Reviewer agent to analyze. This simulation simplifies that.

        app_handle.emit_all("agent_progress", format!("Card {}: Agent {} ({}) started step {}: {}",
            card_id, agent.name, actual_persona.clone().unwrap_or_default(), i + 1, step.instruction))
            .map_err(|e| e.to_string())?;

        // Simulate AI API call (replace with actual API integration for Claude Code or Codex)
        // For production, you would use a dedicated HTTP client (e.g., `reqwest`) and
        // serialize/deserialize JSON for the specific AI API.
        // Remember to securely load API keys (e.g., from environment variables or OS secret store).

        // Example placeholder for actual API call (requires `reqwest` and `serde_json` crates):
        /*
        let client = reqwest::Client::builder()
            .timeout(Duration::from_secs(60)) // Set a reasonable timeout
            .build()
            .map_err(|e| format!("Failed to build HTTP client: {}", e))?;

        let ai_api_url = match agent.model.as_str() {
            "claude-3-opus-20240229" => "https://api.anthropic.com/v1/messages", // Example Claude API
            "gpt-4-turbo" => "https://api.openai.com/v1/chat/completions", // Example OpenAI API
            _ => return Err(format!("Unsupported AI model: {}", agent.model)),
        };

        let request_body = serde_json::json!({
            "model": agent.model,
            "max_tokens": 2048, // Adjust based on expected output length
            "messages": [{"role": "user", "content": prompt}]
            // Add other model-specific parameters (e.g., temperature)
        });

        let response = client.post(ai_api_url)
            .header("x-api-key", &agent.api_key) // Securely retrieved API key
            .header("anthropic-version", "2023-06-01") // For Claude
            .header("content-type", "application/json")
            .json(&request_body)
            .send()
            .await
            .map_err(|e| format!("AI API request failed: {}", e))?;

        if !response.status().is_success() {
            let error_text = response.text().await.unwrap_or_else(|_| "Unknown API error".to_string());
            return Err(format!("AI API returned error status {}: {}", response.status(), error_text));
        }

        let json_response: serde_json::Value = response.json().await
            .map_err(|e| format!("Failed to parse AI API response JSON: {}", e))?;

        let ai_output = json_response["content"][0]["text"].as_str().unwrap_or_default().to_string();
        */

        // For now, let's simulate AI output:
        let ai_output = format!("Simulated AI output for {} persona: \"{}\"", actual_persona.clone().unwrap_or_else(|| "generic agent".to_string()), prompt);
        println!("AI Agent Output for step {}: {}", i, ai_output); // Log to console for debugging

        // Write agent output to file if specified
        if let Some(output_path) = &step.output_path {
            let full_path = card_repo_path.join(output_path);
            if let Some(parent) = full_path.parent() {
                fs::create_dir_all(parent).await.map_err(|e| format!("Failed to create directories for {}: {}", full_path.display(), e))?;
            }
            fs::write(&full_path, &ai_output).await.map_err(|e| format!("Failed to write agent output to {}: {}", full_path.display(), e))?;
            println!("Wrote agent output to: {}", full_path.display());
            // Add the file path to context for subsequent agents if they need to read it
            current_context.insert("last_output_file".to_string(), full_path.to_string_lossy().to_string());
        }

        // Git operations: Stage and commit changes
        // 🧠 Important: In a real scenario, you'd parse AI output for code changes
        // and apply them precisely, not just dump raw output.
        // For this example, we assume the AI writes directly to output_path.
        let mut git_command = Command::new("git");
        git_command
            .current_dir(&card_repo_path)
            .arg("add")
            .arg("."); // Add all changes in the worktree

        let output = git_command.output().await.map_err(|e| format!("Failed to stage changes in {}: {}", card_repo_path.display(), e))?;
        if !output.status.success() {
            return Err(format!("Git add failed: {}", String::from_utf8_lossy(&output.stderr)));
        }

        let mut git_command = Command::new("git");
        git_command
            .current_dir(&card_repo_path)
            .arg("commit")
            .arg("-m")
            .arg(&step.commit_message)
            .env("GIT_AUTHOR_NAME", agent.name.clone()) // Set author name for the commit
            .env("GIT_AUTHOR_EMAIL", format!("{}@kanbots.ai", agent.id)); // Set author email

        let output = git_command.output().await.map_err(|e| format!("Failed to commit changes in {}: {}", card_repo_path.display(), e))?;
        if !output.status.success() {
            // If no changes to commit, Git will return a non-zero status but with a specific message.
            // We should not treat this as an error.
            if !String::from_utf8_lossy(&output.stderr).contains("nothing to commit") {
                return Err(format!("Git commit failed: {}", String::from_utf8_lossy(&output.stderr)));
            }
        } else {
            println!("Committed changes for step {}: {}", i, step.commit_message);
        }

        // Store relevant output in context for the next agent
        current_context.insert("last_output".to_string(), ai_output.clone());
    }

    app_handle.emit_all("agent_workflow_completed", format!("Card {}: Workflow completed successfully!", card_id))
        .map_err(|e| e.to_string())?;

    Ok("Workflow executed successfully".to_string())
}

// Ensure your main function is asynchronous and registers the new command.
// Add `tokio = { version = "1", features = ["full"] }` to your Cargo.toml
// if you haven't already, as `tokio::process::Command` and `tokio::fs` require it.
#[tokio::main]
async fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            // ... your existing commands here (e.g., create_worktree, delete_worktree, execute_agent_task from previous chapters)
            execute_agent_workflow, // Add the new command
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Required Cargo.toml updates: Ensure your src-tauri/Cargo.toml includes tokio with the full feature:

# src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2.0.0-beta", features = ["fs-extra", "shell-open"] } # Version as of 2026-05-24
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] } # Required for async process and file operations
# reqwest = { version = "0.12", features = ["json"], optional = true } # Uncomment for actual AI API calls
# dotenv = { version = "0.15", optional = true } # For loading API keys from .env

If you decide to integrate actual AI APIs, uncomment reqwest and dotenv (or use another secure secret management approach).

Explanation of Rust Code:

  • AgentWorkflowStep: This struct defines a single step in our workflow. It specifies which agent to use (agent_id), what instruction to give it, an optional persona_override, where to write its output, and the commit_message for Git.
  • execute_agent_workflow command:
    • Takes card_id, worktree_path, a Vec<AgentWorkflowStep>, and a list of ai_agents (to look up by ID).
    • It iterates through each workflow_step.
    • For each step, it retrieves the corresponding Agent configuration.
    • It constructs a prompt, prepending the agent’s persona (or an override) and including context from previous steps.
    • AI API Call (Simulated): The commented-out block shows where you would integrate with actual AI APIs like Claude Code or Codex. For this chapter, we’re using a placeholder string to focus on the orchestration logic.
    • UI Feedback: app_handle.emit_all("agent_progress", ...) sends real-time updates to the Svelte frontend.
    • File Output: If output_path is specified, the agent’s simulated output is written to a file within the worktree. tokio::fs::create_dir_all ensures the directory structure exists before writing.
    • Git Operations: After each agent step, git add . and git commit -m "..." are executed. This is crucial for tracking each agent’s contribution and ensuring the next agent can see the changes. We also set GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL to attribute commits to the specific AI agent.
    • Context Passing: current_context is a HashMap used to pass information (like last_output) between sequential agents. This is a simple mechanism; for complex workflows, you might need a more structured context object or a shared “scratchpad” file within the worktree.
  • main function setup: The main function is marked with #[tokio::main] to enable asynchronous operations, which are necessary for tokio::process::Command and tokio::fs functions. The execute_agent_workflow command is registered with Tauri’s invoke_handler.

3. Frontend Integration (Svelte)

Now, let’s update our Svelte frontend to define agents, create a simple workflow, and trigger the new Rust command.

Update Svelte Types

First, update your src/lib/types.ts to reflect the new Rust structures.

// src/lib/types.ts

export interface Agent {
  id: string;
  name: string;
  apiKey: string; // Stored securely, not directly in UI state usually
  model: string;
  persona?: string; // Optional persona field
}

export interface AgentWorkflowStep {
  agent_id: string;
  persona_override?: string;
  instruction: string;
  output_path?: string;
  commit_message: string;
}

export interface Card {
  id: string;
  title: string;
  description: string;
  status: 'todo' | 'in-progress' | 'done';
  worktreePath?: string; // Path to the associated git worktree
  // Add other card properties
  workflowStatus?: string; // To display current workflow status
}

Update Card Detail UI (Svelte)

We’ll add a section to our CardDetail.svelte component (or a similar component) that allows us to define a simple workflow and trigger it.

<!-- src/routes/CardDetail.svelte -->
<script lang="ts">
  import { getContext, onMount, onDestroy } from 'svelte'; // Added onDestroy
  import { cards, agents } from '$lib/stores'; // Assuming you have agents in a store
  import { invoke } from '@tauri-apps/api/tauri';
  import { listen } from '@tauri-apps/api/event';
  import type { Agent, AgentWorkflowStep, Card } from '$lib/types';

  export let cardId: string; // Passed as a prop to the component

  let currentCard: Card | undefined;
  let availableAgents: Agent[] = [];
  let workflowStatus: string = '';

  // Simple workflow definition for demonstration
  let workflowSteps: AgentWorkflowStep[] = [
    {
      agent_id: '', // Will be set by user selection
      persona_override: 'Senior Software Engineer',
      instruction: 'Generate a simple Rust function `add(a: i32, b: i32) -> i32` in `src/lib.rs` that adds two integers. Ensure it includes a basic doc comment.',
      output_path: 'src/lib.rs',
      commit_message: 'feat: add basic add function',
    },
    {
      agent_id: '', // Will be set by user selection
      persona_override: 'Code Reviewer',
      instruction: 'Review the `add` function in `src/lib.rs`. Ensure it follows Rust best practices, is efficient, and improve its doc comment.',
      output_path: 'src/lib.rs', // Reviewer modifies the same file
      commit_message: 'refactor: review and improve add function',
    },
  ];

  let unlistenProgress: (() => void) | undefined;
  let unlistenCompleted: (() => void) | undefined;

  onMount(async () => {
    currentCard = $cards.find(c => c.id === cardId);
    availableAgents = $agents; // Load agents from store (assuming you populate this from local storage or API)

    // Update agent_id placeholders with first available agent for now
    if (availableAgents.length > 0) {
      workflowSteps[0].agent_id = availableAgents[0].id;
      if (availableAgents.length > 1) {
        workflowSteps[1].agent_id = availableAgents[1].id;
      } else {
        // Fallback if only one agent, use same agent for both steps
        workflowSteps[1].agent_id = availableAgents[0].id;
      }
    }

    // Listen for agent progress updates from the backend
    unlistenProgress = await listen('agent_progress', (event) => {
      workflowStatus = (event.payload as string);
      console.log('Agent Progress:', workflowStatus);
    });

    unlistenCompleted = await listen('agent_workflow_completed', (event) => {
      workflowStatus = (event.payload as string);
      console.log('Workflow Completed:', workflowStatus);
      if (currentCard) {
        currentCard.workflowStatus = workflowStatus;
        $cards = $cards; // Trigger Svelte reactivity
      }
    });
  });

  onDestroy(() => {
    if (unlistenProgress) unlistenProgress();
    if (unlistenCompleted) unlistenCompleted();
  });

  async function startWorkflow() {
    if (!currentCard || !currentCard.worktreePath) {
      alert('Card or worktree path not found. Please initialize a worktree first.');
      return;
    }
    if (availableAgents.length === 0) {
        alert('No AI agents configured. Please add agents first.');
        return;
    }
    if (!workflowSteps[0].agent_id || !workflowSteps[1].agent_id) {
        alert('Please select agents for both workflow steps.');
        return;
    }

    workflowStatus = 'Starting workflow...';
    try {
      const result = await invoke('execute_agent_workflow', {
        cardId: currentCard.id,
        worktreePath: currentCard.worktreePath,
        workflowSteps: workflowSteps,
        aiAgents: availableAgents, // Pass all agents for lookup by the Rust backend
      });
      console.log('Workflow result:', result);
      workflowStatus = 'Workflow initiated successfully. Check console/terminal for details.';
      if (currentCard) {
        currentCard.workflowStatus = 'Workflow in progress...';
        $cards = $cards;
      }
    } catch (error) {
      console.error('Failed to execute workflow:', error);
      workflowStatus = `Workflow failed: ${error}`;
      if (currentCard) {
        currentCard.workflowStatus = `Workflow failed: ${error}`;
        $cards = $cards;
      }
      alert(`Workflow execution failed: ${error}`);
    }
  }
</script>

{#if currentCard}
  <h2>{currentCard.title}</h2>
  <p>{currentCard.description}</p>
  <p>Worktree: <code>{currentCard.worktreePath || 'N/A'}</code></p>

  <h3>Agent Workflow</h3>
  <p>Current Status: <strong>{workflowStatus || currentCard.workflowStatus || 'Idle'}</strong></p>

  {#each workflowSteps as step, i}
    <div style="border: 1px solid #eee; padding: 10px; margin-bottom: 10px; border-radius: 5px;">
      <h4>Step {i + 1}: {step.persona_override || 'Generic Agent'}</h4>
      <p>Instruction: {step.instruction}</p>
      <p>Output Path: <code>{step.output_path || 'N/A'}</code></p>
      <p>Commit Message: <code>{step.commit_message}</code></p>
      <div>
        <label for="agentSelect-{i}">Select Agent:</label>
        <select id="agentSelect-{i}" bind:value={step.agent_id}>
          {#each availableAgents as agent (agent.id)}
            <option value={agent.id}>{agent.name} ({agent.model})</option>
          {/each}
        </select>
      </div>
    </div>
  {/each}

  <button on:click={startWorkflow}>Start Multi-Agent Workflow</button>

{:else}
  <p>Loading card details...</p>
{/if}

<style>
  /* Basic styling */
  h2, h3, h4 { color: #333; }
  button {
    background-color: #007bff;
    color: white;
    padding: 10px 15px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 1em;
    margin-top: 20px;
  }
  button:hover {
    background-color: #0056b3;
  }
  select {
    padding: 8px;
    border-radius: 4px;
    border: 1px solid #ddd;
    margin-left: 10px;
    min-width: 200px;
  }
  code {
      background-color: #f4f4f4;
      padding: 2px 4px;
      border-radius: 3px;
      font-family: monospace;
  }
</style>

Explanation of Svelte Code:

  • workflowSteps: This array defines our fixed two-step workflow (Developer then Reviewer) with their respective instructions and persona overrides. In a more advanced application, this would be dynamic and configurable by the user via a form.
  • onMount and onDestroy:
    • onMount loads currentCard and availableAgents from Svelte stores.
    • It initializes workflowSteps with the IDs of the first two available agents as a default.
    • Crucially, it sets up event listeners for agent_progress and agent_workflow_completed emitted by the Rust backend. This allows the UI to update in real-time.
    • onDestroy ensures that these event listeners are cleaned up when the component is unmounted, preventing memory leaks.
  • startWorkflow function:
    • Performs basic validation (card and worktree exist, agents are selected).
    • Invokes the new execute_agent_workflow Tauri command.
    • Passes the cardId, worktreePath, the workflowSteps definition, and availableAgents (for the backend to look up agent details by ID).
    • Handles success and error states, updating workflowStatus on the UI.
  • UI Elements: Displays the defined workflow steps, allows selecting agents for each step via a dropdown, and shows the real-time workflowStatus.

Testing & Verification

With the code in place, let’s verify our multi-agent workflow. This is a crucial step to ensure the orchestration logic is sound and agents are making the expected changes.

  1. Start Kanbots:

    • Launch your Tauri application using cargo tauri dev in the src-tauri directory.
    • Launch your Svelte frontend using npm run dev (or yarn dev) in your Svelte project root.
  2. Ensure a Card and Worktree Exist:

    • In the Kanbots UI, create a new Kanban card (e.g., “Implement User Authentication”).
    • Navigate to its detail view.
    • Use the functionality from a previous chapter (Milestone 2) to “Initialize Worktree” for this card. This will create the dedicated Git repository in your specified base path.
  3. Configure AI Agents:

    • Ensure you have at least two Agent configurations stored in your application (or mock them in your Svelte agents store for testing). These agents should ideally have distinct names (e.g., “CodeDev”, “CodeReview”).
    • In a real setup, you’d provide valid API keys for models like claude-3-opus-20240229 or gpt-4-turbo. For this chapter, our Rust backend simulates the output, so the apiKey can be a placeholder.
  4. Trigger the Workflow:

    • Navigate back to the detail view of the card with the initialized worktree.
    • Observe the “Agent Workflow” section. You should see the two predefined steps (“Senior Software Engineer” and “Code Reviewer”).
    • Use the dropdowns to select your configured agents for each step. If you only have one agent, select the same agent for both steps.
    • Click the “Start Multi-Agent Workflow” button.
  5. Observe UI Updates and Terminal Output:

    • The workflowStatus on the UI should update, showing messages about agent progress as emitted by the Rust backend.
    • Check your terminal where you launched Tauri (cargo tauri dev) for the println! output from the Rust backend, indicating agent activity, simulated AI outputs, and file writes. You should see messages like “Agent … started step …”, “Wrote agent output to: …”, and “Committed changes for step …”.
  6. Inspect the Git Worktree:

    • After the workflow completes, open a terminal or file explorer and navigate to the worktreePath for your card (as displayed in the UI).
    • Inside this directory, run git log --oneline --graph. You should see at least two new commits:
      • One from the “CodeDev” or “Senior Software Engineer” persona (e.g., “feat: add basic add function”).
      • One from the “CodeReview” or “Code Reviewer” persona (e.g., “refactor: review and improve add function”).
    • Inspect the src/lib.rs file within the worktree. You should see the initial add function generated by the Developer, potentially with doc comments or improvements added by the Reviewer (even if simulated, the content should reflect the distinct steps).

Expected Outcome: You should see the UI updating with progress, and a git log in the worktree directory will clearly show distinct commits made by the “Developer” and “Reviewer” agents, demonstrating their sequential collaboration on the src/lib.rs file.

Production Considerations

Implementing multi-agent systems introduces several production-level concerns beyond basic functionality:

  • State Management & Persistence: What happens if the application crashes mid-workflow? For critical tasks, you need to persist the workflow’s current step, agent outputs, and context to disk. This allows for resuming interrupted workflows. This could involve storing workflow state in a local SQLite database or a JSON file.
  • Concurrency & Locking: If multiple cards (and thus multiple worktrees) try to run workflows concurrently, ensure that Git operations on different worktrees don’t conflict, and that AI API rate limits are respected. This might require a global queue or semaphore in the Rust backend for AI calls and Git operations to prevent resource exhaustion or rate-limit errors.
  • Cost Management: AI API calls incur costs. Multi-agent workflows can quickly multiply these costs. Implement proper logging of API usage, and consider adding budget limits, user confirmations for complex workflows, or even a dry-run mode that simulates API calls without actual charges.
  • Robust Error Handling & Recovery: Each step in a multi-agent workflow is a potential point of failure (AI hallucination, API error, Git command failure, file system error). Your orchestration logic needs to:
    • Catch errors gracefully, providing specific error messages.
    • Provide clear feedback to the user on which agent and which step failed.
    • Offer options like retry the current step, skip the current step, or rollback the worktree to a previous state.
  • Agent Communication & Context: For more complex workflows, passing context as a simple HashMap<String, String> might become brittle. Consider a structured context object or a shared “scratchpad” within the worktree (e.g., a workflow_context.json file) that agents can both read and write to, allowing for richer, persistent context sharing.
  • Security: AI API keys are paramount. Ensure they are never hardcoded and are securely managed (e.g., via OS-level secret management, environment variables, or encrypted local storage). Each agent should ideally only have access to the minimum necessary resources.

Common Issues & Solutions

  1. Agents Losing Context / Unfocused Output:

    • Issue: An agent in a later step doesn’t seem to understand what the previous agent did or the overall goal, leading to irrelevant or contradictory output.
    • Solution:
      • Prompt Engineering: Refine agent personas and instructions to be very specific and constrain output. Clearly state the overall task and the agent’s current sub-task.
      • Context Passing: Ensure current_context accurately reflects all necessary information. For code-related tasks, it’s often better to pass the actual content of relevant files (read from the worktree) rather than just summaries or file paths.
      • Iterative Prompting: If a single prompt is too complex, break it down. For example, a reviewer might first be asked to list issues, then asked to fix them based on that list.
  2. Conflicting Changes in Worktrees:

    • Issue: Git operations fail because the worktree is in an unexpected state (e.g., uncommitted changes from a user, merge conflicts from a previous failed run).
    • Solution:
      • Pre-workflow Check: Before running a workflow, check the worktree for uncommitted changes using git status. Prompt the user to commit or stash them.
      • Atomic Commits: Ensure each agent step performs a clear git add . and git commit -m "..." to isolate its changes. This makes it easier to track and potentially revert individual agent contributions.
      • Error Handling for Git: Catch specific Git error messages (e.g., “nothing to commit”) and handle them gracefully, differentiating them from actual failures.
  3. AI Hallucinations or Unexpected Outputs:

    • Issue: An agent generates nonsensical code, irrelevant text, or gets stuck in a repetitive loop.
    • Solution:
      • Strong Prompting: The persona field and instruction are your primary tools. Be explicit about expected output format (e.g., “only output Rust code, no prose”).
      • Output Validation: Implement backend logic to parse and validate AI output. For code, this could mean basic syntax checking (e.g., using a Rust linter or rustfmt) or checking for specific keywords. If output is invalid, retry the agent with a corrective prompt or flag it for user intervention.
      • Timeouts: Implement timeouts for AI API calls to prevent agents from hanging indefinitely.
      • User Intervention: Provide UI controls to pause a workflow, inspect agent output, and manually correct or guide the agent. This is crucial for debugging and recovering from AI failures.

Summary & Next Step

You’ve successfully built the foundation for multi-agent collaboration within Kanbots! You can now:

  • Define AI agents with specific personas that guide their behavior.
  • Orchestrate sequential workflows where agents build upon each other’s work.
  • Observe real-time progress and verify distinct contributions through Git commits, clearly attributing changes to specific AI personas.

This chapter was a significant leap, demonstrating how to leverage AI agents not just for individual tasks, but as a coordinated team. The ability to define personas and orchestrate their interactions opens up vast possibilities for automating complex development processes directly on your desktop.

In the next chapter, we will focus on enhancing the UI Feedback & Control. We’ll refine the user interface to provide more detailed, real-time insights into agent activity, allow users to intervene in workflows (e.g., pause, resume, cancel), and improve the overall user experience of interacting with your AI development team.


References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.