Files
Sybil-2/docs/api/rest.md
buzzert e83281c400 Rename the persistent agent integration to Sybil Agent
Use SYBIL_AGENT_BASE_URL and SYBIL_AGENT_API_KEY with the renamed sybil-agent
service. Update errors, API documentation, and test fixtures while retaining the
existing room protocol and browser storage keys for compatibility.

Validation: 44 server tests, 51 web tests, and both production builds.
2026-10-14 12:37:03 -07:00

31 KiB
Raw Permalink Blame History

REST API Contract

Base URL: /api behind web proxy, or server root directly in local/dev.

Authentication:

  • If ADMIN_TOKEN is set on server, send Authorization: Bearer <token>.
  • If ADMIN_TOKEN is unset, API is open for local/dev use.

Content type:

  • Requests with bodies use application/json.
  • Responses are JSON unless noted otherwise.

Chat upload limits:

  • Chat completion and direct message payloads support inline attachments up to a 32 MB request body.
  • Up to 8 attachments per message.
  • Image attachments: PNG or JPEG only, max 6 MB each.
  • Text attachments: up to 8 MB source size each; server accepts at most 200,000 characters of inlined text content per attachment.

Health + Auth

GET /health

  • Response: { "ok": true }

GET /v1/auth/session

  • Response: { "authenticated": true, "mode": "open" | "token" }

Models

GET /v1/models

  • Response:
{
  "providers": {
    "openai": { "models": ["chat-latest", "gpt-4.1-mini"], "loadedAt": "2026-02-14T00:00:00.000Z", "error": null },
    "anthropic": { "models": ["claude-3-5-sonnet-latest"], "loadedAt": null, "error": null },
    "xai": { "models": ["grok-3-mini"], "loadedAt": null, "error": null },
    "gemini": { "models": ["gemini-3.5-flash"], "loadedAt": null, "error": null },
    "hermes-agent": { "models": ["hermes-agent"], "loadedAt": null, "error": null }
  }
}
  • OpenAI model lists are filtered to models that are expected to work with the backend's Responses API implementation, including the exact chat-latest alias when returned by OpenAI. Audio, image-generation, embedding, moderation, and other specialized model ids remain excluded. See OpenAI's Chat Latest documentation for supported capabilities.
  • Gemini model lists are loaded from Google's native Models API and filtered to Gemini generateContent model ids.
  • hermes-agent is included only when HERMES_AGENT_API_KEY is configured. Set it to Hermes API_SERVER_KEY, or any non-empty value if that local server does not require auth. HERMES_AGENT_API_BASE_URL defaults to http://127.0.0.1:8642/v1; set HERMES_AGENT_MODEL only when you need an additional fallback/override model id.
  • The backend loads provider model lists at startup and refreshes them about once every 24 hours. If a later provider refresh fails, the response keeps the last loaded model list for that provider and sets error to the latest failure message.

Chat Tools

GET /v1/chat-tools

  • Response:
{
  "tools": [
    { "name": "web_search", "description": "..." },
    { "name": "fetch_url", "description": "..." }
  ]
}

Behavior notes:

  • Lists Sybil-managed chat tools that can be enabled for openai, anthropic, xai, and gemini chat completions.
  • Optional tools such as codex_exec and shell_exec appear only when enabled by server environment configuration.

Active Runs

GET /v1/active-runs

  • Response:
{
  "chats": ["chat-id-with-active-stream"],
  "searches": ["search-id-with-active-stream"]
}

Behavior notes:

  • Lists in-memory chat/search streams that are still running on this server process.
  • Clients should use this after app start or page refresh to restore per-row generating indicators.
  • The lists are not durable across server restarts.

Workspace Items

