Changeset 0.28.0.5 (#177)
This commit is contained in:
@@ -14,21 +14,33 @@ multi-model workflows.
|
||||
GET /channels?page=1&per_page=50
|
||||
```
|
||||
|
||||
Optional query filters: `type`, `types` (comma-separated), `folder`,
|
||||
`folder_id`, `search`, `project_id` (UUID or `"none"`), `archived`.
|
||||
|
||||
Returns pagination envelope. Each channel:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"title": "My Chat",
|
||||
"type": "direct|dm|group|channel|workflow|service",
|
||||
"ai_mode": "auto|mention_only|off",
|
||||
"topic": "string|null",
|
||||
"description": "",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"provider_config_id": "uuid|null",
|
||||
"system_prompt": "",
|
||||
"project_id": "uuid|null",
|
||||
"is_archived": false,
|
||||
"is_pinned": false,
|
||||
"folder": "string|null",
|
||||
"folder_id": "uuid|null",
|
||||
"project_id": "uuid|null",
|
||||
"workspace_id": "uuid|null",
|
||||
"tags": ["tag1", "tag2"],
|
||||
"participant_count": 1,
|
||||
"settings": {},
|
||||
"message_count": 0,
|
||||
"unread_count": 0,
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
@@ -60,7 +72,9 @@ POST /channels
|
||||
"provider_config_id": "uuid",
|
||||
"system_prompt": "",
|
||||
"folder": null,
|
||||
"tags": []
|
||||
"folder_id": null,
|
||||
"tags": [],
|
||||
"participants": ["user-uuid"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -69,13 +83,20 @@ participant with `role: "owner"`. If `model` and `provider_config_id`
|
||||
are provided, the channel model roster (`channel_models`) is
|
||||
auto-populated with an initial entry.
|
||||
|
||||
`participants` is only used for `dm` type — array of exactly one
|
||||
other user UUID. The handler enforces DM dedup: if a DM already
|
||||
exists between the two users, the existing channel is returned
|
||||
(HTTP 200, not 201).
|
||||
|
||||
**Get channel:**
|
||||
|
||||
```
|
||||
GET /channels/:id
|
||||
```
|
||||
|
||||
Returns single channel object.
|
||||
Returns single channel object. Accessible by the channel owner
|
||||
**or** any user who is a participant in the channel (via
|
||||
`channel_participants`).
|
||||
|
||||
**Update channel:**
|
||||
|
||||
@@ -84,7 +105,10 @@ PUT /channels/:id
|
||||
```
|
||||
|
||||
Accepts partial updates — only fields present in the body are changed.
|
||||
Same field set as create.
|
||||
Same field set as create, plus `is_archived`, `is_pinned`, `settings`
|
||||
(JSONB merge), `workspace_id`, `ai_mode`, `topic`, `folder_id`.
|
||||
|
||||
**Auth:** Owner only (keyed on `user_id` column).
|
||||
|
||||
**Delete channel:**
|
||||
|
||||
@@ -92,7 +116,10 @@ Same field set as create.
|
||||
DELETE /channels/:id
|
||||
```
|
||||
|
||||
Cascades: deletes messages, attachments, channel_models, channel_kbs.
|
||||
Cascades: deletes messages, files, channel_models, channel_kbs,
|
||||
channel_participants. Storage cleanup runs asynchronously.
|
||||
|
||||
**Auth:** Owner only.
|
||||
|
||||
### Message Tree
|
||||
|
||||
@@ -202,12 +229,12 @@ PUT /channels/:id/cursor
|
||||
|
||||
```json
|
||||
{
|
||||
"message_id": "uuid",
|
||||
"child_id": "uuid"
|
||||
"active_leaf_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Sets which child is active at the `message_id` branch point.
|
||||
Sets which leaf message is active. The server walks the tree from
|
||||
this leaf to the root and returns the full path.
|
||||
Subsequent `GET /channels/:id/path` will follow the new branch.
|
||||
|
||||
**List siblings** — all children of a message's parent:
|
||||
@@ -270,38 +297,39 @@ credentials are loaded and used for the actual API call.
|
||||
|
||||
**SSE Stream Format:**
|
||||
|
||||
The response is `Content-Type: text/event-stream`. Events:
|
||||
The response is `Content-Type: text/event-stream`. Content and
|
||||
reasoning deltas use OpenAI-compatible envelope format. Tool events
|
||||
use named SSE events.
|
||||
|
||||
```
|
||||
data: {"content":"Hello"}
|
||||
data: {"choices":[{"delta":{"content":"Hello"},"finish_reason":null}],"model":"claude-sonnet-4-20250514"}
|
||||
|
||||
data: {"content":" world"}
|
||||
data: {"choices":[{"delta":{"content":" world"},"finish_reason":null}],"model":"claude-sonnet-4-20250514"}
|
||||
|
||||
data: {"reasoning":"Let me think..."}
|
||||
data: {"choices":[{"delta":{"reasoning_content":"Let me think..."},"finish_reason":null}],"model":"claude-sonnet-4-20250514"}
|
||||
|
||||
event: tool_use
|
||||
data: {"id":"call_1","name":"web_search","input":{"query":"..."}}
|
||||
data: [{"id":"call_1","name":"web_search","input":{"query":"..."}}]
|
||||
|
||||
event: tool_result
|
||||
data: {"id":"call_1","content":"Search results..."}
|
||||
data: [{"id":"call_1","content":"Search results..."}]
|
||||
|
||||
data: {"content":"Based on the search..."}
|
||||
data: {"choices":[{"delta":{"content":"Based on the search..."},"finish_reason":null}],"model":"claude-sonnet-4-20250514"}
|
||||
|
||||
event: finish
|
||||
data: {"message_id":"uuid","model":"claude-sonnet-4-20250514","usage":{"input_tokens":150,"output_tokens":42}}
|
||||
data: {"choices":[{"delta":{},"finish_reason":"stop"}],"model":"claude-sonnet-4-20250514"}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
| Event | `event:` field | Payload |
|
||||
|-------|---------------|---------|
|
||||
| Content delta | _(unnamed)_ | `{ "content": "text" }` |
|
||||
| Reasoning delta | _(unnamed)_ | `{ "reasoning": "text" }` |
|
||||
| Tool invocation | `tool_use` | `{ "id", "name", "input" }` |
|
||||
| Tool result | `tool_result` | `{ "id", "content" }` |
|
||||
| Stream complete | `finish` | `{ "message_id", "model", "usage" }` |
|
||||
| Content delta | _(unnamed)_ | OpenAI envelope: `choices[0].delta.content` |
|
||||
| Reasoning delta | _(unnamed)_ | OpenAI envelope: `choices[0].delta.reasoning_content` |
|
||||
| Tool invocation | `tool_use` | `[{ "id", "name", "input" }]` |
|
||||
| Tool result | `tool_result` | `[{ "id", "content" }]` |
|
||||
| Stream complete | _(unnamed)_ | OpenAI envelope: `choices[0].finish_reason` = `"stop"` or `"budget_exceeded"` |
|
||||
| End sentinel | _(unnamed)_ | `[DONE]` (literal string) |
|
||||
| Error mid-stream | `error` | `{ "error": "message" }` |
|
||||
| Error mid-stream | _(unnamed)_ | `{ "error": "message" }` |
|
||||
|
||||
**Tool cycle:** Tool use and tool result events can repeat up to
|
||||
`max_tool_iterations` (configured server-side). The handler executes
|
||||
@@ -311,7 +339,8 @@ tools, feeds results back to the model, and continues streaming.
|
||||
|
||||
```
|
||||
X-Switchboard-Provider: providerID/configID
|
||||
X-Switchboard-Route: policy-name (when routing policy is active)
|
||||
X-Switchboard-Route: policy-name (when routing policy is active)
|
||||
X-Switchboard-Fallback: depth (when fallback chain was triggered)
|
||||
```
|
||||
|
||||
**List available tools:**
|
||||
@@ -401,25 +430,10 @@ Content-Type: multipart/form-data
|
||||
|
||||
Field: `file`. Sets `origin: "user_upload"`. Returns the file object.
|
||||
|
||||
**Create (tool output):**
|
||||
|
||||
```
|
||||
POST /files
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_id": "uuid",
|
||||
"message_id": "uuid",
|
||||
"filename": "generated_image.png",
|
||||
"content_type": "image/png",
|
||||
"display_hint": "inline",
|
||||
"metadata": { "tool_name": "image_generation" }
|
||||
}
|
||||
```
|
||||
|
||||
Body: multipart `file` field or base64 `data` field.
|
||||
Sets `origin: "tool_output"`. Returns file object.
|
||||
**Create (tool output):** Tool-generated files are created internally
|
||||
by the tool execution loop. There is no public REST endpoint for
|
||||
tool output creation — the completion handler writes files directly
|
||||
via the store layer.
|
||||
|
||||
**List by channel:**
|
||||
|
||||
@@ -432,17 +446,19 @@ Returns `{ "files": [...] }`. Filterable by `origin`, `content_type`.
|
||||
**List by message:**
|
||||
|
||||
```
|
||||
GET /messages/:id/files → { "files": [...] }
|
||||
GET /messages/:id/files
|
||||
```
|
||||
|
||||
How the frontend discovers generated artifacts to render inline.
|
||||
Returns `{ "files": [...] }`. How the frontend discovers generated
|
||||
artifacts to render inline.
|
||||
|
||||
**List by user (file manager):**
|
||||
|
||||
```
|
||||
GET /files?page=1&per_page=50 → { "files": [...], "total": N }
|
||||
GET /files?page=1&per_page=50
|
||||
```
|
||||
|
||||
Returns `{ "files": [...], "total": N, "page": N, "per_page": N }`.
|
||||
All files owned by the authenticated user, paginated.
|
||||
|
||||
**Get metadata:**
|
||||
@@ -478,7 +494,7 @@ GET /files/:id/download
|
||||
Returns the file with appropriate `Content-Type` and
|
||||
`Content-Disposition` headers.
|
||||
|
||||
**Thumbnail:**
|
||||
**Thumbnail** _(planned — v0.28.0+):_
|
||||
|
||||
```
|
||||
GET /files/:id/thumbnail
|
||||
@@ -614,35 +630,40 @@ No request body needed. Returns `{ "ok": true }`.
|
||||
**Bulk query:**
|
||||
|
||||
```
|
||||
GET /presence?users=alice,bob,charlie
|
||||
GET /presence?users=uuid1,uuid2,uuid3
|
||||
```
|
||||
|
||||
Returns online status for listed usernames. Threshold: online if
|
||||
last heartbeat < 90 seconds ago.
|
||||
Returns online/offline status for listed user UUIDs. Threshold:
|
||||
online if last heartbeat < 90 seconds ago.
|
||||
|
||||
```json
|
||||
{
|
||||
"users": {
|
||||
"alice": { "status": "online", "last_seen": "..." },
|
||||
"bob": { "status": "offline", "last_seen": "..." }
|
||||
"presence": {
|
||||
"uuid1": "online",
|
||||
"uuid2": "offline"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Values are flat status strings. Capped at 100 user IDs per request.
|
||||
|
||||
Subsequent updates delivered via WebSocket `presence.changed` events.
|
||||
|
||||
**Typing indicators** are sent via WebSocket (see websocket.md). The
|
||||
typing event payload includes `participant_id` and `participant_type`.
|
||||
**Typing indicators** are sent via `POST /channels/:id/typing`
|
||||
(authenticated, broadcasts `typing.user` WebSocket event to other
|
||||
user participants) and via WebSocket (see websocket.md). The typing
|
||||
event payload includes `channel_id`, `user_id`, and `display_name`.
|
||||
|
||||
### Chat Folders
|
||||
|
||||
User-scoped grouping for chats. Folders have no semantic weight — they
|
||||
are named drawers.
|
||||
User-scoped grouping for chats. Folders have no semantic weight —
|
||||
they are named drawers. The schema supports nesting via `parent_id`
|
||||
(DnD nesting not yet implemented in the UI).
|
||||
|
||||
```
|
||||
GET /folders → list user's folders
|
||||
POST /folders ← { "name": "Research" }
|
||||
PUT /folders/:id ← { "name": "Renamed" }
|
||||
GET /folders → { "folders": [...] }
|
||||
POST /folders ← { "name": "Research", "sort_order": 0 }
|
||||
PUT /folders/:id ← { "name": "Renamed", "sort_order": 1 }
|
||||
DELETE /folders/:id → chats become unfiled
|
||||
```
|
||||
|
||||
@@ -651,7 +672,6 @@ DELETE /folders/:id → chats become unfiled
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"name": "Research",
|
||||
"parent_id": "uuid|null",
|
||||
"sort_order": 0,
|
||||
@@ -661,7 +681,9 @@ DELETE /folders/:id → chats become unfiled
|
||||
```
|
||||
|
||||
Moving a chat into/out of a folder is done via `PUT /channels/:id`
|
||||
with `{ "folder_id": "uuid" }` or `{ "folder_id": null }`.
|
||||
with `{ "folder_id": "uuid" }` or `{ "folder_id": "" }` to unbind.
|
||||
|
||||
The channel list also supports `?folder_id=uuid` as a query filter.
|
||||
|
||||
**Auth:** Authenticated (user-scoped).
|
||||
|
||||
@@ -673,8 +695,13 @@ Additional channel fields (set via `PUT /channels/:id`):
|
||||
|-------|------|---------|-------------|
|
||||
| `ai_mode` | string | `auto` | `auto`, `mention_only`, or `off` |
|
||||
| `topic` | string | null | Short description shown in header |
|
||||
| `allow_anonymous` | boolean | false | Session participants can join (workflow channels) |
|
||||
| `kb_auto_inject` | boolean | false | Top-K KB chunks auto-prepend to context |
|
||||
|
||||
Fields managed internally (not settable via channel update):
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `allow_anonymous` | boolean | false | Session participants can join. Set by workflow instance handlers on channel creation. |
|
||||
| `kb_auto_inject` | boolean | false | Top-K KB chunks auto-prepend to context _(v0.28.0 — schema exists, not yet wired)_. |
|
||||
|
||||
`ai_mode` behavioral matrix:
|
||||
|
||||
@@ -702,8 +729,10 @@ calculation.
|
||||
GET /users/search?q=alice
|
||||
```
|
||||
|
||||
Returns `{ "data": [{ "id", "username", "handle", "display_name", "avatar_url" }] }`.
|
||||
Used for DM creation and participant picker. Matches on handle.
|
||||
Returns `{ "users": [{ "id", "username", "display_name", "handle" }] }`.
|
||||
Matches on username, display_name, and handle (case-insensitive
|
||||
substring). Used for DM creation and participant picker. Excludes the
|
||||
calling user. Max 20 results.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user