Use shared agent persistence files
This commit is contained in:
@@ -31,8 +31,8 @@ Removed custom orchestration modules:
|
|||||||
- `hooks.py`
|
- `hooks.py`
|
||||||
- `run_loop.py`
|
- `run_loop.py`
|
||||||
|
|
||||||
`bus.json` remains only as the backward-compatible snapshot filename for old runs and tracer
|
`agents.json` is the only graph/status snapshot. `agents.db` is the only SDK session database;
|
||||||
hydration. It is no longer backed by `AgentMessageBus`.
|
each agent uses its own SDK `session_id` inside that shared database.
|
||||||
|
|
||||||
## Lifecycle Semantics
|
## Lifecycle Semantics
|
||||||
|
|
||||||
@@ -65,5 +65,5 @@ Unknown agent id is the only invalid message target. There is no routing-closed
|
|||||||
- Interactive `wait_for_message` parks and returns control to the continuation loop.
|
- Interactive `wait_for_message` parks and returns control to the continuation loop.
|
||||||
- Non-interactive `wait_for_message` returns the newly appended session message content.
|
- Non-interactive `wait_for_message` returns the newly appended session message content.
|
||||||
- `finish_scan` blocks while child agents are active.
|
- `finish_scan` blocks while child agents are active.
|
||||||
- Resume rebuilds parked child runners from `bus.json` plus per-agent session DBs.
|
- Resume rebuilds parked child runners from `agents.json` plus the shared `agents.db`.
|
||||||
- Graceful stop works for both active-stream and parked agents.
|
- Graceful stop works for both active-stream and parked agents.
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ Examples:
|
|||||||
help=(
|
help=(
|
||||||
"Resume a prior scan by its run name (the dir under ./strix_runs/). "
|
"Resume a prior scan by its run name (the dir under ./strix_runs/). "
|
||||||
"Picks up the root + every non-terminal subagent's full LLM history "
|
"Picks up the root + every non-terminal subagent's full LLM history "
|
||||||
"and bus topology. Skips fresh run-name generation."
|
"and agent topology. Skips fresh run-name generation."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -417,7 +417,7 @@ Examples:
|
|||||||
|
|
||||||
# Capture before ``_load_resume_state`` overrides — used by the resume
|
# Capture before ``_load_resume_state`` overrides — used by the resume
|
||||||
# path in ``run_strix_scan`` to decide whether to inject the new
|
# path in ``run_strix_scan`` to decide whether to inject the new
|
||||||
# instruction into the root's bus inbox after session replay.
|
# instruction into the root's SDK session after replay.
|
||||||
args.user_explicit_instruction = args.instruction if args.resume else None
|
args.user_explicit_instruction = args.instruction if args.resume else None
|
||||||
|
|
||||||
if args.resume:
|
if args.resume:
|
||||||
@@ -427,11 +427,11 @@ Examples:
|
|||||||
"the prior run left off, including the original target list."
|
"the prior run left off, including the original target list."
|
||||||
)
|
)
|
||||||
_load_resume_state(args, parser)
|
_load_resume_state(args, parser)
|
||||||
bus_path = Path("strix_runs") / args.resume / "bus.json"
|
agents_path = Path("strix_runs") / args.resume / "agents.json"
|
||||||
if not bus_path.exists():
|
if not agents_path.exists():
|
||||||
parser.error(
|
parser.error(
|
||||||
f"--resume {args.resume}: missing {bus_path}. The run was "
|
f"--resume {args.resume}: missing {agents_path}. The run was "
|
||||||
f"persisted but never reached its first bus snapshot — "
|
f"persisted but never reached its first agent snapshot — "
|
||||||
f"there's nothing to resume from. Pick a fresh --run-name "
|
f"there's nothing to resume from. Pick a fresh --run-name "
|
||||||
f"or remove --resume to start over with the same targets."
|
f"or remove --resume to start over with the same targets."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
Status = Literal["running", "waiting", "completed", "stopped", "crashed", "failed", "llm_failed"]
|
Status = Literal["running", "waiting", "completed", "stopped", "crashed", "failed", "llm_failed"]
|
||||||
ACTIVE_AGENT_STATUSES = {"running", "waiting", "llm_failed"}
|
ACTIVE_AGENT_STATUSES = {"running", "waiting", "llm_failed"}
|
||||||
_SNAPSHOT_VERSION = 2
|
_SNAPSHOT_VERSION = 3
|
||||||
_WAITING_TIMEOUT_SUBAGENT = 300.0
|
_WAITING_TIMEOUT_SUBAGENT = 300.0
|
||||||
_TIMEOUT_RESUME_MESSAGE = "Waiting timeout reached. Resuming execution."
|
_TIMEOUT_RESUME_MESSAGE = "Waiting timeout reached. Resuming execution."
|
||||||
|
|
||||||
@@ -78,7 +78,6 @@ class AgentCoordinator:
|
|||||||
is_whitebox: bool = False,
|
is_whitebox: bool = False,
|
||||||
scan_mode: str = "deep",
|
scan_mode: str = "deep",
|
||||||
diff_scope: dict[str, Any] | None = None,
|
diff_scope: dict[str, Any] | None = None,
|
||||||
session_path: str | Path | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
self.statuses[agent_id] = "running"
|
self.statuses[agent_id] = "running"
|
||||||
@@ -93,7 +92,6 @@ class AgentCoordinator:
|
|||||||
"is_whitebox": bool(is_whitebox),
|
"is_whitebox": bool(is_whitebox),
|
||||||
"scan_mode": scan_mode,
|
"scan_mode": scan_mode,
|
||||||
"diff_scope": diff_scope,
|
"diff_scope": diff_scope,
|
||||||
"session_path": str(session_path) if session_path is not None else "",
|
|
||||||
}
|
}
|
||||||
self.runtimes.setdefault(agent_id, AgentRuntime())
|
self.runtimes.setdefault(agent_id, AgentRuntime())
|
||||||
logger.info("agent.register %s (%s) parent=%s", agent_id, name, parent_id or "-")
|
logger.info("agent.register %s (%s) parent=%s", agent_id, name, parent_id or "-")
|
||||||
@@ -362,8 +360,6 @@ class AgentCoordinator:
|
|||||||
aid: [dict(msg) for msg in msgs] for aid, msgs in self.queued_messages.items()
|
aid: [dict(msg) for msg in msgs] for aid, msgs in self.queued_messages.items()
|
||||||
},
|
},
|
||||||
"stats_live": {aid: dict(s) for aid, s in self.stats_live.items()},
|
"stats_live": {aid: dict(s) for aid, s in self.stats_live.items()},
|
||||||
# Kept for old tracer hydration shape.
|
|
||||||
"stats_completed": {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def restore(self, snap: dict[str, Any]) -> None:
|
async def restore(self, snap: dict[str, Any]) -> None:
|
||||||
@@ -378,15 +374,7 @@ class AgentCoordinator:
|
|||||||
aid: [dict(msg) for msg in msgs]
|
aid: [dict(msg) for msg in msgs]
|
||||||
for aid, msgs in snap.get("queued_messages", {}).items()
|
for aid, msgs in snap.get("queued_messages", {}).items()
|
||||||
}
|
}
|
||||||
for aid in snap.get("stopping", []):
|
|
||||||
if aid in self.statuses:
|
|
||||||
self.statuses[aid] = "stopped"
|
|
||||||
self.stats_live = {aid: dict(s) for aid, s in snap.get("stats_live", {}).items()}
|
self.stats_live = {aid: dict(s) for aid, s in snap.get("stats_live", {}).items()}
|
||||||
for aid, msgs in snap.get("inboxes", {}).items():
|
|
||||||
# Legacy bus snapshots used inboxes. Preserve them as queued
|
|
||||||
# messages so attaching the SDK session makes them visible.
|
|
||||||
self.pending_counts[aid] = max(self.pending_counts.get(aid, 0), len(msgs))
|
|
||||||
self.queued_messages.setdefault(aid, []).extend(dict(msg) for msg in msgs)
|
|
||||||
for aid in self.statuses:
|
for aid in self.statuses:
|
||||||
self.runtimes.setdefault(aid, AgentRuntime())
|
self.runtimes.setdefault(aid, AgentRuntime())
|
||||||
|
|
||||||
|
|||||||
+33
-52
@@ -1,23 +1,23 @@
|
|||||||
"""Top-level scan entry point with auto-resume.
|
"""Top-level scan entry point with auto-resume.
|
||||||
|
|
||||||
1. Build (or take from caller) the per-scan ``AgentCoordinator``.
|
1. Build (or take from caller) the per-scan ``AgentCoordinator``.
|
||||||
2. Wire a snapshot path so every lifecycle event auto-persists ``bus.json``.
|
2. Wire a snapshot path so lifecycle events auto-persist ``agents.json``.
|
||||||
3. Acquire an advisory file lock so a second ``strix`` process can't run
|
3. Acquire an advisory file lock so a second ``strix`` process can't run
|
||||||
on the same ``scan_id`` concurrently.
|
on the same ``scan_id`` concurrently.
|
||||||
4. **Resume detection**: if ``{run_dir}/bus.json`` already exists, restore
|
4. **Resume detection**: if ``{run_dir}/agents.json`` already exists, restore
|
||||||
the coordinator, hydrate the tracer, reuse the persisted ``root_id`` instead
|
the coordinator, hydrate the tracer, reuse the persisted ``root_id`` instead
|
||||||
of generating a fresh one, and respawn every non-terminal subagent
|
of generating a fresh one, and respawn every non-terminal subagent
|
||||||
from its per-child ``SQLiteSession`` before starting the root.
|
from the shared SDK ``agents.db`` before starting the root.
|
||||||
5. Bring up (or reuse) a sandbox session for ``scan_id``.
|
5. Bring up (or reuse) a sandbox session for ``scan_id``.
|
||||||
6. Build the root ``Agent`` + child factory.
|
6. Build the root ``Agent`` + child factory.
|
||||||
7. Open root ``SQLiteSession`` at the same path so the SDK replays prior
|
7. Open root ``SQLiteSession`` in ``agents.db`` so the SDK replays prior
|
||||||
turns on resume.
|
turns on resume.
|
||||||
8. Call ``Runner.run`` (via ``run_with_continuation``).
|
8. Call ``Runner.run`` (via ``run_with_continuation``).
|
||||||
9. ``finally``: close every per-agent session, take a final snapshot,
|
9. ``finally``: close every per-agent session, take a final snapshot,
|
||||||
tear down the sandbox, release the lock.
|
tear down the sandbox, release the lock.
|
||||||
|
|
||||||
Resume is **always on**: there is no flag — presence of ``bus.json`` is
|
Resume is **always on**: there is no flag — presence of ``agents.json`` is
|
||||||
the trigger. Fresh runs simply have no ``bus.json`` to begin with.
|
the trigger. Fresh runs simply have no ``agents.json`` to begin with.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -229,10 +229,9 @@ async def run_strix_scan(
|
|||||||
teardown_logging = setup_scan_logging(run_dir)
|
teardown_logging = setup_scan_logging(run_dir)
|
||||||
set_scan_id(scan_id)
|
set_scan_id(scan_id)
|
||||||
|
|
||||||
bus_path = run_dir / "bus.json"
|
agents_path = run_dir / "agents.json"
|
||||||
is_resume = bus_path.exists()
|
agents_db = run_dir / "agents.db"
|
||||||
sessions_dir = run_dir / "sessions"
|
is_resume = agents_path.exists()
|
||||||
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
lock_handle = _acquire_run_lock(run_dir)
|
lock_handle = _acquire_run_lock(run_dir)
|
||||||
|
|
||||||
@@ -258,7 +257,7 @@ async def run_strix_scan(
|
|||||||
# commands while the scan loop runs in another thread.
|
# commands while the scan loop runs in another thread.
|
||||||
if coordinator is None:
|
if coordinator is None:
|
||||||
coordinator = AgentCoordinator()
|
coordinator = AgentCoordinator()
|
||||||
coordinator.set_snapshot_path(bus_path)
|
coordinator.set_snapshot_path(agents_path)
|
||||||
|
|
||||||
if tracer is not None and hasattr(tracer, "hydrate_from_run_dir"):
|
if tracer is not None and hasattr(tracer, "hydrate_from_run_dir"):
|
||||||
tracer.hydrate_from_run_dir()
|
tracer.hydrate_from_run_dir()
|
||||||
@@ -275,12 +274,17 @@ async def run_strix_scan(
|
|||||||
root_id: str | None = None
|
root_id: str | None = None
|
||||||
if is_resume:
|
if is_resume:
|
||||||
try:
|
try:
|
||||||
snap = json.loads(bus_path.read_text(encoding="utf-8"))
|
snap = json.loads(agents_path.read_text(encoding="utf-8"))
|
||||||
except (OSError, json.JSONDecodeError) as exc:
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
_release_run_lock(lock_handle)
|
_release_run_lock(lock_handle)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Cannot resume scan {scan_id}: bus.json is unreadable: {exc}",
|
f"Cannot resume scan {scan_id}: agents.json is unreadable: {exc}",
|
||||||
) from exc
|
) from exc
|
||||||
|
if not agents_db.exists():
|
||||||
|
_release_run_lock(lock_handle)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Cannot resume scan {scan_id}: missing SDK session database at {agents_db}",
|
||||||
|
)
|
||||||
await coordinator.restore(snap)
|
await coordinator.restore(snap)
|
||||||
for aid, parent in coordinator.parent_of.items():
|
for aid, parent in coordinator.parent_of.items():
|
||||||
if parent is None:
|
if parent is None:
|
||||||
@@ -289,7 +293,7 @@ async def run_strix_scan(
|
|||||||
if root_id is None:
|
if root_id is None:
|
||||||
_release_run_lock(lock_handle)
|
_release_run_lock(lock_handle)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Cannot resume scan {scan_id}: bus.json has no root agent (parent=None)",
|
f"Cannot resume scan {scan_id}: agents.json has no root agent (parent=None)",
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Resume: restored coordinator with %d agent(s); root=%s",
|
"Resume: restored coordinator with %d agent(s); root=%s",
|
||||||
@@ -368,6 +372,7 @@ async def run_strix_scan(
|
|||||||
"diff_scope": diff_scope,
|
"diff_scope": diff_scope,
|
||||||
"run_id": run_id,
|
"run_id": run_id,
|
||||||
"agent_factory": agent_factory,
|
"agent_factory": agent_factory,
|
||||||
|
"agents_db_path": agents_db,
|
||||||
"_sessions_to_close": sessions_to_close,
|
"_sessions_to_close": sessions_to_close,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +400,7 @@ async def run_strix_scan(
|
|||||||
if is_resume:
|
if is_resume:
|
||||||
await _respawn_subagents(
|
await _respawn_subagents(
|
||||||
coordinator=coordinator,
|
coordinator=coordinator,
|
||||||
sessions_dir=sessions_dir,
|
agents_db_path=agents_db,
|
||||||
factory=agent_factory,
|
factory=agent_factory,
|
||||||
parent_ctx=context,
|
parent_ctx=context,
|
||||||
resolved_model=resolved_model,
|
resolved_model=resolved_model,
|
||||||
@@ -404,18 +409,16 @@ async def run_strix_scan(
|
|||||||
sessions_to_close=sessions_to_close,
|
sessions_to_close=sessions_to_close,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Root SDK session — same path on fresh + resume so SDK replay
|
# All agents share one SQLite database; SDK session_id separates
|
||||||
# picks up prior turns automatically when ``initial_input`` is
|
# each agent's conversation inside that database.
|
||||||
# an empty list.
|
root_session = open_agent_session(root_id, agents_db)
|
||||||
session_db = run_dir / "session.db"
|
|
||||||
root_session = SQLiteSession(session_id=scan_id, db_path=session_db)
|
|
||||||
sessions_to_close.append(root_session)
|
sessions_to_close.append(root_session)
|
||||||
await coordinator.attach_runtime(root_id, session=root_session)
|
await coordinator.attach_runtime(root_id, session=root_session)
|
||||||
|
|
||||||
initial_input: Any = [] if is_resume else _build_root_task(scan_config)
|
initial_input: Any = [] if is_resume else _build_root_task(scan_config)
|
||||||
|
|
||||||
# Resume + new ``--instruction``: SDK replay drives root from
|
# Resume + new ``--instruction``: SDK replay drives root from
|
||||||
# session.db with ``initial_input=[]``, so a brand-new instruction
|
# agents.db with ``initial_input=[]``, so a brand-new instruction
|
||||||
# passed on the resume CLI would otherwise be silently ignored.
|
# passed on the resume CLI would otherwise be silently ignored.
|
||||||
# Inject it as a fresh user message in root's SDK session; the
|
# Inject it as a fresh user message in root's SDK session; the
|
||||||
# next run cycle will replay it with the rest of the session.
|
# next run cycle will replay it with the rest of the session.
|
||||||
@@ -431,7 +434,7 @@ async def run_strix_scan(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Resume: injected new instruction into root inbox (len=%d)",
|
"Resume: injected new instruction into root SDK session (len=%d)",
|
||||||
len(resume_instruction),
|
len(resume_instruction),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -489,7 +492,7 @@ async def run_strix_scan(
|
|||||||
async def _respawn_subagents(
|
async def _respawn_subagents(
|
||||||
*,
|
*,
|
||||||
coordinator: AgentCoordinator,
|
coordinator: AgentCoordinator,
|
||||||
sessions_dir: Path,
|
agents_db_path: Path,
|
||||||
factory: Any,
|
factory: Any,
|
||||||
parent_ctx: dict[str, Any],
|
parent_ctx: dict[str, Any],
|
||||||
resolved_model: str,
|
resolved_model: str,
|
||||||
@@ -499,14 +502,12 @@ async def _respawn_subagents(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Re-spawn subagent runners from a restored coordinator snapshot.
|
"""Re-spawn subagent runners from a restored coordinator snapshot.
|
||||||
|
|
||||||
Each child gets its own :class:`SQLiteSession` reopened at
|
Each child gets its own SDK ``session_id`` inside the shared
|
||||||
``sessions_dir/<child_id>.db`` so the SDK replays its prior
|
``agents.db`` so the SDK replays its prior conversation. Interactive
|
||||||
conversation. Per-child failure (missing/corrupt session DB,
|
mode respawns every registered child as a parked runner unless it was
|
||||||
factory raising) finalizes that child as ``crashed`` and continues.
|
actively running before the crash. That keeps completed/stopped/
|
||||||
Interactive mode respawns every registered child as a parked runner
|
crashed/failed agents addressable: a later message wakes the SDK
|
||||||
unless it was actively running before the crash. That keeps
|
session instead of being dropped into a dead inbox.
|
||||||
completed/stopped/crashed/failed agents addressable: a later message
|
|
||||||
wakes the SDK session instead of being dropped into a dead inbox.
|
|
||||||
"""
|
"""
|
||||||
interactive = bool(parent_ctx.get("interactive", False))
|
interactive = bool(parent_ctx.get("interactive", False))
|
||||||
async with coordinator._lock:
|
async with coordinator._lock:
|
||||||
@@ -534,26 +535,6 @@ async def _respawn_subagents(
|
|||||||
|
|
||||||
for child_id, name, parent_id, md in candidates:
|
for child_id, name, parent_id, md in candidates:
|
||||||
try:
|
try:
|
||||||
session_path = sessions_dir / f"{child_id}.db"
|
|
||||||
if not session_path.exists():
|
|
||||||
if interactive:
|
|
||||||
logger.warning(
|
|
||||||
"respawn %s (%s): session db missing at %s; starting parked with empty session",
|
|
||||||
child_id,
|
|
||||||
name,
|
|
||||||
session_path,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"respawn %s (%s): session db missing at %s — marking crashed",
|
|
||||||
child_id,
|
|
||||||
name,
|
|
||||||
session_path,
|
|
||||||
)
|
|
||||||
await coordinator.set_status(child_id, "crashed")
|
|
||||||
continue
|
|
||||||
session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
restored_status = str(md.get("_restored_status") or "running")
|
restored_status = str(md.get("_restored_status") or "running")
|
||||||
start_parked = interactive and restored_status != "running"
|
start_parked = interactive and restored_status != "running"
|
||||||
|
|
||||||
@@ -565,7 +546,7 @@ async def _respawn_subagents(
|
|||||||
restored_status,
|
restored_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
child_session = open_agent_session(child_id, session_path)
|
child_session = open_agent_session(child_id, agents_db_path)
|
||||||
sessions_to_close.append(child_session)
|
sessions_to_close.append(child_session)
|
||||||
await coordinator.attach_runtime(child_id, session=child_session)
|
await coordinator.attach_runtime(child_id, session=child_session)
|
||||||
|
|
||||||
|
|||||||
+26
-30
@@ -167,29 +167,29 @@ class Tracer:
|
|||||||
json_path,
|
json_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
bus_path = run_dir / "bus.json"
|
agents_path = run_dir / "agents.json"
|
||||||
if bus_path.exists():
|
if agents_path.exists():
|
||||||
try:
|
try:
|
||||||
bus_data = json.loads(bus_path.read_text(encoding="utf-8"))
|
agents_data = json.loads(agents_path.read_text(encoding="utf-8"))
|
||||||
except (OSError, json.JSONDecodeError):
|
except (OSError, json.JSONDecodeError):
|
||||||
# Caller will surface this same corruption via ``bus.restore``;
|
# Caller will surface this same corruption via coordinator restore;
|
||||||
# no need to fail twice. Skip the agents/stats hydrate path.
|
# no need to fail twice. Skip the agents/stats hydrate path.
|
||||||
bus_data = None
|
agents_data = None
|
||||||
if isinstance(bus_data, dict):
|
if isinstance(agents_data, dict):
|
||||||
self._hydrate_agents_tree(bus_data)
|
self._hydrate_agents_tree(agents_data)
|
||||||
self._hydrate_llm_stats(bus_data)
|
self._hydrate_llm_stats(agents_data)
|
||||||
|
|
||||||
def _hydrate_agents_tree(self, bus_data: dict[str, Any]) -> None:
|
def _hydrate_agents_tree(self, agents_data: dict[str, Any]) -> None:
|
||||||
"""Populate ``self.agents`` from a bus snapshot.
|
"""Populate ``self.agents`` from the coordinator snapshot.
|
||||||
|
|
||||||
Without this, the TUI tree on resume would only show agents
|
Without this, the TUI tree on resume would only show agents
|
||||||
currently running (mirrored by ``on_agent_start``); completed /
|
currently running (mirrored by ``on_agent_start``); completed /
|
||||||
crashed / stopped children from the prior run would be invisible
|
crashed / stopped children from the prior run would be invisible
|
||||||
even though the bus knows about them.
|
even though the coordinator knows about them.
|
||||||
"""
|
"""
|
||||||
statuses = bus_data.get("statuses") or {}
|
statuses = agents_data.get("statuses") or {}
|
||||||
names = bus_data.get("names") or {}
|
names = agents_data.get("names") or {}
|
||||||
parent_of = bus_data.get("parent_of") or {}
|
parent_of = agents_data.get("parent_of") or {}
|
||||||
if not isinstance(statuses, dict):
|
if not isinstance(statuses, dict):
|
||||||
return
|
return
|
||||||
timestamp = self.start_time
|
timestamp = self.start_time
|
||||||
@@ -206,29 +206,25 @@ class Tracer:
|
|||||||
}
|
}
|
||||||
logger.info("tracer hydrated %d agent(s) into tree", len(self.agents))
|
logger.info("tracer hydrated %d agent(s) into tree", len(self.agents))
|
||||||
|
|
||||||
def _hydrate_llm_stats(self, bus_data: dict[str, Any]) -> None:
|
def _hydrate_llm_stats(self, agents_data: dict[str, Any]) -> None:
|
||||||
"""Seed ``self._llm_stats`` from the bus snapshot's per-agent counters.
|
"""Seed ``self._llm_stats`` from the coordinator snapshot's counters.
|
||||||
|
|
||||||
Aggregates ``stats_live + stats_completed`` so the resumed scan's
|
This keeps the resumed scan's TUI footer cumulative instead of
|
||||||
TUI footer shows cumulative tokens / requests across the prior
|
resetting to zero.
|
||||||
run plus whatever the resume adds, instead of resetting to zero.
|
|
||||||
"""
|
"""
|
||||||
totals = {"input_tokens": 0, "output_tokens": 0, "cached_tokens": 0, "requests": 0}
|
totals = {"input_tokens": 0, "output_tokens": 0, "cached_tokens": 0, "requests": 0}
|
||||||
for bucket_key in ("stats_live", "stats_completed"):
|
bucket = agents_data.get("stats_live") or {}
|
||||||
bucket = bus_data.get(bucket_key) or {}
|
if isinstance(bucket, dict):
|
||||||
if not isinstance(bucket, dict):
|
|
||||||
continue
|
|
||||||
for entry in bucket.values():
|
for entry in bucket.values():
|
||||||
if not isinstance(entry, dict):
|
if isinstance(entry, dict):
|
||||||
continue
|
totals["input_tokens"] += int(entry.get("in", 0) or 0)
|
||||||
totals["input_tokens"] += int(entry.get("in", 0) or 0)
|
totals["output_tokens"] += int(entry.get("out", 0) or 0)
|
||||||
totals["output_tokens"] += int(entry.get("out", 0) or 0)
|
totals["cached_tokens"] += int(entry.get("cached", 0) or 0)
|
||||||
totals["cached_tokens"] += int(entry.get("cached", 0) or 0)
|
totals["requests"] += int(entry.get("calls", 0) or 0)
|
||||||
totals["requests"] += int(entry.get("calls", 0) or 0)
|
|
||||||
for k, v in totals.items():
|
for k, v in totals.items():
|
||||||
self._llm_stats[k] = v
|
self._llm_stats[k] = v
|
||||||
logger.info(
|
logger.info(
|
||||||
"tracer hydrated llm stats from bus (in=%d out=%d cached=%d requests=%d)",
|
"tracer hydrated llm stats from agents snapshot (in=%d out=%d cached=%d requests=%d)",
|
||||||
totals["input_tokens"],
|
totals["input_tokens"],
|
||||||
totals["output_tokens"],
|
totals["output_tokens"],
|
||||||
totals["cached_tokens"],
|
totals["cached_tokens"],
|
||||||
|
|||||||
@@ -492,13 +492,6 @@ async def create_agent(
|
|||||||
default=str,
|
default=str,
|
||||||
)
|
)
|
||||||
|
|
||||||
tracer = inner.get("tracer")
|
|
||||||
if tracer is not None and hasattr(tracer, "get_run_dir"):
|
|
||||||
child_session_path = tracer.get_run_dir() / "sessions" / f"{child_id}.db"
|
|
||||||
else:
|
|
||||||
run_id = inner.get("run_id") or "default"
|
|
||||||
child_session_path = Path.cwd() / "strix_runs" / str(run_id) / "sessions" / f"{child_id}.db"
|
|
||||||
|
|
||||||
await coordinator.register(
|
await coordinator.register(
|
||||||
child_id,
|
child_id,
|
||||||
name,
|
name,
|
||||||
@@ -508,7 +501,6 @@ async def create_agent(
|
|||||||
is_whitebox=bool(inner.get("is_whitebox", False)),
|
is_whitebox=bool(inner.get("is_whitebox", False)),
|
||||||
scan_mode=str(inner.get("scan_mode", "deep")),
|
scan_mode=str(inner.get("scan_mode", "deep")),
|
||||||
diff_scope=inner.get("diff_scope"),
|
diff_scope=inner.get("diff_scope"),
|
||||||
session_path=child_session_path,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ``ctx.turn_input`` carries the parent's full conversation up to and
|
# ``ctx.turn_input`` carries the parent's full conversation up to and
|
||||||
@@ -569,13 +561,12 @@ async def create_agent(
|
|||||||
"_sessions_to_close": inner.get("_sessions_to_close", []),
|
"_sessions_to_close": inner.get("_sessions_to_close", []),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Per-child SQLiteSession at ``{run_dir}/sessions/{child_id}.db`` so
|
# Every agent gets its own SDK session_id inside the shared agents.db.
|
||||||
# this subagent's full conversation survives a process restart and
|
agents_db_path = inner.get("agents_db_path")
|
||||||
# can be replayed by the SDK on resume. Path is derived from the
|
if not isinstance(agents_db_path, Path):
|
||||||
# tracer's run_dir (root-side construction in
|
run_id = inner.get("run_id") or "default"
|
||||||
# :func:`run_strix_scan`); fall back to ``./strix_runs/{run_id}/`` if
|
agents_db_path = Path.cwd() / "strix_runs" / str(run_id) / "agents.db"
|
||||||
# the tracer is absent (unit-test path).
|
child_session = open_agent_session(child_id, agents_db_path)
|
||||||
child_session = open_agent_session(child_id, child_session_path)
|
|
||||||
sessions_list = child_ctx.get("_sessions_to_close")
|
sessions_list = child_ctx.get("_sessions_to_close")
|
||||||
if isinstance(sessions_list, list):
|
if isinstance(sessions_list, list):
|
||||||
sessions_list.append(child_session)
|
sessions_list.append(child_session)
|
||||||
|
|||||||
Reference in New Issue
Block a user