Files
strix/strix/core/agents.py
T
0xallam 8414c59557 Strip narrative comments and module/helper docstrings
Five rounds of sweep across the tree. Net ~544 lines removed.

Removed:
- Section-divider banners and one-line section labels (# Display
  utilities, # ----- list_requests -----, # CVSS breakdown, etc.).
- Module-level prose docstrings on internal modules. Kept one-line
  summaries; trimmed multi-paragraph narration about SDK/Strix
  responsibility splits, cache strategies, three-source precedence.
- Internal-helper docstrings that just restate the function name —
  caido_api helpers (caido_url, get_client, view_request, etc.),
  settings-class one-liners (LLMSettings, RuntimeSettings, ...),
  UI helper docstrings.
- Args/Returns blocks on non-LLM-facing internal helpers
  (build_strix_agent, render_system_prompt, create_or_reuse,
  bootstrap_caido) — kept only the genuinely non-obvious params.
- Internal-history phrasing — "Mirrors main-branch shape",
  "pre-SDK harness", "previous lookup matched no attribute".
- Narrative comments inside function bodies that explained what the
  next line does, design rationale obvious from the surrounding code,
  or "we used to..." asides.
- Trailing periods on every error-string literal across the tool tree.
- Duplicated roundtripTime quirk comment (kept the LLM-facing copy in
  tools/proxy/tools.py).

Kept (every one names an upstream bug, vendored-code provenance, or
non-obvious data quirk):
- core/runner.py: SDK replay-with-empty-initial-input + on_agent_end
  lifecycle gap.
- runtime/docker_client.py: VERBATIM COPY block of the upstream
  _create_container body, pinned to SDK v0.14.6.
- runtime/session_manager.py: NO_PROXY for agent-browser CDP loopback.
- tools/proxy/caido_api.py: generated-pydantic Request.raw quirk,
  replay double-history pitfall.
- tools/proxy/tools.py: Caido roundtripTime=0 quirk for proxy
  captures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:02:40 -07:00

308 lines
11 KiB
Python

"""SDK-native state for Strix's addressable agent graph."""
from __future__ import annotations
import asyncio
import json
import logging
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, cast
if TYPE_CHECKING:
from agents.items import TResponseInputItem
from agents.memory import Session
logger = logging.getLogger(__name__)
Status = Literal["running", "waiting", "completed", "stopped", "crashed", "failed"]
@dataclass(slots=True)
class AgentRuntime:
session: Session | None = None
task: asyncio.Task[Any] | None = None
stream: Any | None = None
interrupt_on_message: bool = False
wake: asyncio.Event = field(default_factory=asyncio.Event)
class AgentCoordinator:
"""Single owner for graph state, SDK runtimes, messages, and resume snapshots."""
def __init__(self) -> None:
self.statuses: dict[str, Status] = {}
self.parent_of: dict[str, str | None] = {}
self.names: dict[str, str] = {}
self.metadata: dict[str, dict[str, Any]] = {}
self.pending_counts: dict[str, int] = {}
self.runtimes: dict[str, AgentRuntime] = {}
self._lock = asyncio.Lock()
self._snapshot_path: Path | None = None
def set_snapshot_path(self, path: Path) -> None:
self._snapshot_path = path
async def register(
self,
agent_id: str,
name: str,
parent_id: str | None,
*,
task: str | None = None,
skills: list[str] | None = None,
) -> None:
async with self._lock:
self.statuses[agent_id] = "running"
self.parent_of[agent_id] = parent_id
self.names[agent_id] = name
self.pending_counts.setdefault(agent_id, 0)
self.metadata[agent_id] = {
"task": task or "",
"skills": list(skills or []),
}
self.runtimes.setdefault(agent_id, AgentRuntime())
logger.info("agent.register %s (%s) parent=%s", agent_id, name, parent_id or "-")
await self._maybe_snapshot()
async def attach_runtime(
self,
agent_id: str,
*,
session: Session | None = None,
task: asyncio.Task[Any] | None = None,
interrupt_on_message: bool | None = None,
) -> None:
async with self._lock:
runtime = self.runtimes.setdefault(agent_id, AgentRuntime())
if session is not None:
runtime.session = session
if task is not None:
runtime.task = task
if interrupt_on_message is not None:
runtime.interrupt_on_message = interrupt_on_message
async def mark_running(self, agent_id: str) -> None:
async with self._lock:
if agent_id in self.statuses:
self.statuses[agent_id] = "running"
await self._maybe_snapshot()
async def park_waiting(self, agent_id: str) -> None:
await self.set_status(agent_id, "waiting")
async def set_status(self, agent_id: str, status: Status | str) -> None:
async with self._lock:
if agent_id not in self.statuses:
return
self.statuses[agent_id] = status # type: ignore[assignment]
runtime = self.runtimes.setdefault(agent_id, AgentRuntime())
runtime.wake.set()
logger.info("agent.status %s=%s", agent_id, status)
await self._maybe_snapshot()
async def send(self, target_agent_id: str, message: dict[str, Any]) -> bool:
"""Deliver a user/peer message by appending it to the target SDK session."""
async with self._lock:
if target_agent_id not in self.statuses:
logger.debug("agent.send dropped unknown target=%s", target_agent_id)
return False
runtime = self.runtimes.setdefault(target_agent_id, AgentRuntime())
session = runtime.session
stream = runtime.stream
interrupt = runtime.interrupt_on_message
if session is None:
logger.warning(
"agent.send dropped target=%s because its SDK session is not attached",
target_agent_id,
)
return False
try:
await session.add_items([self._message_to_session_item(message)])
except Exception:
logger.exception(
"agent.send failed to append to SDK session target=%s",
target_agent_id,
)
return False
async with self._lock:
self.pending_counts[target_agent_id] = self.pending_counts.get(target_agent_id, 0) + 1
self.runtimes.setdefault(target_agent_id, AgentRuntime()).wake.set()
if stream is not None and interrupt:
stream.cancel(mode="immediate")
await self._maybe_snapshot()
return True
async def wait_for_message(self, agent_id: str) -> None:
while True:
async with self._lock:
if self.pending_counts.get(agent_id, 0) > 0:
return
wake = self.runtimes.setdefault(agent_id, AgentRuntime()).wake
wake.clear()
await wake.wait()
async def consume_pending(
self,
agent_id: str,
*,
include_items: bool = False,
) -> tuple[int, list[Any]]:
async with self._lock:
count = self.pending_counts.get(agent_id, 0)
self.pending_counts[agent_id] = 0
session = self.runtimes.get(agent_id, AgentRuntime()).session
if count <= 0:
return 0, []
await self._maybe_snapshot()
if not include_items or session is None:
return count, []
items = await session.get_items()
return count, list(items[-count:])
async def request_stop(self, agent_id: str) -> None:
async with self._lock:
if agent_id not in self.statuses:
return
self.statuses[agent_id] = "stopped"
runtime = self.runtimes.setdefault(agent_id, AgentRuntime())
runtime.wake.set()
stream = runtime.stream
if stream is not None:
stream.cancel(mode="after_turn")
await self._maybe_snapshot()
async def cancel_descendants(self, agent_id: str) -> None:
tasks = []
async with self._lock:
for aid in reversed(self._subtree_order_locked(agent_id)):
task = self.runtimes.get(aid, AgentRuntime()).task
if task is not None and not task.done():
tasks.append(task)
for task in tasks:
task.cancel()
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
async def cancel_descendants_graceful(self, agent_id: str) -> None:
async with self._lock:
order = self._subtree_order_locked(agent_id)
for aid in reversed(order):
await self.request_stop(aid)
await self._maybe_snapshot()
async def attach_stream(
self,
agent_id: str,
stream: Any,
) -> None:
async with self._lock:
self.runtimes.setdefault(agent_id, AgentRuntime()).stream = stream
async def detach_stream(
self,
agent_id: str,
stream: Any,
) -> None:
async with self._lock:
runtime = self.runtimes.setdefault(agent_id, AgentRuntime())
if runtime.stream is stream:
runtime.stream = None
async def active_agents_except(self, agent_id: str) -> list[dict[str, Any]]:
async with self._lock:
return [
{
"agent_id": aid,
"name": self.names.get(aid, aid),
"status": status,
"parent_id": self.parent_of.get(aid),
}
for aid, status in self.statuses.items()
if aid != agent_id and status in {"running", "waiting"}
]
async def graph_snapshot(
self,
) -> tuple[dict[str, str | None], dict[str, Status], dict[str, str]]:
async with self._lock:
return dict(self.parent_of), dict(self.statuses), dict(self.names)
def _message_to_session_item(self, message: dict[str, Any]) -> TResponseInputItem:
sender = str(message.get("from", "unknown"))
content = str(message.get("content", ""))
if sender == "user":
return cast("TResponseInputItem", {"role": "user", "content": content})
sender_name = self.names.get(sender, sender)
msg_type = message.get("type", "information")
priority = message.get("priority", "normal")
return cast(
"TResponseInputItem",
{
"role": "user",
"content": (
f"[Message from {sender_name} ({sender}) | type={msg_type} "
f"| priority={priority}]\n{content}"
),
},
)
def _subtree_order_locked(self, agent_id: str) -> list[str]:
queue = [agent_id]
order: list[str] = []
while queue:
aid = queue.pop()
order.append(aid)
queue.extend(child for child, parent in self.parent_of.items() if parent == aid)
return order
async def snapshot(self) -> dict[str, Any]:
async with self._lock:
return {
"statuses": dict(self.statuses),
"parent_of": dict(self.parent_of),
"names": dict(self.names),
"metadata": {aid: dict(md) for aid, md in self.metadata.items()},
"pending_counts": dict(self.pending_counts),
}
async def restore(self, snap: dict[str, Any]) -> None:
async with self._lock:
self.statuses = dict(snap.get("statuses", {}))
self.parent_of = dict(snap.get("parent_of", {}))
self.names = dict(snap.get("names", {}))
self.metadata = {aid: dict(md) for aid, md in snap.get("metadata", {}).items()}
self.pending_counts = dict(snap.get("pending_counts", {}))
for aid in self.statuses:
self.runtimes.setdefault(aid, AgentRuntime())
async def _maybe_snapshot(self) -> None:
path = self._snapshot_path
if path is None:
return
try:
data = await self.snapshot()
payload = json.dumps(data, ensure_ascii=False, default=str)
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=str(path.parent),
prefix=f".{path.name}.",
suffix=".tmp",
delete=False,
) as tmp:
tmp.write(payload)
tmp_path = Path(tmp.name)
tmp_path.replace(path)
except Exception:
logger.exception("coordinator snapshot to %s failed", path)
def coordinator_from_context(ctx: dict[str, Any]) -> AgentCoordinator | None:
coordinator = ctx.get("coordinator")
return coordinator if isinstance(coordinator, AgentCoordinator) else None