feat(api): structured run events via /v1/runs SSE endpoint

Add POST /v1/runs to start async agent runs and GET /v1/runs/{run_id}/events
for SSE streaming of typed lifecycle events (tool.started, tool.completed,
message.delta, reasoning.available, run.completed, run.failed).

Changes the internal tool_progress_callback signature from positional
(tool_name, preview, args) to event-type-first
(event_type, tool_name, preview, args, **kwargs). Existing consumers
filter on event_type and remain backward-compatible.

Adds concurrency limit (_MAX_CONCURRENT_RUNS=10) and orphaned run sweep.

Fixes logic inversion in cli.py _on_tool_progress where the original PR
would have displayed internal tools instead of non-internal ones.

Co-authored-by: Mibayy <mibayy@users.noreply.github.com>
This commit is contained in:
Mibayy
2026-04-05 11:52:46 -07:00
committed by Teknium
parent e167ad8f61
commit cc2b56b26a
11 changed files with 337 additions and 44 deletions

View File

@@ -98,11 +98,15 @@ def _build_child_progress_callback(task_index: int, parent_agent, task_count: in
_BATCH_SIZE = 5
_batch: List[str] = []
def _callback(tool_name: str, preview: str = None):
# Special "_thinking" event: model produced text content (reasoning)
if tool_name == "_thinking":
def _callback(event_type: str, tool_name: str = None, preview: str = None, args=None, **kwargs):
# event_type is one of: "tool.started", "tool.completed",
# "reasoning.available", "_thinking", "subagent_progress"
# "_thinking" / reasoning events
if event_type in ("_thinking", "reasoning.available"):
text = preview or tool_name or ""
if spinner:
short = (preview[:55] + "...") if preview and len(preview) > 55 else (preview or "")
short = (text[:55] + "...") if len(text) > 55 else text
try:
spinner.print_above(f" {prefix}├─ 💭 \"{short}\"")
except Exception as e:
@@ -110,11 +114,15 @@ def _build_child_progress_callback(task_index: int, parent_agent, task_count: in
# Don't relay thinking to gateway (too noisy for chat)
return
# Regular tool call event
# tool.completed — no display needed here (spinner shows on started)
if event_type == "tool.completed":
return
# tool.started — display and batch for parent relay
if spinner:
short = (preview[:35] + "...") if preview and len(preview) > 35 else (preview or "")
from agent.display import get_tool_emoji
emoji = get_tool_emoji(tool_name)
emoji = get_tool_emoji(tool_name or "")
line = f" {prefix}├─ {emoji} {tool_name}"
if short:
line += f" \"{short}\""
@@ -124,7 +132,7 @@ def _build_child_progress_callback(task_index: int, parent_agent, task_count: in
logger.debug("Spinner print_above failed: %s", e)
if parent_cb:
_batch.append(tool_name)
_batch.append(tool_name or "")
if len(_batch) >= _BATCH_SIZE:
summary = ", ".join(_batch)
try: