feat: initial bootstrap — structure, task_brief, blackboard, adapter bases, escalation, prompts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 02:19:14 -04:00
commit eaf7fd8f6f
33 changed files with 2141 additions and 0 deletions

0
core/__init__.py Normal file
View File

369
core/blackboard.py Normal file
View File

@@ -0,0 +1,369 @@
"""
core/blackboard.py
SQLite-backed shared state store for a single orchestration run.
One database is created at: runs/<run_id>/blackboard.db
All methods are synchronous and thread-safe at the connection level
(SQLite WAL mode + check_same_thread=False with an explicit lock).
"""
from __future__ import annotations
import json
import os
import sqlite3
import threading
import uuid
from datetime import datetime, timezone
from typing import Any, Optional
# ---------------------------------------------------------------------------
# Import TaskBrief only for type hints to avoid circular imports at runtime.
# ---------------------------------------------------------------------------
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.task_brief import TaskBrief
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _new_uuid() -> str:
return str(uuid.uuid4())
# ---------------------------------------------------------------------------
# SQL schema
# ---------------------------------------------------------------------------
_SCHEMA = """
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY,
goal TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending|active|review|done|failed
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS workstreams (
workstream_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
name TEXT NOT NULL,
tier INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending|active|blocked|done|failed
owner_agent_id TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES runs(run_id)
);
CREATE TABLE IF NOT EXISTS briefs (
brief_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
parent_brief_id TEXT,
workstream_id TEXT,
tier INTEGER NOT NULL,
role TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending|active|done|failed
payload TEXT, -- JSON-serialised TaskBrief.to_dict()
result TEXT, -- JSON result from the agent
retry_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES runs(run_id)
);
CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
brief_id TEXT, -- NULL for run-level events
kind TEXT NOT NULL, -- spawned|completed|failed|escalated|retried
detail TEXT, -- JSON
created_at TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES runs(run_id)
);
"""
# Valid status values per table — used for input validation.
_RUN_STATUSES = {"pending", "active", "review", "done", "failed"}
_WS_STATUSES = {"pending", "active", "blocked", "done", "failed"}
_BRIEF_STATUSES = {"pending", "active", "done", "failed"}
_EVENT_KINDS = {"spawned", "completed", "failed", "escalated", "retried"}
# ---------------------------------------------------------------------------
# Blackboard
# ---------------------------------------------------------------------------
class Blackboard:
"""
Shared, persistent state store for one orchestration run.
Usage
-----
bb = Blackboard(run_id="abc123")
bb.create_run(goal="Build webhook ingestion system")
...
summary = bb.get_run_summary()
"""
def __init__(self, run_id: str) -> None:
self.run_id = run_id
self._run_dir = os.path.join("runs", run_id)
self._db_path = os.path.join(self._run_dir, "blackboard.db")
self._lock = threading.Lock()
# Ensure the run directory exists.
os.makedirs(self._run_dir, exist_ok=True)
# Open a persistent connection (thread-safe via explicit lock).
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
self._conn.row_factory = sqlite3.Row
# Initialise schema.
with self._lock:
self._conn.executescript(_SCHEMA)
self._conn.commit()
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _execute(
self,
sql: str,
params: tuple[Any, ...] = (),
*,
commit: bool = False,
) -> sqlite3.Cursor:
with self._lock:
cur = self._conn.execute(sql, params)
if commit:
self._conn.commit()
return cur
def _executemany(
self,
sql: str,
params_seq: list[tuple[Any, ...]],
*,
commit: bool = False,
) -> None:
with self._lock:
self._conn.executemany(sql, params_seq)
if commit:
self._conn.commit()
# ------------------------------------------------------------------
# Run
# ------------------------------------------------------------------
def create_run(self, goal: str) -> None:
"""Insert a new run row. Status defaults to 'pending'."""
now = _now_iso()
self._execute(
"INSERT OR IGNORE INTO runs (run_id, goal, status, created_at, updated_at) "
"VALUES (?, ?, 'pending', ?, ?)",
(self.run_id, goal, now, now),
commit=True,
)
def update_run_status(self, status: str) -> None:
"""Update run status. Must be one of: pending|active|review|done|failed."""
if status not in _RUN_STATUSES:
raise ValueError(f"Invalid run status {status!r}. Must be one of {_RUN_STATUSES}.")
now = _now_iso()
self._execute(
"UPDATE runs SET status=?, updated_at=? WHERE run_id=?",
(status, now, self.run_id),
commit=True,
)
# ------------------------------------------------------------------
# Workstreams
# ------------------------------------------------------------------
def create_workstream(
self,
*,
workstream_id: Optional[str] = None,
name: str,
tier: int,
owner_agent_id: Optional[str] = None,
) -> str:
"""Create a workstream row and return its workstream_id."""
ws_id = workstream_id or _new_uuid()
now = _now_iso()
self._execute(
"INSERT OR IGNORE INTO workstreams "
"(workstream_id, run_id, name, tier, status, owner_agent_id, created_at, updated_at) "
"VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)",
(ws_id, self.run_id, name, tier, owner_agent_id, now, now),
commit=True,
)
return ws_id
def update_workstream_status(self, workstream_id: str, status: str) -> None:
"""Update workstream status. Must be one of: pending|active|blocked|done|failed."""
if status not in _WS_STATUSES:
raise ValueError(f"Invalid workstream status {status!r}. Must be one of {_WS_STATUSES}.")
now = _now_iso()
self._execute(
"UPDATE workstreams SET status=?, updated_at=? WHERE workstream_id=?",
(status, now, workstream_id),
commit=True,
)
# ------------------------------------------------------------------
# Briefs
# ------------------------------------------------------------------
def create_brief(self, brief: "TaskBrief", workstream_id: Optional[str] = None) -> None:
"""Persist a TaskBrief. The full brief is stored as JSON in `payload`."""
now = _now_iso()
payload_json = json.dumps(brief.to_dict())
self._execute(
"INSERT OR IGNORE INTO briefs "
"(brief_id, run_id, parent_brief_id, workstream_id, tier, role, "
" status, payload, result, retry_count, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, NULL, ?, ?, ?)",
(
brief.brief_id,
self.run_id,
brief.parent_brief_id,
workstream_id,
brief.tier,
brief.role,
payload_json,
brief.retry_count,
now,
now,
),
commit=True,
)
def update_brief_status(self, brief_id: str, status: str) -> None:
"""Update brief status. Must be one of: pending|active|done|failed."""
if status not in _BRIEF_STATUSES:
raise ValueError(f"Invalid brief status {status!r}. Must be one of {_BRIEF_STATUSES}.")
now = _now_iso()
self._execute(
"UPDATE briefs SET status=?, updated_at=? WHERE brief_id=?",
(status, now, brief_id),
commit=True,
)
def update_brief_result(self, brief_id: str, result: dict[str, Any]) -> None:
"""Store the agent result JSON for a brief and mark it done."""
now = _now_iso()
result_json = json.dumps(result)
self._execute(
"UPDATE briefs SET result=?, status='done', updated_at=? WHERE brief_id=?",
(result_json, now, brief_id),
commit=True,
)
def increment_brief_retry(self, brief_id: str) -> None:
"""Bump the retry_count column for a brief."""
now = _now_iso()
self._execute(
"UPDATE briefs SET retry_count = retry_count + 1, updated_at=? WHERE brief_id=?",
(now, brief_id),
commit=True,
)
# ------------------------------------------------------------------
# Events
# ------------------------------------------------------------------
def log_event(
self,
kind: str,
brief_id: Optional[str] = None,
detail: Optional[dict[str, Any]] = None,
) -> str:
"""
Append an event to the events table.
Parameters
----------
kind : One of spawned|completed|failed|escalated|retried.
brief_id : Associated brief, or None for run-level events.
detail : Arbitrary JSON-serialisable dict.
Returns the new event_id.
"""
if kind not in _EVENT_KINDS:
raise ValueError(f"Invalid event kind {kind!r}. Must be one of {_EVENT_KINDS}.")
event_id = _new_uuid()
now = _now_iso()
detail_json = json.dumps(detail or {})
self._execute(
"INSERT INTO events (event_id, run_id, brief_id, kind, detail, created_at) "
"VALUES (?, ?, ?, ?, ?, ?)",
(event_id, self.run_id, brief_id, kind, detail_json, now),
commit=True,
)
return event_id
# ------------------------------------------------------------------
# Summary
# ------------------------------------------------------------------
def get_run_summary(self) -> dict[str, Any]:
"""
Return a snapshot of the run state including workstream and brief
counts broken down by status.
"""
# Run row
run_row = self._execute(
"SELECT * FROM runs WHERE run_id=?", (self.run_id,)
).fetchone()
if run_row is None:
return {"error": f"No run found for run_id={self.run_id!r}"}
run_data: dict[str, Any] = dict(run_row)
# Workstream status counts
ws_rows = self._execute(
"SELECT status, COUNT(*) AS cnt FROM workstreams WHERE run_id=? GROUP BY status",
(self.run_id,),
).fetchall()
run_data["workstreams"] = {r["status"]: r["cnt"] for r in ws_rows}
# Brief status counts
brief_rows = self._execute(
"SELECT status, COUNT(*) AS cnt FROM briefs WHERE run_id=? GROUP BY status",
(self.run_id,),
).fetchall()
run_data["briefs"] = {r["status"]: r["cnt"] for r in brief_rows}
# Event kind counts
event_rows = self._execute(
"SELECT kind, COUNT(*) AS cnt FROM events WHERE run_id=? GROUP BY kind",
(self.run_id,),
).fetchall()
run_data["events"] = {r["kind"]: r["cnt"] for r in event_rows}
return run_data
# ------------------------------------------------------------------
# Cleanup
# ------------------------------------------------------------------
def close(self) -> None:
"""Close the database connection gracefully."""
with self._lock:
self._conn.close()
def __repr__(self) -> str:
return f"Blackboard(run_id={self.run_id!r}, db={self._db_path!r})"