GET /v1/workspace-items

  • Response: { "items": WorkspaceItem[] }
  • Optional query: q (trimmed string, up to 200 characters). Omit or leave blank for the recent-items list.
  • With nonblank q, response: { "items": WorkspaceItem[], "hasMore": boolean }; each item also has searchSnippet: string | null (plain text, no HTML).
  • WorkspaceItem is a discriminated union sorted by updatedAt descending:
{
  "items": [
    {
      "type": "chat",
      "id": "chat-id",
      "title": "optional title",
      "parentChatId": null,
      "titleGenerationPending": false,
      "createdAt": "2026-02-14T00:00:00.000Z",
      "updatedAt": "2026-02-14T00:00:00.000Z",
      "starred": true,
      "starredAt": "2026-02-14T01:00:00.000Z",
      "initiatedProvider": "openai",
      "initiatedModel": "gpt-4.1-mini",
      "lastUsedProvider": "openai",
      "lastUsedModel": "gpt-4.1-mini",
      "additionalSystemPrompt": null,
      "enabledTools": ["web_search", "fetch_url"]
    },
    {
      "type": "search",
      "id": "search-id",
      "title": "optional title",
      "query": "search query",
      "createdAt": "2026-02-14T00:00:00.000Z",
      "updatedAt": "2026-02-14T00:00:00.000Z",
      "starred": false,
      "starredAt": null
    }
  ]
}

Behavior notes:

  • This endpoint is intended for combined conversation/search lists such as sidebars.
  • The legacy GET /v1/chats and GET /v1/searches endpoints remain available for clients that need separate collections.
  • The response currently combines the 100 most recently updated chats, any additional root chats needed to group those forks, and up to 100 searches.
  • Root chats have parentChatId: null. Every fork points directly to its single root chat, including a fork created from another fork, so clients can group rows without traversing a fork chain.
  • starred/starredAt are backed by membership in a reserved Project with id starred; future project folders can reuse the same project item model.

History search (q) notes:

  • Searches all saved history, including items outside the recent-items window. Chats match their title or the text of a user/assistant message; saved web searches match their title, query, or generated answer. System prompts, tool logs, attachments, and raw web-result payloads are excluded.
  • Case- and diacritic-insensitive Unicode word-prefix matching: plan matches planning. All query words must match within the title, a single message, or one saved web search (across its indexed fields). Punctuation separates words; quotes, *, and operators have no special query syntax. Punctuation-only queries return no matches. This is word-prefix search, not arbitrary substring matching.
  • Returns at most 100 distinct matching items, newest updatedAt first, with ties ordered by type then id. hasMore indicates additional matches; refine q to narrow them. Unmatched fork roots are not added in search mode. Selecting a fork result opens that fork directly. Search-mode fork results carry their root's starred/starredAt; clients should star/unstar the parentChatId, as in the grouped sidebar.
  • searchSnippet is a bounded excerpt from a matching message or saved web search; title-only chat matches have null. Clients should render it as plain text.
  • The web and iOS sidebar search fields use this endpoint across saved history. Both display matching excerpts; web title-only chat results display “Title match”.
  • Invalid/non-string queries or queries exceeding 200 characters return 400. Authentication is unchanged.
  • SQLite FTS5 external-content indexes and prefix indexes serve these queries without scanning or transferring every transcript. The migration backfills existing content; transactional triggers maintain the indexes on inserts, updates, deletes, and cascades. Recent chat listing also has an updatedAt index.

Chats

GET /v1/chats

  • Response: { "chats": ChatSummary[] }

POST /v1/chats

  • Body:
{
  "title": "optional title",
  "provider": "optional openai|anthropic|xai|gemini|hermes-agent",
  "model": "optional model id",
  "additionalSystemPrompt": "optional stored system prompt",
  "enabledTools": ["web_search", "fetch_url"],
  "messages": [
    {
      "role": "system|user|assistant|tool",
      "content": "string",
      "name": "optional",
      "attachments": []
    }
  ]
}
  • Response: { "chat": ChatSummary }

Behavior notes:

  • provider and model must be supplied together when present.
  • Newly created non-fork chats have parentChatId: null and titleGenerationPending: false.
  • When provider/model are supplied, the new chat initializes initiatedProvider/initiatedModel and lastUsedProvider/lastUsedModel.
  • additionalSystemPrompt is trimmed and stored on the chat; blank values are stored as null.
  • enabledTools stores the enabled Sybil-managed tool names for future chat completions. Unknown tool names are ignored; omitted values default to all currently available tools.
  • Optional messages are inserted as the initial transcript. Attachment metadata uses the same schema and limits as chat completion messages.

POST /v1/chats/:chatId/fork

  • Body: { "messageId"?: string }
  • Response: { "chat": ChatSummary }
  • Source chat not found: 404 { "message": "chat not found" }
  • Supplied message missing from the source chat: 404 { "message": "message not found in chat" }
  • Supplied message is not an assistant response: 400 { "message": "fork message must be an assistant response" }

