Files
ai-harness/docs/04-multiagent.md
darman 420f00494f M0: scaffold Cargo workspace, core types, event bus, permission engine, storage actor
Sets up the Cargo workspace (harness-core, harness-tools, harness-providers, harness-mcp, harness-lsp, harness-app, harness-tui) and the ten architecture docs under docs/. harness-core gets its foundational types (session/message/part/model ids), an in-process event bus, a table-driven permission evaluate function, and a SQLite-backed storage actor with schema and roundtrip coverage. Every crate compiles empty and a bin stub prints its version.
2026-07-10 16:19:27 +02:00

6.1 KiB

04 — Multiagent Orchestration

The oh-my-opencode-slim model (orchestrator schedules, specialists execute, background jobs tracked on a board) reimplemented natively — but all agent behavior lives in markdown + config. The engine only understands mode, tool filters, permissions, and depth. No prompt text is hardcoded in Rust.

Agent definitions

pub struct AgentDef {
    pub name: String, pub description: String,
    pub mode: AgentMode,                   // Primary | Subagent | All
    pub model: Option<ModelRef>,           // None = follow session model (slim's default)
    pub temperature: Option<f32>,
    pub prompt: String,                    // markdown body
    pub permissions: Ruleset,              // frontmatter `permission:` list
    pub tools: HashMap<String, bool>,      // enable/disable, wildcard keys
    pub max_steps: Option<u32>,
    pub source: AgentSource,               // Bundled | Global | Project | Config
}

Markdown format (opencode-compatible shape):

---
description: Read-only reconnaissance specialist
mode: subagent
model: anthropic/claude-haiku-4-5   # optional; omit to follow session model
temperature: 0.1
tools: { write: false, edit: false, bash: true, task: false }
permission:
  - { permission: "edit", pattern: "*", action: deny }
---
You are Explorer... (prompt body)

Loading order (later wins by name)

  1. Bundledinclude_str! of assets/agents/{orchestrator,explorer,oracle,librarian,fixer,designer}.md. Prompts ported from oh-my-opencode-slim's src/agents/*.ts with plugin-specific mechanics stripped; the orchestrator's <Agents> routing section is generated at load time from the enabled agents' description fields (so disabling an agent removes it from routing text automatically).
  2. Global: ~/.config/ai-harness/agent/*.md
  3. Project: <project>/.harness/agent/*.md
  4. Inline agents table in config — patch semantics (only set fields override: model, temperature, prompt, tools…).

disable: true in frontmatter or config removes an agent. Unknown names in config with at least a prompt/model create custom agents.

Task tool

harness-tools/src/task.rs, input:

{ "description": "...", "prompt": "...", "subagent_type": "explorer",
  "task_id": "exp-1 (optional, reuse)", "background": false }

Execution:

  1. Resolve subagent_type in the registry; must be mode: Subagent | All. Ask permission task/<agent name>.
  2. Depth limit: session.depth + 1 > config.orchestration.depth_limit (default 3) → error string telling the model to do the work itself. Depth is stored on the session at creation → O(1) check.
  3. Session reuse: if task_id is set, JobBoard::resolve(parent_id, alias_or_id); the job must be terminal-completed (reusable). Reuse its child session — history intact — and append the new prompt. Otherwise create a child Session { parent_id, depth: parent.depth + 1, agent }.
  4. Permission intersection: child asks evaluate against both parent-effective and child rulesets, taking the more restrictive verdict (deny > ask > allow) — evaluate_intersected() in permission/rule.rs. Bundled default rules additionally deny task and todo* for any session with depth > 0 (config-overridable).
  5. Foreground: run run_session(child) with a token childed from the calling tool's token; await; return the final assistant text as ToolOutput. A Subtask part on the parent links the child for TUI drill-in.
  6. Background: JobBoard::register_launch (assign alias), tokio::spawn the child run with a token childed from the parent session's run token — not the tool's, since the tool returns immediately but the job must die with the session — and return "Launched background task {alias} ({child_session_id}). Check the job board.". On completion, the spawned task updates the board and publishes JobUpdated.

Job board

engine/jobs.rs — simplified port of slim's src/utils/background-job-board.ts. In-memory RwLock<HashMap> + persisted to the job table (resume works).

pub struct JobRecord {
    pub task_id: String, pub alias: String,          // alias: first 3 chars of agent + counter → "exp-1"
    pub parent_session: SessionId, pub child_session: SessionId,
    pub agent: String, pub description: String, pub objective: Option<String>,
    pub state: JobState,          // Running | Completed | Error | Cancelled
    pub reconciled: bool,         // orchestrator has seen the terminal result
    pub result_summary: Option<String>,               // final text, truncated
    pub context_files: Vec<ContextFile>,              // fed by the read tool in child sessions
    pub launched_at: i64, pub updated_at: i64, pub last_used_at: i64,
}
  • Read-tool executions in child sessions report files (≥10 lines read) to the board; max 8 shown per job.
  • Reusable = terminal-completed; trimmed to max_reusable_per_agent (default 2) per agent per parent, LRU by last_used_at.

Injection

In build_request, if the agent is Primary and the board is non-empty for this session, append a synthetic, non-persisted text part to the last user message:

### Background Job Board
Do not poll running jobs; wait for completion. Reconcile terminal jobs before your final response.
Completed sessions are reusable by alias for the same specialist.

#### Active / Unreconciled
- exp-1 / ses_x / explorer / running — Objective: ...

#### Reusable Sessions
- fix-1 / ses_y / fixer / completed
    Objective: ...
    Context read: src/a.rs, src/b.rs

Terminal jobs included in an injection are marked reconciled after the step finishes successfully.

Reminders (optional, off by default)

slim's phase reminder / post-file-tool nudge become config.orchestration.reminders: Option<HashMap<String, String>> — user-provided strings injected as synthetic parts at the corresponding hook points (turn_start, after_file_tool). No bundled nudge text — this is part of the "less baked-in" requirement.

Config

{
  "orchestration": {
    "depth_limit": 3,
    "job_board": true,
    "max_reusable_per_agent": 2,
    "reminders": null
  }
}