@dataclass
class Executor:
driver: Driver
concurrency: int = 1
extension_policy: ExtensionPolicy | None = None
fix_agent: str = "scribe"
_registry: list = field(default_factory=zettelkasten_tools)
# -- coordinator bridge (lazy) -----------------------------------------
@staticmethod
def _coord():
from coordinator import server as coord
return coord
# -- public -------------------------------------------------------------
def run(self, run_id: str = "") -> dict[str, Any]:
"""Drive the graph identified by ``run_id`` until no tasks remain ready."""
coord = self._coord()
policy = self.extension_policy or default_extension_policy
stalls = 0
while True:
ready = json.loads(coord.get_ready_tasks(run_id))
if not ready:
break
claimed = self._claim_wave(ready, run_id)
if not claimed:
# Everything ready is blocked by a lease whose holder is still
# running elsewhere. Back off briefly, then retry.
stalls += 1
if stalls > 50:
raise RuntimeError(
"Executor stalled: ready tasks remain but none claimable "
"(possible lease deadlock or foreign run holding a lease)."
)
time.sleep(0.2)
continue
stalls = 0
outcomes = self._run_wave(claimed)
for outcome in outcomes:
coord.submit_result(
outcome.task_id, outcome.result.text, success=outcome.success
)
failures = [o for o in outcomes if not o.success]
if failures:
self._maybe_extend(failures, policy, run_id)
return json.loads(coord.get_report(run_id))
# -- wave mechanics -----------------------------------------------------
def _claim_wave(self, ready: list[dict], run_id: str) -> list[dict]:
"""Claim as many ready tasks as possible; skip blocked ones."""
coord = self._coord()
claimed: list[dict] = []
for task in ready:
res = json.loads(coord.claim_task(task["task_id"], run_id))
if res.get("blocked"):
continue
# Merge any claim-time enrichment (model/schema) back onto the task.
merged = {**task, **{k: v for k, v in res.items() if v is not None}}
claimed.append(merged)
return claimed
def _run_wave(self, claimed: list[dict]) -> list[TaskOutcome]:
if self.concurrency <= 1 or len(claimed) == 1:
return [self._run_task(t) for t in claimed]
with ThreadPoolExecutor(max_workers=self.concurrency) as pool:
return list(pool.map(self._run_task, claimed))
def _run_task(self, task: dict) -> TaskOutcome:
coord = self._coord()
task_id = task["task_id"]
agent = task.get("agent", "")
persona_info = json.loads(coord.get_persona(agent))
role = persona_info.get("role", "checker")
persona = persona_info.get("persona", "")
context = json.loads(coord.get_task_context(task_id))
prompt = _build_prompt(context, task)
tools = tools_for_role(role, self._registry)
result = self.driver.run_agent(
role=role,
persona=persona,
prompt=prompt,
tools=tools,
model=task.get("model", ""),
ctx={"task": task, "schema": task.get("schema")},
)
verdict = _parse(result.text, agent)
success, severity = _verdict_status(verdict)
return TaskOutcome(
task=task,
result=result,
verdict=verdict,
success=success,
severity=severity,
)
# -- extensions ---------------------------------------------------------
def _maybe_extend(
self, failures: list[TaskOutcome], policy: ExtensionPolicy, run_id: str
) -> None:
new_tasks = policy(failures, self)
if not new_tasks:
return
coord = self._coord()
after = [f.task_id for f in failures]
context = _failure_context(failures)
import logging
try:
out = coord.extend_graph(new_tasks, after, context=context, run_id=run_id)
except Exception as exc:
# Hard ceiling reached or invalid extension -- stop extending, let
# the run finish.
logging.getLogger(__name__).warning(
"extend_graph rejected (%s); leaving failures in the report.", exc
)
return
# The soft cap returns a structured signal rather than raising. This
# executor is autonomous (no human to authorize a force=True override),
# so the correct response is to stop extending and let the run finish.
try:
import json as _json
if isinstance(out, str) and _json.loads(out).get("soft_cap_reached"):
logging.getLogger(__name__).warning(
"extend_graph soft cap reached; leaving failures in the report."
)
except Exception:
pass