Behavior notes:

  • With no messageId, the server copies the complete source transcript. With an assistant response messageId, it copies the transcript through that response, inclusive.
  • The source chat is unchanged. Copied messages receive new ids while retaining their role, content, name, creation time, order, and metadata except for the source request's transport-only clientRequestId. Attachments and tool-call metadata are retained. LLM call logs, star/project memberships, and other child threads are not copied.
  • The fork inherits the source chat's user, initiated/last-used provider and model, additional system prompt, and enabled tool settings.
  • A fork of a root or another fork always sets parentChatId to the single root chat id. Root chats keep parentChatId: null.
  • A whole-chat fork starts with Fork of <source title> (or Fork of Untitled chat). A message fork starts with Fork of '<message snippet>'.
  • The placeholder title is returned with titleGenerationPending: true. After the first new prompt, call POST /v1/chats/title/suggest; the placeholder is eligible for replacement exactly once.

PATCH /v1/chats/:chatId

  • Body: any subset of { "title": string, "additionalSystemPrompt": string|null, "enabledTools": string[] }
  • Response: { "chat": ChatSummary }
  • Blank titles are rejected. The server trims surrounding whitespace before storing the title.
  • Setting a title clears titleGenerationPending, preventing an in-flight or later automatic suggestion from replacing the manual title.
  • additionalSystemPrompt: null clears the stored prompt. Blank string values are also stored as null.
  • enabledTools: [] disables Sybil-managed tools for this chat. Omitted settings are left unchanged.
  • Updating chat fields changes the returned chat's updatedAt.
  • Not found: 404 { "message": "chat not found" }

PATCH /v1/chats/:chatId/star

  • Body: { "starred": boolean }
  • Response: { "chat": ChatSummary }
  • Not found: 404 { "message": "chat not found" }

Behavior notes:

  • Starring adds the chat to the reserved starred project and sets starredAt to the membership creation time.
  • Unstarring removes that membership and returns starred: false, starredAt: null.
  • This does not modify the chat transcript or chat updatedAt.

POST /v1/chats/title/suggest

  • Body:
{
  "chatId": "chat-id",
  "content": "user request text"
}
  • Response: { "chat": ChatSummary }

Behavior notes:

  • If the chat already has a non-empty title and titleGenerationPending is false, server returns the existing chat unchanged.
  • A fork placeholder with titleGenerationPending: true is eligible for the same title-generation flow as an untitled original chat. A successful or fallback suggestion clears the flag.
  • If a title is set while suggestion generation is in flight, server returns the current chat instead of overwriting that title.
  • For an eligible untitled chat or pending fork placeholder, server uses OpenAI gpt-4.1-mini to generate a one-line title (up to ~4 words), updates the chat title, and returns the updated chat.
  • If the title provider is unavailable or rejects the request, server still persists a deterministic title derived from the first line of content instead of leaving the chat untitled.

DELETE /v1/chats/:chatId

  • Response: { "deleted": true }
  • Not found: 404 { "message": "chat not found" }
  • Active chat or fork: 409 { "message": "chat or fork has an active stream" }
  • Concurrent family deletion: 409 { "message": "chat family deletion already in progress" }
  • Deleting a root chat also deletes its grouped forks. Deleting a fork leaves the root and sibling forks unchanged.
  • A root cannot be deleted while it or any grouped fork has an active completion stream. A fork cannot be deleted while its own completion stream is active.

GET /v1/chats/:chatId

  • Response: { "chat": ChatDetail }

POST /v1/chats/:chatId/messages

  • Body:
{
  "role": "system|user|assistant|tool",
  "content": "string",
  "name": "optional",
  "metadata": {},
  "attachments": [
    {
      "kind": "image",
      "id": "attachment-id",
      "filename": "photo.jpg",
      "mimeType": "image/jpeg",
      "sizeBytes": 12345,
      "dataUrl": "data:image/jpeg;base64,..."
    },
    {
      "kind": "text",
      "id": "attachment-id",
      "filename": "notes.md",
      "mimeType": "text/markdown",
      "sizeBytes": 4567,
      "text": "# Notes\\n...",
      "truncated": false
    }
  ]
}
  • Response: { "message": Message }

