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.
119 lines
6.1 KiB
Markdown
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
|
|
}
|
|
}
|
|
```
|