Type terminal agent event protocol and provider stream adapters
Problem
Refactor the terminal agent loop introduced in dataface/ai/agent.py and dataface/ai/llm.py to use explicit typed event structures and smaller provider adapter helpers. Reduce stringly typed event handling and isolate provider-specific stream parsing from agent-loop orchestration before more agent clients land.
Context
The terminal agent loop (dataface/ai/agent.py) and LLM client adapters (dataface/ai/llm.py) use dict[str, Any] for all events, discriminated by a "type" string key. Consumers (cli/commands/agent.py, tests) switch on event["type"] strings. Provider-specific stream parsing (OpenAI Responses API events, Anthropic Messages API events) lives inline in each client's stream_with_tools method.
Key files: dataface/ai/agent.py, dataface/ai/llm.py, dataface/cli/commands/agent.py, tests/core/test_ai_agent.py, tests/core/test_ai_llm.py.
Constraints: preserve all existing CLI behavior and test coverage; no new dependencies; Python 3.13 (match statements available).
Possible Solutions
-
Typed dataclasses with union type — Define
@dataclass(slots=True)for each event kind. UseStreamEvent = ContentDelta | ThinkingStatus | ToolCallEventfor provider output andAgentEvent = StreamEvent | ToolResultEvent | AgentDone | AgentErrorfor the full agent protocol. Consumers useisinstanceormatch. Recommended — minimal, no magic, zero runtime cost beyond what dataclasses already provide. -
Pydantic discriminated union — Use Pydantic
BaseModelwith a literaltypediscriminator. Adds validation but also a runtime dependency and overhead the agent loop doesn't need. Against anti-slop philosophy. -
TypedDict variants — Keep dicts but use
TypedDictfor static checking. Cheaper change but doesn't actually remove stringly-typed runtime dispatch.
Plan
Selected: approach 1 (typed dataclasses).
- Create
dataface/ai/events.pywith dataclass events andStreamEvent/AgentEventunions. - Extract provider stream parsing into module-level helpers
_iter_openai_streamand_iter_anthropic_streaminllm.py; have client methods delegate to them. - Update
LLMClientprotocol and concrete clients to yieldStreamEvent. - Update
agent.pyto yieldAgentEventand useisinstancechecks. - Update
cli/commands/agent.pyto match on event types. - Update tests to construct/assert typed events.
- Run
just testfor the touched modules.
Implementation Progress
- Created
dataface/ai/events.pywith 6@dataclass(slots=True)event types:ContentDelta,ThinkingStatus,ToolCallEvent(stream-level) andToolResultEvent,AgentDone,AgentError(agent-level). DefinedStreamEventandAgentEventunion type aliases. - Updated
dataface/ai/llm.py:LLMClientprotocol and concrete clients now yieldStreamEventinstead ofdict[str, Any]. Extracted_iter_anthropic_stream()as a standalone stream adapter helper. OpenAI parsing stays inline due to response ID state tracking. - Updated
dataface/ai/agent.py:run_agent()returnsIterator[AgentEvent]and usesisinstancechecks instead of string key lookups. - Updated
dataface/cli/commands/agent.py: switched fromevent["type"]string dispatch tomatchstatement on typed events. - Updated all 3 test files to construct and assert on typed event objects instead of dicts.
- All 9 existing tests pass. Ruff, Black, and Mypy clean.
QA Exploration
- [x] QA exploration completed (or N/A for non-UI tasks)
Review Feedback
- [ ] Review cleared