Notes:

  • attachments is optional and is merged into stored message.metadata.attachments.
  • Tool messages should not include attachments.

Chat Completions (non-streaming)

POST /v1/chat-completions

  • Body:
{
  "chatId": "optional-chat-id",
  "provider": "openai|anthropic|xai|gemini|hermes-agent",
  "model": "string",
  "messages": [
    {
      "role": "system|user|assistant|tool",
      "content": "string",
      "name": "optional",
      "attachments": [
        {
          "kind": "image",
          "id": "attachment-id",
          "filename": "photo.jpg",
          "mimeType": "image/jpeg",
          "sizeBytes": 12345,
          "dataUrl": "data:image/jpeg;base64,..."
        },
        {
          "kind": "text",
          "id": "attachment-id",
          "filename": "notes.md",
          "mimeType": "text/markdown",
          "sizeBytes": 4567,
          "text": "# Notes\\n...",
          "truncated": false
        }
      ]
    }
  ],
  "additionalSystemPrompt": "optional one-off system prompt",
  "enabledTools": ["web_search", "fetch_url"],
  "temperature": 0.2,
  "maxTokens": 256
}
  • Response:
{
  "chatId": "chat-id-or-null",
  "provider": "openai",
  "model": "gpt-4.1-mini",
  "message": { "role": "assistant", "content": "..." },
  "usage": { "inputTokens": 10, "outputTokens": 20, "totalTokens": 30 },
  "raw": {}
}

Behavior notes:

  • If chatId is present, server validates chat existence.
  • For chatId calls, server stores only new non-assistant messages from provided history to avoid duplicates.
  • additionalSystemPrompt, when present directly or loaded from stored chat settings, is prepended to the provider request as a system message and is not inserted into the persisted chat transcript by this endpoint.
  • enabledTools limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools.
  • maxTokens is optional. For anthropic, when omitted the backend requests the selected model's maximum output token limit from Anthropic's Models API and uses that as max_tokens; if the model limit cannot be loaded, the fallback is 128000. For other providers, omitted maxTokens is not sent as an explicit cap.
  • Server persists final assistant output and call metadata (LlmCall) in DB.
  • Server updates chat-level model metadata on each call: lastUsedProvider/lastUsedModel; first successful/failed call also initializes initiatedProvider/initiatedModel if unset.
  • Attachments are optional and currently apply to user messages. Persisted chat history stores them under message.metadata.attachments.
  • Images are forwarded inline to providers as multimodal image parts. Use PNG or JPEG for cross-provider compatibility.
  • Text files are forwarded as explicit text blocks rather than provider-managed file references. Large text attachments should already be truncated client-side before submission.
  • For openai, backend calls OpenAI's Responses API and enables internal tool use with an internal system instruction.
  • For anthropic, backend calls Anthropic's Messages API and enables internal tool use with Anthropic tool_use/tool_result content blocks.
  • For xai, backend calls xAI's OpenAI-compatible Chat Completions API and enables internal tool use with the same internal system instruction.
  • For gemini, backend calls Google's native Gemini generateContent API and enables internal tool use with Gemini function calling.
  • For hermes-agent, backend calls the configured Hermes Agent OpenAI-compatible Chat Completions API without adding Sybil-managed tool definitions; Hermes Agent handles its own tools server-side.
  • For openai, image attachments are sent as Responses input_image items and text attachments are sent as input_text items.
  • For gemini, image attachments are sent as native Gemini inlineData parts and text attachments are sent as text parts.
  • For xai and hermes-agent, image attachments are sent as Chat Completions content parts alongside text.
  • For openai, Responses calls that can enter the server-managed tool loop use store: true so reasoning and function-call items can be passed between tool rounds.
  • For anthropic, image attachments are sent as Messages API image blocks using base64 source data; text attachments are added as text blocks.
  • Available Sybil-managed tool calls for openai, anthropic, xai, and gemini: web_search and fetch_url. When CHAT_CODEX_TOOL_ENABLED=true, codex_exec is also available. When CHAT_SHELL_TOOL_ENABLED=true, shell_exec is also available.
  • web_search returns ranked results with per-result summaries/snippets. Its backend engine is selected by CHAT_WEB_SEARCH_ENGINE: exa (default), brave (requires BRAVE_SEARCH_API_KEY), or searxng (requires SEARXNG_BASE_URL; the instance must allow format=json).
  • Brave searches are queued and evenly paced according to the shortest window in Brave's X-RateLimit-Policy response header. The backend also honors X-RateLimit-Remaining/X-RateLimit-Reset and retries 429 responses up to three times with reset-aware exponential backoff; quota resets beyond the bounded retry window fail immediately.
  • fetch_url fetches a URL with browser-like navigation headers and returns plaintext page content (HTML converted to text server-side).
  • codex_exec delegates coding, shell, repository inspection, and other complex software tasks to a persistent remote Codex CLI workspace over SSH. The server runs codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check <non-interactive wrapped prompt> on the configured devbox inside CHAT_CODEX_REMOTE_WORKDIR, with SSH stdin closed.
  • shell_exec runs arbitrary non-interactive shell commands on the same configured devbox, starting in CHAT_CODEX_REMOTE_WORKDIR. It uses bash -lc when bash exists, otherwise sh -lc, closes SSH stdin, and does not run inside the Sybil server container.
  • Devbox tool configuration:
    • CHAT_MAX_TOOL_ROUNDS=100 (optional; maximum model/tool result cycles before the backend returns a limit message)
    • CHAT_CODEX_TOOL_ENABLED=true
    • CHAT_SHELL_TOOL_ENABLED=true
    • CHAT_CODEX_REMOTE_HOST=<host-or-ip> (required when enabled)
    • CHAT_CODEX_REMOTE_USER=<ssh-user> (optional; omitted if CHAT_CODEX_REMOTE_HOST already contains user@host)
    • CHAT_CODEX_REMOTE_PORT=22 (optional)
    • CHAT_CODEX_REMOTE_WORKDIR=/workspace/sybil-codex (optional; created on the remote host if missing)
    • CHAT_CODEX_SSH_KEY_PATH=/run/secrets/codex_ssh_key (recommended private-key delivery via read-only volume mount)
    • CHAT_CODEX_SSH_PRIVATE_KEY_B64=<base64-private-key> (optional fallback when a volume mount is not practical)
    • CHAT_CODEX_EXEC_TIMEOUT_MS=600000 (optional)
    • CHAT_SHELL_EXEC_TIMEOUT_MS=120000 (optional)
  • When a tool call is executed, backend stores a chat Message with role: "tool" and tool metadata (metadata.kind = "tool_call"). Streaming requests emit an initiated SSE tool_call event before execution, then persist each completed or failed tool call as its terminal SSE tool_call event is emitted, then store the assistant output when the completion finishes.

