@octavus/docs 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/02-server-sdk/02-sessions.md +29 -0
- package/content/03-client-sdk/05-socket-transport.md +24 -49
- package/content/06-examples/03-socket-chat.md +17 -37
- package/dist/chunk-GI574O6S.js +1435 -0
- package/dist/chunk-GI574O6S.js.map +1 -0
- package/dist/chunk-KUB6BGPR.js +1435 -0
- package/dist/chunk-KUB6BGPR.js.map +1 -0
- package/dist/chunk-WKCT4ABS.js +1435 -0
- package/dist/chunk-WKCT4ABS.js.map +1 -0
- package/dist/content.js +1 -1
- package/dist/docs.json +6 -6
- package/dist/index.js +1 -1
- package/dist/search-index.json +1 -1
- package/dist/search.js +1 -1
- package/dist/search.js.map +1 -1
- package/dist/sections.json +6 -6
- package/package.json +1 -1
package/dist/sections.json
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"section": "server-sdk",
|
|
37
37
|
"title": "Overview",
|
|
38
38
|
"description": "Introduction to the Octavus Server SDK for backend integration.",
|
|
39
|
-
"content": "\n# Server SDK Overview\n\nThe `@octavus/server-sdk` package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation loop.\n\n**Current version:** `2.
|
|
39
|
+
"content": "\n# Server SDK Overview\n\nThe `@octavus/server-sdk` package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation loop.\n\n**Current version:** `2.1.0`\n\n## Installation\n\n```bash\nnpm install @octavus/server-sdk\n```\n\nFor agent management (sync, validate), install the CLI as a dev dependency:\n\n```bash\nnpm install --save-dev @octavus/cli\n```\n\n## Basic Usage\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n```\n\n## Key Features\n\n### Agent Management\n\nAgent definitions are managed via the CLI. See the [CLI documentation](/docs/server-sdk/cli) for details.\n\n```bash\n# Sync agent from local files\noctavus sync ./agents/support-chat\n\n# Output: Created: support-chat\n# Agent ID: clxyz123abc456\n```\n\n### Session Management\n\nCreate and manage agent sessions using the agent ID:\n\n```typescript\n// Create a new session (use agent ID from CLI sync)\nconst sessionId = await client.agentSessions.create('clxyz123abc456', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n});\n\n// Get UI-ready session messages (for session restore)\nconst session = await client.agentSessions.getMessages(sessionId);\n```\n\n### Tool Handlers\n\nTools run on your server with your data:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => {\n // Access your database, APIs, etc.\n return await db.users.findById(args.userId);\n },\n },\n});\n```\n\n### Streaming\n\nAll responses stream in real-time:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() returns an async generator of events\nconst events = session.execute({\n type: 'trigger',\n triggerName: 'user-message',\n input: { USER_MESSAGE: 'Hello!' },\n});\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n## API Reference\n\n### OctavusClient\n\nThe main entry point for interacting with Octavus.\n\n```typescript\ninterface OctavusClientConfig {\n baseUrl: string; // Octavus API URL\n apiKey?: string; // Your API key\n}\n\nclass OctavusClient {\n readonly agents: AgentsApi;\n readonly agentSessions: AgentSessionsApi;\n readonly files: FilesApi;\n\n constructor(config: OctavusClientConfig);\n}\n```\n\n### AgentSessionsApi\n\nManages agent sessions.\n\n```typescript\nclass AgentSessionsApi {\n // Create a new session\n async create(agentId: string, input?: Record<string, unknown>): Promise<string>;\n\n // Get full session state (for debugging/internal use)\n async get(sessionId: string): Promise<SessionState>;\n\n // Get UI-ready messages (for client display)\n async getMessages(sessionId: string): Promise<UISessionState>;\n\n // Attach to a session for triggering\n attach(sessionId: string, options?: SessionAttachOptions): AgentSession;\n}\n\n// Full session state (internal format)\ninterface SessionState {\n id: string;\n agentId: string;\n input: Record<string, unknown>;\n variables: Record<string, unknown>;\n resources: Record<string, unknown>;\n messages: ChatMessage[]; // Internal message format\n createdAt: string;\n updatedAt: string;\n}\n\n// UI-ready session state\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready messages for frontend\n}\n```\n\n### AgentSession\n\nHandles request execution and streaming for a specific session.\n\n```typescript\nclass AgentSession {\n // Execute a request and stream parsed events\n execute(request: SessionRequest, options?: TriggerOptions): AsyncGenerator<StreamEvent>;\n\n // Get the session ID\n getSessionId(): string;\n}\n\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n}\n\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n\n// Helper to convert events to SSE stream\nfunction toSSEStream(events: AsyncIterable<StreamEvent>): ReadableStream<Uint8Array>;\n```\n\n### FilesApi\n\nHandles file uploads for sessions.\n\n```typescript\nclass FilesApi {\n // Get presigned URLs for file uploads\n async getUploadUrls(sessionId: string, files: FileUploadRequest[]): Promise<UploadUrlsResponse>;\n}\n\ninterface FileUploadRequest {\n filename: string;\n mediaType: string;\n size: number;\n}\n\ninterface UploadUrlsResponse {\n files: {\n id: string; // File ID for references\n uploadUrl: string; // PUT to this URL\n downloadUrl: string; // GET URL after upload\n }[];\n}\n```\n\nThe client uploads files directly to S3 using the presigned upload URL. See [File Uploads](/docs/client-sdk/file-uploads) for the full integration pattern.\n\n## Next Steps\n\n- [Sessions](/docs/server-sdk/sessions) — Deep dive into session management\n- [Tools](/docs/server-sdk/tools) — Implementing tool handlers\n- [Streaming](/docs/server-sdk/streaming) — Understanding stream events\n",
|
|
40
40
|
"excerpt": "Server SDK Overview The package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation...",
|
|
41
41
|
"order": 1
|
|
42
42
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"section": "server-sdk",
|
|
46
46
|
"title": "Sessions",
|
|
47
47
|
"description": "Managing agent sessions with the Server SDK.",
|
|
48
|
-
"content": "\n# Sessions\n\nSessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions.\n\n## Creating Sessions\n\nCreate a session by specifying the agent ID and initial input variables:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\n// Create a session with the support-chat agent\nconst sessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n USER_ID: 'user-123', // Optional inputs\n});\n\nconsole.log('Session created:', sessionId);\n```\n\n## Getting Session Messages\n\nTo restore a conversation on page load, use `getMessages()` to retrieve UI-ready messages:\n\n```typescript\nconst session = await client.agentSessions.getMessages(sessionId);\n\nconsole.log({\n sessionId: session.sessionId,\n agentId: session.agentId,\n messages: session.messages.length, // UIMessage[] ready for frontend\n});\n```\n\nThe returned messages can be passed directly to the client SDK's `initialMessages` option.\n\n### UISessionState Interface\n\n```typescript\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready conversation history\n}\n```\n\n## Full Session State (Debug)\n\nFor debugging or internal use, you can retrieve the complete session state including all variables and internal message format:\n\n```typescript\nconst state = await client.agentSessions.get(sessionId);\n\nconsole.log({\n id: state.id,\n agentId: state.agentId,\n messages: state.messages.length, // ChatMessage[] (internal format)\n resources: state.resources,\n variables: state.variables,\n createdAt: state.createdAt,\n updatedAt: state.updatedAt,\n});\n```\n\n> **Note**: Use `getMessages()` for client-facing code. The `get()` method returns internal message format that includes hidden content not intended for end users.\n\n## Attaching to Sessions\n\nTo trigger actions on a session, you need to attach to it first:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n // Tool handlers (see Tools documentation)\n },\n resources: [\n // Resource watchers (optional)\n ],\n});\n```\n\n## Executing Requests\n\nOnce attached, execute requests on the session using `execute()`:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() handles both triggers and client tool continuations\nconst events = session.execute(\n { type: 'trigger', triggerName: 'user-message', input: { USER_MESSAGE: 'Hello!' } },\n { signal: request.signal },\n);\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n### Request Types\n\nThe `execute()` method accepts a discriminated union:\n\n```typescript\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\n// Start a new conversation turn\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n}\n\n// Continue after client-side tool handling\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n```\n\nThis makes it easy to pass requests through from the client:\n\n```typescript\n// Simple passthrough from HTTP request body\nexport async function POST(request: Request) {\n const body = await request.json();\n const { sessionId, ...payload } = body;\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n /* ... */\n },\n });\n const events = session.execute(payload, { signal: request.signal });\n\n return new Response(toSSEStream(events));\n}\n```\n\n### Stop Support\n\nPass an abort signal to allow clients to stop generation:\n\n```typescript\nconst events = session.execute(request, {\n signal: request.signal, // Forward the client's abort signal\n});\n```\n\nWhen the client aborts the request, the signal propagates through to the LLM provider, stopping generation immediately. Any partial content is preserved.\n\n## Session Lifecycle\n\n```mermaid\nflowchart TD\n A[1. CREATE] --> B[2. ATTACH]\n B --> C[3. TRIGGER]\n C --> C\n C --> D[4. RETRIEVE]\n D --> C\n C --> E[5. EXPIRE]\n E --> F{6. RESTORE?}\n F -->|Yes| C\n F -->|No| A\n\n A -.- A1[\"`**client.agentSessions.create()**\n Returns sessionId\n Initializes state`\"]\n\n B -.- B1[\"`**client.agentSessions.attach()**\n Configure tool handlers\n Configure resource watchers`\"]\n\n C -.- C1[\"`**session.execute()**\n Execute request\n Stream events\n Update state`\"]\n\n D -.- D1[\"`**client.agentSessions.getMessages()**\n Get UI-ready messages\n Check session status`\"]\n\n E -.- E1[\"`Sessions expire after\n 24 hours (configurable)`\"]\n\n F -.- F1[\"`**client.agentSessions.restore()**\n Restore from stored messages\n Or create new session`\"]\n```\n\n## Session Expiration\n\nSessions expire after a period of inactivity (default: 24 hours). When you call `getMessages()` or `get()`, the response includes a `status` field:\n\n```typescript\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'expired') {\n // Session has expired - restore or create new\n console.log('Session expired:', result.sessionId);\n} else {\n // Session is active\n console.log('Messages:', result.messages.length);\n}\n```\n\n### Response Types\n\n| Status | Type | Description |\n| --------- | --------------------- | ------------------------------------------------------------- |\n| `active` | `UISessionState` | Session is active, includes `messages` array |\n| `expired` | `ExpiredSessionState` | Session expired, includes `sessionId`, `agentId`, `createdAt` |\n\n## Persisting Chat History\n\nTo enable session restoration, store the chat messages in your own database after each interaction:\n\n```typescript\n// After each trigger completes, save messages\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'active') {\n // Store in your database\n await db.chats.update({\n where: { id: chatId },\n data: {\n sessionId: result.sessionId,\n messages: result.messages, // Store UIMessage[] as JSON\n },\n });\n}\n```\n\n> **Best Practice**: Store the full `UIMessage[]` array. This preserves all message parts (text, tool calls, files, etc.) needed for accurate restoration.\n\n## Restoring Sessions\n\nWhen a user returns to your app:\n\n```typescript\n// 1. Load stored data from your database\nconst chat = await db.chats.findUnique({ where: { id: chatId } });\n\n// 2. Check if session is still active\nconst result = await client.agentSessions.getMessages(chat.sessionId);\n\nif (result.status === 'active') {\n // Session is active - use it directly\n return {\n sessionId: result.sessionId,\n messages: result.messages,\n };\n}\n\n// 3. Session expired - restore from stored messages\nif (chat.messages && chat.messages.length > 0) {\n const restored = await client.agentSessions.restore(\n chat.sessionId,\n chat.messages,\n { COMPANY_NAME: 'Acme Corp' }, // Optional: same input as create()\n );\n\n if (restored.restored) {\n // Session restored successfully\n return {\n sessionId: restored.sessionId,\n messages: chat.messages,\n };\n }\n}\n\n// 4. Cannot restore - create new session\nconst newSessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n});\n\nreturn {\n sessionId: newSessionId,\n messages: [],\n};\n```\n\n### Restore Response\n\n```typescript\ninterface RestoreSessionResult {\n sessionId: string;\n restored: boolean; // true if restored, false if session was already active\n}\n```\n\n## Complete Example\n\nHere's a complete session management flow:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nasync function getOrCreateSession(chatId: string, agentId: string, input: Record<string, unknown>) {\n // Load existing chat data\n const chat = await db.chats.findUnique({ where: { id: chatId } });\n\n if (chat?.sessionId) {\n // Check session status\n const result = await client.agentSessions.getMessages(chat.sessionId);\n\n if (result.status === 'active') {\n return { sessionId: result.sessionId, messages: result.messages };\n }\n\n // Try to restore expired session\n if (chat.messages?.length > 0) {\n const restored = await client.agentSessions.restore(chat.sessionId, chat.messages, input);\n if (restored.restored) {\n return { sessionId: restored.sessionId, messages: chat.messages };\n }\n }\n }\n\n // Create new session\n const sessionId = await client.agentSessions.create(agentId, input);\n\n // Save to database\n await db.chats.upsert({\n where: { id: chatId },\n create: { id: chatId, sessionId, messages: [] },\n update: { sessionId, messages: [] },\n });\n\n return { sessionId, messages: [] };\n}\n```\n\n## Error Handling\n\n```typescript\nimport { ApiError } from '@octavus/server-sdk';\n\ntry {\n const session = await client.agentSessions.getMessages(sessionId);\n} catch (error) {\n if (error instanceof ApiError) {\n if (error.status === 404) {\n // Session not found or expired\n console.log('Session expired, create a new one');\n } else {\n console.error('API Error:', error.message);\n }\n }\n throw error;\n}\n```\n",
|
|
48
|
+
"content": "\n# Sessions\n\nSessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions.\n\n## Creating Sessions\n\nCreate a session by specifying the agent ID and initial input variables:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\n// Create a session with the support-chat agent\nconst sessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n USER_ID: 'user-123', // Optional inputs\n});\n\nconsole.log('Session created:', sessionId);\n```\n\n## Getting Session Messages\n\nTo restore a conversation on page load, use `getMessages()` to retrieve UI-ready messages:\n\n```typescript\nconst session = await client.agentSessions.getMessages(sessionId);\n\nconsole.log({\n sessionId: session.sessionId,\n agentId: session.agentId,\n messages: session.messages.length, // UIMessage[] ready for frontend\n});\n```\n\nThe returned messages can be passed directly to the client SDK's `initialMessages` option.\n\n### UISessionState Interface\n\n```typescript\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready conversation history\n}\n```\n\n## Full Session State (Debug)\n\nFor debugging or internal use, you can retrieve the complete session state including all variables and internal message format:\n\n```typescript\nconst state = await client.agentSessions.get(sessionId);\n\nconsole.log({\n id: state.id,\n agentId: state.agentId,\n messages: state.messages.length, // ChatMessage[] (internal format)\n resources: state.resources,\n variables: state.variables,\n createdAt: state.createdAt,\n updatedAt: state.updatedAt,\n});\n```\n\n> **Note**: Use `getMessages()` for client-facing code. The `get()` method returns internal message format that includes hidden content not intended for end users.\n\n## Attaching to Sessions\n\nTo trigger actions on a session, you need to attach to it first:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n // Tool handlers (see Tools documentation)\n },\n resources: [\n // Resource watchers (optional)\n ],\n});\n```\n\n## Executing Requests\n\nOnce attached, execute requests on the session using `execute()`:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() handles both triggers and client tool continuations\nconst events = session.execute(\n { type: 'trigger', triggerName: 'user-message', input: { USER_MESSAGE: 'Hello!' } },\n { signal: request.signal },\n);\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n### Request Types\n\nThe `execute()` method accepts a discriminated union:\n\n```typescript\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\n// Start a new conversation turn\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n}\n\n// Continue after client-side tool handling\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n```\n\nThis makes it easy to pass requests through from the client:\n\n```typescript\n// Simple passthrough from HTTP request body\nexport async function POST(request: Request) {\n const body = await request.json();\n const { sessionId, ...payload } = body;\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n /* ... */\n },\n });\n const events = session.execute(payload, { signal: request.signal });\n\n return new Response(toSSEStream(events));\n}\n```\n\n### Stop Support\n\nPass an abort signal to allow clients to stop generation:\n\n```typescript\nconst events = session.execute(request, {\n signal: request.signal, // Forward the client's abort signal\n});\n```\n\nWhen the client aborts the request, the signal propagates through to the LLM provider, stopping generation immediately. Any partial content is preserved.\n\n## WebSocket Handling\n\nFor WebSocket integrations, use `handleSocketMessage()` which manages abort controller lifecycle internally:\n\n```typescript\nimport type { SocketMessage } from '@octavus/server-sdk';\n\n// In your socket handler\nconn.on('data', async (rawData: string) => {\n const msg = JSON.parse(rawData);\n\n if (msg.type === 'trigger' || msg.type === 'continue' || msg.type === 'stop') {\n await session.handleSocketMessage(msg as SocketMessage, {\n onEvent: (event) => conn.write(JSON.stringify(event)),\n onFinish: () => sendMessagesUpdate(), // Optional callback after streaming\n });\n }\n});\n```\n\nThe `handleSocketMessage()` method:\n\n- Handles `trigger`, `continue`, and `stop` messages\n- Automatically aborts previous requests when a new one arrives\n- Streams events via the `onEvent` callback\n- Calls `onFinish` after streaming completes (not called if aborted)\n\nSee [Socket Chat Example](/docs/examples/socket-chat) for a complete implementation.\n\n## Session Lifecycle\n\n```mermaid\nflowchart TD\n A[1. CREATE] --> B[2. ATTACH]\n B --> C[3. TRIGGER]\n C --> C\n C --> D[4. RETRIEVE]\n D --> C\n C --> E[5. EXPIRE]\n E --> F{6. RESTORE?}\n F -->|Yes| C\n F -->|No| A\n\n A -.- A1[\"`**client.agentSessions.create()**\n Returns sessionId\n Initializes state`\"]\n\n B -.- B1[\"`**client.agentSessions.attach()**\n Configure tool handlers\n Configure resource watchers`\"]\n\n C -.- C1[\"`**session.execute()**\n Execute request\n Stream events\n Update state`\"]\n\n D -.- D1[\"`**client.agentSessions.getMessages()**\n Get UI-ready messages\n Check session status`\"]\n\n E -.- E1[\"`Sessions expire after\n 24 hours (configurable)`\"]\n\n F -.- F1[\"`**client.agentSessions.restore()**\n Restore from stored messages\n Or create new session`\"]\n```\n\n## Session Expiration\n\nSessions expire after a period of inactivity (default: 24 hours). When you call `getMessages()` or `get()`, the response includes a `status` field:\n\n```typescript\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'expired') {\n // Session has expired - restore or create new\n console.log('Session expired:', result.sessionId);\n} else {\n // Session is active\n console.log('Messages:', result.messages.length);\n}\n```\n\n### Response Types\n\n| Status | Type | Description |\n| --------- | --------------------- | ------------------------------------------------------------- |\n| `active` | `UISessionState` | Session is active, includes `messages` array |\n| `expired` | `ExpiredSessionState` | Session expired, includes `sessionId`, `agentId`, `createdAt` |\n\n## Persisting Chat History\n\nTo enable session restoration, store the chat messages in your own database after each interaction:\n\n```typescript\n// After each trigger completes, save messages\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'active') {\n // Store in your database\n await db.chats.update({\n where: { id: chatId },\n data: {\n sessionId: result.sessionId,\n messages: result.messages, // Store UIMessage[] as JSON\n },\n });\n}\n```\n\n> **Best Practice**: Store the full `UIMessage[]` array. This preserves all message parts (text, tool calls, files, etc.) needed for accurate restoration.\n\n## Restoring Sessions\n\nWhen a user returns to your app:\n\n```typescript\n// 1. Load stored data from your database\nconst chat = await db.chats.findUnique({ where: { id: chatId } });\n\n// 2. Check if session is still active\nconst result = await client.agentSessions.getMessages(chat.sessionId);\n\nif (result.status === 'active') {\n // Session is active - use it directly\n return {\n sessionId: result.sessionId,\n messages: result.messages,\n };\n}\n\n// 3. Session expired - restore from stored messages\nif (chat.messages && chat.messages.length > 0) {\n const restored = await client.agentSessions.restore(\n chat.sessionId,\n chat.messages,\n { COMPANY_NAME: 'Acme Corp' }, // Optional: same input as create()\n );\n\n if (restored.restored) {\n // Session restored successfully\n return {\n sessionId: restored.sessionId,\n messages: chat.messages,\n };\n }\n}\n\n// 4. Cannot restore - create new session\nconst newSessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n});\n\nreturn {\n sessionId: newSessionId,\n messages: [],\n};\n```\n\n### Restore Response\n\n```typescript\ninterface RestoreSessionResult {\n sessionId: string;\n restored: boolean; // true if restored, false if session was already active\n}\n```\n\n## Complete Example\n\nHere's a complete session management flow:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nasync function getOrCreateSession(chatId: string, agentId: string, input: Record<string, unknown>) {\n // Load existing chat data\n const chat = await db.chats.findUnique({ where: { id: chatId } });\n\n if (chat?.sessionId) {\n // Check session status\n const result = await client.agentSessions.getMessages(chat.sessionId);\n\n if (result.status === 'active') {\n return { sessionId: result.sessionId, messages: result.messages };\n }\n\n // Try to restore expired session\n if (chat.messages?.length > 0) {\n const restored = await client.agentSessions.restore(chat.sessionId, chat.messages, input);\n if (restored.restored) {\n return { sessionId: restored.sessionId, messages: chat.messages };\n }\n }\n }\n\n // Create new session\n const sessionId = await client.agentSessions.create(agentId, input);\n\n // Save to database\n await db.chats.upsert({\n where: { id: chatId },\n create: { id: chatId, sessionId, messages: [] },\n update: { sessionId, messages: [] },\n });\n\n return { sessionId, messages: [] };\n}\n```\n\n## Error Handling\n\n```typescript\nimport { ApiError } from '@octavus/server-sdk';\n\ntry {\n const session = await client.agentSessions.getMessages(sessionId);\n} catch (error) {\n if (error instanceof ApiError) {\n if (error.status === 404) {\n // Session not found or expired\n console.log('Session expired, create a new one');\n } else {\n console.error('API Error:', error.message);\n }\n }\n throw error;\n}\n```\n",
|
|
49
49
|
"excerpt": "Sessions Sessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions. Creating Sessions Create a session by...",
|
|
50
50
|
"order": 2
|
|
51
51
|
},
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"section": "server-sdk",
|
|
73
73
|
"title": "CLI",
|
|
74
74
|
"description": "Command-line interface for validating and syncing agent definitions.",
|
|
75
|
-
"content": "\n# Octavus CLI\n\nThe `@octavus/cli` package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform.\n\n**Current version:** `2.
|
|
75
|
+
"content": "\n# Octavus CLI\n\nThe `@octavus/cli` package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform.\n\n**Current version:** `2.1.0`\n\n## Installation\n\n```bash\nnpm install --save-dev @octavus/cli\n```\n\n## Configuration\n\nThe CLI requires an API key with the **Agents** permission.\n\n### Environment Variables\n\n| Variable | Description |\n| --------------------- | ---------------------------------------------- |\n| `OCTAVUS_CLI_API_KEY` | API key with \"Agents\" permission (recommended) |\n| `OCTAVUS_API_KEY` | Fallback if `OCTAVUS_CLI_API_KEY` not set |\n| `OCTAVUS_API_URL` | Optional, defaults to `https://octavus.ai` |\n\n### Two-Key Strategy (Recommended)\n\nFor production deployments, use separate API keys with minimal permissions:\n\n```bash\n# CI/CD or .env.local (not committed)\nOCTAVUS_CLI_API_KEY=oct_sk_... # \"Agents\" permission only\n\n# Production .env\nOCTAVUS_API_KEY=oct_sk_... # \"Sessions\" permission only\n```\n\nThis ensures production servers only have session permissions (smaller blast radius if leaked), while agent management is restricted to development/CI environments.\n\n### Multiple Environments\n\nUse separate Octavus projects for staging and production, each with their own API keys. The `--env` flag lets you load different environment files:\n\n```bash\n# Local development (default: .env)\noctavus sync ./agents/my-agent\n\n# Staging project\noctavus --env .env.staging sync ./agents/my-agent\n\n# Production project\noctavus --env .env.production sync ./agents/my-agent\n```\n\nExample environment files:\n\n```bash\n# .env.staging (syncs to your staging project)\nOCTAVUS_CLI_API_KEY=oct_sk_staging_project_key...\n\n# .env.production (syncs to your production project)\nOCTAVUS_CLI_API_KEY=oct_sk_production_project_key...\n```\n\nEach project has its own agents, so you'll get different agent IDs per environment.\n\n## Global Options\n\n| Option | Description |\n| -------------- | ------------------------------------------------------- |\n| `--env <file>` | Load environment from a specific file (default: `.env`) |\n| `--help` | Show help |\n| `--version` | Show version |\n\n## Commands\n\n### `octavus sync <path>`\n\nSync an agent definition to the platform. Creates the agent if it doesn't exist, or updates it if it does.\n\n```bash\noctavus sync ./agents/my-agent\n```\n\n**Options:**\n\n- `--json` — Output as JSON (for CI/CD parsing)\n- `--quiet` — Suppress non-essential output\n\n**Example output:**\n\n```\nℹ Reading agent from ./agents/my-agent...\nℹ Syncing support-chat...\n✓ Created: support-chat\n Agent ID: clxyz123abc456\n```\n\n### `octavus validate <path>`\n\nValidate an agent definition without saving. Useful for CI/CD pipelines.\n\n```bash\noctavus validate ./agents/my-agent\n```\n\n**Exit codes:**\n\n- `0` — Validation passed\n- `1` — Validation errors\n- `2` — Configuration errors (missing API key, etc.)\n\n### `octavus list`\n\nList all agents in your project.\n\n```bash\noctavus list\n```\n\n**Example output:**\n\n```\nSLUG NAME FORMAT ID\n────────────────────────────────────────────────────────────────────────────\nsupport-chat Support Chat Agent interactive clxyz123abc456\n\n1 agent(s)\n```\n\n### `octavus get <slug>`\n\nGet details about a specific agent by its slug.\n\n```bash\noctavus get support-chat\n```\n\n## Agent Directory Structure\n\nThe CLI expects agent definitions in a specific directory structure:\n\n```\nmy-agent/\n├── settings.json # Required: Agent metadata\n├── protocol.yaml # Required: Agent protocol\n└── prompts/ # Optional: Prompt templates\n ├── system.md\n └── user-message.md\n```\n\n### settings.json\n\n```json\n{\n \"slug\": \"my-agent\",\n \"name\": \"My Agent\",\n \"description\": \"A helpful assistant\",\n \"format\": \"interactive\"\n}\n```\n\n### protocol.yaml\n\nSee the [Protocol documentation](/docs/protocol/overview) for details on protocol syntax.\n\n## CI/CD Integration\n\n### GitHub Actions\n\n```yaml\nname: Validate and Sync Agents\n\non:\n push:\n branches: [main]\n paths:\n - 'agents/**'\n\njobs:\n sync:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: '22'\n\n - run: npm install\n\n - name: Validate agent\n run: npx octavus validate ./agents/support-chat\n env:\n OCTAVUS_CLI_API_KEY: ${{ secrets.OCTAVUS_CLI_API_KEY }}\n\n - name: Sync agent\n run: npx octavus sync ./agents/support-chat\n env:\n OCTAVUS_CLI_API_KEY: ${{ secrets.OCTAVUS_CLI_API_KEY }}\n```\n\n### Package.json Scripts\n\nAdd sync scripts to your `package.json`:\n\n```json\n{\n \"scripts\": {\n \"agents:validate\": \"octavus validate ./agents/my-agent\",\n \"agents:sync\": \"octavus sync ./agents/my-agent\"\n },\n \"devDependencies\": {\n \"@octavus/cli\": \"^0.1.0\"\n }\n}\n```\n\n## Workflow\n\nThe recommended workflow for managing agents:\n\n1. **Define agent locally** — Create `settings.json`, `protocol.yaml`, and prompts\n2. **Validate** — Run `octavus validate ./my-agent` to check for errors\n3. **Sync** — Run `octavus sync ./my-agent` to push to platform\n4. **Store agent ID** — Save the output ID in an environment variable\n5. **Use in app** — Read the ID from env and pass to `client.agentSessions.create()`\n\n```bash\n# After syncing: octavus sync ./agents/support-chat\n# Output: Agent ID: clxyz123abc456\n\n# Add to your .env file\nOCTAVUS_SUPPORT_AGENT_ID=clxyz123abc456\n```\n\n```typescript\nconst agentId = process.env.OCTAVUS_SUPPORT_AGENT_ID;\n\nconst sessionId = await client.agentSessions.create(agentId, {\n COMPANY_NAME: 'Acme Corp',\n});\n```\n",
|
|
76
76
|
"excerpt": "Octavus CLI The package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform. Current version: Installation ...",
|
|
77
77
|
"order": 5
|
|
78
78
|
}
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"section": "client-sdk",
|
|
90
90
|
"title": "Overview",
|
|
91
91
|
"description": "Introduction to the Octavus Client SDKs for building chat interfaces.",
|
|
92
|
-
"content": "\n# Client SDK Overview\n\nOctavus provides two packages for frontend integration:\n\n| Package | Purpose | Use When |\n| --------------------- | ------------------------ | ----------------------------------------------------- |\n| `@octavus/react` | React hooks and bindings | Building React applications |\n| `@octavus/client-sdk` | Framework-agnostic core | Using Vue, Svelte, vanilla JS, or custom integrations |\n\n**Most users should install `@octavus/react`** — it includes everything from `@octavus/client-sdk` plus React-specific hooks.\n\n## Installation\n\n### React Applications\n\n```bash\nnpm install @octavus/react\n```\n\n**Current version:** `2.0.0`\n\n### Other Frameworks\n\n```bash\nnpm install @octavus/client-sdk\n```\n\n**Current version:** `2.0.0`\n\n## Transport Pattern\n\nThe Client SDK uses a **transport abstraction** to handle communication with your backend. This gives you flexibility in how events are delivered:\n\n| Transport | Use Case | Docs |\n| ----------------------- | -------------------------------------------- | ----------------------------------------------------- |\n| `createHttpTransport` | HTTP/SSE (Next.js, Express, etc.) | [HTTP Transport](/docs/client-sdk/http-transport) |\n| `createSocketTransport` | WebSocket, SockJS, or other socket protocols | [Socket Transport](/docs/client-sdk/socket-transport) |\n\nWhen the transport changes (e.g., when `sessionId` changes), the `useOctavusChat` hook automatically reinitializes with the new transport.\n\n> **Recommendation**: Use HTTP transport unless you specifically need WebSocket features (custom real-time events, Meteor/Phoenix, etc.).\n\n## React Usage\n\nThe `useOctavusChat` hook provides state management and streaming for React applications:\n\n```tsx\nimport { useMemo } from 'react';\nimport { useOctavusChat, createHttpTransport, type UIMessage } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n // Create a stable transport instance (memoized on sessionId)\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const { messages, status, send } = useOctavusChat({ transport });\n\n const sendMessage = async (text: string) => {\n await send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n };\n\n return (\n <div>\n {messages.map((msg) => (\n <MessageBubble key={msg.id} message={msg} />\n ))}\n </div>\n );\n}\n\nfunction MessageBubble({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Framework-Agnostic Usage\n\nThe `OctavusChat` class can be used with any framework or vanilla JavaScript:\n\n```typescript\nimport { OctavusChat, createHttpTransport } from '@octavus/client-sdk';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n\nconst chat = new OctavusChat({ transport });\n\n// Subscribe to state changes\nconst unsubscribe = chat.subscribe(() => {\n console.log('Messages:', chat.messages);\n console.log('Status:', chat.status);\n // Update your UI here\n});\n\n// Send a message\nawait chat.send('user-message', { USER_MESSAGE: 'Hello' }, { userMessage: { content: 'Hello' } });\n\n// Cleanup when done\nunsubscribe();\n```\n\n## Key Features\n\n### Unified Send Function\n\nThe `send` function handles both user message display and agent triggering in one call:\n\n```tsx\nconst { send } = useOctavusChat({ transport });\n\n// Add user message to UI and trigger agent\nawait send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n\n// Trigger without adding a user message (e.g., button click)\nawait send('request-human');\n```\n\n### Message Parts\n\nMessages contain ordered `parts` for rich content:\n\n```tsx\nconst { messages } = useOctavusChat({ transport });\n\n// Each message has typed parts\nmessage.parts.map((part) => {\n switch (part.type) {\n case 'text': // Text content\n case 'reasoning': // Extended reasoning/thinking\n case 'tool-call': // Tool execution\n case 'operation': // Internal operations (set-resource, etc.)\n }\n});\n```\n\n### Status Tracking\n\n```tsx\nconst { status } = useOctavusChat({ transport });\n\n// status: 'idle' | 'streaming' | 'error' | 'awaiting-input'\n// 'awaiting-input' occurs when interactive client tools need user action\n```\n\n### Stop Streaming\n\n```tsx\nconst { stop } = useOctavusChat({ transport });\n\n// Stop current stream and finalize message\nstop();\n```\n\n## Hook Reference (React)\n\n### useOctavusChat\n\n```typescript\nfunction useOctavusChat(options: OctavusChatOptions): UseOctavusChatReturn;\n\ninterface OctavusChatOptions {\n // Required: Transport for streaming events\n transport: Transport;\n\n // Optional: Function to request upload URLs for file uploads\n requestUploadUrls?: (\n files: { filename: string; mediaType: string; size: number }[],\n ) => Promise<UploadUrlsResponse>;\n\n // Optional: Client-side tool handlers\n // - Function: executes automatically and returns result\n // - 'interactive': appears in pendingClientTools for user input\n clientTools?: Record<string, ClientToolHandler>;\n\n // Optional: Pre-populate with existing messages (session restore)\n initialMessages?: UIMessage[];\n\n // Optional: Callbacks\n onError?: (error: OctavusError) => void; // Structured error with type, source, retryable\n onFinish?: () => void;\n onStop?: () => void; // Called when user stops generation\n onResourceUpdate?: (name: string, value: unknown) => void;\n}\n\ninterface UseOctavusChatReturn {\n // State\n messages: UIMessage[];\n status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n error: OctavusError | null; // Structured error with type, source, retryable\n\n // Connection (socket transport only - undefined for HTTP)\n connectionState: ConnectionState | undefined; // 'disconnected' | 'connecting' | 'connected' | 'error'\n connectionError: Error | undefined;\n\n // Client tools (interactive tools awaiting user input)\n pendingClientTools: Record<string, InteractiveTool[]>; // Keyed by tool name\n\n // Actions\n send: (\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ) => Promise<void>;\n stop: () => void;\n\n // Connection management (socket transport only - undefined for HTTP)\n connect: (() => Promise<void>) | undefined;\n disconnect: (() => void) | undefined;\n\n // File uploads (requires requestUploadUrls)\n uploadFiles: (\n files: FileList | File[],\n onProgress?: (fileIndex: number, progress: number) => void,\n ) => Promise<FileReference[]>;\n}\n\ninterface UserMessageInput {\n content?: string;\n files?: FileList | File[] | FileReference[];\n}\n```\n\n## Transport Reference\n\n### createHttpTransport\n\nCreates an HTTP/SSE transport using native `fetch()`:\n\n```typescript\nimport { createHttpTransport } from '@octavus/react';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n```\n\n### createSocketTransport\n\nCreates a WebSocket/SockJS transport for real-time connections:\n\n```typescript\nimport { createSocketTransport } from '@octavus/react';\n\nconst transport = createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const ws = new WebSocket(`wss://api.example.com/stream?sessionId=${sessionId}`);\n ws.onopen = () => resolve(ws);\n ws.onerror = () => reject(new Error('Connection failed'));\n }),\n});\n```\n\nSocket transport provides additional connection management:\n\n```typescript\n// Access connection state directly\ntransport.connectionState; // 'disconnected' | 'connecting' | 'connected' | 'error'\n\n// Subscribe to state changes\ntransport.onConnectionStateChange((state, error) => {\n /* ... */\n});\n\n// Eager connection (instead of lazy on first send)\nawait transport.connect();\n\n// Manual disconnect\ntransport.disconnect();\n```\n\nFor detailed WebSocket/SockJS usage including custom events, reconnection patterns, and server-side implementation, see [Socket Transport](/docs/client-sdk/socket-transport).\n\n## Class Reference (Framework-Agnostic)\n\n### OctavusChat\n\n```typescript\nclass OctavusChat {\n constructor(options: OctavusChatOptions);\n\n // State (read-only)\n readonly messages: UIMessage[];\n readonly status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n readonly error: OctavusError | null; // Structured error\n readonly pendingClientTools: Record<string, InteractiveTool[]>; // Interactive tools\n\n // Actions\n send(\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ): Promise<void>;\n stop(): void;\n\n // Subscription\n subscribe(callback: () => void): () => void; // Returns unsubscribe function\n}\n```\n\n## Next Steps\n\n- [HTTP Transport](/docs/client-sdk/http-transport) — HTTP/SSE integration (recommended)\n- [Socket Transport](/docs/client-sdk/socket-transport) — WebSocket and SockJS integration\n- [Messages](/docs/client-sdk/messages) — Working with message state\n- [Streaming](/docs/client-sdk/streaming) — Building streaming UIs\n- [Client Tools](/docs/client-sdk/client-tools) — Interactive browser-side tool handling\n- [Operations](/docs/client-sdk/execution-blocks) — Showing agent progress\n- [Error Handling](/docs/client-sdk/error-handling) — Handling errors with type guards\n- [File Uploads](/docs/client-sdk/file-uploads) — Uploading images and documents\n- [Examples](/docs/examples/overview) — Complete working examples\n",
|
|
92
|
+
"content": "\n# Client SDK Overview\n\nOctavus provides two packages for frontend integration:\n\n| Package | Purpose | Use When |\n| --------------------- | ------------------------ | ----------------------------------------------------- |\n| `@octavus/react` | React hooks and bindings | Building React applications |\n| `@octavus/client-sdk` | Framework-agnostic core | Using Vue, Svelte, vanilla JS, or custom integrations |\n\n**Most users should install `@octavus/react`** — it includes everything from `@octavus/client-sdk` plus React-specific hooks.\n\n## Installation\n\n### React Applications\n\n```bash\nnpm install @octavus/react\n```\n\n**Current version:** `2.1.0`\n\n### Other Frameworks\n\n```bash\nnpm install @octavus/client-sdk\n```\n\n**Current version:** `2.1.0`\n\n## Transport Pattern\n\nThe Client SDK uses a **transport abstraction** to handle communication with your backend. This gives you flexibility in how events are delivered:\n\n| Transport | Use Case | Docs |\n| ----------------------- | -------------------------------------------- | ----------------------------------------------------- |\n| `createHttpTransport` | HTTP/SSE (Next.js, Express, etc.) | [HTTP Transport](/docs/client-sdk/http-transport) |\n| `createSocketTransport` | WebSocket, SockJS, or other socket protocols | [Socket Transport](/docs/client-sdk/socket-transport) |\n\nWhen the transport changes (e.g., when `sessionId` changes), the `useOctavusChat` hook automatically reinitializes with the new transport.\n\n> **Recommendation**: Use HTTP transport unless you specifically need WebSocket features (custom real-time events, Meteor/Phoenix, etc.).\n\n## React Usage\n\nThe `useOctavusChat` hook provides state management and streaming for React applications:\n\n```tsx\nimport { useMemo } from 'react';\nimport { useOctavusChat, createHttpTransport, type UIMessage } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n // Create a stable transport instance (memoized on sessionId)\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const { messages, status, send } = useOctavusChat({ transport });\n\n const sendMessage = async (text: string) => {\n await send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n };\n\n return (\n <div>\n {messages.map((msg) => (\n <MessageBubble key={msg.id} message={msg} />\n ))}\n </div>\n );\n}\n\nfunction MessageBubble({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Framework-Agnostic Usage\n\nThe `OctavusChat` class can be used with any framework or vanilla JavaScript:\n\n```typescript\nimport { OctavusChat, createHttpTransport } from '@octavus/client-sdk';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n\nconst chat = new OctavusChat({ transport });\n\n// Subscribe to state changes\nconst unsubscribe = chat.subscribe(() => {\n console.log('Messages:', chat.messages);\n console.log('Status:', chat.status);\n // Update your UI here\n});\n\n// Send a message\nawait chat.send('user-message', { USER_MESSAGE: 'Hello' }, { userMessage: { content: 'Hello' } });\n\n// Cleanup when done\nunsubscribe();\n```\n\n## Key Features\n\n### Unified Send Function\n\nThe `send` function handles both user message display and agent triggering in one call:\n\n```tsx\nconst { send } = useOctavusChat({ transport });\n\n// Add user message to UI and trigger agent\nawait send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n\n// Trigger without adding a user message (e.g., button click)\nawait send('request-human');\n```\n\n### Message Parts\n\nMessages contain ordered `parts` for rich content:\n\n```tsx\nconst { messages } = useOctavusChat({ transport });\n\n// Each message has typed parts\nmessage.parts.map((part) => {\n switch (part.type) {\n case 'text': // Text content\n case 'reasoning': // Extended reasoning/thinking\n case 'tool-call': // Tool execution\n case 'operation': // Internal operations (set-resource, etc.)\n }\n});\n```\n\n### Status Tracking\n\n```tsx\nconst { status } = useOctavusChat({ transport });\n\n// status: 'idle' | 'streaming' | 'error' | 'awaiting-input'\n// 'awaiting-input' occurs when interactive client tools need user action\n```\n\n### Stop Streaming\n\n```tsx\nconst { stop } = useOctavusChat({ transport });\n\n// Stop current stream and finalize message\nstop();\n```\n\n## Hook Reference (React)\n\n### useOctavusChat\n\n```typescript\nfunction useOctavusChat(options: OctavusChatOptions): UseOctavusChatReturn;\n\ninterface OctavusChatOptions {\n // Required: Transport for streaming events\n transport: Transport;\n\n // Optional: Function to request upload URLs for file uploads\n requestUploadUrls?: (\n files: { filename: string; mediaType: string; size: number }[],\n ) => Promise<UploadUrlsResponse>;\n\n // Optional: Client-side tool handlers\n // - Function: executes automatically and returns result\n // - 'interactive': appears in pendingClientTools for user input\n clientTools?: Record<string, ClientToolHandler>;\n\n // Optional: Pre-populate with existing messages (session restore)\n initialMessages?: UIMessage[];\n\n // Optional: Callbacks\n onError?: (error: OctavusError) => void; // Structured error with type, source, retryable\n onFinish?: () => void;\n onStop?: () => void; // Called when user stops generation\n onResourceUpdate?: (name: string, value: unknown) => void;\n}\n\ninterface UseOctavusChatReturn {\n // State\n messages: UIMessage[];\n status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n error: OctavusError | null; // Structured error with type, source, retryable\n\n // Connection (socket transport only - undefined for HTTP)\n connectionState: ConnectionState | undefined; // 'disconnected' | 'connecting' | 'connected' | 'error'\n connectionError: Error | undefined;\n\n // Client tools (interactive tools awaiting user input)\n pendingClientTools: Record<string, InteractiveTool[]>; // Keyed by tool name\n\n // Actions\n send: (\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ) => Promise<void>;\n stop: () => void;\n\n // Connection management (socket transport only - undefined for HTTP)\n connect: (() => Promise<void>) | undefined;\n disconnect: (() => void) | undefined;\n\n // File uploads (requires requestUploadUrls)\n uploadFiles: (\n files: FileList | File[],\n onProgress?: (fileIndex: number, progress: number) => void,\n ) => Promise<FileReference[]>;\n}\n\ninterface UserMessageInput {\n content?: string;\n files?: FileList | File[] | FileReference[];\n}\n```\n\n## Transport Reference\n\n### createHttpTransport\n\nCreates an HTTP/SSE transport using native `fetch()`:\n\n```typescript\nimport { createHttpTransport } from '@octavus/react';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n```\n\n### createSocketTransport\n\nCreates a WebSocket/SockJS transport for real-time connections:\n\n```typescript\nimport { createSocketTransport } from '@octavus/react';\n\nconst transport = createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const ws = new WebSocket(`wss://api.example.com/stream?sessionId=${sessionId}`);\n ws.onopen = () => resolve(ws);\n ws.onerror = () => reject(new Error('Connection failed'));\n }),\n});\n```\n\nSocket transport provides additional connection management:\n\n```typescript\n// Access connection state directly\ntransport.connectionState; // 'disconnected' | 'connecting' | 'connected' | 'error'\n\n// Subscribe to state changes\ntransport.onConnectionStateChange((state, error) => {\n /* ... */\n});\n\n// Eager connection (instead of lazy on first send)\nawait transport.connect();\n\n// Manual disconnect\ntransport.disconnect();\n```\n\nFor detailed WebSocket/SockJS usage including custom events, reconnection patterns, and server-side implementation, see [Socket Transport](/docs/client-sdk/socket-transport).\n\n## Class Reference (Framework-Agnostic)\n\n### OctavusChat\n\n```typescript\nclass OctavusChat {\n constructor(options: OctavusChatOptions);\n\n // State (read-only)\n readonly messages: UIMessage[];\n readonly status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n readonly error: OctavusError | null; // Structured error\n readonly pendingClientTools: Record<string, InteractiveTool[]>; // Interactive tools\n\n // Actions\n send(\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ): Promise<void>;\n stop(): void;\n\n // Subscription\n subscribe(callback: () => void): () => void; // Returns unsubscribe function\n}\n```\n\n## Next Steps\n\n- [HTTP Transport](/docs/client-sdk/http-transport) — HTTP/SSE integration (recommended)\n- [Socket Transport](/docs/client-sdk/socket-transport) — WebSocket and SockJS integration\n- [Messages](/docs/client-sdk/messages) — Working with message state\n- [Streaming](/docs/client-sdk/streaming) — Building streaming UIs\n- [Client Tools](/docs/client-sdk/client-tools) — Interactive browser-side tool handling\n- [Operations](/docs/client-sdk/execution-blocks) — Showing agent progress\n- [Error Handling](/docs/client-sdk/error-handling) — Handling errors with type guards\n- [File Uploads](/docs/client-sdk/file-uploads) — Uploading images and documents\n- [Examples](/docs/examples/overview) — Complete working examples\n",
|
|
93
93
|
"excerpt": "Client SDK Overview Octavus provides two packages for frontend integration: | Package | Purpose | Use When | |...",
|
|
94
94
|
"order": 1
|
|
95
95
|
},
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"section": "client-sdk",
|
|
126
126
|
"title": "Socket Transport",
|
|
127
127
|
"description": "Using WebSocket or SockJS for real-time streaming with Octavus.",
|
|
128
|
-
"content": "\n# Socket Transport\n\nThe socket transport enables real-time bidirectional communication using WebSocket or SockJS. Use this when you need persistent connections, custom server events, or when HTTP/SSE isn't suitable for your infrastructure.\n\n## When to Use Socket Transport\n\n| Use Case | Recommended Transport |\n| ------------------------------------------- | -------------------------------- |\n| Standard web apps (Next.js, etc.) | HTTP (`createHttpTransport`) |\n| Real-time apps with custom events | Socket (`createSocketTransport`) |\n| Apps behind proxies that don't support SSE | Socket |\n| Need for typing indicators, presence, etc. | Socket |\n| Meteor, Phoenix, or socket-based frameworks | Socket |\n\n## Connection Lifecycle\n\nBy default, socket transport uses **lazy connection** — the socket connects only when you first call `send()`. This is efficient but can be surprising if you want to show connection status.\n\nFor UI indicators, use **eager connection**:\n\n```typescript\nimport { useEffect, useMemo } from 'react';\nimport SockJS from 'sockjs-client';\nimport { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction Chat() {\n const transport = useMemo(\n () => createSocketTransport({\n connect: () => new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n sock.onopen = () => resolve(sock);\n sock.onerror = () => reject(new Error('Connection failed'));\n }),\n }),\n [],\n );\n\n const {\n messages,\n status,\n send,\n // Socket-specific connection state\n connectionState, // 'disconnected' | 'connecting' | 'connected' | 'error'\n connectionError, // Error object if connectionState is 'error'\n connect, // () => Promise<void>\n disconnect, // () => void\n } = useOctavusChat({ transport });\n\n // Eagerly connect on mount\n useEffect(() => {\n connect?.();\n return () => disconnect?.();\n }, [connect, disconnect]);\n\n return (\n <div>\n <ConnectionIndicator state={connectionState} />\n {/* Chat UI */}\n </div>\n );\n}\n```\n\n### Connection States\n\n| State | Description |\n| -------------- | ----------------------------------------------------- |\n| `disconnected` | Not connected (initial state or after `disconnect()`) |\n| `connecting` | Connection attempt in progress |\n| `connected` | Socket is open and ready |\n| `error` | Connection failed (check `connectionError`) |\n\n## Patterns Overview\n\nThere are two main patterns for socket-based integrations:\n\n| Pattern | When to Use |\n| --------------------------------------------------------------- | ------------------------------------------------------------------------------- |\n| [Server-Managed Sessions](#server-managed-sessions-recommended) | **Recommended.** Server creates sessions lazily. Client doesn't need sessionId. |\n| [Client-Provided Session ID](#client-provided-session-id) | When client must control session creation or pass sessionId from URL. |\n\n## Server-Managed Sessions (Recommended)\n\nThe cleanest pattern is to have the server manage session lifecycle. The client never needs to know about `sessionId` — the server creates it lazily on first message.\n\n### Client Setup\n\n```typescript\nimport { useEffect, useMemo } from 'react';\nimport SockJS from 'sockjs-client';\nimport { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction connectSocket(): Promise<SocketLike> {\n return new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n sock.onopen = () => resolve(sock);\n sock.onerror = () => reject(new Error('Connection failed'));\n });\n}\n\nfunction Chat() {\n // Transport is stable — no dependencies on sessionId\n const transport = useMemo(() => createSocketTransport({ connect: connectSocket }), []);\n\n const { messages, status, send, stop, connectionState, connect, disconnect } = useOctavusChat({\n transport,\n });\n\n // Eagerly connect for UI status indicator\n useEffect(() => {\n connect?.();\n return () => disconnect?.();\n }, [connect, disconnect]);\n\n const sendMessage = async (text: string) => {\n await send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n };\n\n // ... render chat UI\n}\n```\n\n### Server Setup (Express + SockJS)\n\nThe server creates a session on first trigger message:\n\n```typescript\nimport sockjs from 'sockjs';\nimport { OctavusClient, type AgentSession } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nfunction createSocketHandler() {\n return (conn: sockjs.Connection) => {\n let session: AgentSession | null = null;\n let abortController: AbortController | null = null;\n\n conn.on('data', (rawData: string) => {\n void handleMessage(rawData);\n });\n\n async function handleMessage(rawData: string) {\n const msg = JSON.parse(rawData);\n\n if (msg.type === 'stop') {\n abortController?.abort();\n return;\n }\n\n // Handle both trigger and continue messages\n if (msg.type === 'trigger' || msg.type === 'continue') {\n // Create session lazily on first trigger\n if (!session && msg.type === 'trigger') {\n const sessionId = await client.agentSessions.create('your-agent-id', {\n // Initial input variables\n COMPANY_NAME: 'Acme Corp',\n });\n session = client.agentSessions.attach(sessionId, {\n tools: {\n // Server-side tool handlers only\n // Tools without handlers are forwarded to the client\n },\n });\n }\n\n if (!session) return;\n\n abortController = new AbortController();\n\n // execute() handles both triggers and continuations\n const events = session.execute(msg, {\n signal: abortController.signal,\n });\n\n try {\n for await (const event of events) {\n conn.write(JSON.stringify(event));\n }\n } catch {\n // Handle errors\n }\n }\n }\n\n conn.on('close', () => abortController?.abort());\n };\n}\n\nconst sockServer = sockjs.createServer({ prefix: '/octavus' });\nsockServer.on('connection', createSocketHandler());\nsockServer.installHandlers(httpServer);\n```\n\n**Benefits of this pattern:**\n\n- Client code is simple — no sessionId management\n- No transport caching issues\n- Session is created only when needed\n- Server controls session configuration\n\n## Client-Provided Session ID\n\nIf you need the client to control the session (e.g., resuming a specific session from URL), pass the sessionId in an init message after connecting:\n\n```typescript\nimport { useMemo } from 'react';\nimport SockJS from 'sockjs-client';\nimport { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n const transport = useMemo(\n () =>\n createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n sock.onopen = () => {\n // Send init message with sessionId\n sock.send(JSON.stringify({ type: 'init', sessionId }));\n resolve(sock);\n };\n sock.onerror = () => reject(new Error('Connection failed'));\n }),\n }),\n [sessionId],\n );\n\n const { messages, status, send } = useOctavusChat({ transport });\n // ... render chat\n}\n```\n\nWhen `sessionId` changes, the hook automatically reinitializes with the new transport.\n\n### Server Handler with Init Message\n\nWhen using client-provided sessionId, the server must handle an `init` message:\n\n```typescript\nsockServer.on('connection', (conn) => {\n let session: AgentSession | null = null;\n let abortController: AbortController | null = null;\n\n conn.on('data', (rawData: string) => {\n void handleMessage(rawData);\n });\n\n async function handleMessage(rawData: string) {\n const msg = JSON.parse(rawData);\n\n // Handle session initialization\n if (msg.type === 'init') {\n session = client.agentSessions.attach(msg.sessionId, {\n tools: {\n // Server-side tool handlers\n },\n });\n return;\n }\n\n if (msg.type === 'stop') {\n abortController?.abort();\n return;\n }\n\n // All other messages require initialized session\n if (!session) {\n conn.write(\n JSON.stringify({\n type: 'error',\n errorType: 'validation_error',\n message: 'Session not initialized. Send init message first.',\n source: 'platform',\n retryable: false,\n }),\n );\n return;\n }\n\n // Handle both trigger and continue messages\n if (msg.type === 'trigger' || msg.type === 'continue') {\n abortController = new AbortController();\n\n // execute() handles both triggers and continuations\n const events = session.execute(msg, { signal: abortController.signal });\n\n for await (const event of events) {\n conn.write(JSON.stringify(event));\n }\n }\n }\n});\n```\n\n## Async Session ID\n\nWhen the session ID is fetched asynchronously (e.g., from an API), you have two options:\n\n### Option 1: Conditionally Render (Recommended)\n\nDon't render the chat component until `sessionId` is available:\n\n```tsx\nfunction ChatPage() {\n const [sessionId, setSessionId] = useState<string | null>(null);\n\n useEffect(() => {\n api.createSession().then((res) => setSessionId(res.sessionId));\n }, []);\n\n // Don't render until sessionId is ready\n if (!sessionId) {\n return <LoadingSpinner />;\n }\n\n return <Chat sessionId={sessionId} />;\n}\n```\n\nThis is the cleanest approach — the `Chat` component always receives a valid `sessionId`.\n\n### Option 2: Server-Managed Sessions\n\nUse the [server-managed sessions pattern](#server-managed-sessions-recommended) where the server creates the session lazily. The client never needs to know about `sessionId`.\n\n## Native WebSocket\n\nIf you're using native WebSocket instead of SockJS, you can pass sessionId via URL:\n\n```typescript\nconst transport = useMemo(\n () =>\n createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const ws = new WebSocket(`wss://your-server.com/octavus?sessionId=${sessionId}`);\n ws.onopen = () => resolve(ws);\n ws.onerror = () => reject(new Error('WebSocket connection failed'));\n }),\n }),\n [sessionId],\n);\n```\n\nWhen `sessionId` changes, the hook automatically reinitializes with the new transport.\n\n## Custom Events\n\nHandle custom events alongside Octavus stream events:\n\n```typescript\nconst transport = createSocketTransport({\n connect: connectSocket,\n\n onMessage: (data) => {\n const msg = data as { type: string; [key: string]: unknown };\n\n switch (msg.type) {\n case 'typing-indicator':\n setAgentTyping(msg.isTyping as boolean);\n break;\n\n case 'presence-update':\n setOnlineUsers(msg.users as string[]);\n break;\n\n case 'notification':\n showToast(msg.message as string);\n break;\n\n // Octavus events (text-delta, finish, etc.) are handled automatically\n }\n },\n});\n```\n\n## Connection Management\n\n### Connection State API\n\nThe socket transport provides full connection lifecycle control:\n\n```typescript\nconst transport = createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n sock.onopen = () => resolve(sock);\n sock.onerror = () => reject(new Error('Connection failed'));\n }),\n});\n\n// Access connection state\nconsole.log(transport.connectionState); // 'disconnected' | 'connecting' | 'connected' | 'error'\n\n// Subscribe to state changes\nconst unsubscribe = transport.onConnectionStateChange((state, error) => {\n console.log('Connection state:', state);\n if (error) console.error('Error:', error);\n});\n\n// Eager connection\nawait transport.connect();\n\n// Manual disconnect\ntransport.disconnect();\n```\n\n### Using with useOctavusChat\n\nThe React hook exposes connection state automatically for socket transports:\n\n```typescript\nconst {\n messages,\n status,\n send,\n // Socket-specific (undefined for HTTP transport)\n connectionState,\n connectionError,\n connect,\n disconnect,\n} = useOctavusChat({ transport });\n\n// Eagerly connect on mount\nuseEffect(() => {\n connect?.();\n return () => disconnect?.();\n}, [connect, disconnect]);\n```\n\n### Handling Disconnections\n\n```typescript\nconst transport = createSocketTransport({\n connect: connectSocket,\n\n onClose: () => {\n console.log('Socket disconnected');\n // Connection state is automatically updated to 'disconnected'\n },\n});\n```\n\n### Reconnection with Exponential Backoff\n\n```typescript\nimport { useRef, useCallback, useMemo } from 'react';\nimport { createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction useReconnectingTransport() {\n const reconnectAttempts = useRef(0);\n const maxAttempts = 5;\n\n const connect = useCallback((): Promise<SocketLike> => {\n return new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n\n sock.onopen = () => {\n reconnectAttempts.current = 0;\n resolve(sock);\n };\n\n sock.onerror = () => {\n if (reconnectAttempts.current < maxAttempts) {\n reconnectAttempts.current++;\n const delay = Math.min(1000 * 2 ** reconnectAttempts.current, 30000);\n console.log(`Reconnecting in ${delay}ms...`);\n setTimeout(() => connect().then(resolve).catch(reject), delay);\n } else {\n reject(new Error('Max reconnection attempts reached'));\n }\n };\n });\n }, []);\n\n return useMemo(() => createSocketTransport({ connect }), [connect]);\n}\n```\n\n## Framework Notes\n\n### Meteor\n\nMeteor's bundler may have issues with ES6 imports of `sockjs-client`. Use `require()` instead:\n\n```typescript\n// ❌ May fail in Meteor\nimport SockJS from 'sockjs-client';\n\n// ✅ Works in Meteor\nconst SockJS: typeof import('sockjs-client') = require('sockjs-client');\n```\n\n### SockJS vs WebSocket\n\n| Feature | WebSocket | SockJS |\n| ------------------- | -------------------- | -------------------------------- |\n| Browser support | Modern browsers | All browsers (with fallbacks) |\n| Session ID | Via URL query params | Via init message |\n| Proxy compatibility | Varies | Excellent (polling fallback) |\n| Setup complexity | Lower | Higher (requires server library) |\n\n## Protocol Reference\n\n### Client → Server Messages\n\n```typescript\n// Initialize session (only for client-provided sessionId pattern)\n{ type: 'init', sessionId: string }\n\n// Trigger an action (start a new conversation turn)\n{ type: 'trigger', triggerName: string, input?: Record<string, unknown> }\n\n// Continue execution (after client-side tool handling)\n{ type: 'continue', executionId: string, toolResults: ToolResult[] }\n\n// Stop current stream\n{ type: 'stop' }\n```\n\n### Server → Client Messages\n\nThe server sends Octavus `StreamEvent` objects as JSON. See [Streaming Events](/docs/server-sdk/streaming#event-types) for the full list.\n\n```typescript\n// Examples\n{ type: 'start', messageId: '...', executionId: '...' }\n{ type: 'text-delta', id: '...', delta: 'Hello' }\n{ type: 'tool-input-start', toolCallId: '...', toolName: 'get-user' }\n{ type: 'finish', finishReason: 'stop' }\n{ type: 'error', errorType: 'internal_error', message: 'Something went wrong', source: 'platform', retryable: false }\n\n// Client tool request (tools without server handlers)\n{ type: 'client-tool-request', executionId: '...', toolCalls: [...], serverToolResults: [...] }\n{ type: 'finish', finishReason: 'client-tool-calls', executionId: '...' }\n```\n\nWhen a `client-tool-request` event is received, the client handles the tools and sends a `continue` message to resume.\n\n## Full Example\n\nFor a complete walkthrough of building a chat interface with SockJS, see the [Socket Chat Example](/docs/examples/socket-chat).\n",
|
|
128
|
+
"content": "\n# Socket Transport\n\nThe socket transport enables real-time bidirectional communication using WebSocket or SockJS. Use this when you need persistent connections, custom server events, or when HTTP/SSE isn't suitable for your infrastructure.\n\n## When to Use Socket Transport\n\n| Use Case | Recommended Transport |\n| ------------------------------------------- | -------------------------------- |\n| Standard web apps (Next.js, etc.) | HTTP (`createHttpTransport`) |\n| Real-time apps with custom events | Socket (`createSocketTransport`) |\n| Apps behind proxies that don't support SSE | Socket |\n| Need for typing indicators, presence, etc. | Socket |\n| Meteor, Phoenix, or socket-based frameworks | Socket |\n\n## Connection Lifecycle\n\nBy default, socket transport uses **lazy connection** — the socket connects only when you first call `send()`. This is efficient but can be surprising if you want to show connection status.\n\nFor UI indicators, use **eager connection**:\n\n```typescript\nimport { useEffect, useMemo } from 'react';\nimport SockJS from 'sockjs-client';\nimport { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction Chat() {\n const transport = useMemo(\n () => createSocketTransport({\n connect: () => new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n sock.onopen = () => resolve(sock);\n sock.onerror = () => reject(new Error('Connection failed'));\n }),\n }),\n [],\n );\n\n const {\n messages,\n status,\n send,\n // Socket-specific connection state\n connectionState, // 'disconnected' | 'connecting' | 'connected' | 'error'\n connectionError, // Error object if connectionState is 'error'\n connect, // () => Promise<void>\n disconnect, // () => void\n } = useOctavusChat({ transport });\n\n // Eagerly connect on mount\n useEffect(() => {\n connect?.();\n return () => disconnect?.();\n }, [connect, disconnect]);\n\n return (\n <div>\n <ConnectionIndicator state={connectionState} />\n {/* Chat UI */}\n </div>\n );\n}\n```\n\n### Connection States\n\n| State | Description |\n| -------------- | ----------------------------------------------------- |\n| `disconnected` | Not connected (initial state or after `disconnect()`) |\n| `connecting` | Connection attempt in progress |\n| `connected` | Socket is open and ready |\n| `error` | Connection failed (check `connectionError`) |\n\n## Patterns Overview\n\nThere are two main patterns for socket-based integrations:\n\n| Pattern | When to Use |\n| --------------------------------------------------------------- | ------------------------------------------------------------------------------- |\n| [Server-Managed Sessions](#server-managed-sessions-recommended) | **Recommended.** Server creates sessions lazily. Client doesn't need sessionId. |\n| [Client-Provided Session ID](#client-provided-session-id) | When client must control session creation or pass sessionId from URL. |\n\n## Server-Managed Sessions (Recommended)\n\nThe cleanest pattern is to have the server manage session lifecycle. The client never needs to know about `sessionId` — the server creates it lazily on first message.\n\n### Client Setup\n\n```typescript\nimport { useEffect, useMemo } from 'react';\nimport SockJS from 'sockjs-client';\nimport { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction connectSocket(): Promise<SocketLike> {\n return new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n sock.onopen = () => resolve(sock);\n sock.onerror = () => reject(new Error('Connection failed'));\n });\n}\n\nfunction Chat() {\n // Transport is stable — no dependencies on sessionId\n const transport = useMemo(() => createSocketTransport({ connect: connectSocket }), []);\n\n const { messages, status, send, stop, connectionState, connect, disconnect } = useOctavusChat({\n transport,\n });\n\n // Eagerly connect for UI status indicator\n useEffect(() => {\n connect?.();\n return () => disconnect?.();\n }, [connect, disconnect]);\n\n const sendMessage = async (text: string) => {\n await send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n };\n\n // ... render chat UI\n}\n```\n\n### Server Setup (Express + SockJS)\n\nThe server creates a session on first trigger message:\n\n```typescript\nimport sockjs from 'sockjs';\nimport { OctavusClient, type AgentSession, type SocketMessage } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nfunction createSocketHandler() {\n return (conn: sockjs.Connection) => {\n let session: AgentSession | null = null;\n\n const send = (data: unknown) => conn.write(JSON.stringify(data));\n\n conn.on('data', (rawData: string) => {\n void handleMessage(rawData);\n });\n\n async function handleMessage(rawData: string) {\n const msg = JSON.parse(rawData);\n\n if (msg.type === 'trigger' || msg.type === 'continue' || msg.type === 'stop') {\n // Create session lazily on first trigger\n if (!session && msg.type === 'trigger') {\n const sessionId = await client.agentSessions.create('your-agent-id', {\n COMPANY_NAME: 'Acme Corp',\n });\n session = client.agentSessions.attach(sessionId, {\n tools: {\n // Server-side tool handlers only\n // Tools without handlers are forwarded to the client\n },\n });\n }\n\n if (!session) return;\n\n // handleSocketMessage manages abort controller internally\n await session.handleSocketMessage(msg as SocketMessage, {\n onEvent: send,\n });\n }\n }\n\n conn.on('close', () => {});\n };\n}\n\nconst sockServer = sockjs.createServer({ prefix: '/octavus' });\nsockServer.on('connection', createSocketHandler());\nsockServer.installHandlers(httpServer);\n```\n\n**Benefits of this pattern:**\n\n- Client code is simple — no sessionId management\n- No transport caching issues\n- Session is created only when needed\n- Server controls session configuration\n\n## Client-Provided Session ID\n\nIf you need the client to control the session (e.g., resuming a specific session from URL), pass the sessionId in an init message after connecting:\n\n```typescript\nimport { useMemo } from 'react';\nimport SockJS from 'sockjs-client';\nimport { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n const transport = useMemo(\n () =>\n createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n sock.onopen = () => {\n // Send init message with sessionId\n sock.send(JSON.stringify({ type: 'init', sessionId }));\n resolve(sock);\n };\n sock.onerror = () => reject(new Error('Connection failed'));\n }),\n }),\n [sessionId],\n );\n\n const { messages, status, send } = useOctavusChat({ transport });\n // ... render chat\n}\n```\n\nWhen `sessionId` changes, the hook automatically reinitializes with the new transport.\n\n### Server Handler with Init Message\n\nWhen using client-provided sessionId, the server must handle an `init` message:\n\n```typescript\nimport type { SocketMessage } from '@octavus/server-sdk';\n\nsockServer.on('connection', (conn) => {\n let session: AgentSession | null = null;\n\n const send = (data: unknown) => conn.write(JSON.stringify(data));\n\n conn.on('data', (rawData: string) => {\n void handleMessage(rawData);\n });\n\n async function handleMessage(rawData: string) {\n const msg = JSON.parse(rawData);\n\n // Handle session initialization\n if (msg.type === 'init') {\n session = client.agentSessions.attach(msg.sessionId, {\n tools: {\n // Server-side tool handlers\n },\n });\n return;\n }\n\n // All other messages require initialized session\n if (!session) {\n send({\n type: 'error',\n errorType: 'validation_error',\n message: 'Session not initialized. Send init message first.',\n source: 'platform',\n retryable: false,\n });\n return;\n }\n\n // handleSocketMessage handles trigger, continue, and stop\n if (msg.type === 'trigger' || msg.type === 'continue' || msg.type === 'stop') {\n await session.handleSocketMessage(msg as SocketMessage, {\n onEvent: send,\n });\n }\n }\n});\n```\n\n## Async Session ID\n\nWhen the session ID is fetched asynchronously (e.g., from an API), you have two options:\n\n### Option 1: Conditionally Render (Recommended)\n\nDon't render the chat component until `sessionId` is available:\n\n```tsx\nfunction ChatPage() {\n const [sessionId, setSessionId] = useState<string | null>(null);\n\n useEffect(() => {\n api.createSession().then((res) => setSessionId(res.sessionId));\n }, []);\n\n // Don't render until sessionId is ready\n if (!sessionId) {\n return <LoadingSpinner />;\n }\n\n return <Chat sessionId={sessionId} />;\n}\n```\n\nThis is the cleanest approach — the `Chat` component always receives a valid `sessionId`.\n\n### Option 2: Server-Managed Sessions\n\nUse the [server-managed sessions pattern](#server-managed-sessions-recommended) where the server creates the session lazily. The client never needs to know about `sessionId`.\n\n## Native WebSocket\n\nIf you're using native WebSocket instead of SockJS, you can pass sessionId via URL:\n\n```typescript\nconst transport = useMemo(\n () =>\n createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const ws = new WebSocket(`wss://your-server.com/octavus?sessionId=${sessionId}`);\n ws.onopen = () => resolve(ws);\n ws.onerror = () => reject(new Error('WebSocket connection failed'));\n }),\n }),\n [sessionId],\n);\n```\n\nWhen `sessionId` changes, the hook automatically reinitializes with the new transport.\n\n## Custom Events\n\nHandle custom events alongside Octavus stream events:\n\n```typescript\nconst transport = createSocketTransport({\n connect: connectSocket,\n\n onMessage: (data) => {\n const msg = data as { type: string; [key: string]: unknown };\n\n switch (msg.type) {\n case 'typing-indicator':\n setAgentTyping(msg.isTyping as boolean);\n break;\n\n case 'presence-update':\n setOnlineUsers(msg.users as string[]);\n break;\n\n case 'notification':\n showToast(msg.message as string);\n break;\n\n // Octavus events (text-delta, finish, etc.) are handled automatically\n }\n },\n});\n```\n\n## Connection Management\n\n### Connection State API\n\nThe socket transport provides full connection lifecycle control:\n\n```typescript\nconst transport = createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n sock.onopen = () => resolve(sock);\n sock.onerror = () => reject(new Error('Connection failed'));\n }),\n});\n\n// Access connection state\nconsole.log(transport.connectionState); // 'disconnected' | 'connecting' | 'connected' | 'error'\n\n// Subscribe to state changes\nconst unsubscribe = transport.onConnectionStateChange((state, error) => {\n console.log('Connection state:', state);\n if (error) console.error('Error:', error);\n});\n\n// Eager connection\nawait transport.connect();\n\n// Manual disconnect\ntransport.disconnect();\n```\n\n### Using with useOctavusChat\n\nThe React hook exposes connection state automatically for socket transports:\n\n```typescript\nconst {\n messages,\n status,\n send,\n // Socket-specific (undefined for HTTP transport)\n connectionState,\n connectionError,\n connect,\n disconnect,\n} = useOctavusChat({ transport });\n\n// Eagerly connect on mount\nuseEffect(() => {\n connect?.();\n return () => disconnect?.();\n}, [connect, disconnect]);\n```\n\n### Handling Disconnections\n\n```typescript\nconst transport = createSocketTransport({\n connect: connectSocket,\n\n onClose: () => {\n console.log('Socket disconnected');\n // Connection state is automatically updated to 'disconnected'\n },\n});\n```\n\n### Reconnection with Exponential Backoff\n\n```typescript\nimport { useRef, useCallback, useMemo } from 'react';\nimport { createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction useReconnectingTransport() {\n const reconnectAttempts = useRef(0);\n const maxAttempts = 5;\n\n const connect = useCallback((): Promise<SocketLike> => {\n return new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n\n sock.onopen = () => {\n reconnectAttempts.current = 0;\n resolve(sock);\n };\n\n sock.onerror = () => {\n if (reconnectAttempts.current < maxAttempts) {\n reconnectAttempts.current++;\n const delay = Math.min(1000 * 2 ** reconnectAttempts.current, 30000);\n console.log(`Reconnecting in ${delay}ms...`);\n setTimeout(() => connect().then(resolve).catch(reject), delay);\n } else {\n reject(new Error('Max reconnection attempts reached'));\n }\n };\n });\n }, []);\n\n return useMemo(() => createSocketTransport({ connect }), [connect]);\n}\n```\n\n## Framework Notes\n\n### Meteor\n\nMeteor's bundler may have issues with ES6 imports of `sockjs-client`. Use `require()` instead:\n\n```typescript\n// ❌ May fail in Meteor\nimport SockJS from 'sockjs-client';\n\n// ✅ Works in Meteor\nconst SockJS: typeof import('sockjs-client') = require('sockjs-client');\n```\n\n### SockJS vs WebSocket\n\n| Feature | WebSocket | SockJS |\n| ------------------- | -------------------- | -------------------------------- |\n| Browser support | Modern browsers | All browsers (with fallbacks) |\n| Session ID | Via URL query params | Via init message |\n| Proxy compatibility | Varies | Excellent (polling fallback) |\n| Setup complexity | Lower | Higher (requires server library) |\n\n## Protocol Reference\n\n### Client → Server Messages\n\n```typescript\n// Initialize session (only for client-provided sessionId pattern)\n{ type: 'init', sessionId: string }\n\n// Trigger an action (start a new conversation turn)\n{ type: 'trigger', triggerName: string, input?: Record<string, unknown> }\n\n// Continue execution (after client-side tool handling)\n{ type: 'continue', executionId: string, toolResults: ToolResult[] }\n\n// Stop current stream\n{ type: 'stop' }\n```\n\n### Server → Client Messages\n\nThe server sends Octavus `StreamEvent` objects as JSON. See [Streaming Events](/docs/server-sdk/streaming#event-types) for the full list.\n\n```typescript\n// Examples\n{ type: 'start', messageId: '...', executionId: '...' }\n{ type: 'text-delta', id: '...', delta: 'Hello' }\n{ type: 'tool-input-start', toolCallId: '...', toolName: 'get-user' }\n{ type: 'finish', finishReason: 'stop' }\n{ type: 'error', errorType: 'internal_error', message: 'Something went wrong', source: 'platform', retryable: false }\n\n// Client tool request (tools without server handlers)\n{ type: 'client-tool-request', executionId: '...', toolCalls: [...], serverToolResults: [...] }\n{ type: 'finish', finishReason: 'client-tool-calls', executionId: '...' }\n```\n\nWhen a `client-tool-request` event is received, the client handles the tools and sends a `continue` message to resume.\n\n## Full Example\n\nFor a complete walkthrough of building a chat interface with SockJS, see the [Socket Chat Example](/docs/examples/socket-chat).\n",
|
|
129
129
|
"excerpt": "Socket Transport The socket transport enables real-time bidirectional communication using WebSocket or SockJS. Use this when you need persistent connections, custom server events, or when HTTP/SSE...",
|
|
130
130
|
"order": 5
|
|
131
131
|
},
|
|
@@ -338,7 +338,7 @@
|
|
|
338
338
|
"section": "examples",
|
|
339
339
|
"title": "Socket Chat",
|
|
340
340
|
"description": "Building a chat interface with SockJS for real-time frameworks.",
|
|
341
|
-
"content": "\n# Socket Chat Example\n\nThis example builds a chat interface using SockJS for bidirectional communication. Use this pattern for Meteor, Phoenix, or when you need custom real-time events.\n\n## What You're Building\n\nA chat interface that:\n\n- Uses SockJS for real-time streaming\n- Manages sessions server-side (client doesn't need sessionId)\n- Supports custom events alongside chat\n- Works with frameworks that use WebSocket-like transports\n\n## Architecture\n\n```mermaid\nflowchart LR\n Browser[\"Browser<br/>(React)\"] <-->|\"SockJS\"| Server[\"Your Server<br/>(Express)\"]\n Server --> Platform[\"Octavus Platform\"]\n```\n\n**Key difference from HTTP:** The server maintains a persistent socket connection and manages sessions internally. The client never needs to know about `sessionId`.\n\n## Prerequisites\n\n- Express (or similar Node.js server)\n- React frontend\n- `sockjs` (server) and `sockjs-client` (client)\n- Octavus account with API key\n\n## Step 1: Install Dependencies\n\n**Server:**\n\n```bash\nnpm install @octavus/server-sdk sockjs express\nnpm install -D @types/sockjs @types/express\n```\n\n**Client:**\n\n```bash\nnpm install @octavus/react sockjs-client\nnpm install -D @types/sockjs-client\n```\n\n## Step 2: Configure Environment\n\n```bash\n# .env\nOCTAVUS_API_URL=https://octavus.ai\nOCTAVUS_API_KEY=your-api-key\nOCTAVUS_AGENT_ID=your-agent-id\n```\n\n## Step 3: Create the Octavus Client (Server)\n\n```typescript\n// server/octavus/client.ts\nimport { OctavusClient } from '@octavus/server-sdk';\n\nexport const octavus = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nexport const AGENT_ID = process.env.OCTAVUS_AGENT_ID!;\n```\n\n## Step 4: Create the Socket Handler (Server)\n\nThis is the core of socket integration. Each connection gets its own session:\n\n```typescript\n// server/octavus/socket-handler.ts\nimport type { Connection } from 'sockjs';\nimport { OctavusClient, type AgentSession } from '@octavus/server-sdk';\n\nconst octavus = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nconst AGENT_ID = process.env.OCTAVUS_AGENT_ID!;\n\nexport function createSocketHandler() {\n return (conn: Connection) => {\n let session: AgentSession | null = null;\n let abortController: AbortController | null = null;\n\n conn.on('data', (rawData: string) => {\n void handleMessage(rawData);\n });\n\n async function handleMessage(rawData: string) {\n const msg = JSON.parse(rawData);\n\n // Handle stop request\n if (msg.type === 'stop') {\n abortController?.abort();\n return;\n }\n\n // Handle trigger and continue messages\n if (msg.type === 'trigger' || msg.type === 'continue') {\n // Create session lazily on first trigger\n if (!session && msg.type === 'trigger') {\n const sessionId = await octavus.agentSessions.create(AGENT_ID, {\n // Initial input variables from your protocol\n COMPANY_NAME: 'Acme Corp',\n });\n\n session = octavus.agentSessions.attach(sessionId, {\n tools: {\n // Server-side tool handlers\n 'get-user-account': async () => {\n // Fetch from your database\n return { name: 'Demo User', plan: 'pro' };\n },\n 'create-support-ticket': async () => {\n return { ticketId: 'TKT-123', estimatedResponse: '24h' };\n },\n // Tools without handlers are forwarded to the client\n },\n });\n }\n\n if (!session) return;\n\n abortController = new AbortController();\n\n // execute() handles both triggers and continuations\n const events = session.execute(msg, { signal: abortController.signal });\n\n try {\n for await (const event of events) {\n if (abortController.signal.aborted) break;\n conn.write(JSON.stringify(event));\n }\n } catch {\n // Handle errors\n }\n }\n }\n\n conn.on('close', () => {\n abortController?.abort();\n });\n };\n}\n```\n\n## Step 5: Set Up the Express Server\n\n```typescript\n// server/index.ts\nimport express from 'express';\nimport http from 'http';\nimport sockjs from 'sockjs';\nimport { createSocketHandler } from './octavus/socket-handler';\n\nconst app = express();\nconst server = http.createServer(app);\n\n// Create SockJS server\nconst sockServer = sockjs.createServer({\n prefix: '/octavus',\n log: () => {}, // Silence logs\n});\n\n// Attach handler\nsockServer.on('connection', createSocketHandler());\nsockServer.installHandlers(server);\n\n// Serve your frontend\napp.use(express.static('dist/client'));\n\nserver.listen(3001, () => {\n console.log('Server running on http://localhost:3001');\n});\n```\n\n## Step 6: Create the Socket Hook (Client)\n\n```typescript\n// src/hooks/useOctavusSocket.ts\nimport { useEffect, useMemo } from 'react';\nimport SockJS from 'sockjs-client';\nimport { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction connectSocket(): Promise<SocketLike> {\n return new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n\n sock.onopen = () => resolve(sock);\n sock.onerror = () => reject(new Error('Connection failed'));\n });\n}\n\nexport function useOctavusSocket() {\n // Transport is stable - empty deps because server manages sessions\n const transport = useMemo(() => createSocketTransport({ connect: connectSocket }), []);\n\n const {\n messages,\n status,\n error,\n send,\n stop,\n // Socket-specific connection state\n connectionState,\n connectionError,\n connect,\n disconnect,\n } = useOctavusChat({\n transport,\n onError: (err) => console.error('Chat error:', err),\n });\n\n // Eagerly connect for UI status indicator\n useEffect(() => {\n connect?.();\n return () => disconnect?.();\n }, [connect, disconnect]);\n\n const sendMessage = async (message: string) => {\n await send('user-message', { USER_MESSAGE: message }, { userMessage: { content: message } });\n };\n\n return { messages, status, error, connectionState, connectionError, sendMessage, stop };\n}\n```\n\n## Step 7: Build the Chat Component\n\n```tsx\n// src/components/Chat.tsx\nimport { useState } from 'react';\nimport { useOctavusSocket } from '../hooks/useOctavusSocket';\n\nfunction ConnectionIndicator({ state }: { state: string | undefined }) {\n const colors: Record<string, string> = {\n connected: 'bg-green-500',\n connecting: 'bg-yellow-500',\n error: 'bg-red-500',\n disconnected: 'bg-gray-400',\n };\n const color = colors[state ?? 'disconnected'];\n return <div className={`w-2 h-2 rounded-full ${color}`} title={state} />;\n}\n\nexport function Chat() {\n const [inputValue, setInputValue] = useState('');\n const { messages, status, connectionState, sendMessage, stop } = useOctavusSocket();\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!inputValue.trim() || status === 'streaming') return;\n\n const message = inputValue.trim();\n setInputValue('');\n await sendMessage(message);\n };\n\n return (\n <div className=\"flex flex-col h-screen\">\n {/* Header with connection status */}\n <div className=\"p-4 border-b flex items-center justify-between\">\n <h1 className=\"font-semibold\">AI Chat</h1>\n <ConnectionIndicator state={connectionState} />\n </div>\n\n {/* Messages */}\n <div className=\"flex-1 overflow-y-auto p-4 space-y-4\">\n {messages.map((msg) => (\n <div key={msg.id} className={msg.role === 'user' ? 'text-right' : 'text-left'}>\n <div\n className={`inline-block p-3 rounded-lg ${\n msg.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-100'\n }`}\n >\n {msg.parts.map((part, i) => {\n if (part.type === 'text') return <p key={i}>{part.text}</p>;\n return null;\n })}\n </div>\n </div>\n ))}\n </div>\n\n {/* Input */}\n <form onSubmit={handleSubmit} className=\"p-4 border-t flex gap-2\">\n <input\n type=\"text\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n placeholder=\"Type a message...\"\n className=\"flex-1 px-4 py-2 border rounded-lg\"\n disabled={status === 'streaming'}\n />\n {status === 'streaming' ? (\n <button\n type=\"button\"\n onClick={stop}\n className=\"px-4 py-2 bg-red-500 text-white rounded-lg\"\n >\n Stop\n </button>\n ) : (\n <button type=\"submit\" className=\"px-4 py-2 bg-blue-500 text-white rounded-lg\">\n Send\n </button>\n )}\n </form>\n </div>\n );\n}\n```\n\n## Custom Events\n\nSocket transport supports custom events alongside Octavus events:\n\n```typescript\n// Client - handle custom events\nconst transport = useMemo(\n () =>\n createSocketTransport({\n connect: connectSocket,\n onMessage: (data) => {\n const msg = data as { type: string; [key: string]: unknown };\n\n if (msg.type === 'typing-indicator') {\n setAgentTyping(msg.isTyping as boolean);\n }\n\n if (msg.type === 'custom-notification') {\n showToast(msg.message as string);\n }\n\n // Octavus events (text-delta, finish, etc.) are handled automatically\n },\n }),\n [],\n);\n```\n\n```typescript\n// Server - send custom events\nconn.write(\n JSON.stringify({\n type: 'typing-indicator',\n isTyping: true,\n }),\n);\n```\n\n## Protocol Integration\n\n### Messages\n\nThe socket handler receives messages and forwards them to Octavus:\n\n```typescript\n// Client sends trigger:\n{ type: 'trigger', triggerName: 'user-message', input: { USER_MESSAGE: 'Hello' } }\n\n// Client sends continuation (after client tool handling):\n{ type: 'continue', executionId: '...', toolResults: [...] }\n\n// Server handles both:\nif (msg.type === 'trigger' || msg.type === 'continue') {\n const events = session.execute(msg);\n for await (const event of events) {\n conn.write(JSON.stringify(event));\n }\n}\n```\n\n### Tools\n\nTools are defined in your agent's protocol. Server-side tools have handlers, client-side tools don't:\n\n```yaml\n# protocol.yaml\ntools:\n get-user-account:\n description: Fetch user details\n parameters:\n userId:\n type: string\n\n get-browser-location:\n description: Get user's location from browser\n # No server handler - handled on client\n```\n\n```typescript\n// Server tool handlers (only for server tools)\ntools: {\n 'get-user-account': async (args) => {\n const userId = args.userId as string;\n return await db.users.find(userId);\n },\n // get-browser-location has no handler - forwarded to client\n}\n```\n\nSee [Client Tools](/docs/client-sdk/client-tools) for handling tools on the frontend.\n\n## Meteor Integration Note\n\nMeteor's bundler may have issues with ES6 imports of `sockjs-client`:\n\n```typescript\n// Use require() instead of import\nconst SockJS: typeof import('sockjs-client') = require('sockjs-client');\n```\n\n## Next Steps\n\n- [Socket Transport](/docs/client-sdk/socket-transport) — Advanced socket patterns\n- [Protocol Overview](/docs/protocol/overview) — Define agent behavior\n- [Tools](/docs/protocol/tools) — Building tool handlers\n",
|
|
341
|
+
"content": "\n# Socket Chat Example\n\nThis example builds a chat interface using SockJS for bidirectional communication. Use this pattern for Meteor, Phoenix, or when you need custom real-time events.\n\n## What You're Building\n\nA chat interface that:\n\n- Uses SockJS for real-time streaming\n- Manages sessions server-side (client doesn't need sessionId)\n- Supports custom events alongside chat\n- Works with frameworks that use WebSocket-like transports\n\n## Architecture\n\n```mermaid\nflowchart LR\n Browser[\"Browser<br/>(React)\"] <-->|\"SockJS\"| Server[\"Your Server<br/>(Express)\"]\n Server --> Platform[\"Octavus Platform\"]\n```\n\n**Key difference from HTTP:** The server maintains a persistent socket connection and manages sessions internally. The client never needs to know about `sessionId`.\n\n## Prerequisites\n\n- Express (or similar Node.js server)\n- React frontend\n- `sockjs` (server) and `sockjs-client` (client)\n- Octavus account with API key\n\n## Step 1: Install Dependencies\n\n**Server:**\n\n```bash\nnpm install @octavus/server-sdk sockjs express\nnpm install -D @types/sockjs @types/express\n```\n\n**Client:**\n\n```bash\nnpm install @octavus/react sockjs-client\nnpm install -D @types/sockjs-client\n```\n\n## Step 2: Configure Environment\n\n```bash\n# .env\nOCTAVUS_API_URL=https://octavus.ai\nOCTAVUS_API_KEY=your-api-key\nOCTAVUS_AGENT_ID=your-agent-id\n```\n\n## Step 3: Create the Octavus Client (Server)\n\n```typescript\n// server/octavus/client.ts\nimport { OctavusClient } from '@octavus/server-sdk';\n\nexport const octavus = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nexport const AGENT_ID = process.env.OCTAVUS_AGENT_ID!;\n```\n\n## Step 4: Create the Socket Handler (Server)\n\nThis is the core of socket integration. Each connection gets its own session:\n\n```typescript\n// server/octavus/socket-handler.ts\nimport type { Connection } from 'sockjs';\nimport { OctavusClient, type AgentSession, type SocketMessage } from '@octavus/server-sdk';\n\nconst octavus = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nconst AGENT_ID = process.env.OCTAVUS_AGENT_ID!;\n\nexport function createSocketHandler() {\n return (conn: Connection) => {\n let session: AgentSession | null = null;\n\n const send = (data: unknown) => conn.write(JSON.stringify(data));\n\n conn.on('data', (rawData: string) => {\n void handleMessage(rawData);\n });\n\n async function handleMessage(rawData: string) {\n const msg = JSON.parse(rawData);\n\n // Handle trigger, continue, and stop messages\n if (msg.type === 'trigger' || msg.type === 'continue' || msg.type === 'stop') {\n // Create session lazily on first trigger\n if (!session && msg.type === 'trigger') {\n const sessionId = await octavus.agentSessions.create(AGENT_ID, {\n COMPANY_NAME: 'Acme Corp',\n });\n\n session = octavus.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async () => {\n return { name: 'Demo User', plan: 'pro' };\n },\n 'create-support-ticket': async () => {\n return { ticketId: 'TKT-123', estimatedResponse: '24h' };\n },\n },\n });\n }\n\n if (!session) return;\n\n // handleSocketMessage manages abort controller internally\n await session.handleSocketMessage(msg as SocketMessage, {\n onEvent: send,\n });\n }\n }\n\n conn.on('close', () => {});\n };\n}\n```\n\n## Step 5: Set Up the Express Server\n\n```typescript\n// server/index.ts\nimport express from 'express';\nimport http from 'http';\nimport sockjs from 'sockjs';\nimport { createSocketHandler } from './octavus/socket-handler';\n\nconst app = express();\nconst server = http.createServer(app);\n\n// Create SockJS server\nconst sockServer = sockjs.createServer({\n prefix: '/octavus',\n log: () => {}, // Silence logs\n});\n\n// Attach handler\nsockServer.on('connection', createSocketHandler());\nsockServer.installHandlers(server);\n\n// Serve your frontend\napp.use(express.static('dist/client'));\n\nserver.listen(3001, () => {\n console.log('Server running on http://localhost:3001');\n});\n```\n\n## Step 6: Create the Socket Hook (Client)\n\n```typescript\n// src/hooks/useOctavusSocket.ts\nimport { useEffect, useMemo } from 'react';\nimport SockJS from 'sockjs-client';\nimport { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';\n\nfunction connectSocket(): Promise<SocketLike> {\n return new Promise((resolve, reject) => {\n const sock = new SockJS('/octavus');\n\n sock.onopen = () => resolve(sock);\n sock.onerror = () => reject(new Error('Connection failed'));\n });\n}\n\nexport function useOctavusSocket() {\n // Transport is stable - empty deps because server manages sessions\n const transport = useMemo(() => createSocketTransport({ connect: connectSocket }), []);\n\n const {\n messages,\n status,\n error,\n send,\n stop,\n // Socket-specific connection state\n connectionState,\n connectionError,\n connect,\n disconnect,\n } = useOctavusChat({\n transport,\n onError: (err) => console.error('Chat error:', err),\n });\n\n // Eagerly connect for UI status indicator\n useEffect(() => {\n connect?.();\n return () => disconnect?.();\n }, [connect, disconnect]);\n\n const sendMessage = async (message: string) => {\n await send('user-message', { USER_MESSAGE: message }, { userMessage: { content: message } });\n };\n\n return { messages, status, error, connectionState, connectionError, sendMessage, stop };\n}\n```\n\n## Step 7: Build the Chat Component\n\n```tsx\n// src/components/Chat.tsx\nimport { useState } from 'react';\nimport { useOctavusSocket } from '../hooks/useOctavusSocket';\n\nfunction ConnectionIndicator({ state }: { state: string | undefined }) {\n const colors: Record<string, string> = {\n connected: 'bg-green-500',\n connecting: 'bg-yellow-500',\n error: 'bg-red-500',\n disconnected: 'bg-gray-400',\n };\n const color = colors[state ?? 'disconnected'];\n return <div className={`w-2 h-2 rounded-full ${color}`} title={state} />;\n}\n\nexport function Chat() {\n const [inputValue, setInputValue] = useState('');\n const { messages, status, connectionState, sendMessage, stop } = useOctavusSocket();\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!inputValue.trim() || status === 'streaming') return;\n\n const message = inputValue.trim();\n setInputValue('');\n await sendMessage(message);\n };\n\n return (\n <div className=\"flex flex-col h-screen\">\n {/* Header with connection status */}\n <div className=\"p-4 border-b flex items-center justify-between\">\n <h1 className=\"font-semibold\">AI Chat</h1>\n <ConnectionIndicator state={connectionState} />\n </div>\n\n {/* Messages */}\n <div className=\"flex-1 overflow-y-auto p-4 space-y-4\">\n {messages.map((msg) => (\n <div key={msg.id} className={msg.role === 'user' ? 'text-right' : 'text-left'}>\n <div\n className={`inline-block p-3 rounded-lg ${\n msg.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-100'\n }`}\n >\n {msg.parts.map((part, i) => {\n if (part.type === 'text') return <p key={i}>{part.text}</p>;\n return null;\n })}\n </div>\n </div>\n ))}\n </div>\n\n {/* Input */}\n <form onSubmit={handleSubmit} className=\"p-4 border-t flex gap-2\">\n <input\n type=\"text\"\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n placeholder=\"Type a message...\"\n className=\"flex-1 px-4 py-2 border rounded-lg\"\n disabled={status === 'streaming'}\n />\n {status === 'streaming' ? (\n <button\n type=\"button\"\n onClick={stop}\n className=\"px-4 py-2 bg-red-500 text-white rounded-lg\"\n >\n Stop\n </button>\n ) : (\n <button type=\"submit\" className=\"px-4 py-2 bg-blue-500 text-white rounded-lg\">\n Send\n </button>\n )}\n </form>\n </div>\n );\n}\n```\n\n## Custom Events\n\nSocket transport supports custom events alongside Octavus events:\n\n```typescript\n// Client - handle custom events\nconst transport = useMemo(\n () =>\n createSocketTransport({\n connect: connectSocket,\n onMessage: (data) => {\n const msg = data as { type: string; [key: string]: unknown };\n\n if (msg.type === 'typing-indicator') {\n setAgentTyping(msg.isTyping as boolean);\n }\n\n if (msg.type === 'custom-notification') {\n showToast(msg.message as string);\n }\n\n // Octavus events (text-delta, finish, etc.) are handled automatically\n },\n }),\n [],\n);\n```\n\n```typescript\n// Server - send custom events\nconn.write(\n JSON.stringify({\n type: 'typing-indicator',\n isTyping: true,\n }),\n);\n```\n\n## Protocol Integration\n\n### Messages\n\nThe socket handler receives messages and forwards them to Octavus:\n\n```typescript\n// Client sends trigger:\n{ type: 'trigger', triggerName: 'user-message', input: { USER_MESSAGE: 'Hello' } }\n\n// Client sends continuation (after client tool handling):\n{ type: 'continue', executionId: '...', toolResults: [...] }\n\n// Client sends stop:\n{ type: 'stop' }\n\n// Server handles all three with handleSocketMessage:\nawait session.handleSocketMessage(msg, {\n onEvent: (event) => conn.write(JSON.stringify(event)),\n});\n```\n\n### Tools\n\nTools are defined in your agent's protocol. Server-side tools have handlers, client-side tools don't:\n\n```yaml\n# protocol.yaml\ntools:\n get-user-account:\n description: Fetch user details\n parameters:\n userId:\n type: string\n\n get-browser-location:\n description: Get user's location from browser\n # No server handler - handled on client\n```\n\n```typescript\n// Server tool handlers (only for server tools)\ntools: {\n 'get-user-account': async (args) => {\n const userId = args.userId as string;\n return await db.users.find(userId);\n },\n // get-browser-location has no handler - forwarded to client\n}\n```\n\nSee [Client Tools](/docs/client-sdk/client-tools) for handling tools on the frontend.\n\n## Meteor Integration Note\n\nMeteor's bundler may have issues with ES6 imports of `sockjs-client`:\n\n```typescript\n// Use require() instead of import\nconst SockJS: typeof import('sockjs-client') = require('sockjs-client');\n```\n\n## Next Steps\n\n- [Socket Transport](/docs/client-sdk/socket-transport) — Advanced socket patterns\n- [Protocol Overview](/docs/protocol/overview) — Define agent behavior\n- [Tools](/docs/protocol/tools) — Building tool handlers\n",
|
|
342
342
|
"excerpt": "Socket Chat Example This example builds a chat interface using SockJS for bidirectional communication. Use this pattern for Meteor, Phoenix, or when you need custom real-time events. What You're...",
|
|
343
343
|
"order": 3
|
|
344
344
|
}
|