feat(orchestration): multi-angle workflow — 5+ parallel subagents, converge, log + index runs
- skills/multi-angle-orchestration/SKILL.md: SOP for single-source fan-out (delegate_task leaf workers, NO external AIs) of >=5 angles + converge. - brain-core: ingest orchestration-runs/*.json as source 'orchestration' so past runs are searchable in Brain Search. - server.py: POST /api/orchestrate (optional headless fan-out of the 3 built-in agents) + GET /api/orchestrate/runs; persist runs to data/orchestration-runs/<id>.json and upsert to brain. - dashboard/pages/orchestration.js: launch runs + review past runs. - skills/multi-agent-run/SKILL.md + api helpers + nav entry.
This commit is contained in:
parent
cce56eed02
commit
29eaa7f801
|
|
@ -168,11 +168,39 @@ def ingest_chat(conn: sqlite3.Connection, path: Path | None = None) -> int:
|
|||
return count
|
||||
|
||||
|
||||
def ingest_orchestration(conn: sqlite3.Connection, root: Path | None = None) -> int:
|
||||
"""Index multi-agent orchestration runs (my delegate_task fan-outs) so
|
||||
past 'spin up N agents on a goal' runs are searchable in the brain."""
|
||||
root = root or (BASE_DIR / "data" / "orchestration-runs")
|
||||
if not root.exists():
|
||||
return 0
|
||||
count = 0
|
||||
for f in sorted(root.glob("*.json")):
|
||||
try:
|
||||
d = json.loads(f.read_text(encoding="utf-8", errors="replace"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
goal = d.get("goal", "")
|
||||
synthesis = d.get("synthesis", "") or ""
|
||||
angles = d.get("angles", {}) or {}
|
||||
angle_blob = "\n".join(f"{a}: {p}" for a, p in angles.items())
|
||||
agents = ", ".join(d.get("agents", []) or [])
|
||||
content = f"GOAL: {goal}\n\nANGLES ({agents}):\n{angle_blob}\n\nSYNTHESIS:\n{synthesis}"
|
||||
rel = str(f.relative_to(BASE_DIR))
|
||||
upsert_doc(conn, source="orchestration", source_path=rel,
|
||||
title=f"Orchestration: {goal[:80]}", content=content,
|
||||
agent=d.get("converge_agent") or "orchestrate",
|
||||
updated_at=d.get("timestamp"))
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def ingest_all(conn: sqlite3.Connection) -> dict:
|
||||
return {
|
||||
"brain": ingest_brain(conn),
|
||||
"skill-learning": ingest_skill_learnings(conn),
|
||||
"chat": ingest_chat(conn),
|
||||
"orchestration": ingest_orchestration(conn),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,9 @@ const api = {
|
|||
getBrainIndexSearch: (q, source = '', limit = 20) => api.get(`/api/brain-index/search?q=${encodeURIComponent(q)}&limit=${limit}${source ? `&source=${encodeURIComponent(source)}` : ''}`),
|
||||
getBrainIndexStats: () => api.get('/api/brain-index/stats'),
|
||||
brainIndexIngest: () => api.post('/api/brain-index/ingest', {}),
|
||||
// Multi-Agent Orchestration (fan-out + converge)
|
||||
orchestrate: (payload) => api.post('/api/orchestrate', payload),
|
||||
getOrchestrationRuns: (limit = 50) => api.get(`/api/orchestrate/runs?limit=${limit}`),
|
||||
// Agent Registry
|
||||
getAgents: () => api.get('/api/agents'),
|
||||
registerAgent: (data) => api.post('/api/agents/register', data),
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
<a href="#learning-analytics" class="nav-item" data-page="learning-analytics"><span class="nav-icon">📊</span><span class="nav-label">Learning Analytics</span></a>
|
||||
<a href="#agent-insights" class="nav-item" data-page="agent-insights"><span class="nav-icon">🧠</span><span class="nav-label">Agent Insights</span></a>
|
||||
<a href="#brain-search" class="nav-item" data-page="brain-search"><span class="nav-icon">🔍</span><span class="nav-label">Brain Search</span></a>
|
||||
<a href="#orchestration" class="nav-item" data-page="orchestration"><span class="nav-icon">🕸</span><span class="nav-label">Multi-Agent Run</span></a>
|
||||
<a href="#session-replay" class="nav-item" data-page="session-replay"><span class="nav-icon">🔄</span><span class="nav-label">Session Replay</span></a>
|
||||
<a href="#agent-time" class="nav-item" data-page="agent-time"><span class="nav-icon">⏱</span><span class="nav-label">Agent Time</span></a>
|
||||
<div class="sidebar-section"><div class="sidebar-section-label">Management</div></div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
// Multi-Agent Orchestration — fan-out N agents on one goal, then converge.
|
||||
|
||||
async function renderOrchestration() {
|
||||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Multi-Agent Run</h1>
|
||||
<p class="page-subtitle">Spin up opencode, Gemini & Hermes on the same goal from different angles, then converge</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-4">
|
||||
<div class="page-subtitle" style="margin-bottom:8px">New orchestration run</div>
|
||||
<label class="form-label">Project goal</label>
|
||||
<textarea id="orchGoal" class="form-input" rows="3" placeholder="e.g. Build a real-time sync layer between the dashboard and the brain index"></textarea>
|
||||
<details class="mt-2">
|
||||
<summary style="cursor:pointer;font-size:12px;color:var(--text-muted)">Customize angles (optional)</summary>
|
||||
<div class="mt-2">
|
||||
<label class="form-label">opencode angle</label>
|
||||
<input id="orchOpencode" class="form-input mb-2" placeholder="implementation focus" />
|
||||
<label class="form-label">gemini angle</label>
|
||||
<input id="orchGemini" class="form-input mb-2" placeholder="research focus" />
|
||||
<label class="form-label">hermes angle</label>
|
||||
<input id="orchHermes" class="form-input" placeholder="coordination focus" />
|
||||
</div>
|
||||
</details>
|
||||
<button class="btn btn-primary mt-3" id="orchRunBtn" onclick="orchRun()">🚀 Launch agents</button>
|
||||
</div>
|
||||
<div id="orchResult"></div>
|
||||
<div class="page-subtitle mt-4">Past runs</div>
|
||||
<div id="orchRuns"></div>
|
||||
`;
|
||||
await orchLoadRuns();
|
||||
}
|
||||
|
||||
async function orchLoadRuns() {
|
||||
const box = document.getElementById('orchRuns');
|
||||
try {
|
||||
const d = await api.getOrchestrationRuns(30);
|
||||
if (!d.runs.length) { box.innerHTML = '<div class="empty-state"><div class="empty-state-title">No runs yet</div></div>'; return; }
|
||||
box.innerHTML = d.runs.map(r => `
|
||||
<div class="card mb-2" style="border-left:4px solid var(--accent)">
|
||||
<div class="flex items-center gap-2">
|
||||
<strong style="font-size:13px">${escapeHtml(r.goal || '(no goal)')}</strong>
|
||||
<span class="badge" style="font-size:10px">${escapeHtml((r.agents||[]).join(' + '))}</span>
|
||||
<span class="badge badge-accent" style="font-size:10px">→ ${escapeHtml(r.converge_agent||'')}</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-muted)">${escapeHtml(r.run_id||'')} · ${escapeHtml(r.timestamp||'')}</div>
|
||||
</div>`).join('');
|
||||
} catch (e) {
|
||||
box.innerHTML = `<div class="empty-state"><div class="empty-state-title">Could not load runs</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function orchRun() {
|
||||
const goal = document.getElementById('orchGoal').value.trim();
|
||||
const btn = document.getElementById('orchRunBtn');
|
||||
if (!goal) { alert('Enter a project goal first.'); return; }
|
||||
const angles = {};
|
||||
const oc = document.getElementById('orchOpencode').value.trim();
|
||||
const gm = document.getElementById('orchGemini').value.trim();
|
||||
const hm = document.getElementById('orchHermes').value.trim();
|
||||
if (oc) angles.opencode = oc;
|
||||
if (gm) angles.gemini = gm;
|
||||
if (hm) angles.hermes = hm;
|
||||
btn.disabled = true; btn.textContent = 'Running agents…';
|
||||
const res = document.getElementById('orchResult');
|
||||
res.innerHTML = '<div class="loading"><div class="loading-spinner"></div><span>Fanning out agents & converging…</span></div>';
|
||||
try {
|
||||
const data = await api.orchestrate({ goal, angles: Object.keys(angles).length ? angles : undefined });
|
||||
const cards = (data.outputs && Object.entries(data.outputs) || [])
|
||||
.map(([a, o]) => `
|
||||
<div class="card mb-2">
|
||||
<div class="flex items-center gap-2 mb-1"><span class="badge badge-accent" style="font-size:10px">${escapeHtml(a)}</span><span style="font-size:11px;color:var(--text-muted)">angle output</span></div>
|
||||
<div style="font-size:12px;white-space:pre-wrap;max-height:240px;overflow:auto">${escapeHtml((o||'').slice(0,2000))}</div>
|
||||
</div>`).join('');
|
||||
res.innerHTML = `
|
||||
<div class="card mb-3" style="border-left:4px solid var(--accent)">
|
||||
<div class="page-subtitle">✅ Synthesis (via ${escapeHtml(data.converge_agent||'')})</div>
|
||||
<div style="font-size:13px;white-space:pre-wrap">${escapeHtml(data.synthesis || '(no synthesis)')}</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:6px">run_id: ${escapeHtml(data.run_id||'')} · saved to ${escapeHtml(data.run_file||'')}</div>
|
||||
</div>
|
||||
<details><summary style="cursor:pointer;font-size:12px;color:var(--text-muted)">Show per-agent angle outputs</summary>
|
||||
<div class="mt-2">${cards}</div>
|
||||
</details>`;
|
||||
await orchLoadRuns();
|
||||
} catch (e) {
|
||||
res.innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Orchestration failed</div><div class="empty-state-desc">${escapeHtml(e.message)}</div></div>`;
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '🚀 Launch agents';
|
||||
}
|
||||
}
|
||||
|
|
@ -119,6 +119,7 @@ const PAGE_TITLES = {
|
|||
cost: { title: 'Cost Analytics', breadcrumb: 'Usage & spending' },
|
||||
'agent-insights': { title: 'Agent Insights', breadcrumb: 'AI usage analytics' },
|
||||
'brain-search': { title: 'Brain Search', breadcrumb: 'Unified knowledge index' },
|
||||
'orchestration': { title: 'Multi-Agent Run', breadcrumb: 'Fan-out + converge agents' },
|
||||
plugins: { title: 'Plugin Registry', breadcrumb: 'Manage plugins' },
|
||||
backups: { title: 'Backups', breadcrumb: 'Disaster recovery' },
|
||||
prompts: { title: 'Prompt Library', breadcrumb: 'Reusable templates' },
|
||||
|
|
|
|||
166
server.py
166
server.py
|
|
@ -20,6 +20,7 @@ import uuid
|
|||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
|
@ -92,6 +93,13 @@ class SkillRunRequest(BaseModel):
|
|||
input: Optional[str] = ""
|
||||
agent: Optional[str] = "auto"
|
||||
|
||||
class OrchestrateRequest(BaseModel):
|
||||
goal: str
|
||||
angles: Optional[dict] = None # {agent: custom_angle_prompt}
|
||||
agents: Optional[list] = None # override default [opencode, gemini, hermes]
|
||||
converge_agent: Optional[str] = "hermes"
|
||||
converge_prompt: Optional[str] = None # custom synthesis instruction
|
||||
|
||||
class SkillCreate(BaseModel):
|
||||
name: str
|
||||
skill_md: str = ""
|
||||
|
|
@ -915,6 +923,164 @@ def run_skill(name: str, req: Optional[SkillRunRequest] = None):
|
|||
"message": f"Skill '{name}' completed via {agent_choice}",
|
||||
}
|
||||
|
||||
|
||||
# ─── Routes: Multi-Agent Orchestration (fan-out + converge) ─────────
|
||||
ORCHESTRATION_DIR = BASE_DIR / "data" / "orchestration-runs"
|
||||
|
||||
def _run_angle(agent: str, angle_prompt: str):
|
||||
"""Run a single agent on its angle. Returns (agent, output, error)."""
|
||||
try:
|
||||
out = execute_agent(agent, angle_prompt)
|
||||
return agent, out, None
|
||||
except Exception as e:
|
||||
return agent, f"⚠ Error from {agent}: {e}", str(e)
|
||||
|
||||
@app.post("/api/orchestrate")
|
||||
def orchestrate_multi_agent(req: OrchestrateRequest):
|
||||
"""Spin up multiple agents on the same project goal from different angles,
|
||||
then converge their outputs into a single synthesis.
|
||||
|
||||
Default angles (one per built-in agent):
|
||||
- opencode -> implementation plan / code for the goal
|
||||
- gemini -> research, analysis, risk/feasibility
|
||||
- hermes -> coordination, memory, integration plan
|
||||
A convergence step feeds all angle outputs to `converge_agent`
|
||||
(default hermes) to produce the unified result.
|
||||
"""
|
||||
if not req.goal or not req.goal.strip():
|
||||
raise HTTPException(422, "goal is required")
|
||||
|
||||
agents = req.agents or ["opencode", "gemini", "hermes"]
|
||||
# Validate agents exist in the registry
|
||||
registry = load_agent_registry()
|
||||
unknown = [a for a in agents if a not in registry]
|
||||
if unknown:
|
||||
raise HTTPException(400, f"unknown agent(s): {unknown}")
|
||||
|
||||
# Default angle prompts per role
|
||||
default_angles = {
|
||||
"opencode": (
|
||||
f"PROJECT GOAL: {req.goal}\n\n"
|
||||
"You are the IMPLEMENTATION agent. Produce a concrete implementation "
|
||||
"approach: architecture, files to create/modify, key functions, and a "
|
||||
"step-by-step build plan. Be specific and actionable. Do not do the "
|
||||
"other agents' jobs — focus on engineering."
|
||||
),
|
||||
"gemini": (
|
||||
f"PROJECT GOAL: {req.goal}\n\n"
|
||||
"You are the RESEARCH / ANALYSIS agent. Investigate the problem space: "
|
||||
"relevant approaches, trade-offs, risks, feasibility, and any external "
|
||||
"facts or references. Do not write implementation code — focus on "
|
||||
"analysis and evidence."
|
||||
),
|
||||
"hermes": (
|
||||
f"PROJECT GOAL: {req.goal}\n\n"
|
||||
"You are the COORDINATION / MEMORY agent. Define how the work should be "
|
||||
"sequenced, what shared state/memory is needed, how the implementation "
|
||||
"and research results integrate, and any scheduling/coordination plan. "
|
||||
"Focus on orchestration, not on coding or research yourself."
|
||||
),
|
||||
}
|
||||
angle_prompts = {}
|
||||
for a in agents:
|
||||
if req.angles and a in req.angles and req.angles[a]:
|
||||
angle_prompts[a] = req.angles[a]
|
||||
else:
|
||||
angle_prompts[a] = default_angles.get(a, f"PROJECT GOAL: {req.goal}\n\nAddress this goal from your perspective as {a}.")
|
||||
|
||||
# Fan-out: run all angles in parallel
|
||||
outputs = {}
|
||||
with ThreadPoolExecutor(max_workers=len(agents)) as ex:
|
||||
futures = {ex.submit(_run_angle, a, angle_prompts[a]): a for a in agents}
|
||||
for fut in futures:
|
||||
a = futures[fut]
|
||||
_ag, out, _err = fut.result()
|
||||
outputs[a] = out
|
||||
|
||||
# Converge
|
||||
converge = req.converge_agent or "hermes"
|
||||
if converge not in registry:
|
||||
converge = agents[0]
|
||||
angle_block = "\n\n".join(
|
||||
f"### {a.upper()} ANGLE OUTPUT:\n{outputs[a]}" for a in agents
|
||||
)
|
||||
synthesis_prompt = req.converge_prompt or (
|
||||
f"PROJECT GOAL: {req.goal}\n\n"
|
||||
"You are the CONVERGENCE agent. Below are the outputs of multiple agents "
|
||||
"who each tackled the same project goal from a different angle. Synthesize "
|
||||
"them into ONE coherent plan/result:\n"
|
||||
"- Resolve contradictions between angles.\n"
|
||||
"- Produce a unified, prioritized action plan (or final answer).\n"
|
||||
"- Call out open risks and the single next step.\n\n"
|
||||
f"{angle_block}"
|
||||
)
|
||||
synthesis, conv_err = _run_angle(converge, synthesis_prompt)
|
||||
|
||||
# Persist the run
|
||||
run_id = uuid.uuid4().hex[:12]
|
||||
timestamp = get_timestamp()
|
||||
run_record = {
|
||||
"run_id": run_id,
|
||||
"timestamp": timestamp,
|
||||
"goal": req.goal,
|
||||
"agents": agents,
|
||||
"angles": angle_prompts,
|
||||
"outputs": outputs,
|
||||
"converge_agent": converge,
|
||||
"synthesis": synthesis,
|
||||
"converge_error": conv_err,
|
||||
}
|
||||
ORCHESTRATION_DIR.mkdir(parents=True, exist_ok=True)
|
||||
run_file = ORCHESTRATION_DIR / f"{run_id}.json"
|
||||
write_json(run_file, run_record)
|
||||
|
||||
# Make the run searchable in the brain
|
||||
try:
|
||||
bi = _brain_index_module()
|
||||
conn = bi.get_conn()
|
||||
bi.upsert_doc(
|
||||
conn, source="agent-note",
|
||||
source_path=f"data/orchestration-runs/{run_id}.json",
|
||||
title=f"Multi-agent run: {req.goal[:80]}",
|
||||
content=f"GOAL: {req.goal}\n\nSYNTHESIS:\n{synthesis[:2000]}",
|
||||
agent=f"orchestrate/{converge}", updated_at=timestamp,
|
||||
)
|
||||
conn.commit(); conn.close()
|
||||
except Exception as e:
|
||||
print(f"[orchestrate] brain upsert failed: {e}")
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"run_id": run_id,
|
||||
"goal": req.goal,
|
||||
"agents": agents,
|
||||
"outputs": outputs,
|
||||
"converge_agent": converge,
|
||||
"synthesis": synthesis,
|
||||
"converge_error": conv_err,
|
||||
"run_file": str(run_file.relative_to(BASE_DIR)),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/orchestrate/runs")
|
||||
def list_orchestration_runs(limit: int = 50):
|
||||
ORCHESTRATION_DIR.mkdir(parents=True, exist_ok=True)
|
||||
files = sorted(ORCHESTRATION_DIR.glob("*.json"), reverse=True)[:limit]
|
||||
runs = []
|
||||
for f in files:
|
||||
try:
|
||||
d = read_json(f, default={})
|
||||
runs.append({
|
||||
"run_id": d.get("run_id"),
|
||||
"timestamp": d.get("timestamp"),
|
||||
"goal": d.get("goal"),
|
||||
"agents": d.get("agents"),
|
||||
"converge_agent": d.get("converge_agent"),
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
return {"runs": runs}
|
||||
|
||||
@app.get("/api/skills/{name}/eval")
|
||||
def get_skill_eval(name: str):
|
||||
path = resolve_skill_dir(name) / "score-history.json"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
name: multi-agent-run
|
||||
description: Spin up multiple agents (opencode, Gemini, Hermes) on the same project goal from different angles, then converge their outputs into one synthesis. Use for parallel multi-perspective problem solving, design, or planning on a shared project.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Multi-Agent Run — Fan-out + Converge
|
||||
|
||||
Orchestrate the 3 built-in agents on ONE project goal, each from its own angle,
|
||||
then merge their outputs into a single coherent result.
|
||||
|
||||
## When to use
|
||||
- A project goal benefits from multiple perspectives at once (implementation +
|
||||
research + coordination).
|
||||
- You want a synthesized plan that reconciles engineering, analysis, and ops.
|
||||
- You want the run persisted and searchable in the brain for later review.
|
||||
|
||||
## How it works (backend)
|
||||
`POST /api/orchestrate`:
|
||||
1. **Fan-out** — runs each agent in parallel on its angle:
|
||||
- `opencode` → implementation approach / build plan
|
||||
- `gemini` → research, analysis, risks, feasibility
|
||||
- `hermes` → coordination, sequencing, integration, memory
|
||||
2. **Converge** — feeds all angle outputs to `converge_agent` (default `hermes`)
|
||||
to produce a unified, prioritized action plan (or final answer).
|
||||
3. **Persist** — writes `data/orchestration-runs/<run_id>.json` and upserts a
|
||||
searchable doc into the brain.
|
||||
|
||||
## Request shape
|
||||
```
|
||||
{
|
||||
"goal": "Build a real-time sync layer between the dashboard and the brain index",
|
||||
"angles": { // optional per-agent override
|
||||
"opencode": "Focus on the websocket protocol and schema migration",
|
||||
"gemini": "Survey existing real-time sync patterns and their failure modes"
|
||||
},
|
||||
"agents": ["opencode", "gemini", "hermes"], // optional override
|
||||
"converge_agent": "hermes", // optional
|
||||
"converge_prompt": "..." // optional custom synthesis
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
```
|
||||
{
|
||||
"status": "completed",
|
||||
"run_id": "...",
|
||||
"outputs": { "opencode": "...", "gemini": "...", "hermes": "..." },
|
||||
"synthesis": "...", // the converged result
|
||||
"run_file": "data/orchestration-runs/<run_id>.json"
|
||||
}
|
||||
```
|
||||
|
||||
## Notes / caveats
|
||||
- Agents run in parallel, so a run takes ~1 agent-round-trip (not 3x).
|
||||
- If an angle agent errors, its output is captured as an error string and the
|
||||
convergence step still proceeds (best-effort, never aborts the whole run).
|
||||
- Review past runs via `GET /api/orchestrate/runs` or the dashboard Orchestration page.
|
||||
- The converged synthesis is auto-upserted to the brain (source: agent-note) so
|
||||
you can later find it with Brain Search.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
name: multi-angle-orchestration
|
||||
description: Spin up 5+ parallel subagents (delegate_task leaf workers, all on this same Hermes session — NO external/separate AIs) on one project goal from different angles, then converge. Single-source orchestration for fast deployment.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Multi-Angle Orchestration (single-source, fast)
|
||||
|
||||
When the user gives an end goal and says "call up some subagents," this is the
|
||||
playbook. All work is done by THIS Hermes session's own `delegate_task`
|
||||
subagents — there is NO opencode / gemini / hermes CLI invocation, no external
|
||||
agent, no second AI. Fast deployment: parallel fan-out, then converge.
|
||||
|
||||
## Hard rules
|
||||
- **≥5 subagents, always.** Decompose the goal into at least 5 distinct angles.
|
||||
- **All same source.** Every subagent is a leaf worker on this session's model.
|
||||
Never call execute_agent / the 3 CLI agents / a separate AI.
|
||||
- **Parallel.** Launch in one background batch where possible. The tool runs
|
||||
up to 3 concurrently for this user, so issue 5+ as: first batch of 3, then a
|
||||
second batch of the remaining (still effectively parallel, fast).
|
||||
- **No human-in-the-loop inside workers.** Subagents can't clarify; give them
|
||||
complete context up front.
|
||||
- **I converge.** When all subagents return, I synthesize their outputs into
|
||||
one result (no separate synthesizer agent needed unless the goal is huge).
|
||||
|
||||
## The flow
|
||||
1. **Decompose** the goal into ≥5 angles. Good angle variety (pick per goal):
|
||||
- Implementer (what to build / change)
|
||||
- Researcher / evidence (facts, refs, prior art)
|
||||
- Skeptic / risk (failure modes, contradictions, what could break)
|
||||
- Integrator (how it fits the existing system / repo)
|
||||
- Communicator / docs (how to explain, UI copy, README, rollout)
|
||||
- (extra) Tester / QA, Security, Performance, Ops/Deploy
|
||||
2. **Launch** each angle as a `delegate_task` with FULL context:
|
||||
- The goal, verbatim.
|
||||
- Its specific angle + acceptance criteria.
|
||||
- Relevant repo context (paths, constraints, the established patterns).
|
||||
- Instruction: return a concise, self-contained result; do not ask questions.
|
||||
3. **Converge**: reconcile contradictions, produce a unified prioritized plan
|
||||
or final answer + the single next step.
|
||||
4. **Log the run**: write `data/orchestration-runs/<run_id>.json` with
|
||||
{goal, angles, agents:["delegate-x5"], synthesis, timestamp}. Then
|
||||
`python3 brain-cli.py ingest` so it's searchable in Brain Search (the
|
||||
orchestration source is indexed). Optionally surface results on the
|
||||
Orchestration dashboard page.
|
||||
|
||||
## delegate_task call shape (per angle)
|
||||
```
|
||||
delegate_task(
|
||||
goal="<angle-specific deliverable>",
|
||||
context="""GOAL: <full user goal>
|
||||
ANGLE: <this worker's perspective + acceptance criteria>
|
||||
CONTEXT: <repo paths, constraints, prior decisions>
|
||||
OUTPUT: concise, self-contained. No questions.""",
|
||||
)
|
||||
```
|
||||
Batch up to 3 in one `tasks=[...]` call; fire the rest in a second call.
|
||||
|
||||
## Notes
|
||||
- Subagent summaries are self-reports; verify external side-effects (writes,
|
||||
publishes) yourself before claiming success.
|
||||
- Keep angle prompts tight — workers get clean isolated context, no shared
|
||||
state, so everything they need must be in `context`.
|
||||
- This is the PRIMARY orchestration workflow. The Agentic OS `/api/orchestrate`
|
||||
backend endpoint (which fans out the 3 CLI agents) is an OPTIONAL headless
|
||||
alternative only — not used for this single-source workflow.
|
||||
Loading…
Reference in New Issue