Streaming Chat

POST /v1/chat-completions/stream

  • The request accepts the chat-completion fields above plus optional persist and clientRequestId fields.
  • clientRequestId is only valid for a persisted request with a chatId, may be up to 128 characters, and should be a stable unique value generated once per user submission.
  • Retrying with the same chatId and clientRequestId replays the matching active or completed stream rather than starting a duplicate provider call.
  • The server persists the ID in metadata.clientRequestId on the submitted user message and completed assistant message.
  • The complete request, SSE event, persistence, retry, and attach contracts are defined in docs/api/streaming-chat.md.

Searches

GET /v1/searches

  • Response: { "searches": SearchSummary[] }

POST /v1/searches

  • Body: { "title"?: string, "query"?: string, "reuseByQuery"?: boolean }
  • Response: { "search": SearchSummary, "reused": boolean, "cacheHit": boolean }

Behavior notes:

  • reuseByQuery defaults to false, preserving the normal create-a-new-search behavior.
  • When reuseByQuery is true and query is present, the backend normalizes the query with trim().toLowerCase() and returns the most recently updated existing search with that normalized query instead of creating a duplicate.
  • cacheHit is true only when the reused search has persisted results or answer text, is not currently streaming, and was updated within the 24-hour search cache window. Clients can then fetch GET /v1/searches/:searchId and display it without running another search.
  • If a matching search exists but cacheHit is false, clients may run the search again on the returned search.id; the run endpoints replace that search's persisted results and answer with the latest run.

PATCH /v1/searches/:searchId/star

  • Body: { "starred": boolean }
  • Response: { "search": SearchSummary }
  • Not found: 404 { "message": "search not found" }

