SSE Events
Server-Sent Events for streaming updates.
SSE (Server-Sent Events) are used for streaming AI responses and real-time updates from the server.
Chat Events
During a chat interaction, the system emits a stream of typed events.
Channel distinction: The server's
POST /chatSSE stream emitsChatChunkDTOobjects with types:text,tool_call,tool_result,error,done. The desktop app's IPC layer (chat:event) extends this with additional types (thinking,title-updated,tool_confirm) that are generated in the desktop main process — they are not part of the server SSE stream.
Server SSE Event Types (ChatChunkDTO)
| Type | Fields | Description |
|---|---|---|
text | text: string | A chunk of streaming text from the LLM. Append to the current response. |
tool_call | toolCall: { id, name, args, serverHandled? } | The LLM invoked a tool. |
tool_result | toolResult: { callId, result } | A tool call completed. |
error | error: string, code?: string | Fatal error — stream ends. code is a machine-readable classifier (e.g. 'QUOTA_EXCEEDED') when the underlying HTTP response carried one — see Quota-Exceeded Errors below. |
done | usage: { inputTokens, outputTokens }, quotaWarning?: 'approaching_limit' | 'at_limit' | Stream complete. quotaWarning is a proactive daily-quota signal — see Metering Module. |
Desktop IPC-only Event Types
These types exist only in the desktop chat:event IPC channel, not in the server SSE stream:
| Type | Fields | Description |
|---|---|---|
thinking | — | Processing heartbeat (desktop only). |
title-updated | title: string | Session title generated (desktop only). |
tool_confirm | confirmId, risk, explanation | Awaiting user tool approval (desktop only). |
contextUsage | (on done event) | Token usage appended by desktop to the done event. |
Server SSE Event Shape
interface ChatChunkDTO {
type: 'text' | 'tool_call' | 'tool_result' | 'error' | 'done'
text?: string
error?: string
toolCall?: { id: string; name: string; args: Record<string, unknown>; serverHandled?: boolean }
toolResult?: { callId: string; result: unknown }
usage?: { inputTokens: number; outputTokens: number }
quotaWarning?: 'approaching_limit' | 'at_limit' // on 'done'
}Note: a quota-exceeded rejection happens before the SSE stream starts at all (QuotaGuard runs pre-controller) — it arrives as a non-OK HTTP response with a JSON body, not as an in-stream error chunk. @cosmohq/core's client-side provider (packages/core/src/llm/providers/server.ts) parses that body and synthesizes the equivalent { type: 'error', code: 'QUOTA_EXCEEDED', quota: {...} } chunk so downstream consumers (the agentic loop, the desktop chat manager) see one consistent shape regardless of whether the failure was pre-stream or mid-stream.
Typical Event Sequence
A simple text response:
thinking → text → text → text → doneA response that calls tools:
thinking → text → tool_call → tool_result → thinking → text → text → doneMultiple tool calls in one turn:
thinking → tool_call → tool_call → tool_result → tool_result → thinking → text → doneRendering Guidance
When building a chat UI that consumes these events:
- On
thinking— Show a subtle loading indicator (e.g., animated dots with "Thinking" label). Remove it when the nexttextortool_callevent arrives. - On
text— Append to the current text segment. If the previous segment was athinkingindicator, replace it with a text bubble. - On
tool_call— Show an activity badge with the tool name and a spinner. Display alongside text segments, not replacing them. - On
tool_result— Mark the corresponding tool call badge as complete (spinner to checkmark). Optionally show the result preview on expand. - On
done— Finalize: combine all text segments into the persisted message, clear streaming state. IfcontextUsageis present, update the context meter UI. IfquotaWarningis present, show a proactive (non-blocking) warning once per threshold crossing — don't re-show it on every subsequentdonewhile still at the same level. - On
error— For most errors, save any partial text as a turn-failure notice (with a Retry affordance) rather than a plain chat message, so a failed turn is never mistaken for real AI output. Exception:code === 'QUOTA_EXCEEDED'— this is an expected plan limit, not a failure, and there's never partial text to preserve (the guard blocks the request before streaming starts). Route it to a dedicated "daily limit reached" notice instead, usingquota.resetAtfor messaging, and skip the Retry affordance entirely — retrying fails identically until the quota resets. See Usage and Quota for the user-facing behavior.
Context Usage
The done event may include a contextUsage field with the current context window consumption. Use this to display a context meter:
usedTokens— how many tokens the conversation is usingmaxTokens— the model's context window limit (e.g., 200,000 for Claude models)percentage— usage as a percentage
When percentage exceeds 80%, the engine may auto-compact older messages to free space. The next done event will reflect the reduced usage.
Quota-Exceeded Errors
When a user hits their daily token quota, POST /chat rejects with HTTP 403 before any SSE data is sent — see QuotaGuard. The client-side provider normalizes this into an error chunk carrying:
{
type: 'error'
error: string // friendly message, e.g. "Token quota exceeded for your FREE plan..."
code: 'QUOTA_EXCEEDED'
quota: { usedTokens: number; limitTokens: number; percentage: number; plan: string; resetAt?: string }
}Treat this as a distinct case, not a generic failure: there's nothing to retry until quota.resetAt, and the message should tell the user when that is (and, ideally, offer an upgrade path) rather than reading like a broken response.
Server-Handled Tool Calls
Some tool calls are executed server-side by the AI provider (e.g., Anthropic's web search). These follow the same event flow as regular tool calls but with key differences:
- The
tool_callevent has the same shape, but the engine does not execute the tool — the provider handles it - The
tool_resultevent is emitted by the provider once the server-side execution completes - The engine's agentic loop does not re-run for server-handled calls — they resolve within a single provider turn
Web search event sequence:
thinking → tool_call (web_search) → tool_result (search complete) → text → text → doneFrom the UI's perspective, server-handled tools look identical to regular tools — you show a spinner, then a checkmark. The difference is only in the engine layer.
Tool Name Formatting
Tool names follow conventions that can be formatted for display:
cosmo_tasks_create— Cosmo built-in tool. Format as "Tasks: Create"cosmo_artifacts_move— Format as "Artifacts: Move"mcp__notion__search— MCP connector tool. Format as "Notion: Search"web_search— Provider built-in tool. Format as "Web Search"