agent-orchestrator-sdk 0.1.1

Rust SDK for orchestrating LLM-powered agents, shared task execution, and teammate coordination
Documentation
# Agent Teams

`AgentTeam` is the high-level orchestration API for running multiple agents in parallel with shared task state, mailbox messaging, and shared memory.

## Mental Model

An agent team has:

- one team lead
- zero or more named teammates, or a generic pool if no teammates are provided
- a file-backed task store
- a file-backed mailbox broker
- a file-backed shared memory store

The lead spawns teammates, teammates claim tasks, dependencies gate execution, and completion is tracked on disk.

## Build A Team

```rust
use agent_sdk::{AgentConfig, AgentTeam, LlmConfig, Task};

let analyze = Task::new(
    "analysis",
    "Inspect crate exports",
    "Read src/lib.rs and list the public SDK entrypoints.",
    "docs/exports.md",
)
.with_priority(0);

let write = Task::new(
    "docs",
    "Write team usage guide",
    "Document how AgentTeam coordinates teammates and tasks.",
    "docs/team-usage.md",
)
.with_dependencies(vec![analyze.id])
.with_priority(1);

let team = AgentTeam::new(LlmConfig::default(), AgentConfig::default())
    .source_root(".")
    .work_dir(".")
    .add_teammate("api-reader", "Read the Rust API surface and summarize it")
    .add_teammate("writer", "Write markdown documentation from implementation details")
    .add_task(analyze)
    .add_task(write);
```

Run it:

```rust
let result = team.run("Document the SDK").await?;
println!("tokens: {}", result.total_tokens());
```

## Result Types

`AgentTeam::run(...)` returns `TeamResult`:

```rust
pub enum TeamResult {
    Single(AgentLoopResult),
    Team(ExecutionSummary),
}
```

Current `run(...)` always follows the team path and returns `Team(...)`. The `Single(...)` variant is useful conceptually alongside `run_single(...)`, but team execution itself is task-driven.

`ExecutionSummary` contains:

- `total_tasks`
- `tasks_completed`
- `tasks_failed`
- `total_tokens_used`
- `agents_spawned`

## Task Model

Tasks are explicit work items:

```rust
use agent_sdk::Task;
use serde_json::json;

let task = Task::new(
    "docs",
    "Write getting started guide",
    "Explain installation, config, and first program.",
    "docs/getting-started.md",
)
.with_priority(0)
.with_context(json!({
    "assigned_teammate": "writer",
    "audience": "sdk users"
}));
```

Key fields:

- `kind`: free-form category string
- `title`
- `description`
- `target_file`
- `dependencies`
- `priority`
- `max_retries`
- `context`

Task state machine:

- `Pending`
- `Claimed`
- `InProgress`
- `Completed`
- `Failed`
- `Blocked`

Dependency resolution is based on completed task ids. Lower `priority` values are claimed first.

## Named Teammates

Named teammates let you shape specialization:

```rust
let team = AgentTeam::new(LlmConfig::default(), AgentConfig::default())
    .add_teammate("security-reviewer", "Review code for security issues")
    .add_teammate("perf-reviewer", "Review code for performance bottlenecks");
```

If you do not add teammates explicitly, the lead spawns a generic pool up to `max_parallel_agents`.

## Plan Approval Mode

You can require a teammate to submit a plan before implementation:

```rust
let team = AgentTeam::new(LlmConfig::default(), AgentConfig::default())
    .add_teammate_with_plan_approval(
        "architect",
        "Refactor the configuration layer carefully",
    );
```

Current behavior:

- the teammate switches into plan mode
- it submits a plan to the lead by mailbox message
- the lead asks the LLM to approve or reject the plan
- approved plans proceed to execution
- rejected plans are returned with feedback

Relevant events:

- `PlanSubmitted`
- `PlanApproved`
- `PlanRejected`

## Shared Infrastructure On Disk

The runtime layout now follows the Claude Code agent-team model:

- project-local `.agent/` is for shared config checked into the repo
- team config lives under `~/.agent/teams/<team-name>/config.json`
- shared task state lives under `~/.agent/tasks/<team-name>/`
- other mutable team resources stay under the same `~/.agent/teams/<team-name>/` team directory

Current runtime layout:

```text
~/.agent/
  settings.json
  teams/
    <team-name>/
      config.json
      mailbox/
        team-lead/
          inbox.jsonl
          inbox.lock
        agents/
          <agent-id>/
            inbox.jsonl
            inbox.lock
      memory/
        <key>.json
  tasks/
    <team-name>/
      pending/
      in_progress/
      completed/
      failed/
```

Project-local config layout:

```text
.agent/
  settings.json
  settings.local.json
```

Each task is persisted as JSON, with a `.lock` file used during claiming.

## Shared Memory

Teammates can use a key-value store for coordination.

Built-in team tools:

- `read_memory`
- `write_memory`
- `list_memory`

Example payloads:

```json
{ "key": "style-guide", "value": { "tone": "concise", "format": "markdown" } }
```

Keys are stored as JSON files under `~/.agent/teams/<team-name>/memory/`.

## Task Context Tools

Teammates can inspect completed work using:

- `get_task_context`
- `list_completed_tasks`

This is useful when one task depends on artifacts or notes produced by another teammate.

## Hooks

Hooks let you enforce quality gates:

```rust
use agent_sdk::{Hook, HookEvent, HookResult};

struct RequireTests;

impl Hook for RequireTests {
    fn on_event(&self, event: &HookEvent) -> HookResult {
        match event {
            HookEvent::TaskCompleted { task, .. } => {
                let has_test_note = task
                    .result
                    .as_ref()
                    .map(|r| r.notes.to_lowercase().contains("test"))
                    .unwrap_or(false);

                if has_test_note {
                    HookResult::Continue
                } else {
                    HookResult::Reject {
                        feedback: "Task completion must mention test coverage".to_string(),
                    }
                }
            }
            _ => HookResult::Continue,
        }
    }
}
```

Hook events currently available:

- `TeammateIdle`
- `TaskCreated`
- `TaskCompleted`

## Event Monitoring

You can subscribe to `AgentEvent` through an unbounded channel:

```rust
use agent_sdk::AgentEvent;

let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<AgentEvent>();

tokio::spawn(async move {
    while let Some(event) = rx.recv().await {
        println!("{:?}", event);
    }
});

let team = AgentTeam::new(LlmConfig::default(), AgentConfig::default())
    .event_channel(tx);
```

Important team events:

- `TeamSpawned`
- `TeammateSpawned`
- `TaskStarted`
- `TaskCompleted`
- `TaskFailed`
- `PlanSubmitted`
- `PlanApproved`
- `PlanRejected`
- `ShutdownRequested`
- `HookRejected`

## Custom Prompt Builder

For domain-specific systems, override prompt generation:

```rust
use std::sync::Arc;

use agent_sdk::traits::prompt_builder::PromptBuilder;
use agent_sdk::tools::registry::ToolRegistry;
use agent_sdk::Task;

struct DocsPromptBuilder;

impl PromptBuilder for DocsPromptBuilder {
    fn build_system_prompt(
        &self,
        task: &Task,
        _source_root: &std::path::Path,
        _work_dir: &std::path::Path,
    ) -> String {
        format!("You write SDK documentation. Task: {}", task.title)
    }

    fn build_user_message(&self, task: &Task) -> String {
        format!("Complete this docs task:\n{}", task.description)
    }

    fn customize_tools(&self, _task: &Task, registry: ToolRegistry) -> ToolRegistry {
        registry
    }
}

let team = AgentTeam::new(LlmConfig::default(), AgentConfig::default())
    .prompt_builder(Arc::new(DocsPromptBuilder));
```

## Background Execution

Both `spawn_agent_team` and `spawn_subagent` support background mode. When `background: true` is set:

1. The tool returns immediately with a status message
2. The agent/team runs concurrently in a separate tokio task
3. When work completes, the result is delivered back to the parent agent's conversation via the `BackgroundResult` channel
4. The parent agent sees the result injected as a user message before its next LLM call

This mirrors Claude Code's behavior where background agents run independently and the parent is automatically notified on completion.

### When to use background mode

- **Foreground (default):** Use when you need the result before proceeding. The tool blocks until complete.
- **Background:** Use when you have genuinely independent work to do in parallel. Continue working while the agent/team runs concurrently.

### Wiring up background results

```rust
use agent_sdk::agent::agent_loop::BackgroundResult;

// Create the channel
let (bg_tx, bg_rx) = tokio::sync::mpsc::unbounded_channel::<BackgroundResult>();

// Pass bg_tx to SpawnAgentTeamTool and SpawnSubAgentTool
let team_tool = SpawnAgentTeamTool {
    // ...
    background_tx: Some(bg_tx.clone()),
};

// Set bg_rx on the parent AgentLoop
loop_.set_background_rx(bg_rx);
```

## Practical Caveats

- `source_root` is the read side used by `read_file`, `list_directory`, and `search_files`
- `work_dir` is the write side used by `write_file` and command execution
- if you set both to the repository root, agents read and write in the same tree
- if you separate them, teammates read from source and write generated output elsewhere