Behavior notes:

  • Starring adds the search to the reserved starred project and sets starredAt to the membership creation time.
  • Unstarring removes that membership and returns starred: false, starredAt: null.
  • This does not modify the search results or search updatedAt.

DELETE /v1/searches/:searchId

  • Response: { "deleted": true }
  • Not found: 404 { "message": "search not found" }

GET /v1/searches/:searchId

  • Response: { "search": SearchDetail }

POST /v1/searches/:searchId/chat

  • Body: { "title"?: string }
  • Response: { "chat": ChatSummary }
  • Not found: 404 { "message": "search not found" }

Behavior notes:

  • Creates a new chat seeded with a hidden system message containing the search query, answer text, answer citations, and top search results.
  • Clients should include existing system messages when sending the chat history to /v1/chat-completions or /v1/chat-completions/stream; they may hide those messages in the transcript UI.
  • The default chat title is Search: <query-or-title>, unless title is supplied.

POST /v1/searches/:searchId/run

  • Body:
{
  "query": "optional override",
  "title": "optional override",
  "type": "auto|fast|deep|instant",
  "numResults": 10,
  "includeDomains": ["example.com"],
  "excludeDomains": ["example.org"]
}
  • Response: { "search": SearchDetail }

Search run notes:

  • Backend executes Exa search and Exa answer.
  • Search mode is independent from chat web_search tool configuration and remains Exa-only.
  • Persists answer text/citations + ranked results.
  • If both search and answer fail, endpoint returns an error.

POST /v1/searches/:searchId/run/stream

  • Body: same as POST /v1/searches/:searchId/run
  • Response: text/event-stream

Events:

  • search_results: { "requestId": string|null, "results": SearchResultItem[] }
  • search_error: { "error": string }
  • answer: { "answerText": string|null, "answerRequestId": string|null, "answerCitations": SearchDetail["answerCitations"] }
  • answer_error: { "error": string }
  • terminal done: { "search": SearchDetail }
  • terminal error: { "message": string }

Behavior notes:

  • The stream is owned by the backend after it starts. If the original HTTP client disconnects, the backend keeps running and persists the final search state.
  • While a search stream is active, GET /v1/active-runs includes the searchId.
  • If a stream is already active for the same searchId, this endpoint attaches to the existing stream instead of starting a second run.

POST /v1/searches/:searchId/run/stream/attach

  • Body: none
  • Response: text/event-stream with the same event names as POST /v1/searches/:searchId/run/stream
  • Not found: 404 { "message": "active search stream not found" }

Behavior notes:

  • Replays buffered events for the active in-memory stream, then emits new events until done or error.
  • Intended for clients that discovered a pending search via GET /v1/active-runs, such as after browser refresh.

Type Shapes

ChatSummary

{
  "id": "...",
  "title": null,
  "parentChatId": null,
  "titleGenerationPending": false,
  "createdAt": "...",
  "updatedAt": "...",
  "starred": false,
  "starredAt": null,
  "initiatedProvider": "openai|anthropic|xai|gemini|hermes-agent|null",
  "initiatedModel": "string|null",
  "lastUsedProvider": "openai|anthropic|xai|gemini|hermes-agent|null",
  "lastUsedModel": "string|null",
  "additionalSystemPrompt": null,
  "enabledTools": ["web_search", "fetch_url"]
}

Message

{
  "id": "...",
  "createdAt": "...",
  "role": "system|user|assistant|tool",
  "content": "...",
  "name": null,
  "metadata": {
    "attachments": [
      {
        "kind": "image",
        "id": "attachment-id",
        "filename": "photo.jpg",
        "mimeType": "image/jpeg",
        "sizeBytes": 12345,
        "dataUrl": "data:image/jpeg;base64,..."
      },
      {
        "kind": "text",
        "id": "attachment-id",
        "filename": "notes.md",
        "mimeType": "text/markdown",
        "sizeBytes": 4567,
        "text": "# Notes\\n...",
        "truncated": false
      }
    ]
  }
}

metadata remains nullable. Tool-call log messages still use metadata.kind = "tool_call"; regular user messages with attachments use metadata.attachments.

ChatDetail

