Files
iris_x_hermes/docs/protocol/frames.schema.json
T

113 lines
12 KiB
JSON

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Iris x Hermes wire protocol",
"description": "Machine-readable description of the JSON frames exchanged over the gateway WebSocket. Mirrors gateway-plugin/protocol.py and docs/04-wire-protocol.md. Media travels as binary WS frames referenced by header/end frames.",
"protocol_version": 1,
"envelope": {
"type": "object",
"required": ["v", "type"],
"properties": {
"v": { "type": "integer", "const": 1, "description": "Protocol version." },
"id": { "type": "integer", "description": "Request id; present on requests and their responses/acks. Absent on pure events." },
"type": { "type": "string", "description": "Frame type (see frame_types)." },
"chat_id": { "type": "string", "description": "Optional chat scope (e.g. android:default, android:chan_7)." },
"thread_id": { "type": "string", "description": "Optional thread scope within a chat_id." },
"payload": { "type": "object", "description": "Type-specific payload." }
}
},
"frame_types": {
"server_to_app": {
"hello.ack": {
"description": "Pairing succeeded.",
"payload": {
"server_caps": { "type": "object", "properties": { "streaming": {"type":"boolean"}, "reasoning": {"type":"boolean"}, "tools": {"type":"boolean"}, "media": {"type":"boolean"}, "search": {"type":"boolean"}, "push": {"type":"string","enum":["fcm","ntfy","none"]}, "push_ntfy_server": {"type":"string","description":"ntfy server URL for the app's listener; empty string when the backend is not ntfy."}, "pickers": {"type":"boolean"} } },
"sync_cursor": { "type": "integer" },
"channels": { "type": "array", "items": { "$ref": "#/definitions/channel" } }
}
},
"message": {
"description": "A final / standalone message.",
"payload": {
"message_id": { "type": "string" },
"role": { "type": "string", "enum": ["user", "assistant", "system", "cron"] },
"text": { "type": "string" },
"reasoning": { "type": "string", "description": "Optional; render ABOVE text." },
"media": { "type": "array", "items": { "$ref": "#/definitions/media_ref" } },
"reply_to": { "type": "string" },
"model": { "type": "string" },
"tokens": { "type": "integer" },
"ts": { "type": "integer", "description": "epoch millis" }
}
},
"message.start": { "payload": { "message_id": { "type": "string" }, "role": { "type": "string" } } },
"message.update": { "payload": { "message_id": { "type": "string" }, "text": { "type": "string", "description": "Full current text (app replaces)." } } },
"message.stop": { "payload": { "message_id": { "type": "string" }, "final_text": { "type": "string" }, "reasoning": { "type": "string" }, "model": { "type": "string" }, "tokens": { "type": "integer" }, "ts": { "type": "integer" } } },
"commentary": { "description": "Intermediate assistant beat.", "payload": { "message_id": { "type": "string" }, "text": { "type": "string" } } },
"tool.start": { "payload": { "index": { "type": "integer" }, "name": { "type": "string" }, "preview": { "type": "string" }, "args": { "type": "object" } } },
"tool.progress": { "payload": { "index": { "type": "integer" }, "name": { "type": "string" }, "note": { "type": "string" } } },
"tool.end": { "payload": { "index": { "type": "integer" }, "name": { "type": "string" }, "ok": { "type": "boolean" }, "duration": { "type": "number" }, "output_preview": { "type": "string" } } },
"typing": { "payload": { "on": { "type": "boolean" } } },
"notification": { "payload": { "kind": { "type": "string", "enum": ["channel_renamed", "channel_created", "channel_deleted", "cron", "approval", "clarify", "generic"] }, "title": { "type": "string" }, "body": { "type": "string" }, "ts": { "type": "integer" } } },
"channel.list": { "description": "Full channel directory (response to a channel.list request).", "payload": { "channels": { "type": "array", "items": { "$ref": "#/definitions/channel" } } } },
"channel.created": { "payload": { "$ref": "#/definitions/channel" } },
"channel.renamed": { "description": "Also the response to channel.set_default (carries the full entry incl. is_default).", "payload": { "$ref": "#/definitions/channel" } },
"channel.deleted": { "payload": { "chat_id": { "type": "string" } } },
"search.results": { "payload": { "query": { "type": "string" }, "scope": { "type": "string", "enum": ["all", "chat"] }, "hits": { "type": "array", "items": { "type": "object", "properties": { "message_id": {"type":"string"}, "chat_id": {"type":"string"}, "thread_id": {"type":["string","null"]}, "role": {"type":"string"}, "snippet": {"type":"string"}, "ts": {"type":"integer"} } } } } },
"media.offer": { "description": "Agent-sent media available; app pulls bytes.", "payload": { "$ref": "#/definitions/media_ref" } },
"read.receipt": { "description": "Agent received and started processing the user's message; app shows ✓✓ on user bubbles. Emitted to the originating connection when a message.send is accepted for processing.", "payload": { "message_id": { "type": "string" } } },
"status": { "description": "Gateway health state; broadcast to all connected clients at startup (state=online).", "payload": { "state": { "type": "string", "enum": ["online", "restarting", "degraded"] } } },
"error": { "payload": { "code": { "type": "string", "enum": ["auth", "not_found", "rate_limited", "media_too_large", "unsupported", "internal"] }, "message": { "type": "string" } } },
"pong": { "payload": { "ts": { "type": "integer" } } },
"sync.done": { "payload": { "cursor": { "type": "integer" } } },
"media.pull.end": { "payload": { "ok": { "type": "boolean" } } },
"media.upload.ack": { "description": "Response to media.upload.end; ref is cached and usable in message.send media_refs.", "payload": { "ok": { "type": "boolean" }, "media_ref": { "type": "string" } } }
},
"app_to_server": {
"hello": { "description": "First frame; auth + caps.", "payload": { "token": { "type": "string" }, "device_id": { "type": "string" }, "device_name": { "type": "string" }, "caps": { "type": "object", "properties": { "min_protocol": {"type":"integer"}, "media": {"type":"boolean"}, "push": {"type":"string"} } }, "fcm_token": { "type": "string" }, "ntfy_topic": { "type": "string" } } },
"message.send": { "payload": { "text": { "type": "string" }, "reply_to": { "type": "string" }, "media_refs": { "type": "array", "items": { "type": "string" } } } },
"media.upload.start": { "payload": { "media_ref": { "type": "string" }, "kind": { "$ref": "#/definitions/kind" }, "mime": { "type": "string" }, "size": { "type": "integer" }, "filename": { "type": "string" } } },
"media.upload.end": { "payload": { "media_ref": { "type": "string" }, "sha256": { "type": "string" } } },
"media.pull": { "payload": { "media_id": { "type": "string" } } },
"channel.create": { "payload": { "name": { "type": "string" }, "kind": { "type": "string", "enum": ["channel", "thread"] }, "parent_chat_id": { "type": ["string", "null"] } } },
"channel.rename": { "payload": { "name": { "type": "string" } } },
"channel.set_default": { "payload": {} },
"channel.delete": { "payload": {} },
"channel.list": { "description": "Request the full channel directory; answered by the server_to_app channel.list frame.", "payload": {} },
"search": { "payload": { "query": { "type": "string" }, "scope": { "type": "string", "enum": ["all", "chat"] }, "chat_id": { "type": "string" }, "thread_id": { "type": "string" }, "limit": { "type": "integer", "description": "Optional; server default 20." } } },
"sync": { "description": "Reconnect catch-up; replays undelivered outbox frames only (not full history).", "payload": { "cursor": { "type": "integer" } } },
"fcm.register": { "payload": { "fcm_token": { "type": "string" }, "ntfy_topic": { "type": "string" } } },
"ping": { "payload": { "ts": { "type": "integer" } } }
}
},
"definitions": {
"kind": { "type": "string", "enum": ["image", "audio", "video", "document", "voice"] },
"channel": { "type": "object", "properties": { "chat_id": {"type":"string"}, "name": {"type":"string"}, "kind": {"type":"string","enum":["default","channel","thread"]}, "parent_chat_id": {"type":["string","null"]}, "is_default": {"type":"boolean"}, "archived": {"type":"boolean"} } },
"media_ref": { "type": "object", "properties": { "media_id": {"type":"string"}, "kind": { "$ref": "#/definitions/kind" }, "mime": {"type":"string"}, "size": {"type":"integer"}, "filename": {"type":"string"}, "message_id": {"type":"string","description":"Optional; set on media.offer to associate the offer with the assistant message it belongs to."} } }
},
"x-planned-frames": [
{ "name": "picker.model", "direction": "server_to_app", "note": "Model/provider picker prompt. Planned, not implemented." },
{ "name": "picker.choice", "direction": "server_to_app", "note": "Generic choice picker prompt. Planned, not implemented." },
{ "name": "picker.clarify", "direction": "server_to_app", "note": "Clarify picker prompt. Planned, not implemented (clarifies arrive as notification + message)." },
{ "name": "picker.approval", "direction": "server_to_app", "note": "Approval picker prompt. Planned, not implemented (approvals arrive as notification)." },
{ "name": "picker.confirm", "direction": "server_to_app", "note": "Confirmation picker prompt. Planned, not implemented." },
{ "name": "picker.select", "direction": "app_to_server", "note": "Picker answer. Planned, not implemented." },
{ "name": "history", "direction": "server_to_app", "note": "Paged history response. Planned, not implemented (catch-up is sync/outbox replay)." },
{ "name": "history", "direction": "app_to_server", "note": "Paged history request. Planned, not implemented (catch-up is sync/outbox replay)." },
{ "name": "commands.catalog", "direction": "server_to_app", "note": "Slash-command catalog. Planned, not implemented." },
{ "name": "commands.catalog", "direction": "app_to_server", "note": "Slash-command catalog request. Planned, not implemented." },
{ "name": "commands.complete", "direction": "server_to_app", "note": "Slash-command autocomplete. Planned, not implemented." },
{ "name": "commands.complete", "direction": "app_to_server", "note": "Slash-command autocomplete request. Planned, not implemented." },
{ "name": "agent.busy", "direction": "server_to_app", "note": "Agent-busy indicator. Planned, not implemented (typing frames cover it)." },
{ "name": "agent.idle", "direction": "server_to_app", "note": "Agent-idle indicator. Planned, not implemented." },
{ "name": "agent.stop", "direction": "app_to_server", "note": "Abort current agent turn. Planned, not implemented." },
{ "name": "agent.steer", "direction": "app_to_server", "note": "Steer the agent mid-turn. Planned, not implemented." },
{ "name": "read.receipt", "direction": "app_to_server", "note": "User-viewed receipt (multi-device read state). Planned, not implemented (read.receipt is server→app only)." }
],
"reliability": {
"ordering": "Per-connection (TCP/WS). message.update for a message_id is monotonic; app may coalesce to latest.",
"never_dropped": ["message", "message.stop", "tool.end", "notification", "channel.*", "search.results", "error"],
"coalescable_under_backpressure": ["message.update", "tool.progress"],
"offline": "Undelivered frames go to the outbox; replayed by sync. Terminal frames always outboxed."
}
}