"""Agent event dataclasses — emitted during run_stream() and forwarded to WebSocket clients."""
from dataclasses import dataclass, field
@dataclass
class ToolStarted:
"""Emitted immediately when a tool call begins, before execution completes."""
tool_name: str
arguments: dict
is_subagent: bool = False # True when emitted from inside run_ephemeral
@dataclass
class ToolEvent:
"""Emitted when a tool call finishes — carries the result."""
tool_name: str
arguments: dict
result: str
success: bool
is_subagent: bool = False # True when emitted from inside run_ephemeral
metadata: dict = field(default_factory=dict) # Extra data for client rendering
@dataclass
class TextDelta:
"""A chunk of text from the streaming LLM response."""
delta: str
@dataclass
class ThinkingDelta:
"""A chunk of thinking/reasoning text from the streaming LLM response."""
delta: str
@dataclass
class ThinkingEnd:
"""Marks the end of the thinking phase."""
@dataclass
class StreamEnd:
"""Marks the end of the streaming response."""
full_content: str
context_tokens: int | None = None # total tokens used in this turn
max_context_tokens: int = 0 # ollama_num_ctx from config
elapsed_seconds: float | None = None
tool_call_count: int = 0
token_count: int | None = None # same as context_tokens; kept separate for clarity
@dataclass
class StreamStopped:
"""Emitted when the user stops generation mid-stream (cooperative stop)."""
@dataclass
class ContextCompressed:
"""Emitted after context compression runs successfully."""
messages_before: int
messages_after: int
summary: str = "" # the actual summary text produced by the LLM
@dataclass
class ProfileSwitched:
"""Emitted by switch_profile tool when it successfully changes the session profile."""
profile_id: str
profile_name: str
@dataclass
class PlanningStatus:
"""Emitted at the start of each planning phase to show progress in the UI.
phase: 1 = Analysis, 2 = Execution plan, 3 = Plan review (AIHelper critic).
label: short human-readable description shown next to the spinner.
is_subagent: True when emitted from inside run_ephemeral (subagent planning).
"""
phase: int
label: str
is_subagent: bool = False
@dataclass
class PlanReady:
"""Emitted before the main agent loop when profile.planning_enabled is True.
The plan text has already been injected into session.context as an assistant
message so the LLM will see it and follow it during execution.
is_subagent: True when emitted from inside run_ephemeral (subagent planning).
"""
plan: str
is_subagent: bool = False
@dataclass
class TurnThinking:
"""Full thinking/reasoning block from a tool-calling turn (complete() response).
Unlike ThinkingDelta (which streams chunks), this carries the full text at once
because complete() is non-streaming. Emitted before tool calls for that turn.
is_subagent=True when emitted from run_ephemeral().
"""
thinking: str
is_subagent: bool = False
@dataclass
class SubagentComplete:
"""Internal: emitted by run_ephemeral into the parent sink to report metrics.
Never forwarded to WebSocket clients."""
token_count: int = 0
tool_call_count: int = 0
@dataclass
class PlanningDebugData:
"""Internal: raw outputs from all planning phases, for debug storage in session.
Never forwarded to WebSocket clients. Only emitted for the main agent (not subagents)."""
log: dict # {timestamp, result, phases: {1: {output, prompt_tokens, completion_tokens}, ...}}
@dataclass
class AIHelperTokensUsed:
"""Internal: emitted by AIHelper after each LLM call to report token usage.
Never forwarded to WebSocket clients."""
prompt_tokens: int = 0
completion_tokens: int = 0
@property
def total(self) -> int:
return self.prompt_tokens + self.completion_tokens
AgentEvent = (
ToolStarted | ToolEvent | TextDelta | ThinkingDelta | ThinkingEnd
| StreamEnd | StreamStopped | ContextCompressed | TurnThinking | ProfileSwitched
| PlanningStatus | PlanReady | SubagentComplete | AIHelperTokensUsed | PlanningDebugData
)