Cosmo Docs
API Reference

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 /chat SSE stream emits ChatChunkDTO objects 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)

TypeFieldsDescription
texttext: stringA chunk of streaming text from the LLM. Append to the current response.
tool_calltoolCall: { id, name, args, serverHandled? }The LLM invoked a tool.
tool_resulttoolResult: { callId, result }A tool call completed.
errorerror: string, code?: stringFatal 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.
doneusage: { 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:

TypeFieldsDescription
thinking—Processing heartbeat (desktop only).
title-updatedtitle: stringSession title generated (desktop only).
tool_confirmconfirmId, risk, explanationAwaiting 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 → done

A response that calls tools:

thinking → text → tool_call → tool_result → thinking → text → text → done

Multiple tool calls in one turn:

thinking → tool_call → tool_call → tool_result → tool_result → thinking → text → done

Rendering Guidance

When building a chat UI that consumes these events:

  1. On thinking — Show a subtle loading indicator (e.g., animated dots with "Thinking" label). Remove it when the next text or tool_call event arrives.
  2. On text — Append to the current text segment. If the previous segment was a thinking indicator, replace it with a text bubble.
  3. On tool_call — Show an activity badge with the tool name and a spinner. Display alongside text segments, not replacing them.
  4. On tool_result — Mark the corresponding tool call badge as complete (spinner to checkmark). Optionally show the result preview on expand.
  5. On done — Finalize: combine all text segments into the persisted message, clear streaming state. If contextUsage is present, update the context meter UI. If quotaWarning is present, show a proactive (non-blocking) warning once per threshold crossing — don't re-show it on every subsequent done while still at the same level.
  6. 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, using quota.resetAt for 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 using
  • maxTokens — 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_call event has the same shape, but the engine does not execute the tool — the provider handles it
  • The tool_result event 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 → done

From 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"