Files
agent-orchestrator/agents.example.toml
autonomic-bot 289ef07df4 feat: agent-orchestrator v0.1.0 — generic multi-agent harness
Extracted and generalized from a project-specific agent launch engine. No project
specifics remain in code: paths, the loop kickoff preamble, handoff conventions, and the
on-complete hook are all config/template driven; session_prefix + log_dir are required.

- agents.py: driver + watchdog (data-driven backends via prompt_delivery arg|ping|exec;
  required session_prefix/log_dir; project-rooted path resolution; configurable kickoff
  template, handoff patterns, on_complete task; tmux-safe; selftest + init verbs)
- agent-log.py: config-driven claude transcript renderer
- agents.example.toml: self-contained 2-agent example (dependency-free demo backend)
- prompts/: generic builder/adversary/kickoff templates
- smoke.sh: isolated up+down sandbox proof that cleans up after itself
- flake.nix/.lock: devShell (python311 + tmux + git)
- README.md: schema + verbs + AI-PO usage + nix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 18:39:00 +00:00

110 lines
6.0 KiB
TOML

# agent-orchestrator — example project config.
#
# One file declares: which agents exist, their backend, model, prompt, kind, and how the
# watchdog supervises them. Read by agents.py (driver + watchdog). Runtime state (phase index,
# resume ids, limit windows) lives under <log_dir>/state/, NOT here.
#
# This example is self-contained: its agents use a dependency-free `demo` backend (a shell that
# just idles), so the whole project can be brought up and torn down with no external agent CLI —
# see ./smoke.sh. The `claude` and `opencode` backends below are the real ones; point an agent at
# them with `backend = "claude"` for a live run.
# ─────────────────────────── global watchdog cadence ───────────────────────────
[watchdog]
signal_interval = 30 # s between handoff / stall / limit checks (light)
heavy_interval = 300 # s between heal / phase-advance checks
limit_probe_fallback = 300 # flat probe cadence when a reset time can't be parsed
limit_reset_slack = 45 # s past a parsed reset before probing
stall_grace = 180 # s of slack past a WAITING-UNTIL marker before a stall reboot
# ─────────────────────────── defaults inherited by every agent ───────────────────────────
[defaults]
session_prefix = "ao-example-" # REQUIRED — tmux namespace for this project (no implicit default)
log_dir = ".ao-state" # REQUIRED — logs + state/, resolved relative to this file
backend = "demo"
model = ""
watch = "none" # none | heal | heal+stall
# ─────────────────────────── backends (declared as data) ───────────────────────────
# A backend is fully described by config. `prompt_delivery` selects how the kickoff reaches the
# agent: "arg" (CLI argument, claude-style), "ping" (typed in after a TUI connects, opencode),
# or "exec" (a plain command; {kickoff}=prompt file, {session}=session name, {model}=model).
[backend.demo] # dependency-free backend used by this example's smoke run
bin = "echo '[demo] {session} up (kickoff: {kickoff})'; exec sleep 1000000"
prompt_delivery = "exec"
[backend.claude] # the real claude backend
bin = "claude"
flags = "--dangerously-skip-permissions"
remote_control = true
supports_resume = true
prompt_delivery = "arg"
process_name = "claude" # used for backend-mismatch healing
submit_key = "Enter"
stall_idle = 300
active_re = "esc to interrupt|Running tool|⠇|⠙|· \\d+"
limit_re = "spend limit|usage limit|limit reached|reached your .*limit|out of (credits|tokens)"
fatal_re = "redacted_thinking|blocks cannot be modified|cannot be modified"
[backend.opencode] # the real opencode backend (a TUI; prompt typed after connect)
bin = "opencode"
attach = "{bin} attach {server} --dir {dir}"
server = "http://127.0.0.1:4096"
supports_resume = false
prompt_delivery = "ping"
process_name = "opencode"
footer_ui = true # static footer lingers after a turn → only the bottom = activity
log_grace = 180
connect_delay = 12
submit_key = "C-m"
model_env = true # pass the model via OPENCODE_CONFIG_CONTENT
stall_idle = 900
active_re = "esc interrupt|thinking|inferring|running tool|tool call|preparing patch|reading|searching"
limit_re = "spend limit|usage limit|limit reached|reached your .*limit|out of (credits|tokens)"
fatal_re = "redacted_thinking|blocks cannot be modified|cannot be modified"
# ─────────────────────────── agents ───────────────────────────
# A minimal 2-agent loop pair: a Builder that does the work and an Adversary that verifies it.
[[agent]]
name = "builder" # tmux session: ao-example-builder
kind = "loop" # kickoff = kickoff_template + prompts/builder.md, per phase
role = "builder"
[[agent]]
name = "adversary" # tmux session: ao-example-adversary
kind = "loop"
role = "adversary"
# A persistent supervisor and a one-shot task are also supported:
# [[agent]]
# name = "orchestrator"
# kind = "persistent"
# backend = "claude"
# resume = true
# watch = "heal"
# wake = { interval = 3600, prompt_file = "prompts/supervise.md" }
# prompt = "You supervise this project. On startup, check status and report."
# Non-AI helper services (started by a bare `up`, not AI sessions):
# [[service]]
# name = "cleanlogs"
# command = "python3 agent-log.py follow-all"
# ─────────────────────────── the phase machine (kind="loop" agents) ───────────────────────────
[loop]
state_file = "phase-idx" # under <log_dir>/state/
resume_phase = true # keep the current index across restarts (don't reset to 0)
auto_advance = true # advance when the current phase's status file says the done_marker
done_marker = "## DONE"
kickoff_template = "prompts/kickoff.md" # project preamble; slots {phase_id}/{plan}/{status}/{role}
roles_dir = "prompts" # role prompt = <roles_dir>/<role>.md
handoff = { repo = ".", claim_pings = "adversary", review_pings = "builder", inboxes = ["ADVERSARY-INBOX.md", "BUILDER-INBOX.md"], claim_pattern = "^claim", review_pattern = "^review", state_subdir = "machine-docs" }
# on_complete = { trigger_file = ".run-on-complete", run = "reporter" }
phases = [
{ id = "demo1", plan = "examples/PLAN-demo1.md", status = "STATUS-demo1.md" },
{ id = "demo2", plan = "examples/PLAN-demo2.md", status = "STATUS-demo2.md", models = { builder = "claude-opus-4-8" } },
]