agent-runtime/agent/process.py
Nico 7458b2ea35 v0.8.0: refactor agent.py into modular package
Split 1161-line monolith into agent/ package:
auth, llm, types, process, runtime, api, and
nodes/ (base, sensor, input, output, thinker, memorizer).
No logic changes — pure structural split.
uvicorn agent:app entrypoint unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 01:36:41 +01:00

105 lines
3.8 KiB
Python

"""Process Manager: observable tool execution via subprocess."""
import asyncio
import subprocess
import tempfile
import time
class Process:
"""A single observable tool execution."""
_next_id = 0
def __init__(self, tool: str, code: str, send_hud):
Process._next_id += 1
self.pid = Process._next_id
self.tool = tool
self.code = code
self.send_hud = send_hud
self.status = "pending"
self.output_lines: list[str] = []
self.exit_code: int | None = None
self.started_at: float = 0
self.ended_at: float = 0
self._subprocess: subprocess.Popen | None = None
async def hud(self, event: str, **data):
await self.send_hud({"node": "process", "event": event, "pid": self.pid,
"tool": self.tool, "status": self.status, **data})
def run_sync(self) -> str:
"""Execute the tool synchronously. Returns output."""
self.status = "running"
self.started_at = time.time()
try:
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
f.write(self.code)
f.flush()
self._subprocess = subprocess.Popen(
['python3', f.name],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, cwd=tempfile.gettempdir()
)
stdout, stderr = self._subprocess.communicate(timeout=10)
self.exit_code = self._subprocess.returncode
if stdout:
self.output_lines.extend(stdout.strip().split("\n"))
if self.exit_code != 0 and stderr:
self.output_lines.append(f"[stderr: {stderr.strip()}]")
self.status = "done" if self.exit_code == 0 else "failed"
except subprocess.TimeoutExpired:
if self._subprocess:
self._subprocess.kill()
self.output_lines.append("[error: timed out after 10s]")
self.status = "failed"
self.exit_code = -1
except Exception as e:
self.output_lines.append(f"[error: {e}]")
self.status = "failed"
self.exit_code = -1
finally:
self.ended_at = time.time()
return "\n".join(self.output_lines) or "[no output]"
def cancel(self):
if self._subprocess and self.status == "running":
self._subprocess.kill()
self.status = "cancelled"
self.ended_at = time.time()
self.output_lines.append("[cancelled by user]")
class ProcessManager:
"""Manages all tool executions as observable processes."""
def __init__(self, send_hud):
self.send_hud = send_hud
self.processes: dict[int, Process] = {}
async def execute(self, tool: str, code: str) -> Process:
"""Create and run a process. Returns the completed Process."""
proc = Process(tool, code, self.send_hud)
self.processes[proc.pid] = proc
await proc.hud("process_start", code=code[:200])
loop = asyncio.get_event_loop()
output = await loop.run_in_executor(None, proc.run_sync)
elapsed = round(proc.ended_at - proc.started_at, 2)
await proc.hud("process_done", exit_code=proc.exit_code,
output=output[:500], elapsed=elapsed)
return proc
def cancel(self, pid: int) -> bool:
proc = self.processes.get(pid)
if proc:
proc.cancel()
return True
return False
def get_status(self) -> list[dict]:
return [{"pid": p.pid, "tool": p.tool, "status": p.status,
"elapsed": round((p.ended_at or time.time()) - p.started_at, 2) if p.started_at else 0}
for p in self.processes.values()]