630 lines
21 KiB
Markdown
630 lines
21 KiB
Markdown
# 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
|
|
|
|
1. [Core Concepts](#1-core-concepts)
|
|
2. [UI Component Taxonomy](#2-ui-component-taxonomy)
|
|
3. [Page Structure](#3-page-structure)
|
|
4. [Navigation & Deep Links](#4-navigation--deep-links)
|
|
5. [SSE Streaming Events](#5-sse-streaming-events)
|
|
6. [GraphQL Conventions](#6-graphql-conventions)
|
|
7. [UI Labels & Copy](#7-ui-labels--copy)
|
|
8. [Iconography](#8-iconography)
|
|
9. [State Terminology](#9-state-terminology)
|
|
10. [Conventions](#10-conventions)
|
|
11. [Appendix: Hermes API Mapping](#11-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](#2-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](#4-navigation--deep-links).
|
|
|
|
---
|
|
|
|
## 2. UI Component Taxonomy
|
|
|
|
Every component has this shape:
|
|
|
|
```json
|
|
{
|
|
"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 `title` is `null`, show the `preview` as the primary text (truncated to 1 line)
|
|
- If `title` is set, show `title` as primary, `preview` as secondary
|
|
- `model_badge` shown as a rounded pill
|
|
- `timestamp` right-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`:**
|
|
```json
|
|
{
|
|
"name": "report.pdf",
|
|
"size": 245760,
|
|
"mime_type": "application/pdf"
|
|
}
|
|
```
|
|
|
|
**Display rules:**
|
|
- User messages: right-aligned, tinted background
|
|
- Assistant messages: left-aligned, default background
|
|
- `model` shown 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 `label` present, 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`:**
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"page": {
|
|
"id": "string",
|
|
"header": { ... } | null,
|
|
"items": [ ... ],
|
|
"footer": { ... } | null
|
|
}
|
|
}
|
|
```
|
|
|
|
### Header
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
// 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`):
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
1. **Present** links push a new page onto the navigation stack (iPhone) or select a column (iPad)
|
|
2. **Action** links trigger a side effect (POST, DELETE) and are handled without navigation
|
|
3. The app never constructs URLs dynamically — all links come from BFF responses
|
|
4. 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.
|
|
|
|
```json
|
|
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.
|
|
|
|
```json
|
|
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).
|
|
|
|
```json
|
|
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).
|
|
|
|
```json
|
|
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.
|
|
|
|
```json
|
|
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:
|
|
|
|
```json
|
|
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:
|
|
|
|
```graphql
|
|
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.*
|