{"openapi":"3.0.3","info":{"title":"Victoria Planning Assistant API","version":"langgraph-production","description":"HTTP surface for the Victoria (victoria-agent) planning assistant.\n\n**Auth:** when `VICTORIA_AUTH_ENABLED=true`, `/orchestrate` and `/api/chat` require `Authorization: Bearer `; the token is resolved to an identity via Shield `/api/internal/v1/auth/resolve` (cached in Redis). When auth is disabled (dev/local) these routes are open.\n\n**MCP transport:** the Model Context Protocol streamable-http endpoint is served at `/mcp` (used by Claude Desktop / ChatGPT clients). It speaks the MCP protocol rather than a REST contract, so it is not modeled as a path operation below."},"servers":[{"url":"https://victoria-agent.sojoshield.com","description":"This host"},{"url":"https://victoria-agent.sojoshield.dev","description":"Staging"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Stytch session JWT. Pass as 'Authorization: Bearer '."}}},"tags":[{"name":"Chat","description":"Orchestration / streaming chat"},{"name":"Conversations","description":"Chat-history list/read/rename/delete (requires AGENT_PERSISTENCE_ENABLED; 404 when the feature is off)"},{"name":"Files","description":"Image upload (files are uploaded via Shield, then referenced by S3 key)"},{"name":"Health","description":"Health probe"}],"paths":{"/orchestrate":{"post":{"tags":["Chat"],"summary":"Run a (non-streaming) orchestration turn","description":"Routes a user message + optional files through the unified LangGraph pipeline and returns a structured result.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"session_id":{"type":"string","nullable":true},"files":{"type":"array","items":{"type":"object","description":"Reference an uploaded file by exactly one of: `file_path` (preferred — the S3 key returned after uploading via Shield's `/cloud/access-url`, e.g. `victoria///`; the server reads the bytes through Shield S3) or `data` (inline base64 fallback). The chat surface also accepts these as `file` message parts: `{type:\"file\", url, mediaType, filename}`.","properties":{"type":{"type":"string"},"media_type":{"type":"string"},"file_path":{"type":"string","description":"S3 key from Shield's access-url flow; preferred reference"},"filename":{"type":"string","description":"leaf filename; display name shown in the UI"},"data":{"type":"string","description":"base64-encoded bytes (inline fallback path)"}}}}},"required":["message"]}}}},"responses":{"200":{"description":"Orchestration result","content":{"application/json":{"schema":{"type":"object","properties":{"response":{"type":"string"},"session_id":{"type":"string"},"tool_used":{"type":"string","nullable":true},"metadata":{"type":"object"},"needs_input":{"type":"boolean"}}}}}},"400":{"description":"Message or files required"},"401":{"description":"Missing/invalid token (when auth enabled)"},"422":{"description":"An attached file could not be read (out-of-scope key or unreadable object) — re-upload and retry"},"503":{"description":"Retrieval failed at the connector level (treated as infra error)"}}}},"/api/chat":{"post":{"tags":["Chat"],"summary":"Streaming chat (Server-Sent Events)","description":"SSE wrapper around orchestration, speaking the **AI SDK UI Message Stream Protocol** (`@ai-sdk/react`). The response is a `text/event-stream` of `data: {json}\\n\\n` frames: `data-conversation` (the very first frame, before `start` — only when chat history is enabled), `start`, `data-plan`, `data-thinking`, `text-start`/`text-delta`/`text-end`, `tool-input-available`, `tool-output-available`/`tool-output-error`, `data-artifact`, `data-final-text`, `data-cancelled`, `data-debug` (dev-only, off by default), `error`, and exactly one terminal `finish`. See `contracts/frontend-streaming-contract.md`.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","description":"Chat history (AI SDK shape); the latest user message is used.","items":{"type":"object"}},"message":{"type":"string","description":"Alternative to messages: a single user message."},"runConfig":{"type":"object","description":"AI SDK run config — the FE's native way to pass the conversation id.","properties":{"sessionId":{"type":"string","description":"Conversation id (the contract field). Omit (or send empty) to start a NEW conversation — the server mints a uuid and returns it on the first SSE frame (data-conversation). Precedence: runConfig.sessionId > runConfig.conversation_id (forward-compat alias for the sc-20601 rename) > server-generated."},"conversation_id":{"type":"string","description":"Forward-compat alias of runConfig.sessionId (the future sc-20601 rename); same value. sessionId wins when both are sent."}}},"files":{"type":"array","items":{"type":"object","description":"Reference an uploaded file by exactly one of: `file_path` (preferred — the S3 key returned after uploading via Shield's `/cloud/access-url`, e.g. `victoria///`; the server reads the bytes through Shield S3) or `data` (inline base64 fallback). The chat surface also accepts these as `file` message parts: `{type:\"file\", url, mediaType, filename}`.","properties":{"type":{"type":"string"},"media_type":{"type":"string"},"file_path":{"type":"string","description":"S3 key from Shield's access-url flow; preferred reference"},"filename":{"type":"string","description":"leaf filename; display name shown in the UI"},"data":{"type":"string","description":"base64-encoded bytes (inline fallback path)"}}}}}}}}},"responses":{"200":{"description":"SSE stream (AI SDK UI Message Stream Protocol)","content":{"text/event-stream":{"schema":{"type":"string"}}}},"400":{"description":"No user message provided"},"401":{"description":"Missing/invalid token (when auth enabled)"}}}},"/api/chat/conversations":{"get":{"tags":["Conversations"],"summary":"List the caller's conversations (paginated)","description":"Most-recent-first. Identity comes from the resolved token; admins see their whole org. Returns 404 when chat history is disabled (`AGENT_PERSISTENCE_ENABLED=false`) — deterministic feature detection for the FE.","security":[{"BearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":15}}],"responses":{"200":{"description":"Page of conversations","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"session_id":{"type":"string","format":"uuid"},"title":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}}},"total":{"type":"integer"}}}}}},"401":{"description":"No identity (missing/invalid token, or bypass origin)"},"404":{"description":"Chat history is not enabled"},"422":{"description":"Invalid page/limit"}}}},"/api/chat/conversation/{session_id}":{"parameters":[{"name":"session_id","in":"path","required":true,"description":"The conversation id — the same uuid the FE receives on the `data-conversation` SSE frame / sends as `runConfig.sessionId`.","schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Conversations"],"summary":"Get the full message thread","description":"Messages are Vercel `ai` UIMessage-shaped (`parts`, never `content`). 404 for absent, deleted, or another tenant's conversation (never 403).","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Conversation thread","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","format":"uuid"},"title":{"type":"string"},"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["user","assistant"]},"parts":{"type":"array","description":"AI SDK UIMessage parts. `text` parts carry the message text; `file` parts carry an attachment. On a user message the `url` is the S3 key from Shield's upload flow (`victoria///`); on an assistant message it is a readable url for a Victoria-generated file.","items":{"type":"object","properties":{"type":{"type":"string","enum":["text","file"]},"text":{"type":"string","description":"for text parts"},"mediaType":{"type":"string","description":"for file parts"},"filename":{"type":"string","description":"for file parts"},"url":{"type":"string","description":"for file parts: S3 key (user upload) or readable url (generated file)"}}}}}}}}}}}},"401":{"description":"No identity"},"404":{"description":"Absent/deleted/foreign conversation, or chat history disabled"}}},"patch":{"tags":["Conversations"],"summary":"Rename the conversation","description":"The only title writer besides first-turn auto-titling; later turns never clobber a rename.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":60}},"required":["title"]}}}},"responses":{"200":{"description":"Renamed","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","format":"uuid"},"title":{"type":"string"}}}}}},"401":{"description":"No identity"},"404":{"description":"Absent/deleted/foreign conversation, or chat history disabled"},"422":{"description":"Title must be 1-60 characters after trimming"}}},"delete":{"tags":["Conversations"],"summary":"Delete the conversation (soft)","description":"Sets status=deleted; afterwards every read/rename/delete/append answers 404. Rows are physically purged by the retention job.","security":[{"BearerAuth":[]}],"responses":{"204":{"description":"Deleted"},"401":{"description":"No identity"},"404":{"description":"Absent/already-deleted/foreign conversation, or chat history disabled"}}}},"/api/chat/conversation/export/{session_id}":{"parameters":[{"name":"session_id","in":"path","required":true,"description":"The conversation id to export.","schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Conversations"],"summary":"Export the thread as a debug snapshot","description":"Legacy export schema: flattened `content` + `timestamp` per message, with assistant messages also carrying the persisted observability `metadata`/`tool_used`/`images_count`. Response body is wrapped under `data` like every other conversations endpoint (sc-21556). Conversations created before debug-metadata persistence shipped export with `metadata: null`. 404 for absent, deleted, or another tenant's conversation (never 403).","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Conversation export","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"exported_at":{"type":"string","format":"date-time"},"title":{"type":"string"},"session_id":{"type":"string","format":"uuid"},"mode":{"type":"string"},"user_agent":{"type":"string","nullable":true},"file_url_ttl_seconds":{"type":"integer","description":"present only when a message carries a presigned url"},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"},"timestamp":{"type":"string","format":"date-time","nullable":true},"tool_used":{"type":"string","nullable":true,"description":"assistant messages only"},"metadata":{"type":"object","nullable":true,"description":"assistant messages only; debug observability trace"},"images_count":{"type":"integer","description":"assistant messages only"}}}}}}}}}}},"401":{"description":"No identity"},"404":{"description":"Absent/deleted/foreign conversation, or chat history disabled"}}}},"/upload-image":{"post":{"tags":["Files"],"summary":"Upload raw image bytes","requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Stored","content":{"application/json":{"schema":{"type":"object","properties":{"image_id":{"type":"string"},"size_kb":{"type":"number"},"expires_in":{"type":"integer"}}}}}},"400":{"description":"Empty body"}}}},"/health":{"get":{"tags":["Health"],"summary":"Health check (load balancers)","description":"Service status plus `database`, `pinecone`, `auth_enabled`, `redis` (auth-cache), and `hitl` (clarify kill-switch state, incl. unrecognized env values) health. Redis and hitl are report-only and do not gate health.","responses":{"200":{"description":"Healthy/degraded","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"service":{"type":"string"},"environment":{"type":"string"},"version":{"type":"string"},"database":{"type":"object"},"pinecone":{"type":"object"},"auth_enabled":{"type":"boolean"},"redis":{"type":"object"},"hitl":{"type":"object"}}}}}},"503":{"description":"Unhealthy"}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe (fail-closed on persistence)","description":"200 when persistence is disabled or its deep check (pool + required tables) passes; 503 with a reason otherwise. Intended for the k8s readinessProbe once the flag is flipped.","responses":{"200":{"description":"Ready","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"persistence":{"type":"string"}}}}}},"503":{"description":"Persistence enabled but not healthy (reason included)"}}}}}}