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.
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)
- Bundled —
include_str!ofassets/agents/{orchestrator,explorer,oracle,librarian,fixer,designer}.md. Prompts ported from oh-my-opencode-slim'ssrc/agents/*.tswith plugin-specific mechanics stripped; the orchestrator's<Agents>routing section is generated at load time from the enabled agents'descriptionfields (so disabling an agent removes it from routing text automatically). - Global:
~/.config/ai-harness/agent/*.md - Project:
<project>/.harness/agent/*.md - Inline
agentstable 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:
- Resolve
subagent_typein the registry; must bemode: Subagent | All. Ask permissiontask/<agent name>. - 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. - Session reuse: if
task_idis 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 childSession { parent_id, depth: parent.depth + 1, agent }. - Permission intersection: child asks evaluate against both parent-effective and child rulesets, taking the more restrictive verdict (
deny > ask > allow) —evaluate_intersected()inpermission/rule.rs. Bundled default rules additionally denytaskandtodo*for any session withdepth > 0(config-overridable). - Foreground: run
run_session(child)with a token childed from the calling tool's token; await; return the final assistant text asToolOutput. ASubtaskpart on the parent links the child for TUI drill-in. - Background:
JobBoard::register_launch(assign alias),tokio::spawnthe 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 publishesJobUpdated.
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 bylast_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
}
}