# 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, // None = follow session model (slim's default) pub temperature: Option, pub prompt: String, // markdown body pub permissions: Ruleset, // frontmatter `permission:` list pub tools: HashMap, // enable/disable, wildcard keys pub max_steps: Option, 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 `` 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: `/.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`/``. 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` + 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, pub state: JobState, // Running | Completed | Error | Cancelled pub reconciled: bool, // orchestrator has seen the terminal result pub result_summary: Option, // final text, truncated pub context_files: Vec, // 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>` — 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 } } ```