| 1 | from __future__ import annotations |
| 2 | |
| 3 | from dataclasses import dataclass, field |
| 4 | from threading import Event |
| 5 | from time import time |
| 6 | from typing import Any, Literal |
| 7 | from uuid import uuid4 |
| 8 | |
| 9 | |
| 10 | @dataclass(slots=True) |
| 11 | class ToolCall: |
| 12 | name: str |
| 13 | arguments: dict[str, Any] = field(default_factory=dict) |
| 14 | id: str = field(default_factory=lambda: f"tool-{uuid4().hex[:12]}") |
| 15 | |
| 16 | def as_dict(self) -> dict[str, Any]: |
| 17 | return {"id": self.id, "name": self.name, "arguments": self.arguments} |
| 18 | |
| 19 | |
| 20 | @dataclass(slots=True) |
| 21 | class ToolResult: |
| 22 | name: str |
| 23 | ok: bool |
| 24 | content: str |
| 25 | metadata: dict[str, Any] = field(default_factory=dict) |
| 26 | # Transient multimodal blocks for the next LLM turn. Deliberately omitted |
| 27 | # from as_dict() so image bytes never enter JSONL logs or session history. |
| 28 | model_content: list[dict[str, Any]] | None = field(default=None, repr=False) |
| 29 | |
| 30 | def as_dict(self) -> dict[str, Any]: |
| 31 | return {"name": self.name, "ok": self.ok, "content": self.content, "metadata": dict(self.metadata)} |
| 32 | |
| 33 | |
| 34 | @dataclass(slots=True) |
| 35 | class TurnControl: |
| 36 | turn_id: str = field(default_factory=lambda: f"turn-{uuid4().hex[:12]}") |
| 37 | cancel_event: Event = field(default_factory=Event) |
| 38 | cancel_reason: str = "" |
| 39 | |
| 40 | def cancel(self, reason: str = "") -> None: |
| 41 | self.cancel_reason = reason.strip() |
| 42 | self.cancel_event.set() |
| 43 | |
| 44 | |
| 45 | @dataclass(slots=True) |
| 46 | class SessionRecord: |
| 47 | session_id: str |
| 48 | working_dir: str |
| 49 | idea: str = "" |
| 50 | user_requirement: str = "" |
| 51 | style: str = "" |
| 52 | stage: str = "created" |
| 53 | summary: str = "" |
| 54 | stale: dict[str, bool] = field(default_factory=dict) |
| 55 | created_at: str = "" |
| 56 | updated_at: str = "" |
| 57 | |
| 58 | |
| 59 | StreamEventType = Literal["turn", "status", "token", "tool_start", "tool_progress", "tool_result", "terminal", "done", "session", "error", "prompt_trace"] |
| 60 | |
| 61 | |
| 62 | def now_ts() -> float: |
| 63 | return time() |
| 64 |