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"}
]