Skip to content

UI Event Projection

Transform the RuntimeEvent stream into serializable UIState snapshots for frontend rendering.

Quick Start

from cognitia.ui.projection import ChatProjection, project_stream

projection = ChatProjection()

# Stream UIState snapshots directly
async for state in project_stream(runtime.run(...), projection):
    payload = state.to_dict()  # JSON-ready for WebSocket/SSE
    await websocket.send_json(payload)

Core Concepts

UIState

A serializable snapshot of the current UI state:

@dataclass
class UIState:
    messages: list[UIMessage]    # conversation messages
    status: str                   # "thinking", "tool_calling", "done", etc.
    metadata: dict                # session_id, total_cost_usd, etc.

UIMessage

A single message with typed content blocks:

@dataclass
class UIMessage:
    role: str                     # "assistant", "tool", etc.
    blocks: list[UIBlock]         # content blocks
    timestamp: float | None

UIBlock Types

Block Fields When
TextBlock text Assistant text responses (accumulated from deltas)
ToolCallBlock name, args, correlation_id Tool invocation started
ToolResultBlock name, ok, summary, correlation_id Tool execution completed
ErrorBlock kind, message Runtime error occurred

Usage Patterns

Pattern 1: Streaming to Frontend

from cognitia.ui.projection import ChatProjection, project_stream

projection = ChatProjection()

async for state in project_stream(runtime.run(messages, system_prompt, tools), projection):
    # state.to_dict() is JSON-serializable
    await send_to_frontend(state.to_dict())

Pattern 2: Manual Event Application

projection = ChatProjection()

async for event in runtime.run(messages, system_prompt, tools):
    state = projection.apply(event)
    # Process state after each event

Pattern 3: Post-hoc Reconstruction

projection = ChatProjection()
events = collect_all_events(...)

for event in events:
    state = projection.apply(event)

# Final state contains the complete conversation
print(state.messages)

Serialization

UIState supports round-trip serialization with type-discriminated blocks:

# Serialize
payload = state.to_dict()
# {
#   "messages": [
#     {
#       "role": "assistant",
#       "blocks": [
#         {"type": "text", "text": "Let me check..."},
#         {"type": "tool_call", "name": "search", "args": {"q": "..."}, "correlation_id": "abc"},
#         {"type": "tool_result", "name": "search", "ok": true, "summary": "Found 3 results", "correlation_id": "abc"},
#         {"type": "text", "text": "Based on the results..."}
#       ]
#     }
#   ],
#   "status": "done",
#   "metadata": {"session_id": "...", "total_cost_usd": 0.003}
# }

# Deserialize
restored = UIState.from_dict(payload)

Event Mapping

ChatProjection maps RuntimeEvent types to UI updates:

RuntimeEvent.type UI Action
assistant_delta Accumulate text in last TextBlock (or create new one)
tool_call_started Add ToolCallBlock
tool_call_finished Add ToolResultBlock
error Add ErrorBlock
status Update UIState.status
final Set status="done", copy metadata

Custom Projections

Implement the EventProjection protocol for custom UI representations:

from cognitia.ui.projection import EventProjection, UIState
from cognitia.runtime.types import RuntimeEvent

class MinimalProjection:
    def __init__(self):
        self._state = UIState(messages=[], status="idle", metadata={})

    def apply(self, event: RuntimeEvent) -> UIState:
        # Custom event handling logic
        if event.is_final:
            self._state.status = "done"
        return self._state