Skip to content

Chat Spec

The chat layer defines the message exchange and agent metadata used by both the protocol-level wire stream and the UI's ChatMessage[] state.

ChatMessage

Field Type Required Notes
id string yes Stable identity across re-renders
role 'user' \| 'assistant' \| 'tool' \| 'system' yes UI-rendered role
content string yes Plain-text body (UI may render as Markdown)
parts ContentPart[] no Structured content (Wave 1). When present it is authoritative and content is its plain-text projection; when absent the message is text-only. Producers of multimodal messages MUST still fill content
createdAt string (ISO 8601) yes UTC timestamp
chatId string no Owning chat
agentId string no Owning agent (multi-agent mode)
toolCalls ToolCall[] no Calls emitted by the assistant
toolResult ToolResult no Result attached inline (or use a separate tool row)
updatedAt string (ISO 8601) no Last edit time

additionalProperties is enabled. UI consumers (e.g. deeppath/apps/web) layer additional render-only fields on top via intersection types — those don't go on the wire.

ContentPart

Field Type Required Notes
type 'text' \| 'image' yes Part discriminant
text string no Text payload; present iff type == "text"
url string no Remote image source (type == "image")
data string no Base64 image payload (type == "image", inline)
mediaType string no Image MIME type; accompanies data, advisory for url

An image part carries exactly one of url / data. The runtime's LLMMessage.content mirrors this as list[ContentPart] (TextPart / ImagePart); providers serialize text-only messages in the legacy string shorthand, so text-only wire bytes are unchanged.

ChatAgent

Field Type Required Notes
id string yes
name string yes Display name
createdAt string yes
updatedAt string yes
rolePrompt string no System-level role prompt
forbiddenPrompt string no Negative constraints
skillIds string[] no Tool / skill allowlist
allowExternalSkills boolean no Permit any registered skill
isBuiltin boolean no Hidden from user-facing agent picker
isArchived boolean no
sortOrder number no
icon string no Emoji or icon URL
color string no Hex color for the agent badge

The schema is intentionally broad — products layer their own routing, billing, and access control on top via the metadata escape hatch (open for forward-compatibility).

Conversational ownership

Every ChatMessage belongs to at most one ChatAgent. In a multi-agent orchestration run, the agent SSE event flips the "currently speaking" agent identity, and downstream content events inherit that ownership until the next agent event.

Variants (UI extension)

The framework's protocol intentionally does not define activeVariantId / variantsCount — those are UI-layer concepts (ChatGPT-style "regenerate" branches) carried in @steerable/agent-ui's extended message type. They live in your ChatMessage extension, not on the wire.

Example exchange

[
  {"id":"m1","role":"user","content":"Find me a Mars mission timeline","createdAt":"2026-05-14T10:00:00Z"},
  {"id":"m2","role":"assistant","agentId":"researcher","content":"Sure — searching…","createdAt":"2026-05-14T10:00:01Z",
   "toolCalls":[{"id":"c1","name":"web_search","arguments":{"q":"Mars mission timeline"}}],
   "toolResult":{"success":true,"data":{"results":[]}}},
  {"id":"m3","role":"assistant","agentId":"researcher","content":"Here's a 2024-2030 plan: …","createdAt":"2026-05-14T10:00:05Z"}
]