Files
ai-harness/docs/04-multiagent.md
T
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

119 lines
6.1 KiB
Markdown

# 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
```rust
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):
```markdown
---
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. **Bundled**`include_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:
```json
{ "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).
```rust
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
```jsonc
{
"orchestration": {
"depth_limit": 3,
"job_board": true,
"max_reusable_per_agent": 2,
"reminders": null
}
}
```