21 KiB
Keryx — Domain Language & Conventions
Purpose: Define every term, convention, and naming rule used across Keryx (iOS app + BFF). The iOS app design, code, UI labels, and deep links must match this document exactly. If a term appears in the UI, a GraphQL query, or a deep link, it is defined here.
Table of Contents
- Core Concepts
- UI Component Taxonomy
- Page Structure
- Navigation & Deep Links
- SSE Streaming Events
- GraphQL Conventions
- UI Labels & Copy
- Iconography
- State Terminology
- Conventions
- Appendix: Hermes API Mapping
1. Core Concepts
Keryx
The iOS/iPadOS app. Named after the keryx (κήρυξ) — the ancient Greek herald who precedes the main messenger (Hermes). The app is the herald that announces what Hermes has to say.
Keryx BFF
The Python FastAPI + Strawberry GraphQL backend that the iOS app communicates with. Owns the app experience — drives content, layout, labels, and navigation. Not a thin proxy.
Hermes Agent
The underlying AI agent framework running on the user's server. Keryx never talks to Hermes directly; all communication goes through the Keryx BFF.
Session
A single conversation with Hermes. Has an ID, a model, a start time, messages, and token usage. Sessions are created and managed by the Hermes API server. Keryx references them by their Hermes session ID.
Conversation
Synonym for Session in user-facing contexts. In code and APIs, use session. In UI labels, prefer "Conversation" or "Chat".
Message
A single turn in a session. Has a role (user or assistant), content, and a timestamp. Messages are never deleted by Keryx.
Component
A single piece of UI rendered inside a page feed. Every component has a type, an id, data, and actions. See UI Component Taxonomy for the full list.
Feed
The ordered array of items within a page. The iOS app renders components in array order, left-to-right, top-to-bottom.
Page
A full-screen view presented by the iOS app. Every page has the structure {header, items[], footer}. The BFF builds pages; the iOS app renders them.
Workspace
A directory on the Hermes server containing project files the user interacts with. Defined by a single root path in the BFF config. Read-only.
Deep Link
A keryx:// URL that the iOS app uses for all navigation and actions. No screen transitions happen without a deep link. See Navigation & Deep Links.
2. UI Component Taxonomy
Every component has this shape:
{
"type": "component_type_name",
"id": "unique_identifier",
"data": { ... },
"actions": [
{"type": "tap|swipe_left|swipe_right", "label": "Action", "style": "normal|destructive", "link": "keryx://..."}
],
"streaming": false
}
streaming is true when the component is still receiving data via the SSE stream. The iOS app may show a shimmer/typing indicator for streaming components.
2.1 session_card
A session/conversation in the sessions list.
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Hermes session ID |
title |
String? | ❌ | Session title. null if untitled. |
model_badge |
String? | ❌ | Model name shown as a badge (e.g. "deepseek-v4-flash") |
message_count |
Int | ✅ | Total messages in the session |
timestamp |
String | ✅ | Relative time string (e.g. "2m ago", "Yesterday") — generated by BFF |
preview |
String? | ❌ | First ~80 chars of the session for preview |
Display rules:
- If
titleisnull, show thepreviewas the primary text (truncated to 1 line) - If
titleis set, showtitleas primary,previewas secondary model_badgeshown as a rounded pilltimestampright-aligned- Swipe left reveals "Delete" (destructive)
2.2 message_bubble
A single message in a conversation.
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Message ID from Hermes |
role |
Enum | ✅ | "user" or "assistant" |
text |
String? | ❌ | Message content. May be empty during streaming. |
streaming |
Bool | ✅ | true while assistant is still generating |
timestamp |
Float? | ❌ | Unix timestamp |
model |
String? | ❌ | Model used for this response (assistant only) |
attachments |
[Attachment]? | ❌ | Files attached to this message |
Attachment:
{
"name": "report.pdf",
"size": 245760,
"mime_type": "application/pdf"
}
Display rules:
- User messages: right-aligned, tinted background
- Assistant messages: left-aligned, default background
modelshown as a small label above assistant messages- Streaming messages show a blinking cursor at end of text
2.3 thinking_block
Collapsible reasoning from the assistant.
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Unique ID |
text |
String | ✅ | Reasoning content |
collapsed_by_default |
Bool | ❌ | Default: true |
Display rules:
- Tappable header "Thinking..."
- Expand/collapse animation
- Grey/dim text when collapsed
- Header shows token count if available
2.4 tool_call_card
A tool invocation by Hermes (terminal, web search, file read, etc.).
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Unique ID |
tool |
String | ✅ | Tool name (e.g. "terminal", "web_search") |
command |
String? | ❌ | The command/query |
status |
Enum | ✅ | "running", "completed", "failed" |
output |
String? | ❌ | Tool output (truncated) |
duration_ms |
Int? | ❌ | Execution time |
Display rules:
- Show tool icon + name as a compact card
- Show command as monospaced text
- Status indicator: spinner (running), checkmark (completed), X (failed)
- Tappable to expand full output (post-MVP)
2.5 file_row
A file or directory in the workspace browser.
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | File path (used as identifier) |
name |
String | ✅ | File/directory name |
is_directory |
Bool | ✅ | True if directory |
size |
Int? | ❌ | File size in bytes (null for directories) |
mime_type |
String? | ❌ | MIME type for files |
modified_at |
String? | ❌ | Relative time string |
children |
[FileRow]? | ❌ | When depth > 0, nested files |
Display rules:
- Directory icon (folder) or file icon based on type
- Tap on directory → navigates into it
- Tap on file → opens preview (post-MVP: in-app viewer)
- Indentation for nested children
2.6 code_block
A rendered code snippet inside a message_bubble. When Hermes returns markdown with code fences, the BFF extracts them into standalone components.
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Unique ID |
language |
String? | ❌ | Language identifier (e.g. "python", "json") |
code |
String | ✅ | The code content |
streaming |
Bool | ✅ | true while still receiving code |
Display rules:
- Monospaced font, syntax highlighting (SF Mono)
- Language badge in top-right
- Copy button in top-right (copy to clipboard)
- Scrollable horizontal for long lines
2.7 divider
A visual separator.
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Unique ID |
label |
String? | ❌ | Optional text in the divider |
Display rules:
- Thin horizontal line
- If
labelpresent, text centered in the line
2.8 compose_bar
The chat input area. Only appears in the footer of a chat page, never in items.
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Always "compose" |
placeholder |
String | ✅ | Placeholder text (e.g. "Ask something...") |
actions |
ComposeActions | ✅ | Deep links for send, attach, voice |
disabled |
Bool | ✅ | true while a message is being sent/streaming |
ComposeActions:
{
"send": "keryx://chat/{id}/send",
"attach": "keryx://chat/{id}/attach",
"voice": "keryx://chat/{id}/voice"
}
Display rules:
- Fixed at bottom of screen
- Text field + send button + attach button + voice button
- Disabled state: greyed out, non-interactive
- Shows attachment chip when a file is staged
3. Page Structure
Every page response has this structure:
{
"page": {
"id": "string",
"header": { ... } | null,
"items": [ ... ],
"footer": { ... } | null
}
}
Header
{
"title": "Conversations",
"subtitle": "82 total",
"actions": [
{"icon": "plus", "label": "New Chat", "link": "keryx://new-chat"}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
title |
String? | ❌ | Main header text |
subtitle |
String? | ❌ | Smaller text below title |
actions |
[LinkAction]? | ❌ | Right-side action buttons |
Footer
The footer is either null (no footer) or contains the compose bar for chat pages. There is no pagination — all lists load in full.
// Chat page footer
{
"type": "compose_bar",
"placeholder": "Ask something...",
"actions": {
"send": "keryx://chat/{id}/send",
"attach": "keryx://chat/{id}/attach",
"voice": "keryx://chat/{id}/voice"
},
"disabled": false
}
| Type | Description |
|---|---|
compose_bar |
Chat input area (chat page only) |
null |
No footer (settings page, workspace, sessions list) |
Empty State
When items is empty, the BFF may include an empty_state field at the page level (not inside header):
{
"page": {
"id": "sessions",
"header": {"title": "Conversations"},
"empty_state": {
"icon": "message",
"title": "No conversations yet",
"subtitle": "Start a new chat to begin",
"action": {"label": "Start Chat", "link": "keryx://new-chat"}
},
"items": [],
"footer": null
}
}
The iOS app shows the empty state view when items.length === 0 and empty_state is present.
4. Navigation & Deep Links
All navigation is driven by keryx:// deep links. The iOS app registers the keryx URL scheme and routes links to screens. No screen transitions are hardcoded.
Link Format
keryx://<action>/<param>?query=val
Deep Link Registry
| Link | Action | Description |
|---|---|---|
keryx://sessions |
Present | Sessions list page |
keryx://new-chat |
Present | Create new session → navigate to chat |
keryx://chat/{session_id} |
Present | Open chat view for an existing session |
keryx://chat/{session_id}/send |
Action | Send message (POST) — triggered by compose bar |
keryx://chat/{session_id}/attach |
Action | Attach file (POST) — triggered by compose bar |
keryx://chat/{session_id}/voice |
Action | Start voice input — triggered by compose bar |
keryx://delete/{session_id} |
Action | Delete a session |
keryx://rename/{session_id} |
Action | Rename a session |
keryx://workspace |
Present | Workspace file browser page |
keryx://workspace/{path} |
Present | Navigate into a directory in workspace |
keryx://workspace/read/{path} |
Present | Preview a workspace file |
keryx://settings |
Present | Settings page |
keryx://release-notes |
Present | Release notes / changelog |
keryx://{anything} |
Fallback | Open in system browser or show error |
Navigation Rules
- Present links push a new page onto the navigation stack (iPhone) or select a column (iPad)
- Action links trigger a side effect (POST, DELETE) and are handled without navigation
- The app never constructs URLs dynamically — all links come from BFF responses
- Unknown link schemes are silently dropped (app doesn't crash)
5. SSE Streaming Events
The SSE protocol for POST /v1/ios/chat/{session_id}/stream.
Event: component
Sent when a new UI component should be appended to the feed.
event: component
data: {"type": "message_bubble", "id": "msg_1", "data": {"role": "user", "text": "Hello"}}
iOS action: Append a new FeedComponent to items[]. Set streaming: true if the component may receive further updates.
Event: chunk
Sent when text content should be appended to an existing component.
event: chunk
data: {"component_id": "msg_2", "text": "def fibonacci"}
iOS action: Find component by component_id in items[]. Append text to its data.text field. Re-render in place.
Event: component_patch
Sent when a component's metadata should be updated (e.g. tool status change).
event: component_patch
data: {"id": "tc_1", "data": {"status": "completed", "output": "src/\ntests/"}}
iOS action: Find component by id. Deep-merge data into the component's existing data. Fields not in data are preserved.
Event: component_complete
Sent when a component is finalised (no more updates).
event: component_complete
data: {"id": "msg_2", "usage": {"input_tokens": 16267, "output_tokens": 20}}
iOS action: Find component by id. Set streaming: false. Merge any additional data fields (e.g. usage).
Event: done
Sent when the stream is complete.
event: done
data: {}
iOS action: Close SSE connection. Re-enable the send button. The compose_bar in the footer should set disabled: false.
Error Handling
If the BFF encounters an error mid-stream:
event: error
data: {"code": "hermes_unreachable", "message": "Hermes API server is not responding"}
iOS action: Show inline error banner at the bottom of the feed. The stream is not closed — the user can retry.
6. GraphQL Conventions
Naming
| Convention | Rule | Example |
|---|---|---|
| Queries | camelCase |
appData, sessionList |
| Mutations | camelCase, verb-first |
sendMessage, deleteSession |
| Types | PascalCase |
AppData, SessionCard |
| Fields | camelCase |
modelBadge, messageCount |
| Enums | PascalCase |
MessageRole, ToolStatus |
| Enum values | SCREAMING_SNAKE_CASE |
USER, ASSISTANT, COMPLETED |
Union Types for Feed Components
The FeedComponent union type uses Strawberry's @strawberry.type with @strawberry.union. The iOS app receives the __typename field to determine which fragment to use:
query SessionList {
page(id: "sessions") {
items {
__typename
... on SessionCard {
id title modelBadge messageCount timestamp preview
}
... on Divider {
id label
}
}
}
}
Pagination
No pagination. All lists load in full — sessions, workspace trees, and chat message histories. The page query returns every item at once.
7. UI Labels & Copy
Standard Labels
| Context | Label | Notes |
|---|---|---|
| App display name | Keryx | Not "Keryx iOS" or "Keryx Client" |
| Tab: sessions | Chats | User-friendly term |
| Tab: workspace | Files | |
| Tab: settings | Settings | |
| New chat button | New Chat | |
| Delete action | Delete | Destructive |
| Rename action | Rename | |
| Send button | (None — icon only) | Paper plane icon |
| Attach button | (None — icon only) | Paperclip icon |
| Voice button | (None — icon only) | Microphone icon |
| Empty sessions | No conversations yet | |
| Empty workspace | No files yet | |
| Model selector | (None — label varies) | e.g. "DeepSeek V4 Flash" |
| Streaming indicator | (None — visual only) | Blinking cursor / shimmer |
| Connection: loading | Connecting... | |
| Connection: error | Could not connect | With retry button |
| Session: untitled | (Show first 80 chars of preview) | Not "Untitled" |
| Delete confirmation | Delete conversation? | Action sheet |
Tone & Voice
- Friendly but direct — the app is a tool, not a companion
- Minimal labels — every visible word serves a purpose
- No "wizard" or "magic" language — Hermes is an agent, not a magician
- Use "Chat", not "Chat with Hermes" or "Conversation with AI"
- Use "Files", not "Workspace Files" or "Project Files"
8. Iconography
SF Symbols Mappings
| Context | SF Symbol Name | Notes |
|---|---|---|
| Tab: Chats | message.fill |
|
| Tab: Files | folder.fill |
|
| Tab: Settings | gearshape.fill |
|
| New chat | plus |
|
| Send | paperplane.fill |
|
| Attach | paperclip |
|
| Voice | mic.fill |
|
| Delete | trash.fill |
|
| Rename | pencil |
|
| Copy code | doc.on.doc |
|
| Expand | chevron.down |
|
| Collapse | chevron.right |
|
| Directory | folder |
Non-filled for file rows |
| File (generic) | doc |
|
| File (code) | doc.text |
|
| File (image) | photo |
|
| File (PDF) | doc.richtext |
|
| Empty state | tray |
|
| Success | checkmark.circle.fill |
Green |
| Error | xmark.circle.fill |
Red |
| Pending | ellipsis |
Animated |
9. State Terminology
Connection States
| State | BFF app_data field |
iOS App Behaviour |
|---|---|---|
| Connected | server.hermes_connected: true |
Normal operation |
| Hermes Unreachable | server.hermes_connected: false |
Show banner "Hermes is not responding. Responses may be delayed." |
| BFF Error | 500 / timeout | Show error screen with retry |
| Version Mismatch | app.force_update_url not null |
Show force-update screen until app is updated |
Session States
| State | BFF Signal | iOS App Behaviour |
|---|---|---|
| Loading | N/A (page fetch in progress) | Shimmer / skeleton |
| Loaded | Items array populated | Render feed |
| Empty | empty_state present, items empty |
Render empty state |
| Error | Error in response | Show error with retry button |
Component States
| State | streaming field |
iOS App Behaviour |
|---|---|---|
| Static | false |
Render normally |
| Streaming | true |
Show shimmer/blinking cursor |
| Updating | true (with chunk events arriving) |
Append text in place |
| Complete | false (after component_complete) |
Remove shimmer, show final content |
10. Conventions
Naming
| Domain | Convention | Example |
|---|---|---|
| Files | snake_case |
session_card, keryx_bff |
| GraphQL fields | camelCase |
messageCount, createdAt |
| Component types | snake_case |
message_bubble, tool_call_card |
| Deep links | keryx://resource/action |
keryx://chat/{id}/send |
| SSE events | lowercase_snake |
component_complete, component_patch |
| Config keys | snake_case |
staging_cleanup_days |
| HTTP headers | Pascal-Case |
Authorization, Content-Type |
| Enum values (Swift) | lowerCamelCase |
.user, .assistant |
| Enum values (Python) | SCREAMING_SNAKE |
USER, ASSISTANT |
Dates & Times
- BFF → iOS: Relative time strings for display ("2m ago", "Yesterday", "3 weeks ago")
- BFF → iOS: Unix timestamps (Float) for sorting
- BFF generation: Use user's timezone (configured in BFF, or sent by iOS app in headers)
- iOS display: Never show raw timestamps to users
Files
- File sizes returned as bytes (Int). iOS converts for display (KB, MB, etc.)
- MIME types follow standard IANA values (
application/pdf,text/plain,image/png) - Workspace paths are always relative to the configured workspace root. The BFF resolves absolute paths internally.
Errors
| Error Code | HTTP Status | Meaning |
|---|---|---|
auth_invalid |
401 | API key missing or wrong |
session_not_found |
404 | Session ID doesn't exist |
file_too_large |
413 | Upload exceeds max_upload_size_mb |
unsupported_file_type |
415 | MIME type not allowed |
hermes_unreachable |
502 | Hermes API server not responding |
hermes_error |
502 | Hermes returned an error |
workspace_out_of_bounds |
403 | Path traversal attempt blocked |
rate_limited |
429 | Too many requests |
11. Appendix: Hermes API Mapping
This documents how BFF concepts map to Hermes API Server endpoints and data.
| Keryx Concept | Hermes API Endpoint | Notes |
|---|---|---|
| Session ID | API Server session ID | Direct 1:1 mapping |
| Create session | POST /api/sessions |
Returns Hermes session ID |
| Session list | GET /api/sessions |
BFF transforms to session_card components |
| Session messages | GET /api/sessions/{id}/messages |
BFF transforms to message_bubble + thinking_block + tool_call_card |
| Delete session | DELETE /api/sessions/{id} |
|
| Rename session | PATCH /api/sessions/{id} |
Update title field |
| Send message | POST /api/sessions/{id}/chat/stream |
SSE stream of events |
| Model list | GET /v1/models |
Proxied through BFF |
| Health check | GET /v1/health |
|
| File upload | (Not in Hermes API) | BFF handles staging |
| Workspace files | (Not in Hermes API) | BFF reads filesystem |
This document is the source of truth for all naming, terminology, and conventions used in the Keryx project. If a UI label, GraphQL field, deep link, or code identifier contradicts this document, this document wins.