245
core/escalation.py Normal file
View File

@@ -0,0 +1,245 @@
"""
core/escalation.py
Failure classification and escalation logic for the-agency pipeline.
When an agent returns a result the EscalationHandler decides what to do next:
retry, escalate, salvage-and-retry, or mark complete.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional
from core.task_brief import TaskBrief
# ---------------------------------------------------------------------------
# FailureType
# ---------------------------------------------------------------------------
class FailureType(str, Enum):
"""Classification of an agent result."""
SUCCESS = "success"
"""Agent completed the task and the output looks valid."""
BAD_OUTPUT = "bad_output"
"""Agent returned output that fails acceptance criteria or is malformed."""
PARTIAL = "partial"
"""Agent completed some but not all of the required work."""
BLOCKED = "blocked"
"""Agent could not proceed due to a dependency or missing information."""
# ---------------------------------------------------------------------------
# EscalationResult
# ---------------------------------------------------------------------------
@dataclass
class EscalationResult:
"""
The decision produced by EscalationHandler.handle().
Attributes
----------
action : What the runner should do next.
"retry" — resubmit amended_brief as-is.
"escalate" — pass the failure up to the parent tier.
"salvage_and_retry" — use salvaged_result as partial context
and resubmit amended_brief.
"complete" — accept the result and move on.
amended_brief : A (possibly modified) TaskBrief to re-submit.
None when action == "complete" or "escalate".
salvaged_result : Partial output worth keeping from a PARTIAL result.
None unless action == "salvage_and_retry".
reason : Human-readable explanation of the decision.
"""
action: str # "retry" | "escalate" | "salvage_and_retry" | "complete"
amended_brief: Optional[TaskBrief] = None
salvaged_result: Optional[dict[str, Any]] = None
reason: str = ""
def __post_init__(self) -> None:
valid_actions = {"retry", "escalate", "salvage_and_retry", "complete"}
if self.action not in valid_actions:
raise ValueError(f"Invalid action {self.action!r}. Must be one of {valid_actions}.")
# ---------------------------------------------------------------------------
# EscalationHandler
# ---------------------------------------------------------------------------
class EscalationHandler:
"""
Stateless handler that classifies agent results and produces escalation
decisions.
Usage
-----
handler = EscalationHandler()
failure_type = handler.classify_result(result)
decision = handler.handle(brief, result)
"""
# ------------------------------------------------------------------
# Classification
# ------------------------------------------------------------------
def classify_result(self, result: dict[str, Any]) -> FailureType:
"""
Inspect the result dict returned by an agent and return a FailureType.
The result dict is expected to contain at minimum a "status" key.
Recognised status values:
"done" / "passed" → SUCCESS
"partial" → PARTIAL
"blocked" → BLOCKED
"failed" → BAD_OUTPUT (default for unknown statuses too)
The handler also inspects an optional "blocking_issues" or "blockers"
key: if present and non-empty the result is treated as BLOCKED even if
status says "failed".
"""
status = str(result.get("status", "")).lower().strip()
# Explicit blocked signals
if status == "blocked":
return FailureType.BLOCKED
# Check for blocking_issues / blockers fields
blocking_issues: list = result.get("blocking_issues") or result.get("blockers") or []
if blocking_issues:
return FailureType.BLOCKED
if status in ("done", "passed"):
return FailureType.SUCCESS
if status == "partial":
return FailureType.PARTIAL
# "failed" or anything unrecognised
return FailureType.BAD_OUTPUT
# ------------------------------------------------------------------
# Decision
# ------------------------------------------------------------------
def handle(self, brief: TaskBrief, result: dict[str, Any]) -> EscalationResult:
"""
Decide what to do with an agent result.
Rules
-----
BLOCKED → escalate immediately (no retries for blocked states)
BAD_OUTPUT + retries remaining → retry with amended brief
(retry_count incremented)
BAD_OUTPUT + retries exhausted → escalate
PARTIAL → salvage the good parts and retry with amended brief
SUCCESS → complete
Parameters
----------
brief : The TaskBrief that was executed.
result : The dict returned by the agent runtime.
Returns
-------
An EscalationResult with the recommended action.
"""
failure_type = self.classify_result(result)
# ---- SUCCESS ---------------------------------------------------
if failure_type == FailureType.SUCCESS:
return EscalationResult(
action="complete",
reason="Agent reported success; acceptance criteria satisfied.",
)
# ---- BLOCKED ---------------------------------------------------
if failure_type == FailureType.BLOCKED:
blocking_issues = (
result.get("blocking_issues")
or result.get("blockers")
or [result.get("reason", "unspecified blocker")]
)
return EscalationResult(
action="escalate",
reason=(
f"Agent is blocked and cannot proceed. "
f"Blockers: {blocking_issues}"
),
)
# ---- PARTIAL ---------------------------------------------------
if failure_type == FailureType.PARTIAL:
salvaged: dict[str, Any] = {}
if "completed" in result:
salvaged["completed"] = result["completed"]
if "artifacts" in result:
salvaged["artifacts"] = result["artifacts"]
amended = self._amend_brief_for_retry(brief, result, failure_type)
return EscalationResult(
action="salvage_and_retry",
amended_brief=amended,
salvaged_result=salvaged or result,
reason=(
"Agent returned partial output. "
"Salvaging completed artifacts and retrying remaining work."
),
)
# ---- BAD_OUTPUT ------------------------------------------------
# failure_type == FailureType.BAD_OUTPUT
retries_remaining = brief.retry_budget - brief.retry_count
if retries_remaining > 0:
amended = self._amend_brief_for_retry(brief, result, failure_type)
return EscalationResult(
action="retry",
amended_brief=amended,
reason=(
f"Agent returned bad output. "
f"Retrying (attempt {amended.retry_count}/{amended.retry_budget})."
),
)
else:
return EscalationResult(
action="escalate",
reason=(
f"Agent returned bad output and retry budget "
f"({brief.retry_budget}) is exhausted. Escalating."
),
)
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _amend_brief_for_retry(
self,
brief: TaskBrief,
result: dict[str, Any],
failure_type: FailureType,
) -> TaskBrief:
"""
Return a copy of the brief with retry_count incremented and
previous-failure context injected into the context dict.
"""
import copy
amended = copy.deepcopy(brief)
amended.retry_count += 1
# Inject failure context so the agent knows what went wrong.
amended.context["_previous_failure"] = {
"failure_type": failure_type.value,
"result": result,
"attempt": amended.retry_count,
}
return amended

213
core/task_brief.py Normal file
View File

@@ -0,0 +1,213 @@
"""
core/task_brief.py
Dataclass-based schema for a TaskBrief — the fundamental unit of work passed
between tiers in the-agency pipeline.
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Optional
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _now_iso() -> str:
"""Return the current UTC time as an ISO-8601 string."""
return datetime.now(timezone.utc).isoformat()
def _new_uuid() -> str:
return str(uuid.uuid4())
# ---------------------------------------------------------------------------
# TaskBrief
# ---------------------------------------------------------------------------
@dataclass
class TaskBrief:
"""
Immutable-intent carrier that travels down (and sometimes back up) the
T1→T5 tier hierarchy.
Fields
------
brief_id : Unique identifier for this brief (UUID).
run_id : Identifier for the top-level orchestration run.
parent_brief_id : brief_id of the parent, None for the T1 root brief.
tier : Tier level 15.
role : Role key used to look up an agent personality file.
goal_anchor : High-level goal set by T1; NEVER mutated by child tiers.
workstream : Logical workstream name (e.g. "backend", "infra").
task : Concrete description of what this brief asks for.
acceptance_criteria : Ordered list of pass/fail criteria.
constraints : Hard constraints the agent must respect.
context : Arbitrary key/value context bag.
retry_budget : Maximum number of retry attempts allowed.
retry_count : How many retries have been consumed so far.
preferred_runtime : Execution runtime hint ("standard" | "coding_agent").
agent_personality : Optional path to an agent .md persona file.
created_at : ISO-8601 creation timestamp (UTC).
"""
# Identity
brief_id: str = field(default_factory=_new_uuid)
run_id: str = field(default_factory=_new_uuid)
parent_brief_id: Optional[str] = None
# Tier / role
tier: int = 1
role: str = ""
# Goal — set once by T1, propagated unchanged
goal_anchor: str = ""
# Work description
workstream: str = ""
task: str = ""
acceptance_criteria: list[str] = field(default_factory=list)
constraints: list[str] = field(default_factory=list)
context: dict[str, Any] = field(default_factory=dict)
# Retry tracking
retry_budget: int = 3
retry_count: int = 0
# Runtime / persona
preferred_runtime: str = "standard" # "standard" | "coding_agent"
agent_personality: Optional[str] = None # path to .md file
# Metadata
created_at: str = field(default_factory=_now_iso)
# ------------------------------------------------------------------
# Validation
# ------------------------------------------------------------------
def validate(self) -> None:
"""
Raise ValueError if any required field is missing or out of range.
Call this before handing a brief to a runner or storing it.
"""
errors: list[str] = []
if not self.brief_id:
errors.append("brief_id must not be empty")
if not self.run_id:
errors.append("run_id must not be empty")
if self.tier not in range(1, 6):
errors.append(f"tier must be 15, got {self.tier!r}")
if not self.role:
errors.append("role must not be empty")
if not self.goal_anchor:
errors.append("goal_anchor must not be empty")
if not self.task:
errors.append("task must not be empty")
if self.retry_budget < 0:
errors.append(f"retry_budget must be >= 0, got {self.retry_budget}")
if self.retry_count < 0:
errors.append(f"retry_count must be >= 0, got {self.retry_count}")
if self.retry_count > self.retry_budget:
errors.append(
f"retry_count ({self.retry_count}) exceeds retry_budget ({self.retry_budget})"
)
if self.preferred_runtime not in ("standard", "coding_agent"):
errors.append(
f"preferred_runtime must be 'standard' or 'coding_agent', got {self.preferred_runtime!r}"
)
if errors:
raise ValueError("TaskBrief validation failed:\n" + "\n".join(f" - {e}" for e in errors))
# ------------------------------------------------------------------
# Serialisation
# ------------------------------------------------------------------
def to_dict(self) -> dict[str, Any]:
"""Serialise to a plain Python dict (JSON-safe)."""
return {
"brief_id": self.brief_id,
"run_id": self.run_id,
"parent_brief_id": self.parent_brief_id,
"tier": self.tier,
"role": self.role,
"goal_anchor": self.goal_anchor,
"workstream": self.workstream,
"task": self.task,
"acceptance_criteria": list(self.acceptance_criteria),
"constraints": list(self.constraints),
"context": dict(self.context),
"retry_budget": self.retry_budget,
"retry_count": self.retry_count,
"preferred_runtime": self.preferred_runtime,
"agent_personality": self.agent_personality,
"created_at": self.created_at,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "TaskBrief":
"""Deserialise from a plain dict. Unknown keys are silently ignored."""
known_fields = {f.name for f in cls.__dataclass_fields__.values()} # type: ignore[attr-defined]
filtered = {k: v for k, v in data.items() if k in known_fields}
return cls(**filtered)
# ------------------------------------------------------------------
# Factory: child brief
# ------------------------------------------------------------------
def make_child_brief(
self,
*,
tier: int,
role: str,
task: str,
workstream: str = "",
acceptance_criteria: Optional[list[str]] = None,
constraints: Optional[list[str]] = None,
context: Optional[dict[str, Any]] = None,
preferred_runtime: str = "standard",
agent_personality: Optional[str] = None,
retry_budget: int = 3,
) -> "TaskBrief":
"""
Create a child brief that inherits run_id and — critically —
goal_anchor verbatim from this parent.
The child's parent_brief_id is set to this brief's brief_id.
"""
return TaskBrief(
# New identity
brief_id=_new_uuid(),
run_id=self.run_id,
parent_brief_id=self.brief_id,
# Tier / role
tier=tier,
role=role,
# goal_anchor is ALWAYS copied unchanged from parent
goal_anchor=self.goal_anchor,
# Work specifics supplied by caller
workstream=workstream or self.workstream,
task=task,
acceptance_criteria=acceptance_criteria or [],
constraints=constraints or list(self.constraints),
context=context or {},
# Runtime
retry_budget=retry_budget,
retry_count=0,
preferred_runtime=preferred_runtime,
agent_personality=agent_personality,
)
# ------------------------------------------------------------------
# Repr
# ------------------------------------------------------------------
def __repr__(self) -> str:
return (
f"TaskBrief(brief_id={self.brief_id!r}, tier={self.tier}, "
f"role={self.role!r}, workstream={self.workstream!r})"
)

99
core/team_runner.py Normal file
View File

@@ -0,0 +1,99 @@
"""
core/team_runner.py
Top-level orchestration entry point — Phase 2 stub.
The TeamRunner is responsible for:
1. Loading config/team.yaml and config/role_registry.yaml.
2. Instantiating the correct adapter implementations (LLM, VCS, notify, runtime).
3. Creating a Blackboard for the run.
4. Constructing the root T1 TaskBrief and dispatching it to the T1 Visionary.
5. Recursively spawning T2→T5 briefs based on tier outputs.
6. Using EscalationHandler to manage retries, salvage, and escalation.
7. Writing final run status and summary to the Blackboard.
TODO (Phase 2):
- Load and validate team.yaml configuration.
- Build adapter registry (map adapter keys → concrete adapter classes).
- Implement tier dispatch loop: T1 → T2 (per workstream) → T3 → T4 → T5.
- Parse tier JSON outputs into child TaskBrief objects via make_child_brief().
- Integrate EscalationHandler into the dispatch loop.
- Support --dry-run flag (log actions without executing).
- Emit blackboard events at each stage (spawned, completed, failed, etc.).
- Expose a CLI entry point (argparse or click).
"""
from __future__ import annotations
# TODO (Phase 2): Uncomment and implement imports as adapters are built.
# import argparse
# import yaml
# from core.task_brief import TaskBrief
# from core.blackboard import Blackboard
# from core.escalation import EscalationHandler
# from adapters.llm.anthropic import AnthropicAdapter
# from adapters.vcs.github import GitHubAdapter
# from adapters.notify.openclaw import OpenClawNotifyAdapter
# from adapters.runtime.openclaw import OpenClawRuntimeAdapter
# from adapters.runtime.claude_code import ClaudeCodeRuntimeAdapter
class TeamRunner:
"""
Orchestrates a full T1→T5 agent pipeline run.
Usage (Phase 2)::
runner = TeamRunner(config_path="config/team.yaml")
runner.run()
"""
def __init__(self, config_path: str = "config/team.yaml") -> None:
# TODO (Phase 2): Load YAML config.
# Instantiate adapters based on config.adapters keys.
# Create a Blackboard for this run.
raise NotImplementedError("TeamRunner.__init__ is not yet implemented.")
def run(self) -> None:
"""
Execute the full pipeline from T1 decomposition through T5 verification.
TODO (Phase 2):
- Build root T1 brief from config.run.goal.
- Dispatch to T1 Visionary via LLM adapter.
- Parse workstreams from T1 output.
- For each workstream: dispatch T2 Architect.
- For each T2 subtask: dispatch T3 Squad Lead.
- For each T3 task: dispatch T4 Implementer.
- For each T4 artifact set: dispatch T5 Verifier.
- Run escalation handler at each tier on failure.
- Commit passing artifacts via VCS adapter.
- Notify on completion or terminal failure via notify adapter.
"""
raise NotImplementedError("TeamRunner.run is not yet implemented.")
def _dispatch_brief(self, brief) -> dict:
"""
Send a single TaskBrief to the appropriate agent and return the result.
TODO (Phase 2):
- Select runtime based on brief.preferred_runtime.
- Load agent personality from brief.agent_personality (if set).
- Compose prompt from tier system prompt + brief payload.
- Spawn agent via runtime adapter.
- Await result via runtime.get_result().
- Log spawned/completed/failed events to Blackboard.
"""
raise NotImplementedError("TeamRunner._dispatch_brief is not yet implemented.")
# ---------------------------------------------------------------------------
# CLI entry point (Phase 2)
# ---------------------------------------------------------------------------
# TODO (Phase 2): Implement argparse CLI.
# if __name__ == "__main__":
# parser = argparse.ArgumentParser(description="Run the-agency pipeline.")
# parser.add_argument("--config", default="config/team.yaml", help="Path to team.yaml")
# parser.add_argument("--dry-run", action="store_true", help="Log actions without executing")
# args = parser.parse_args()
# runner = TeamRunner(config_path=args.config)
# runner.run()