Skip to content

Commit a20f968

Browse files
committed
refactor: split runtime into modules, trim orchestrator prompt, cleanup
Split the monolithic atn/runtime.py (3250 lines) into focused modules: - runtime/__init__.py: Runtime class core, init, shutdown - runtime/agent_registry.py: agent register/unregister/activate - runtime/execution_engine.py: pipeline + cognitive execution - runtime/provider_manager.py: LLM provider lifecycle - runtime/scheduler.py: cron + heartbeat scheduling - runtime/session_manager.py: conversation + delegate session mgmt - runtime/snapshot.py: fleet snapshot for dashboard - runtime/execution_control.py: kill switches - runtime/orchestrator_setup.py: orchestrator agent bootstrap - runtime/config_helpers.py: config persistence helpers Other changes: - Trim orchestrator system prompt (~40% smaller, same semantics) - Add delegation-first thinking guidance to orchestrator - Auto-activate agents with heartbeats (not just schedules) - Add voice announcement categories config - Sanitize corrupted conversation history on hydration - Add cognitive step parent-child wake-up notifications - Bridge provider delegate lifecycle events - Remove stale files (BACKLOG.md, rpb_presentation.md, slide_prompts.txt) - Add architecture audit doc and orchestrator prompt reference 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent a08740f commit a20f968

27 files changed

Lines changed: 5083 additions & 4079 deletions

BACKLOG.md

Lines changed: 0 additions & 205 deletions
This file was deleted.

atn/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,9 @@ async def _load_agents(runtime: Runtime, config: ATNConfig) -> int:
329329
elif not errors:
330330
console.print(f" [dim]No agent files in {config.agents_dir}[/]")
331331

332-
# Auto-activate agents that have schedules
332+
# Auto-activate agents that have schedules or heartbeats
333333
for defn in agents:
334-
if defn.schedule:
334+
if defn.schedule or defn.heartbeat:
335335
await runtime.activate_agent(defn.id)
336336

337337
return len(agents)

atn/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ class VoiceConfig:
7777
tools_volume: float = 0.55
7878
effects_volume: float = 0.35
7979
narrate_tools: bool = True
80+
announcements: list[str] = field(default_factory=lambda: [
81+
"agent_runs", "agent_created", "agent_completed", "delegate_lifecycle"
82+
])
8083
output_device: str | None = None
8184
input_device: str | None = None
8285
kokoro_model_dir: str | None = None # directory containing kokoro-v1.0.onnx
@@ -246,6 +249,9 @@ def load_config(path: Path | None = None) -> ATNConfig:
246249
tools_volume=voice_raw.get("tools_volume", 0.55),
247250
effects_volume=voice_raw.get("effects_volume", 0.35),
248251
narrate_tools=voice_raw.get("narrate_tools", True),
252+
announcements=voice_raw.get("announcements", [
253+
"agent_runs", "agent_created", "agent_completed", "delegate_lifecycle"
254+
]),
249255
output_device=voice_raw.get("output_device"),
250256
input_device=voice_raw.get("input_device"),
251257
kokoro_model_dir=voice_raw.get("kokoro_model_dir"),

atn/conversation.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,72 @@ def _hydrate(self) -> None:
266266
return
267267
self._turns = self._load_jsonl(self._active_path)
268268
if self._turns:
269-
log.info("Hydrated active conversation: %d turns", len(self._turns))
269+
dirty = self._sanitize_turns()
270+
log.info("Hydrated active conversation: %d turns%s",
271+
len(self._turns), " (sanitized)" if dirty else "")
272+
273+
def _sanitize_turns(self) -> bool:
274+
"""Fix user turns that were corrupted by history-prepending.
275+
276+
Prior to the fix, the execution engine stored the full prompt
277+
(including ``System: ...``, ``User: ...``, ``Orchestrator: ...``
278+
history) as a single user turn. This strips those back to just
279+
the actual user message and removes exact duplicates.
280+
"""
281+
dirty = False
282+
cleaned: list[ConversationTurn] = []
283+
for turn in self._turns:
284+
content = turn.content
285+
# Detect history-embedded user turns: content has role prefixes
286+
# like "System: ...\nUser: ...\nOrchestrator: ..." baked in.
287+
if turn.role == "user" and "\nUser: " in content and (
288+
content.startswith("System: ")
289+
or content.startswith("User: ")
290+
or "\nOrchestrator: " in content
291+
):
292+
# Extract the last "User: ..." segment as the real message.
293+
parts = content.split("\nUser: ")
294+
raw = parts[-1].strip()
295+
if raw:
296+
turn = ConversationTurn(
297+
role="user", content=raw,
298+
timestamp=turn.timestamp,
299+
execution_id=turn.execution_id,
300+
)
301+
dirty = True
302+
# Strip leading "[Current time: ...]\n\n" injected by the engine
303+
if turn.role == "user" and turn.content.startswith("[Current time:"):
304+
newline_idx = turn.content.find("\n\n")
305+
if newline_idx != -1:
306+
turn = ConversationTurn(
307+
role=turn.role,
308+
content=turn.content[newline_idx + 2:],
309+
timestamp=turn.timestamp,
310+
execution_id=turn.execution_id,
311+
)
312+
dirty = True
313+
# Deduplicate consecutive identical turns
314+
if cleaned and cleaned[-1].role == turn.role and cleaned[-1].content == turn.content:
315+
dirty = True
316+
continue
317+
cleaned.append(turn)
318+
if dirty:
319+
self._turns = cleaned
320+
self._rewrite_active()
321+
return dirty
322+
323+
def _rewrite_active(self) -> None:
324+
"""Rewrite the active JSONL from the in-memory turns."""
325+
tmp = self._active_path.with_suffix(".jsonl.tmp")
326+
try:
327+
with open(tmp, "w", encoding="utf-8") as f:
328+
for turn in self._turns:
329+
f.write(json.dumps(turn.to_dict(), ensure_ascii=False) + "\n")
330+
tmp.replace(self._active_path)
331+
except Exception:
332+
log.exception("Failed to rewrite sanitized conversation")
333+
if tmp.exists():
334+
tmp.unlink()
270335

271336
@staticmethod
272337
def _load_jsonl(path: Path) -> list[ConversationTurn]:

atn/mcp_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ async def run_server() -> None:
102102
agents, errors = load_agents_dir(config.agents_dir)
103103
for defn in agents:
104104
await rt.register_agent(defn)
105-
if defn.schedule:
105+
if defn.schedule or defn.heartbeat:
106106
await rt.activate_agent(defn.id)
107107
# Note: execution history is hydrated automatically in register_agent()
108108
if agents:

0 commit comments

Comments
 (0)