Event Streaming
How AGH records session events, streams them over SSE, and persists them in per-session SQLite databases.
- Audience
- Operators running durable agent work
- Focus
- Sessions guidance shaped for scanability, day-two clarity, and operator context.
Events are the durable record of a session. They drive:
- live session views in clients
- replay and transcript reconstruction
- turn history
- token usage tracking
- permission audit trails
AGH stores session events in a per-session SQLite database and exposes them through both query and SSE surfaces.
Two event surfaces
AGH exposes two different streaming models for session work:
| Surface | Endpoint | What you get |
|---|---|---|
| Persisted session stream | GET /api/sessions/:id/stream | Stored SessionEventPayload rows with stable sequence numbers and SSE replay support |
| Prompt stream | POST /api/sessions/:id/prompt | Raw live prompt events in the AI SDK UI stream format (x-vercel-ai-ui-message-stream: v1) |
Use the persisted session stream when you need auditability or reconnect support. Use the prompt stream when you are driving one interactive prompt call.
Outer persisted event envelope
Every stored event row is returned as SessionEventPayload:
{
"id": "evt-000042",
"session_id": "sess-1234",
"sequence": 42,
"turn_id": "turn-9abc",
"type": "tool_result",
"agent_name": "general",
"workspace_id": "repo-alpha",
"workspace_path": "/absolute/path/to/repo",
"content": {
"schema": "agh.session.event.v1",
"type": "tool_result",
"session_id": "acp-session-77",
"turn_id": "turn-9abc",
"timestamp": "2026-04-16T01:10:06Z",
"tool_call_id": "tool-call-1",
"tool_name": "Read",
"tool_result": {
"content": "package session"
},
"tool_error": false,
"raw": {
"status": "completed"
}
},
"timestamp": "2026-04-16T01:10:06Z"
}Two IDs matter here:
- outer
session_id: the stable AGH session ID - inner
content.session_id: the ACP runtime session ID carried by the underlying event payload
Those IDs can diverge after a resume that falls back to a fresh ACP session.
Event type catalog
The persisted event type values currently emitted by AGH are:
| Type | When it is recorded | Key content fields |
|---|---|---|
user_message | Before AGH submits a prompt to ACP | schema, type, session_id, turn_id, timestamp, text |
agent_message | Agent text chunk | text, plus the shared canonical fields |
thought | Agent reasoning chunk | text, plus the shared canonical fields |
tool_call | Tool call started or updated before completion | title, tool_name, tool_call_id, optional tool_input, optional raw |
tool_result | Tool call completed or failed | tool_call_id, tool_name, tool_result, tool_error, optional raw |
plan | ACP plan update | optional raw; AGH stores the canonical envelope even when the payload is sparse |
permission | ACP permission request or resolved decision | request_id, action, resource, decision, title, tool_call_id, raw |
usage | Token or context usage update | usage with token counts, context counts, cost fields, and timestamp |
runtime_progress | Low-frequency long-running prompt progress update | text, runtime activity payload |
runtime_warning | Runtime supervision warning or inactivity timeout notice | text, runtime activity payload, optional raw |
system | Available-command updates, mode updates, or other non-chat ACP system updates | title, optional raw |
done | End of a prompt turn | stop_reason, optional usage, optional raw |
error | Prompt processing error | error, optional raw |
session_stopped | Session finalization | stop_reason, optional failure, optional error, optional text |
Runtime supervision events
runtime_progress and runtime_warning are separate from agent_message. They are progress
projections for clients and bridges, not assistant-authored text to append to the final answer.
AGH does not write heartbeat ticks into the event log. Heartbeats update session liveness metadata; only lower-frequency progress, warning, and timeout notices become stored events.
Example runtime_progress content:
{
"schema": "agh.session.event.v1",
"type": "runtime_progress",
"turn_id": "turn-9abc",
"timestamp": "2026-04-16T02:10:06Z",
"text": "Still working... (10 min elapsed)",
"runtime": {
"turn_id": "turn-9abc",
"turn_source": "user",
"last_activity_at": "2026-04-16T02:10:06Z",
"last_activity_kind": "agent_waiting",
"last_activity_detail": "waiting for session/prompt response",
"current_tool": "Bash",
"tool_call_id": "tool-call-1",
"last_progress_at": "2026-04-16T02:10:06Z",
"idle_seconds": 0,
"elapsed_seconds": 600
}
}runtime_warning is emitted once when session.supervision.inactivity_warning_after is crossed.
If session.supervision.inactivity_timeout is crossed, AGH cancels the active prompt
cooperatively. If the prompt does not finish within timeout_cancel_grace, AGH stops the session
with stop reason timeout and stop detail activity_timeout.
Shared canonical fields inside content
The stored canonical envelope can include these fields when they apply:
| Field | Meaning |
|---|---|
schema | Currently agh.session.event.v1 |
type | Inner event type |
session_id | ACP session ID from the runtime event |
turn_id | Prompt turn ID |
request_id | Permission request ID |
timestamp | Event timestamp |
text | User text, assistant text, or thought text |
title | Tool or update title |
tool_name | Derived tool name |
tool_call_id | Stable tool correlation ID |
tool_input | Structured tool input when present |
tool_result | Structured tool output (stdout, stderr, file_path, content, structured_patch, error, raw_output) |
tool_error | Whether AGH classified the tool result as failed |
stop_reason | Turn or session termination reason |
failure | Typed lifecycle failure object with kind, redacted summary, and optional crash_bundle_path |
action | Permission action name |
resource | Permission resource target |
decision | Permission decision |
error | Error text |
usage | Token and cost payload |
runtime | Long-running prompt activity payload with current tool, last activity, progress, idle, and elapsed fields |
raw | Raw ACP update payload AGH preserved |
Querying stored events
Read recent events from the CLI:
agh session events sess-1234 --last 20Filter to one type:
agh session events sess-1234 --type tool_callGroup by turn:
agh session history sess-1234The HTTP query surfaces accept these filters:
typeagent_nameturn_idsincelimitafter_sequence
For HTTP, since must be RFC3339 or RFC3339Nano. The CLI is more convenient: agh session events --since 5m converts the duration into an absolute UTC timestamp before calling the API.
Subscribing over SSE
Follow persisted events live:
agh session events sess-1234 --followOpen the raw SSE stream directly:
curl -N http://localhost:2123/api/sessions/sess-1234/streamThe stream writes:
id:as the persisted event sequence numberevent:as the persisted event typedata:as oneSessionEventPayload
Example SSE frame:
id: 42
event: tool_result
data: {"id":"evt-000042","session_id":"sess-1234","sequence":42,"turn_id":"turn-9abc","type":"tool_result","agent_name":"general","content":{"schema":"agh.session.event.v1","type":"tool_result","tool_call_id":"tool-call-1","tool_result":{"content":"package session"}},"timestamp":"2026-04-16T01:10:06Z"}Reconnect and backlog replay
Reconnect from a known sequence:
curl -N \
-H "Last-Event-ID: 42" \
http://localhost:2123/api/sessions/sess-1234/streamLast-Event-ID must be a numeric persisted sequence. AGH first replays backlog rows after that
sequence and then continues polling for new rows.
If the session is already stopped and no new stored rows arrive, AGH emits a terminal synthetic
session_stopped SSE event and closes the stream. When the stopped session has failure
diagnostics, that terminal payload includes the same failure object stored in session metadata.
Example terminal failure payload:
{
"id": "session-stopped-sess-1234",
"session_id": "sess-1234",
"type": "session_stopped",
"stop_reason": "agent_crashed",
"failure": {
"kind": "process_exit",
"summary": "provider exited with status 1",
"crash_bundle_path": "/Users/you/.agh/logs/crash-bundles/sess-1234-process_exit-1770000000000000000.json"
},
"timestamp": "2026-04-16T01:10:06Z"
}SQLite persistence
Each session stores its durable history here:
~/.agh/sessions/<session-id>/events.dbThe per-session database contains:
| Table | Purpose |
|---|---|
events | Every persisted session event row |
token_usage | One merged usage record per turn |
hook_runs | Lifecycle hook execution history for that session |
The events table stores:
idsequenceturn_idtypeagent_namecontenttimestamp
Queries are returned in ascending sequence order. If you request limit, AGH selects the newest
matching rows first and then re-sorts them ascending before returning them.
Relationship to replay
The resume and replay path depends on this stored event log:
agh session historygroups the stored rows byturn_idGET /api/sessions/:id/transcriptassembles canonical replay messages from these rows- resume validation requires
events.dbto exist and contain at least one event
Next steps
- Use Session Lifecycle to understand how stop and resume interact with the state machine.
- Use Permissions to understand the approval events you will see in this stream.