@octavus/docs 0.0.6 → 0.0.8
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/01-getting-started/02-quickstart.md +28 -19
- package/content/02-server-sdk/01-overview.md +34 -17
- package/content/02-server-sdk/02-sessions.md +6 -3
- package/content/02-server-sdk/03-tools.md +4 -2
- package/content/02-server-sdk/04-streaming.md +12 -4
- package/content/03-client-sdk/01-overview.md +107 -42
- package/content/03-client-sdk/02-messages.md +71 -19
- package/content/03-client-sdk/03-streaming.md +38 -28
- package/content/03-client-sdk/05-socket-transport.md +414 -0
- package/content/03-client-sdk/06-http-transport.md +280 -0
- package/content/04-protocol/03-triggers.md +48 -21
- package/content/04-protocol/04-tools.md +25 -15
- package/content/05-api-reference/01-overview.md +3 -17
- package/content/06-examples/01-overview.md +27 -0
- package/content/06-examples/02-nextjs-chat.md +343 -0
- package/content/06-examples/03-socket-chat.md +392 -0
- package/content/06-examples/_meta.md +5 -0
- package/dist/chunk-232K4EME.js +439 -0
- package/dist/chunk-232K4EME.js.map +1 -0
- package/dist/chunk-2JDZLMS3.js +439 -0
- package/dist/chunk-2JDZLMS3.js.map +1 -0
- package/dist/chunk-5M7DS4DF.js +519 -0
- package/dist/chunk-5M7DS4DF.js.map +1 -0
- package/dist/chunk-7AS4ST73.js +421 -0
- package/dist/chunk-7AS4ST73.js.map +1 -0
- package/dist/chunk-H6JGSSAJ.js +519 -0
- package/dist/chunk-H6JGSSAJ.js.map +1 -0
- package/dist/chunk-JZRABTHU.js +519 -0
- package/dist/chunk-JZRABTHU.js.map +1 -0
- package/dist/chunk-OECAPVSX.js +439 -0
- package/dist/chunk-OECAPVSX.js.map +1 -0
- package/dist/chunk-OL5QDJ42.js +483 -0
- package/dist/chunk-OL5QDJ42.js.map +1 -0
- package/dist/chunk-PMOVVTHO.js +519 -0
- package/dist/chunk-PMOVVTHO.js.map +1 -0
- package/dist/chunk-R5MTVABN.js +439 -0
- package/dist/chunk-R5MTVABN.js.map +1 -0
- package/dist/chunk-RJ4H4YVA.js +519 -0
- package/dist/chunk-RJ4H4YVA.js.map +1 -0
- package/dist/chunk-S5U4IWCR.js +439 -0
- package/dist/chunk-S5U4IWCR.js.map +1 -0
- package/dist/chunk-UCJE36LL.js +519 -0
- package/dist/chunk-UCJE36LL.js.map +1 -0
- package/dist/chunk-WW7TRC7S.js +519 -0
- package/dist/chunk-WW7TRC7S.js.map +1 -0
- package/dist/content.js +1 -1
- package/dist/docs.json +57 -12
- 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 +65 -12
- package/package.json +2 -2
|
@@ -10,7 +10,7 @@ The Client SDK provides real-time access to streaming content through the messag
|
|
|
10
10
|
## Streaming State
|
|
11
11
|
|
|
12
12
|
```tsx
|
|
13
|
-
const { messages, status, error } = useOctavusChat({
|
|
13
|
+
const { messages, status, error } = useOctavusChat({ transport });
|
|
14
14
|
|
|
15
15
|
// status: 'idle' | 'streaming' | 'error'
|
|
16
16
|
// Each message has status: 'streaming' | 'done'
|
|
@@ -20,8 +20,24 @@ const { messages, status, error } = useOctavusChat({...});
|
|
|
20
20
|
## Building a Streaming UI
|
|
21
21
|
|
|
22
22
|
```tsx
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
import { useMemo } from 'react';
|
|
24
|
+
import { useOctavusChat, createHttpTransport } from '@octavus/react';
|
|
25
|
+
|
|
26
|
+
function Chat({ sessionId }: { sessionId: string }) {
|
|
27
|
+
const transport = useMemo(
|
|
28
|
+
() =>
|
|
29
|
+
createHttpTransport({
|
|
30
|
+
triggerRequest: (triggerName, input) =>
|
|
31
|
+
fetch('/api/trigger', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ sessionId, triggerName, input }),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
[sessionId],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const { messages, status, error, send, stop } = useOctavusChat({ transport });
|
|
25
41
|
|
|
26
42
|
return (
|
|
27
43
|
<div>
|
|
@@ -31,14 +47,10 @@ function Chat() {
|
|
|
31
47
|
))}
|
|
32
48
|
|
|
33
49
|
{/* Error state */}
|
|
34
|
-
{error &&
|
|
35
|
-
<div className="text-red-500">{error.message}</div>
|
|
36
|
-
)}
|
|
50
|
+
{error && <div className="text-red-500">{error.message}</div>}
|
|
37
51
|
|
|
38
52
|
{/* Stop button during streaming */}
|
|
39
|
-
{status === 'streaming' &&
|
|
40
|
-
<button onClick={stop}>Stop</button>
|
|
41
|
-
)}
|
|
53
|
+
{status === 'streaming' && <button onClick={stop}>Stop</button>}
|
|
42
54
|
</div>
|
|
43
55
|
);
|
|
44
56
|
}
|
|
@@ -72,11 +84,9 @@ function ReasoningPart({ part }: { part: UIReasoningPart }) {
|
|
|
72
84
|
{part.status === 'streaming' ? '💭 Thinking...' : '💭 Thought process'}
|
|
73
85
|
{expanded ? '▼' : '▶'}
|
|
74
86
|
</button>
|
|
75
|
-
|
|
87
|
+
|
|
76
88
|
{expanded && (
|
|
77
|
-
<pre className="mt-2 text-sm text-gray-600">
|
|
78
|
-
{part.text}
|
|
79
|
-
</pre>
|
|
89
|
+
<pre className="mt-2 text-sm text-gray-600">{part.text}</pre>
|
|
80
90
|
)}
|
|
81
91
|
</div>
|
|
82
92
|
);
|
|
@@ -95,19 +105,17 @@ function ToolCallPart({ part }: { part: UIToolCallPart }) {
|
|
|
95
105
|
<div className="border rounded p-3">
|
|
96
106
|
<div className="flex items-center gap-2">
|
|
97
107
|
<span className="text-lg">🔧</span>
|
|
98
|
-
<span className="font-medium">
|
|
99
|
-
{part.displayName || part.toolName}
|
|
100
|
-
</span>
|
|
108
|
+
<span className="font-medium">{part.displayName || part.toolName}</span>
|
|
101
109
|
<StatusBadge status={part.status} />
|
|
102
110
|
</div>
|
|
103
|
-
|
|
111
|
+
|
|
104
112
|
{/* Show result when done */}
|
|
105
113
|
{part.status === 'done' && part.result && (
|
|
106
114
|
<pre className="mt-2 text-xs bg-gray-50 p-2 rounded">
|
|
107
115
|
{JSON.stringify(part.result, null, 2)}
|
|
108
116
|
</pre>
|
|
109
117
|
)}
|
|
110
|
-
|
|
118
|
+
|
|
111
119
|
{/* Show error if failed */}
|
|
112
120
|
{part.status === 'error' && (
|
|
113
121
|
<p className="mt-2 text-red-500 text-sm">{part.error}</p>
|
|
@@ -133,9 +141,7 @@ function StatusBadge({ status }: { status: UIToolCallPart['status'] }) {
|
|
|
133
141
|
## Status Indicator
|
|
134
142
|
|
|
135
143
|
```tsx
|
|
136
|
-
function StatusIndicator() {
|
|
137
|
-
const { status } = useOctavusChat({...});
|
|
138
|
-
|
|
144
|
+
function StatusIndicator({ status }: { status: ChatStatus }) {
|
|
139
145
|
switch (status) {
|
|
140
146
|
case 'idle':
|
|
141
147
|
return null;
|
|
@@ -151,7 +157,7 @@ function StatusIndicator() {
|
|
|
151
157
|
|
|
152
158
|
```tsx
|
|
153
159
|
useOctavusChat({
|
|
154
|
-
|
|
160
|
+
transport,
|
|
155
161
|
onFinish: () => {
|
|
156
162
|
console.log('Stream completed');
|
|
157
163
|
// Scroll to bottom, play sound, etc.
|
|
@@ -168,7 +174,7 @@ useOctavusChat({
|
|
|
168
174
|
Stop the current stream and finalize any partial message:
|
|
169
175
|
|
|
170
176
|
```tsx
|
|
171
|
-
const { status, stop } = useOctavusChat({
|
|
177
|
+
const { status, stop } = useOctavusChat({ transport });
|
|
172
178
|
|
|
173
179
|
// Stop button
|
|
174
180
|
{status === 'streaming' && (
|
|
@@ -188,17 +194,19 @@ When `stop()` is called:
|
|
|
188
194
|
Content from named threads (like "summary") streams separately and is identified by the `thread` property:
|
|
189
195
|
|
|
190
196
|
```tsx
|
|
191
|
-
import { isOtherThread } from '@octavus/react';
|
|
197
|
+
import { isOtherThread, type UIMessage } from '@octavus/react';
|
|
192
198
|
|
|
193
199
|
function MessageBubble({ message }: { message: UIMessage }) {
|
|
194
200
|
// Separate main thread from named threads
|
|
195
|
-
const mainParts = message.parts.filter(p => !isOtherThread(p));
|
|
196
|
-
const otherParts = message.parts.filter(p => isOtherThread(p));
|
|
201
|
+
const mainParts = message.parts.filter((p) => !isOtherThread(p));
|
|
202
|
+
const otherParts = message.parts.filter((p) => isOtherThread(p));
|
|
197
203
|
|
|
198
204
|
return (
|
|
199
205
|
<div>
|
|
200
206
|
{/* Main conversation */}
|
|
201
|
-
{mainParts.map((part, i) =>
|
|
207
|
+
{mainParts.map((part, i) => (
|
|
208
|
+
<PartRenderer key={i} part={part} />
|
|
209
|
+
))}
|
|
202
210
|
|
|
203
211
|
{/* Named thread content (e.g., summarization) */}
|
|
204
212
|
{otherParts.length > 0 && (
|
|
@@ -206,7 +214,9 @@ function MessageBubble({ message }: { message: UIMessage }) {
|
|
|
206
214
|
<div className="text-amber-600 font-medium mb-2">
|
|
207
215
|
Background processing
|
|
208
216
|
</div>
|
|
209
|
-
{otherParts.map((part, i) =>
|
|
217
|
+
{otherParts.map((part, i) => (
|
|
218
|
+
<PartRenderer key={i} part={part} />
|
|
219
|
+
))}
|
|
210
220
|
</div>
|
|
211
221
|
)}
|
|
212
222
|
</div>
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Socket Transport
|
|
3
|
+
description: Using WebSocket or SockJS for real-time streaming with Octavus.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Socket Transport
|
|
7
|
+
|
|
8
|
+
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 isn't suitable for your infrastructure.
|
|
9
|
+
|
|
10
|
+
## When to Use Socket Transport
|
|
11
|
+
|
|
12
|
+
| Use Case | Recommended Transport |
|
|
13
|
+
|----------|----------------------|
|
|
14
|
+
| Standard web apps (Next.js, etc.) | HTTP (`createHttpTransport`) |
|
|
15
|
+
| Real-time apps with custom events | Socket (`createSocketTransport`) |
|
|
16
|
+
| Apps behind proxies that don't support SSE | Socket |
|
|
17
|
+
| Need for typing indicators, presence, etc. | Socket |
|
|
18
|
+
| Meteor, Phoenix, or socket-based frameworks | Socket |
|
|
19
|
+
|
|
20
|
+
## Patterns Overview
|
|
21
|
+
|
|
22
|
+
There are two main patterns for socket-based integrations:
|
|
23
|
+
|
|
24
|
+
| Pattern | When to Use |
|
|
25
|
+
|---------|-------------|
|
|
26
|
+
| [Server-Managed Sessions](#server-managed-sessions-recommended) | **Recommended.** Server creates sessions lazily. Client doesn't need sessionId. |
|
|
27
|
+
| [Client-Provided Session ID](#client-provided-session-id) | When client must control session creation or pass sessionId from URL. |
|
|
28
|
+
|
|
29
|
+
## Server-Managed Sessions (Recommended)
|
|
30
|
+
|
|
31
|
+
The 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.
|
|
32
|
+
|
|
33
|
+
### Client Setup
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { useMemo } from 'react';
|
|
37
|
+
import SockJS from 'sockjs-client';
|
|
38
|
+
import { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';
|
|
39
|
+
|
|
40
|
+
function connectSocket(): Promise<SocketLike> {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const sock = new SockJS('/octavus');
|
|
43
|
+
sock.onopen = () => resolve(sock);
|
|
44
|
+
sock.onerror = () => reject(new Error('Connection failed'));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Chat() {
|
|
49
|
+
// Transport is stable — no dependencies on sessionId
|
|
50
|
+
const transport = useMemo(
|
|
51
|
+
() => createSocketTransport({ connect: connectSocket }),
|
|
52
|
+
[],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const { messages, status, send } = useOctavusChat({ transport });
|
|
56
|
+
|
|
57
|
+
const sendMessage = async (text: string) => {
|
|
58
|
+
await send(
|
|
59
|
+
'user-message',
|
|
60
|
+
{ USER_MESSAGE: text },
|
|
61
|
+
{ userMessage: { content: text } },
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ... render messages
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Server Setup (Express + SockJS)
|
|
70
|
+
|
|
71
|
+
The server creates a session on first trigger message:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import sockjs from 'sockjs';
|
|
75
|
+
import { OctavusClient, type AgentSession } from '@octavus/server-sdk';
|
|
76
|
+
|
|
77
|
+
const client = new OctavusClient({
|
|
78
|
+
baseUrl: process.env.OCTAVUS_API_URL!,
|
|
79
|
+
apiKey: process.env.OCTAVUS_API_KEY!,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function createSocketHandler() {
|
|
83
|
+
return (conn: sockjs.Connection) => {
|
|
84
|
+
let session: AgentSession | null = null;
|
|
85
|
+
let abortController: AbortController | null = null;
|
|
86
|
+
|
|
87
|
+
conn.on('data', (rawData: string) => {
|
|
88
|
+
void handleMessage(rawData);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
async function handleMessage(rawData: string) {
|
|
92
|
+
const msg = JSON.parse(rawData);
|
|
93
|
+
|
|
94
|
+
if (msg.type === 'stop') {
|
|
95
|
+
abortController?.abort();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (msg.type === 'trigger') {
|
|
100
|
+
// Create session lazily on first trigger
|
|
101
|
+
if (!session) {
|
|
102
|
+
const sessionId = await client.agentSessions.create('your-agent-id', {
|
|
103
|
+
// Initial input variables
|
|
104
|
+
COMPANY_NAME: 'Acme Corp',
|
|
105
|
+
});
|
|
106
|
+
session = client.agentSessions.attach(sessionId, {
|
|
107
|
+
tools: {
|
|
108
|
+
// Your tool handlers
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
abortController = new AbortController();
|
|
114
|
+
|
|
115
|
+
// Iterate events directly — no SSE parsing needed
|
|
116
|
+
const events = session.trigger(msg.triggerName, msg.input);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
for await (const event of events) {
|
|
120
|
+
if (abortController.signal.aborted) break;
|
|
121
|
+
conn.write(JSON.stringify(event));
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Handle errors
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
conn.on('close', () => abortController?.abort());
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const sockServer = sockjs.createServer({ prefix: '/octavus' });
|
|
134
|
+
sockServer.on('connection', createSocketHandler());
|
|
135
|
+
sockServer.installHandlers(httpServer);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Benefits of this pattern:**
|
|
139
|
+
- Client code is simple — no sessionId management
|
|
140
|
+
- No transport caching issues
|
|
141
|
+
- Session is created only when needed
|
|
142
|
+
- Server controls session configuration
|
|
143
|
+
|
|
144
|
+
## Client-Provided Session ID
|
|
145
|
+
|
|
146
|
+
If 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:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { useMemo } from 'react';
|
|
150
|
+
import SockJS from 'sockjs-client';
|
|
151
|
+
import { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';
|
|
152
|
+
|
|
153
|
+
function Chat({ sessionId }: { sessionId: string }) {
|
|
154
|
+
const transport = useMemo(
|
|
155
|
+
() =>
|
|
156
|
+
createSocketTransport({
|
|
157
|
+
connect: () =>
|
|
158
|
+
new Promise((resolve, reject) => {
|
|
159
|
+
const sock = new SockJS('/octavus');
|
|
160
|
+
sock.onopen = () => {
|
|
161
|
+
// Send init message with sessionId
|
|
162
|
+
sock.send(JSON.stringify({ type: 'init', sessionId }));
|
|
163
|
+
resolve(sock);
|
|
164
|
+
};
|
|
165
|
+
sock.onerror = () => reject(new Error('Connection failed'));
|
|
166
|
+
}),
|
|
167
|
+
}),
|
|
168
|
+
[sessionId],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const { messages, status, send } = useOctavusChat({ transport });
|
|
172
|
+
// ... render chat
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
When `sessionId` changes, the hook automatically reinitializes with the new transport.
|
|
177
|
+
|
|
178
|
+
### Server Handler with Init Message
|
|
179
|
+
|
|
180
|
+
When using client-provided sessionId, the server must handle an `init` message:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
sockServer.on('connection', (conn) => {
|
|
184
|
+
let session: AgentSession | null = null;
|
|
185
|
+
let abortController: AbortController | null = null;
|
|
186
|
+
|
|
187
|
+
conn.on('data', (rawData: string) => {
|
|
188
|
+
void handleMessage(rawData);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
async function handleMessage(rawData: string) {
|
|
192
|
+
const msg = JSON.parse(rawData);
|
|
193
|
+
|
|
194
|
+
// Handle session initialization
|
|
195
|
+
if (msg.type === 'init') {
|
|
196
|
+
session = client.agentSessions.attach(msg.sessionId, {
|
|
197
|
+
tools: { /* ... */ },
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (msg.type === 'stop') {
|
|
203
|
+
abortController?.abort();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// All other messages require initialized session
|
|
208
|
+
if (!session) {
|
|
209
|
+
conn.write(JSON.stringify({
|
|
210
|
+
type: 'error',
|
|
211
|
+
errorText: 'Session not initialized. Send init message first.',
|
|
212
|
+
}));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (msg.type === 'trigger') {
|
|
217
|
+
// ... handle trigger (same as server-managed pattern)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Async Session ID
|
|
224
|
+
|
|
225
|
+
When the session ID is fetched asynchronously (e.g., from an API), you have two options:
|
|
226
|
+
|
|
227
|
+
### Option 1: Conditionally Render (Recommended)
|
|
228
|
+
|
|
229
|
+
Don't render the chat component until `sessionId` is available:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
function ChatPage() {
|
|
233
|
+
const [sessionId, setSessionId] = useState<string | null>(null);
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
api.createSession().then(res => setSessionId(res.sessionId));
|
|
237
|
+
}, []);
|
|
238
|
+
|
|
239
|
+
// Don't render until sessionId is ready
|
|
240
|
+
if (!sessionId) {
|
|
241
|
+
return <LoadingSpinner />;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return <Chat sessionId={sessionId} />;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This is the cleanest approach — the `Chat` component always receives a valid `sessionId`.
|
|
249
|
+
|
|
250
|
+
### Option 2: Server-Managed Sessions
|
|
251
|
+
|
|
252
|
+
Use the [server-managed sessions pattern](#server-managed-sessions-recommended) where the server creates the session lazily. The client never needs to know about `sessionId`.
|
|
253
|
+
|
|
254
|
+
## Native WebSocket
|
|
255
|
+
|
|
256
|
+
If you're using native WebSocket instead of SockJS, you can pass sessionId via URL:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const transport = useMemo(
|
|
260
|
+
() =>
|
|
261
|
+
createSocketTransport({
|
|
262
|
+
connect: () =>
|
|
263
|
+
new Promise((resolve, reject) => {
|
|
264
|
+
const ws = new WebSocket(
|
|
265
|
+
`wss://your-server.com/octavus?sessionId=${sessionId}`
|
|
266
|
+
);
|
|
267
|
+
ws.onopen = () => resolve(ws);
|
|
268
|
+
ws.onerror = () => reject(new Error('WebSocket connection failed'));
|
|
269
|
+
}),
|
|
270
|
+
}),
|
|
271
|
+
[sessionId],
|
|
272
|
+
);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
When `sessionId` changes, the hook automatically reinitializes with the new transport.
|
|
276
|
+
|
|
277
|
+
## Custom Events
|
|
278
|
+
|
|
279
|
+
Handle custom events alongside Octavus stream events:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const transport = createSocketTransport({
|
|
283
|
+
connect: connectSocket,
|
|
284
|
+
|
|
285
|
+
onMessage: (data) => {
|
|
286
|
+
const msg = data as { type: string; [key: string]: unknown };
|
|
287
|
+
|
|
288
|
+
switch (msg.type) {
|
|
289
|
+
case 'typing-indicator':
|
|
290
|
+
setAgentTyping(msg.isTyping as boolean);
|
|
291
|
+
break;
|
|
292
|
+
|
|
293
|
+
case 'presence-update':
|
|
294
|
+
setOnlineUsers(msg.users as string[]);
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case 'notification':
|
|
298
|
+
showToast(msg.message as string);
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
// Octavus events (text-delta, finish, etc.) are handled automatically
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Connection Management
|
|
308
|
+
|
|
309
|
+
### Handling Disconnections
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const transport = createSocketTransport({
|
|
313
|
+
connect: connectSocket,
|
|
314
|
+
|
|
315
|
+
onClose: () => {
|
|
316
|
+
console.log('Socket disconnected');
|
|
317
|
+
setConnectionStatus('disconnected');
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Reconnection with Exponential Backoff
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { useRef, useCallback, useMemo } from 'react';
|
|
326
|
+
import { createSocketTransport, type SocketLike } from '@octavus/react';
|
|
327
|
+
|
|
328
|
+
function useReconnectingTransport() {
|
|
329
|
+
const reconnectAttempts = useRef(0);
|
|
330
|
+
const maxAttempts = 5;
|
|
331
|
+
|
|
332
|
+
const connect = useCallback((): Promise<SocketLike> => {
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
const sock = new SockJS('/octavus');
|
|
335
|
+
|
|
336
|
+
sock.onopen = () => {
|
|
337
|
+
reconnectAttempts.current = 0;
|
|
338
|
+
resolve(sock);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
sock.onerror = () => {
|
|
342
|
+
if (reconnectAttempts.current < maxAttempts) {
|
|
343
|
+
reconnectAttempts.current++;
|
|
344
|
+
const delay = Math.min(1000 * 2 ** reconnectAttempts.current, 30000);
|
|
345
|
+
console.log(`Reconnecting in ${delay}ms...`);
|
|
346
|
+
setTimeout(() => connect().then(resolve).catch(reject), delay);
|
|
347
|
+
} else {
|
|
348
|
+
reject(new Error('Max reconnection attempts reached'));
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
}, []);
|
|
353
|
+
|
|
354
|
+
return useMemo(
|
|
355
|
+
() => createSocketTransport({ connect }),
|
|
356
|
+
[connect],
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Framework Notes
|
|
362
|
+
|
|
363
|
+
### Meteor
|
|
364
|
+
|
|
365
|
+
Meteor's bundler may have issues with ES6 imports of `sockjs-client`. Use `require()` instead:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// ❌ May fail in Meteor
|
|
369
|
+
import SockJS from 'sockjs-client';
|
|
370
|
+
|
|
371
|
+
// ✅ Works in Meteor
|
|
372
|
+
const SockJS: typeof import('sockjs-client') = require('sockjs-client');
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### SockJS vs WebSocket
|
|
376
|
+
|
|
377
|
+
| Feature | WebSocket | SockJS |
|
|
378
|
+
|---------|-----------|--------|
|
|
379
|
+
| Browser support | Modern browsers | All browsers (with fallbacks) |
|
|
380
|
+
| Session ID | Via URL query params | Via init message |
|
|
381
|
+
| Proxy compatibility | Varies | Excellent (polling fallback) |
|
|
382
|
+
| Setup complexity | Lower | Higher (requires server library) |
|
|
383
|
+
|
|
384
|
+
## Protocol Reference
|
|
385
|
+
|
|
386
|
+
### Client → Server Messages
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// Initialize session (only for client-provided sessionId pattern)
|
|
390
|
+
{ type: 'init', sessionId: string }
|
|
391
|
+
|
|
392
|
+
// Trigger an action
|
|
393
|
+
{ type: 'trigger', triggerName: string, input?: Record<string, unknown> }
|
|
394
|
+
|
|
395
|
+
// Stop current stream
|
|
396
|
+
{ type: 'stop' }
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Server → Client Messages
|
|
400
|
+
|
|
401
|
+
The server sends Octavus `StreamEvent` objects as JSON. See [Streaming Events](/docs/server-sdk/streaming#event-types) for the full list.
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Examples
|
|
405
|
+
{ type: 'start', messageId: '...' }
|
|
406
|
+
{ type: 'text-delta', id: '...', delta: 'Hello' }
|
|
407
|
+
{ type: 'tool-input-start', toolCallId: '...', toolName: 'get-user' }
|
|
408
|
+
{ type: 'finish', finishReason: 'stop' }
|
|
409
|
+
{ type: 'error', errorText: 'Something went wrong' }
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Full Example
|
|
413
|
+
|
|
414
|
+
For a complete walkthrough of building a chat interface with SockJS, see the [Socket Chat Example](/docs/examples/socket-chat).
|