Frontend Integration
AI — Frontend Integration
For web (React/Next.js) and mobile developers building the chat UI on the v2
multi-agent assistant (LangGraph). The live surface is the WebSocket namespace
/ai-v2/chat; REST (/ai-v2/...) is the blocking fallback.
Auth
Both surfaces use the admin login JWT — the same token as the rest of the API.
There's no separate AI permission; routes run AiV2EnabledGuard + AdminAuthGuard,
so any authenticated admin can chat when AI_V2_ENABLED is on (else 404).
Authorization: Bearer <jwt>Base URL https://<workspace>.corteksa.com/api/v1. On the WebSocket, pass the JWT in
the handshake (auth.token, query.token, or the Authorization header). The tenant
is derived from the request host.
The flow
1. Create conversation POST /ai-v2/conversations → { slug }
2. Connect WS WS /ai-v2/chat → auth in handshake → assistant:ready
3. Send a message emit assistant:send { conversationId, message }
4. Render the stream on assistant:agent / :token / :tool_start / :tool_result
5. Handle a write on assistant:confirmation_required → emit assistant:confirm / :reject
6. Turn done on assistant:complete (or assistant:handoff)Prefer the WebSocket for anything interactive. Use REST
(POST /ai-v2/conversations/{slug}/messages) only when you want the full answer in
one blocking call — see REST API.
Streaming events (WebSocket /ai-v2/chat)
On connect the server emits assistant:ready { agent: "supervisor" }. Then, per turn:
Server → client
| Event | Fires when | Key fields |
|---|---|---|
assistant:thinking | Turn accepted | conversationId |
assistant:agent | The supervisor routed the turn to a specialist (v2-only) | which agent — switch UI mode |
assistant:token | A token of the answer streamed | conversationId, delta |
assistant:tool_start | A tool call began | tool name + args |
assistant:tool_result | A tool call returned | tool name, success, summary |
assistant:confirmation_required | A write batch needs approval (the gate) | the pending calls + their ids |
assistant:complete | Turn finished | the final message + usage |
assistant:handoff | The supervisor handed off (e.g. open the workspace builder) — terminal (v2-only) | the target |
assistant:error | Something failed | code, message, conversationId?, retryAfter? |
Client → server
| Event | Payload | Meaning |
|---|---|---|
assistant:send | { conversationId?, message, agent?, object_slug? } | Start / continue a turn. Omit conversationId to start fresh; agent pins a specialist; object_slug is focus context. |
assistant:confirm | { conversationId?, approvedCallIds? } | Approve pending writes — the server runs only the echoed call ids |
assistant:reject | { conversationId? } | Decline the pending writes |
assistant:cancel_generation | — | Stop the in-flight turn |
Send a message
socket.emit('assistant:send', { conversationId, message: 'how many open deals?' });
socket.on('assistant:agent', (e) => setActiveAgent(e)); // route → UI mode
socket.on('assistant:token', (e) => appendDelta(e.delta)); // stream the answer
socket.on('assistant:tool_start', (e) => showToolChip(e));
socket.on('assistant:complete', (e) => finalize(e));The write-confirmation gate
When a turn wants to create, update, or delete, it does not act — it emits
assistant:confirmation_required with the pending calls and pauses. Render a card,
then echo back the exact call ids to approve:
socket.on('assistant:confirmation_required', (c) => {
showCard(c); // list the pending writes
// On Approve — run only what the card showed:
socket.emit('assistant:confirm', { conversationId: c.conversationId, approvedCallIds: c.callIds });
// On Decline:
// socket.emit('assistant:reject', { conversationId: c.conversationId });
});The gate is backend-enforced: the server re-checks each approved call and runs only those still pending — a missing card can never cause an unconfirmed write. This is a hard law: see Write-confirmation law.
UI concerns
- Stream, don't block — render
assistant:tokeninto the bubble; spinner onassistant:thinking, tool chips onassistant:tool_start, agent badge onassistant:agent. - Never auto-run writes — always surface
assistant:confirmation_requiredas an explicit Approve/Decline card, and only sendapprovedCallIdsthe user OK'd. - Handle hand-offs — on
assistant:handoff, transition the UI (e.g. open the workspace builder) rather than treating it as a normal completion. - Errors — map
assistant:error.code(AUTH_INVALID,RATE_LIMIT,TOKEN_BUDGET_EXCEEDED, …); see Troubleshooting.