{
  "id": "...",
  "title": null,
  "parentChatId": null,
  "titleGenerationPending": false,
  "createdAt": "...",
  "updatedAt": "...",
  "starred": false,
  "starredAt": null,
  "initiatedProvider": "openai|anthropic|xai|gemini|hermes-agent|null",
  "initiatedModel": "string|null",
  "lastUsedProvider": "openai|anthropic|xai|gemini|hermes-agent|null",
  "lastUsedModel": "string|null",
  "additionalSystemPrompt": null,
  "enabledTools": ["web_search", "fetch_url"],
  "messages": [Message]
}

SearchSummary

{ "id": "...", "title": null, "query": null, "createdAt": "...", "updatedAt": "...", "starred": false, "starredAt": null }

SearchDetail

{
  "id": "...",
  "title": "...",
  "query": "...",
  "createdAt": "...",
  "updatedAt": "...",
  "starred": false,
  "starredAt": null,
  "requestId": "...",
  "latencyMs": 123,
  "error": null,
  "answerText": "...",
  "answerRequestId": "...",
  "answerCitations": [],
  "answerError": null,
  "results": []
}

For streaming contracts, see docs/api/streaming-chat.md.

Sybil Agent Room

The Sybil Agent tab connects to one persistent root conversation. The agent owns its messages, background work, and history; these endpoints do not create ordinary Sybil chats or copy the transcript into the Sybil message database. All endpoints use normal Sybil REST authentication. The server connects privately to SYBIL_AGENT_BASE_URL (default http://sybil-agent:8788) using SYBIL_AGENT_API_KEY. The agent key never reaches browsers.

GET /v1/agent

Returns the Sybil Agent room descriptor: id, title, type: "persistent-agent", model, protocol: "pi-room.v1", activity, and websocket: "/v1/agent/ws". The socket path is relative to the API base, so web deployments use /api/v1/agent/ws. Missing agent configuration returns 503; an unreachable agent server or malformed upstream JSON returns 502.

GET /v1/agent/messages

Query parameters: optional after or before (mutually exclusive nonnegative numeric cursor strings), and limit (1–200). Without a cursor, returns recent history. after catches up forward; before loads earlier history.

{
  "conversationId": "1",
  "messages": [{ "id": "outbound:43", "createdAt": "2026-10-14T18:00:00.000Z", "role": "assistant", "content": "The task finished.", "name": "Sybil Agent", "metadata": { "kind": "message", "entryId": 43, "status": "complete" } }],
  "cursor": "43",
  "beforeCursor": "43",
  "hasMore": false
}

Messages use user, assistant, and tool roles. Tools use metadata.kind: "tool_call" and stable IDs; merge by message id, since later pages can update initiated tools to completed or failed. Internal worker reports, event prompts, and model reasoning are excluded. Cursors refer to underlying durable entries and can advance without visible messages. Use returned cursors; do not derive them from message IDs. limit bounds source entries, not displayed messages.

POST /v1/agent/messages

Body: { "content": "Check the oven", "requestId": "client-generated-uuid" }. Content must contain non-whitespace text and is limited to 40,000 characters; requestId is required (1–256 characters). Preserve the ID when retrying uncertain network results. The proxy fixes channel: "sybil", whenBusy: "followUp", and wait: false.

Returns the agent's 202 Accepted receipt, including id (submission ID), status, and messageId such as inbound:sybil:client-generated-uuid. This is admission, not completion: watch the room for answers, including unsolicited later replies. Multiple requests can be accepted while the agent works. Attachments are not yet supported. The proxy verifies successful receipts contain a positive submission ID, a known status (queued, placed, done, or unanswered), and the expected messageId. Invalid receipts return 502; delivery remains uncertain, so retry using the original requestId to avoid creating a duplicate message.

GET /v1/agent/read-state

PUT /v1/agent/read-state

GET returns { "cursor": "43", "updatedAt": "2026-10-14T18:00:00.000Z" }. Initially: { "cursor": "0", "updatedAt": null }. PUT takes { "cursor": "43" } and returns the same shape. The cursor is shared across this personal installation and persisted in SQLite. Atomic, monotonic updates prevent a stale browser from lowering a cursor saved by another browser. Advance it only after showing the corresponding history; poll it to reconcile unread indicators across clients. Agent REST responses use Cache-Control: no-store.

See the persistent Agent WebSocket section in streaming-chat.md for live updates.