Event Stream Types
This document describes the event types produced by the application
layer (AgentApplication + ToolLoopRunner) and exposed via:
- HTTP polling:
GET /eventsinapplication/python/agent_terminal_app/server.py - Bridge streaming:
session_eventmessages forwarded byapplication/python/agent_terminal_app/bridge_client.pyandbridge-elixir.
Each event is a JSON object stored in an EventStore implementation
(core/python/agent_app/events.py) and augmented with a monotonically
increasing string event_id when published.
Application and session notifications
Notification lifecycle events reuse GET /events and
ActionDisplayPayload.
Application notifications are stored durably by the runtime-injected
ApplicationNotificationStore:
application_notification_createdapplication_notification_updatedapplication_notification_dismissedapplication_notification_expired- omit
session_idand are polled without a session filter - survive
AgentApplicationreplacement and server-process restart - retain terminal history until retention removes it
- apply TTL only when a frontend freshly presents a notification; TTL does not emit an event or remove an already displayed notification
- rehydrate through
GET /application/notifications; event replay remains best effort and is not the persistence authority
Session notifications are persisted under
Session.metadata.notifications.items:
session_notification_createdsession_notification_updatedsession_notification_resolvedsession_notification_dismissedsession_notification_expired- include
session_idand use ordinary session-filtered polling - reload from session metadata on reconnect
- remain hidden from model/provider history in Stage 1
Both records have stable id and key, an integer revision,
pending/dismissed/resolved/expired status, info/success/warning/error severity,
title/body, timestamps, source provenance, an action display, and
model_visibility: "hidden". Created/updated events upsert by id; creating
with the key of an active notification updates it in place. Creating with the
key of a terminal notification creates a new lifecycle occurrence.
Server-side dismiss, resolve, and expire mutations retain terminal records for
authoritative listing. Existing records without revision accept their
updated_at as the compatibility expected-revision token and gain integer
revision 1 on first mutation.
Mobile Dismiss controls cache and hide the exact displayed revision before using the server mutation routes. A failed request is retried for that unchanged revision during reconnect/rehydration; a newer revision invalidates the cache and remains presentable. Desktop uses server-authoritative success-only dismissal: a failed mutation leaves the notification visible.
Notification delivery is best effort. Event publication and the tool-facing
notification_sink cannot fail the user request.
Notification state endpoints
GET /application/notifications
POST /application/notifications/{notification_id}/dismiss
GET /sessions/{session_id}/notifications
POST /sessions/{session_id}/notifications/{notification_id}/dismiss
List endpoints return active records by default. Set
include_terminal=true to include dismissed, resolved, and expired records.
Dismiss accepts optional expected_revision and reason fields. A stale
revision or invalid lifecycle transition returns HTTP 409 with the current
notification record.
Standard display presentations
Presentation support depends on the producer and host:
| Producer | banner |
inline |
modal |
|---|---|---|---|
| Application notification | Mobile Sessions rail; aggregated desktop message/detail rail | Rejected because no application-global inline owner exists | Mobile Sessions modal; desktop normalizes to inline in the aggregated message/detail rail |
| Session notification | Matching mobile Chat rail; active desktop ChatPane rail | Persistent matching-Chat notification region | Matching mobile Chat modal; desktop normalizes to inline in the active ChatPane rail |
| Direct action response | Owning mobile Sessions/Chat rail; connection-level or active-session desktop rail | Nearest owning Sessions or Chat region | Mobile modal on the owning screen; desktop normalizes to the corresponding rail |
On mobile, application notifications render only on Sessions below the search/filter row. Session notifications render only on the exact matching Chat below its header. Both rails participate in normal layout and push session-list/transcript content down. Displays received while another surface is active remain pending until their owning surface opens. Connection and session labels are omitted because placement supplies that context.
Desktop aggregates connection-level notifications from every open connection in one normal-flow rail in the message/detail area. Each connection-level card includes the friendly connection name, falling back to connection id. The active ChatPane contributes its own session notifications to the same visual location without a redundant session label. When no ChatPane is open, the connection-level rail remains above the detail placeholder.
Direct action responses without a presentation default to modal; session
notifications default to inline. Application notifications must use
banner or modal; attempts to create an inline application notification are
rejected by the application contract.
Display ownership is independent from storage lifetime. Application
notifications are connection-level. Session notifications are session-level
and require session_id. Direct action responses default to session ownership
when they carry session_id, while session-list/application surfaces may
explicitly retain connection ownership.
Storage and Transport
- Event store protocol:
core/python/agent_app/events.py EventStoreinterface (publish,list).-
InMemoryEventStoreaddsevent_id: stronpublish. -
Application event producer:
core/python/agent_app/application_future.py -
AgentApplication.send_request(...)yields events fromToolLoopRunner.run(...)and prepends arequest_startedevent. -
Application notification store:
core/python/agent_app/application_notifications.py ApplicationNotificationStoredefines lifecycle persistence.SQLiteApplicationNotificationStoreis the terminal/server runtime implementation.InMemoryApplicationNotificationStoresupports tests and embedding.-
Also handles cancellation and marks requests as completed.
-
Tool loop and streaming:
core/python/agent_app/tool_loop.py ToolLoopRunneremits request / assistant / tool / error events.-
Internal
session_checkpointevents include liveSessionobjects and are meant for in-process persistence only. -
HTTP exposure:
application/python/agent_terminal_app/server.py POST /sessions/{session_id}/sendstarts a background worker which callsAgentApplication.send_request(..., stream=True)and publishes all yielded events into the shared event store.GET /eventsreturns a window of events filtered bysince_id,session_id, and/orrequest_id.- When called with
session_idbut nosince_id, the server defaults to the per-session cursor (cursor_event_id) so that polling does not replay the entire history for that session. - When called with no
since_id, nosession_id, and norequest_id, the server returns an emptyeventslist and alatest_event_idfield suitable for bootstrapping polling without replaying existing events. - For non-bootstrap calls, the server applies a lightweight
compaction pass that drops redundant
partial/tool_partialevents before terminal events and merges consecutive partials for the same request into a single event.
- When called with
-
Session/message routes publish additional lifecycle events (
session_created,message_appended, etc.). -
Bridge forwarding (streaming):
application/python/agent_terminal_app/bridge_client.py:_forward_events_loop()polls the event store withsince_idand sends each new event as asession_eventPhoenix message on topicserver:<server_id>.
bridge-elixir/lib/bridge/server_channel.ex:handle_in("session_event", ...)re-broadcasts events viaPhoenix.PubSubon"events:server:<server_id>".
bridge-elixir/lib/bridge/app_channel.ex:- App sockets subscribe to
"events:server:<server_id>"when a pairing session is established. handle_info({:session_event, event}, ...)pushes the event to the app asevent: "session_event", payload: {"event": event}.
- App sockets subscribe to
- Mobile bridge client (
BridgeAdapter) receives thesesession_eventmessages and passes the raweventto listeners.
Common Fields
All externally visible events share a few common patterns:
event_id: str- Assigned by
InMemoryEventStore.publishwhen the event is stored. -
Used with
since_idin/eventsand the bridge forwarder. -
type: str -
Discriminator for the event’s schema.
-
session_id: str -
The logical session this event belongs to.
-
request_id: str - Present for request/streaming/tool events.
- Absent for pure session lifecycle events such as
session_created.
Additional fields depend on type and are described below.
Request Lifecycle Events
Defined in:
core/python/agent_app/application_future.pyAgentApplication.send_request(request_started, request_cancelled)core/python/agent_app/tool_loop.pyToolLoopRunner.run(request_completed, error)
request_started
Emitted once per logical request, before any streaming tokens or tool events.
{
"type": "request_started",
"session_id": "<session-id>",
"request_id": "<request-id>",
"stream": true
}
request_cancelled
Emitted when a request is cancelled via
AgentApplication.cancel_request(request_id).
{
"type": "request_cancelled",
"session_id": "<session-id>",
"request_id": "<request-id>"
}
request_completed
Emitted after the tool loop finishes successfully or with an error
(always the final event for a given request_id).
{
"type": "request_completed",
"session_id": "<session-id>",
"request_id": "<request-id>"
}
error
Represents a fatal error during the request/tool loop.
{
"type": "error",
"session_id": "<session-id>",
"request_id": "<request-id>",
"detail": "<human-readable message>"
}
Assistant Streaming Events
Defined in:
core/python/agent_app/tool_loop.py_run_llm_phase(assistant_message, partial, final)
assistant_message
Signals the lifecycle of a streamed assistant reply.
{
"type": "assistant_message",
"session_id": "<session-id>",
"request_id": "<request-id>",
"phase": "start" | "end"
}
partial
Streaming assistant chunks. Each partial event contains a single
assistant message chunk.
{
"type": "partial",
"session_id": "<session-id>",
"request_id": "<request-id>",
"message": {
"role": "assistant",
"content": "...", // may be incremental
"metadata": { /* optional, provider-specific */ }
}
}
final
Final assistant messages for a phase.
{
"type": "final",
"session_id": "<session-id>",
"request_id": "<request-id>",
"messages": [
{ "role": "assistant", "content": "...", "metadata": { /* ... */ } },
// possibly multiple assistant/tool/system messages
]
}
Note: Internal
finalevents inside the tool loop also carry a livesessionobject. That field is not required or relied upon by external consumers; the shared event store and bridge forwarding work with the serializable parts.
Tool Calling Events
Defined in:
core/python/agent_app/tool_loop.py_apply_tool_calls(tool_calls, tool_partial, tool_results)
tool_calls
Emitted when the LLM requests tools to be executed.
{
"type": "tool_calls",
"session_id": "<session-id>",
"request_id": "<request-id>",
"tool_calls": [
{
"id": "<call-id>", // or "tool_call_id"
"function": {
"name": "tool_name",
"arguments": "{...}" // JSON-encoded arguments
},
// additional provider-specific fields may be present
}
]
}
tool_partial
Optional streaming events for tool output when stream_tools=True.
{
"type": "tool_partial",
"session_id": "<session-id>",
"request_id": "<request-id>",
"tool_call_id": "<call-id>",
"phase": "start" | "stream" | "end",
"payload": {
// For "start" / "stream": chunk from iter_tool_messages (includes "part")
// For "end": { "result": <final tool message dict> }
}
}
tool_results
Summary of final tool messages appended to the session.
{
"type": "tool_results",
"session_id": "<session-id>",
"request_id": "<request-id>",
"messages": [
{
"role": "tool", // or another valid message role
"content": "...",
"metadata": {
"tool_call_id": "<call-id>",
"tool_name": "tool_name",
// provider- or tool-specific metadata
//
// Optional display helper used by terminal and other UIs.
// When present and display.type == "text", renderers prefer
// display.content over the raw message.content when showing
// tool output.
"display": {
"type": "text",
"content": "human-friendly, possibly truncated output"
}
}
}
]
}
Internal Checkpoint Events
Defined in:
core/python/agent_app/tool_loop.py_run_llm_phase,_apply_tool_calls
session_checkpoint (internal only)
These events are internal only and are used by the server and
terminal app to persist Session objects incrementally.
{
"type": "session_checkpoint",
"session_id": "<session-id>",
"request_id": "<request-id>",
"session": { /* live Session object; not intended for external use */ }
}
The application-layer session request coordinator
(core/python/agent_app/request_execution.py) consumes these events but does
not forward them to /events or the bridge as public API. An optional
in-process checkpoint callback lets terminal-style consumers track the live
Session. The package-local maintainer guide lives at
application/python/docs/development/request-execution.md.
The low-level application request flow persists request checkpoints and emits lightweight, serializable checkpoint marker events into the shared event store (described below).
checkpoint (marker event)
Defined in:
core/python/agent_app/application_future.py- request checkpoint persistence and
record_checkpoint core/python/agent_app/request_execution.py- shared request execution and internal checkpoint filtering
application/python/agent_terminal_app/server.py- non-request session/message lifecycle endpoints
Lightweight marker emitted whenever a session has just been persisted to disk, either:
- by the low-level application request flow after processing a request checkpoint, or
- after a non-streaming HTTP operation that modifies a session and saves it (append/modify/delete messages, create/fork sessions, certain agent updates).
{
"type": "checkpoint",
"session_id": "<session-id>",
"request_id": "<request-id>", // may be omitted for non-request ops
"timestamp": "2025-01-01T00:00:00Z"
}
Semantics:
checkpointevents are written to the sameEventStoreas all other events and therefore receive anevent_idon publish.- The server tracks the most recent session cursor event id per
session and exposes it as
cursor_event_idfromGET /sessions/{session_id}/messages. The cursor may correspond to theevent_idof acheckpointevent or to another event (for example, afinalormessage_appendedevent) whose effects are known to be reflected in the persisted session state. - Within a single LLM/tool phase,
ToolLoopRunneremits public events (such asfinalortool_results) before the internalsession_checkpointthat captures the updatedSession. When the server handles thissession_checkpointand records a correspondingcheckpointmarker, the resulting session cursor is always at or after the events that produced the last messages visible in the persisted session history. - A client that loads messages and then starts polling
GET /eventswithsince_id = cursor_event_idwill not receive events whose effects are already reflected in that snapshot.
Session & Message Lifecycle Events
Defined in application/python/agent_terminal_app/server.py:
create_session(POST /sessions)fork_session(POST /sessions/{session_id}/fork)add_message(POST /sessions/{session_id}/messages)update_message(PATCH /sessions/{session_id}/messages/{index})delete_messages(DELETE /sessions/{session_id}/messages)
These events are written into the same shared event store so that terminal and HTTP/mobile frontends see a unified history.
session_created
Emitted when a new session is created, either directly or via fork.
{
"type": "session_created",
"session_id": "<session-id>",
"agent_id": "default" | "<agent-id>",
"source_session_id": "<id>"?, // present for forked sessions
"session": { /* full serialized Session */ }
}
message_appended
Emitted when a user/system/assistant/tool message is appended.
{
"type": "message_appended",
"session_id": "<session-id>",
"role": "user" | "assistant" | "system" | "tool",
"content": "...",
"metadata": { /* per-message metadata, if any */ },
"message": { /* serialized Message */ },
"session": { /* full serialized Session after append */ }
}
message_modified
Emitted when a message’s content is modified.
{
"type": "message_modified",
"session_id": "<session-id>",
"index": 0,
"content": "new content",
"session": { /* full serialized Session after modification */ }
}
messages_deleted
Emitted when one or more messages are deleted from a session.
{
"type": "messages_deleted",
"session_id": "<session-id>",
"indices": [0, 1, 2],
"session": { /* full serialized Session after deletion */ }
}
Where These Events Are Consumed
- Terminal rendering:
-
application/python/agent_terminal_app/event_rendering.py- Renders
partial,final,tool_*,request_*, and error events into a human-readable terminal view. - For tool messages/results, prefers
metadata.display.content(whendisplay.type == "text") over the rawcontentfield, so tools can provide a dedicated, user-facing rendering separate from their full machine-oriented output.
- Renders
-
Terminal application logic:
-
application/python/agent_terminal_app/terminal_app.py- Uses the shared event store to poll and aggregate events for the
interactive terminal (see
poll_new_events,list_events).
- Uses the shared event store to poll and aggregate events for the
interactive terminal (see
-
HTTP API:
-
application/python/agent_terminal_app/server.pyGET /eventsexposes stored events for web/mobile clients, applying cursor-aware defaults and basic compaction as described above.
-
Bridge + mobile:
application/python/agent_terminal_app/bridge_client.py- Forwards stored events to the Elixir bridge via
session_event.
- Forwards stored events to the Elixir bridge via
bridge-elixir/lib/bridge/server_channel.ex- Receives
session_eventfrom servers and broadcasts via PubSub.
- Receives
bridge-elixir/lib/bridge/app_channel.ex- Pushes
session_eventmessages to app sockets.
- Pushes
mobile/crystal-lattice-control-rn/App.tsxBridgeAdapterreceivessession_eventand surfaces the raw event to listeners.
mobile/crystal-lattice-control-rn/src/useSessionEvents.ts- High-level hook that either consumes pushed events (bridge) or
polls
/eventsand passes them to screens.
- High-level hook that either consumes pushed events (bridge) or
polls
mobile/crystal-lattice-control-rn/src/SessionChatScreen.tsx- Currently logs per-session events; will later use them to drive live message-list updates.
Together, these components define the end-to-end contract for event streaming across terminal, HTTP, bridge, and mobile clients.