@octavus/docs 1.0.0 → 2.0.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/01-getting-started/02-quickstart.md +8 -5
- package/content/02-server-sdk/01-overview.md +22 -6
- package/content/02-server-sdk/02-sessions.md +51 -10
- package/content/02-server-sdk/03-tools.md +39 -14
- package/content/02-server-sdk/04-streaming.md +55 -7
- package/content/02-server-sdk/05-cli.md +9 -9
- package/content/03-client-sdk/01-overview.md +22 -9
- package/content/03-client-sdk/02-messages.md +6 -4
- package/content/03-client-sdk/03-streaming.md +7 -3
- package/content/03-client-sdk/05-socket-transport.md +31 -10
- package/content/03-client-sdk/06-http-transport.md +81 -17
- package/content/03-client-sdk/07-structured-output.md +3 -2
- package/content/03-client-sdk/08-file-uploads.md +6 -4
- package/content/03-client-sdk/10-client-tools.md +557 -0
- package/content/04-protocol/02-input-resources.md +12 -0
- package/content/04-protocol/03-triggers.md +8 -5
- package/content/04-protocol/06-handlers.md +10 -0
- package/content/04-protocol/07-agent-config.md +34 -1
- package/content/05-api-reference/01-overview.md +18 -0
- package/content/05-api-reference/02-sessions.md +2 -0
- package/content/05-api-reference/03-agents.md +12 -0
- package/content/06-examples/02-nextjs-chat.md +12 -7
- package/content/06-examples/03-socket-chat.md +27 -13
- package/content/07-migration/01-v1-to-v2.md +366 -0
- package/content/07-migration/_meta.md +4 -0
- package/dist/chunk-3ER2T7S7.js +663 -0
- package/dist/chunk-3ER2T7S7.js.map +1 -0
- package/dist/{chunk-WJ2W3DUC.js → chunk-HFF2TVGV.js} +13 -13
- package/dist/chunk-HFF2TVGV.js.map +1 -0
- package/dist/chunk-S5JUVAKE.js +1409 -0
- package/dist/chunk-S5JUVAKE.js.map +1 -0
- package/dist/chunk-TMJG4CJH.js +1409 -0
- package/dist/chunk-TMJG4CJH.js.map +1 -0
- package/dist/chunk-YJPO6KOJ.js +1435 -0
- package/dist/chunk-YJPO6KOJ.js.map +1 -0
- package/dist/chunk-ZSCRYD5P.js +1409 -0
- package/dist/chunk-ZSCRYD5P.js.map +1 -0
- package/dist/content.js +1 -1
- package/dist/docs.json +44 -26
- 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 +52 -26
- package/package.json +1 -1
- package/dist/chunk-WJ2W3DUC.js.map +0 -1
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Client Tools
|
|
3
|
+
description: Handling tool calls on the client side for interactive UI and browser-only operations.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Client Tools
|
|
7
|
+
|
|
8
|
+
By default, tools execute on your server where you have access to databases and APIs. However, some tools are better suited for client-side execution:
|
|
9
|
+
|
|
10
|
+
- **Browser-only operations** — Geolocation, clipboard, local storage
|
|
11
|
+
- **Interactive UIs** — Confirmation dialogs, form inputs, selections
|
|
12
|
+
- **Real-time feedback** — Progress indicators, approval workflows
|
|
13
|
+
|
|
14
|
+
## How Client Tools Work
|
|
15
|
+
|
|
16
|
+
When the agent calls a tool, the Server SDK checks for a registered handler:
|
|
17
|
+
|
|
18
|
+
1. **Handler exists** → Execute on server, continue automatically
|
|
19
|
+
2. **No handler** → Forward to client as `client-tool-request` event
|
|
20
|
+
|
|
21
|
+
The client SDK receives pending tool calls, executes them (automatically or via user interaction), and sends results back to continue the conversation.
|
|
22
|
+
|
|
23
|
+
```mermaid
|
|
24
|
+
sequenceDiagram
|
|
25
|
+
participant LLM
|
|
26
|
+
participant Platform
|
|
27
|
+
participant Server as Server SDK
|
|
28
|
+
participant Client as Client SDK
|
|
29
|
+
participant User
|
|
30
|
+
|
|
31
|
+
LLM->>Platform: Call get-location
|
|
32
|
+
Platform->>Server: tool-request
|
|
33
|
+
Note over Server: No handler for<br/>get-location
|
|
34
|
+
Server->>Client: client-tool-request
|
|
35
|
+
Client->>User: Request permission
|
|
36
|
+
User->>Client: Grant
|
|
37
|
+
Client->>Server: Tool results
|
|
38
|
+
Server->>Platform: Continue with results
|
|
39
|
+
Platform->>LLM: Process results
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Setup
|
|
43
|
+
|
|
44
|
+
### Server Side
|
|
45
|
+
|
|
46
|
+
Define tools in your protocol but don't register handlers for client tools:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Only register server-side tools
|
|
50
|
+
const session = client.agentSessions.attach(sessionId, {
|
|
51
|
+
tools: {
|
|
52
|
+
// Server tools have handlers
|
|
53
|
+
'get-user-account': async (args) => {
|
|
54
|
+
return await db.users.findById(args.userId);
|
|
55
|
+
},
|
|
56
|
+
// Client tools have NO handler here
|
|
57
|
+
// 'get-browser-location' - handled on client
|
|
58
|
+
// 'request-feedback' - handled on client
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Client Side
|
|
64
|
+
|
|
65
|
+
Register client tool handlers when creating the chat:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const { messages, status, pendingClientTools } = useOctavusChat({
|
|
69
|
+
transport,
|
|
70
|
+
clientTools: {
|
|
71
|
+
// Automatic client tool
|
|
72
|
+
'get-browser-location': async () => {
|
|
73
|
+
const pos = await new Promise((resolve, reject) => {
|
|
74
|
+
navigator.geolocation.getCurrentPosition(resolve, reject);
|
|
75
|
+
});
|
|
76
|
+
return { lat: pos.coords.latitude, lng: pos.coords.longitude };
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Interactive client tool (requires user action)
|
|
80
|
+
'request-feedback': 'interactive',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Automatic Client Tools
|
|
86
|
+
|
|
87
|
+
Automatic tools execute immediately when called. Use these for browser operations that don't require user input.
|
|
88
|
+
|
|
89
|
+
### Example: Geolocation
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const { messages, status } = useOctavusChat({
|
|
93
|
+
transport,
|
|
94
|
+
clientTools: {
|
|
95
|
+
'get-browser-location': async (args, ctx) => {
|
|
96
|
+
// ctx provides toolCallId, toolName, and abort signal
|
|
97
|
+
const pos = await new Promise<GeolocationPosition>((resolve, reject) => {
|
|
98
|
+
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
|
99
|
+
timeout: 10000,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
latitude: pos.coords.latitude,
|
|
105
|
+
longitude: pos.coords.longitude,
|
|
106
|
+
accuracy: pos.coords.accuracy,
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Example: Clipboard
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
clientTools: {
|
|
117
|
+
'copy-to-clipboard': async (args) => {
|
|
118
|
+
await navigator.clipboard.writeText(args.text as string);
|
|
119
|
+
return { success: true };
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
'read-clipboard': async () => {
|
|
123
|
+
const text = await navigator.clipboard.readText();
|
|
124
|
+
return { text };
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Context Object
|
|
130
|
+
|
|
131
|
+
Automatic handlers receive a context object:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
interface ClientToolContext {
|
|
135
|
+
toolCallId: string; // Unique ID for this call
|
|
136
|
+
toolName: string; // Name of the tool
|
|
137
|
+
signal: AbortSignal; // Aborted if user stops generation
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use the signal to cancel long-running operations:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
'fetch-external-data': async (args, ctx) => {
|
|
145
|
+
const response = await fetch(args.url, {
|
|
146
|
+
signal: ctx.signal, // Cancels if user stops
|
|
147
|
+
});
|
|
148
|
+
return await response.json();
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Interactive Client Tools
|
|
153
|
+
|
|
154
|
+
Interactive tools require user input before completing. Use these for confirmations, forms, or any UI that needs user action.
|
|
155
|
+
|
|
156
|
+
Mark a tool as interactive by setting its handler to `'interactive'`:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const { messages, status, pendingClientTools } = useOctavusChat({
|
|
160
|
+
transport,
|
|
161
|
+
clientTools: {
|
|
162
|
+
'request-feedback': 'interactive',
|
|
163
|
+
'confirm-action': 'interactive',
|
|
164
|
+
'select-option': 'interactive',
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Accessing Pending Tools
|
|
170
|
+
|
|
171
|
+
Interactive tools appear in `pendingClientTools`, keyed by tool name:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// pendingClientTools structure:
|
|
175
|
+
{
|
|
176
|
+
'request-feedback': [
|
|
177
|
+
{
|
|
178
|
+
toolCallId: 'call_abc123',
|
|
179
|
+
toolName: 'request-feedback',
|
|
180
|
+
args: { question: 'How would you rate this response?' },
|
|
181
|
+
submit: (result) => void, // Call with user's input
|
|
182
|
+
cancel: (reason?) => void, // Call if user dismisses
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
'confirm-action': [
|
|
186
|
+
// Multiple calls to same tool are possible
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Rendering Interactive UIs
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
function Chat() {
|
|
195
|
+
const { messages, status, pendingClientTools, send } = useOctavusChat({
|
|
196
|
+
transport,
|
|
197
|
+
clientTools: {
|
|
198
|
+
'request-feedback': 'interactive',
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const feedbackTools = pendingClientTools['request-feedback'] ?? [];
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
{/* Chat messages */}
|
|
207
|
+
<MessageList messages={messages} />
|
|
208
|
+
|
|
209
|
+
{/* Interactive tool UIs */}
|
|
210
|
+
{feedbackTools.map((tool) => (
|
|
211
|
+
<FeedbackModal
|
|
212
|
+
key={tool.toolCallId}
|
|
213
|
+
question={tool.args.question as string}
|
|
214
|
+
onSubmit={(rating, comment) => {
|
|
215
|
+
tool.submit({ rating, comment });
|
|
216
|
+
}}
|
|
217
|
+
onCancel={() => {
|
|
218
|
+
tool.cancel('User dismissed');
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
|
|
223
|
+
{/* Input disabled while awaiting user action */}
|
|
224
|
+
<ChatInput disabled={status === 'awaiting-input'} />
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Example: Confirmation Dialog
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
function ConfirmationDialog({ tool }: { tool: InteractiveTool }) {
|
|
234
|
+
const { action, description } = tool.args as {
|
|
235
|
+
action: string;
|
|
236
|
+
description: string;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
|
|
241
|
+
<div className="bg-white p-6 rounded-lg max-w-md">
|
|
242
|
+
<h3 className="text-lg font-semibold">Confirm {action}</h3>
|
|
243
|
+
<p className="mt-2 text-gray-600">{description}</p>
|
|
244
|
+
<div className="mt-4 flex gap-3 justify-end">
|
|
245
|
+
<button onClick={() => tool.cancel()} className="px-4 py-2 border rounded">
|
|
246
|
+
Cancel
|
|
247
|
+
</button>
|
|
248
|
+
<button
|
|
249
|
+
onClick={() => tool.submit({ confirmed: true })}
|
|
250
|
+
className="px-4 py-2 bg-blue-500 text-white rounded"
|
|
251
|
+
>
|
|
252
|
+
Confirm
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Example: Form Input
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
function FormInputTool({ tool }: { tool: InteractiveTool }) {
|
|
265
|
+
const [values, setValues] = useState<Record<string, string>>({});
|
|
266
|
+
const fields = tool.args.fields as { name: string; label: string; type: string }[];
|
|
267
|
+
|
|
268
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
tool.submit(values);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<form onSubmit={handleSubmit} className="p-4 border rounded-lg">
|
|
275
|
+
{fields.map((field) => (
|
|
276
|
+
<div key={field.name} className="mb-4">
|
|
277
|
+
<label className="block text-sm font-medium">{field.label}</label>
|
|
278
|
+
<input
|
|
279
|
+
type={field.type}
|
|
280
|
+
value={values[field.name] ?? ''}
|
|
281
|
+
onChange={(e) => setValues({ ...values, [field.name]: e.target.value })}
|
|
282
|
+
className="mt-1 w-full px-3 py-2 border rounded"
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
))}
|
|
286
|
+
<div className="flex gap-2 justify-end">
|
|
287
|
+
<button type="button" onClick={() => tool.cancel()} className="px-4 py-2 border rounded">
|
|
288
|
+
Cancel
|
|
289
|
+
</button>
|
|
290
|
+
<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">
|
|
291
|
+
Submit
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
</form>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Status: awaiting-input
|
|
300
|
+
|
|
301
|
+
When interactive tools are pending, the chat status changes to `'awaiting-input'`:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
type ChatStatus = 'idle' | 'streaming' | 'error' | 'awaiting-input';
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Use this to:
|
|
308
|
+
|
|
309
|
+
- Disable the send button
|
|
310
|
+
- Show "Waiting for input" indicators
|
|
311
|
+
- Prevent new messages until tools complete
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
function ChatInput({ status }: { status: ChatStatus }) {
|
|
315
|
+
const isDisabled = status === 'streaming' || status === 'awaiting-input';
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div>
|
|
319
|
+
{status === 'awaiting-input' && (
|
|
320
|
+
<div className="text-amber-600 text-sm mb-2">
|
|
321
|
+
Please respond to the prompt above to continue
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
<input disabled={isDisabled} placeholder="Type a message..." />
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Mixed Server and Client Tools
|
|
331
|
+
|
|
332
|
+
Tools can be split between server and client based on where they should execute:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// Server (API route)
|
|
336
|
+
const session = client.agentSessions.attach(sessionId, {
|
|
337
|
+
tools: {
|
|
338
|
+
// Server tools - data access, mutations
|
|
339
|
+
'get-user-account': async (args) => db.users.findById(args.userId),
|
|
340
|
+
'create-order': async (args) => orderService.create(args),
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Client
|
|
345
|
+
const chat = useOctavusChat({
|
|
346
|
+
transport,
|
|
347
|
+
clientTools: {
|
|
348
|
+
// Automatic - browser capabilities
|
|
349
|
+
'get-browser-location': async () => getGeolocation(),
|
|
350
|
+
|
|
351
|
+
// Interactive - user confirmation
|
|
352
|
+
'confirm-order': 'interactive',
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
When the LLM calls multiple tools in one turn:
|
|
358
|
+
|
|
359
|
+
1. Server tools execute first on the server
|
|
360
|
+
2. Server results are included in the `client-tool-request` event
|
|
361
|
+
3. Client tools execute (automatic immediately, interactive waits)
|
|
362
|
+
4. All results are sent together to continue
|
|
363
|
+
|
|
364
|
+
## HTTP Transport
|
|
365
|
+
|
|
366
|
+
The HTTP transport handles client tool continuation automatically via a unified request pattern:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
const transport = createHttpTransport({
|
|
370
|
+
// Single request handler for both triggers and continuations
|
|
371
|
+
request: (payload, options) =>
|
|
372
|
+
fetch('/api/trigger', {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
375
|
+
body: JSON.stringify({ sessionId, ...payload }),
|
|
376
|
+
signal: options?.signal,
|
|
377
|
+
}),
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Your API route handles both request types:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// app/api/trigger/route.ts
|
|
385
|
+
export async function POST(request: Request) {
|
|
386
|
+
const body = await request.json();
|
|
387
|
+
const { sessionId, ...payload } = body;
|
|
388
|
+
|
|
389
|
+
const session = client.agentSessions.attach(sessionId, {
|
|
390
|
+
tools: {
|
|
391
|
+
// Server tools only
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// execute() handles both triggers and continuations
|
|
396
|
+
const events = session.execute(payload, { signal: request.signal });
|
|
397
|
+
|
|
398
|
+
return new Response(toSSEStream(events), {
|
|
399
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Socket Transport
|
|
405
|
+
|
|
406
|
+
The socket transport sends a `continue` message with tool results:
|
|
407
|
+
|
|
408
|
+
### Client → Server Messages
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Trigger (start new conversation turn)
|
|
412
|
+
{ type: 'trigger', triggerName: string, input?: object }
|
|
413
|
+
|
|
414
|
+
// Continue (after client tool handling)
|
|
415
|
+
{ type: 'continue', executionId: string, toolResults: ToolResult[] }
|
|
416
|
+
|
|
417
|
+
// Stop (cancel current operation)
|
|
418
|
+
{ type: 'stop' }
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Server Handler
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
async function handleMessage(rawData: string, conn: Connection, session: AgentSession) {
|
|
425
|
+
const msg = JSON.parse(rawData);
|
|
426
|
+
|
|
427
|
+
if (msg.type === 'stop') {
|
|
428
|
+
abortController?.abort();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// execute() handles both trigger and continue
|
|
433
|
+
const events = session.execute(msg, { signal: abortController?.signal });
|
|
434
|
+
|
|
435
|
+
for await (const event of events) {
|
|
436
|
+
conn.write(JSON.stringify(event));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Error Handling
|
|
442
|
+
|
|
443
|
+
### Automatic Tool Errors
|
|
444
|
+
|
|
445
|
+
Errors in automatic handlers are caught and sent back to the LLM:
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
clientTools: {
|
|
449
|
+
'get-browser-location': async () => {
|
|
450
|
+
// If geolocation fails, the error is captured
|
|
451
|
+
const pos = await new Promise((resolve, reject) => {
|
|
452
|
+
navigator.geolocation.getCurrentPosition(resolve, reject);
|
|
453
|
+
});
|
|
454
|
+
return { lat: pos.coords.latitude, lng: pos.coords.longitude };
|
|
455
|
+
},
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
The LLM receives: `"Tool error: User denied geolocation"`
|
|
460
|
+
|
|
461
|
+
### Interactive Tool Cancellation
|
|
462
|
+
|
|
463
|
+
When users cancel interactive tools, provide a reason:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
tool.cancel('User chose not to confirm');
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
The LLM receives the cancellation reason and can respond appropriately.
|
|
470
|
+
|
|
471
|
+
### Missing Handlers
|
|
472
|
+
|
|
473
|
+
If a client tool has no handler registered, an error is sent automatically:
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
"No client handler for tool: some-tool-name"
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Best Practices
|
|
480
|
+
|
|
481
|
+
### 1. Keep Server Tools on Server
|
|
482
|
+
|
|
483
|
+
Don't move database or API operations to client tools:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// Good - data access on server
|
|
487
|
+
// Server:
|
|
488
|
+
tools: { 'get-user': async (args) => db.users.find(args.id) }
|
|
489
|
+
|
|
490
|
+
// Bad - exposing data access to client
|
|
491
|
+
// Client:
|
|
492
|
+
clientTools: { 'get-user': async (args) => fetch('/api/users/' + args.id) }
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### 2. Use Interactive for Confirmations
|
|
496
|
+
|
|
497
|
+
Any destructive or important action should confirm with the user:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
clientTools: {
|
|
501
|
+
'confirm-delete': 'interactive',
|
|
502
|
+
'confirm-purchase': 'interactive',
|
|
503
|
+
'confirm-send-email': 'interactive',
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### 3. Handle Cancellation Gracefully
|
|
508
|
+
|
|
509
|
+
Always provide cancel buttons for interactive tools:
|
|
510
|
+
|
|
511
|
+
```tsx
|
|
512
|
+
<Dialog>
|
|
513
|
+
<button onClick={() => tool.submit(result)}>Confirm</button>
|
|
514
|
+
<button onClick={() => tool.cancel()}>Cancel</button>
|
|
515
|
+
</Dialog>
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### 4. Validate Results
|
|
519
|
+
|
|
520
|
+
Validate user input before submitting:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
const handleSubmit = () => {
|
|
524
|
+
if (!rating || rating < 1 || rating > 5) {
|
|
525
|
+
setError('Please select a rating');
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
tool.submit({ rating });
|
|
529
|
+
};
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## Type Reference
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
// Handler types
|
|
536
|
+
type ClientToolHandler =
|
|
537
|
+
| ((args: Record<string, unknown>, ctx: ClientToolContext) => Promise<unknown>)
|
|
538
|
+
| 'interactive';
|
|
539
|
+
|
|
540
|
+
interface ClientToolContext {
|
|
541
|
+
toolCallId: string;
|
|
542
|
+
toolName: string;
|
|
543
|
+
signal: AbortSignal;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Interactive tool (with bound methods)
|
|
547
|
+
interface InteractiveTool {
|
|
548
|
+
toolCallId: string;
|
|
549
|
+
toolName: string;
|
|
550
|
+
args: Record<string, unknown>;
|
|
551
|
+
submit: (result: unknown) => void;
|
|
552
|
+
cancel: (reason?: string) => void;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Chat status
|
|
556
|
+
type ChatStatus = 'idle' | 'streaming' | 'error' | 'awaiting-input';
|
|
557
|
+
```
|
|
@@ -59,6 +59,18 @@ const sessionId = await client.agentSessions.create('support-chat', {
|
|
|
59
59
|
});
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Inputs can also be used for [dynamic model selection](/docs/protocol/agent-config#dynamic-model-selection):
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
input:
|
|
66
|
+
MODEL:
|
|
67
|
+
type: string
|
|
68
|
+
description: The LLM model to use
|
|
69
|
+
|
|
70
|
+
agent:
|
|
71
|
+
model: MODEL # Resolved from session input
|
|
72
|
+
```
|
|
73
|
+
|
|
62
74
|
In prompts, reference with `{{INPUT_NAME}}`:
|
|
63
75
|
|
|
64
76
|
```markdown
|
|
@@ -89,11 +89,12 @@ function Chat({ sessionId }: { sessionId: string }) {
|
|
|
89
89
|
const transport = useMemo(
|
|
90
90
|
() =>
|
|
91
91
|
createHttpTransport({
|
|
92
|
-
|
|
92
|
+
request: (payload, options) =>
|
|
93
93
|
fetch('/api/trigger', {
|
|
94
94
|
method: 'POST',
|
|
95
95
|
headers: { 'Content-Type': 'application/json' },
|
|
96
|
-
body: JSON.stringify({ sessionId,
|
|
96
|
+
body: JSON.stringify({ sessionId, ...payload }),
|
|
97
|
+
signal: options?.signal,
|
|
97
98
|
}),
|
|
98
99
|
}),
|
|
99
100
|
[sessionId],
|
|
@@ -115,9 +116,11 @@ function Chat({ sessionId }: { sessionId: string }) {
|
|
|
115
116
|
### From Server SDK
|
|
116
117
|
|
|
117
118
|
```typescript
|
|
118
|
-
//
|
|
119
|
-
const events = session.
|
|
120
|
-
|
|
119
|
+
// execute() returns an async generator of events
|
|
120
|
+
const events = session.execute({
|
|
121
|
+
type: 'trigger',
|
|
122
|
+
triggerName: 'user-message',
|
|
123
|
+
input: { USER_MESSAGE: 'Help me with billing' },
|
|
121
124
|
});
|
|
122
125
|
|
|
123
126
|
// Iterate events directly
|
|
@@ -150,6 +150,16 @@ Start summary thread:
|
|
|
150
150
|
input: [COMPANY_NAME] # Variables for prompt
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
The `model` field can also reference a variable for dynamic model selection:
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
Start summary thread:
|
|
157
|
+
block: start-thread
|
|
158
|
+
thread: summary
|
|
159
|
+
model: SUMMARY_MODEL # Resolved from input variable
|
|
160
|
+
system: escalation-summary
|
|
161
|
+
```
|
|
162
|
+
|
|
153
163
|
### serialize-thread
|
|
154
164
|
|
|
155
165
|
Convert conversation to text:
|
|
@@ -21,7 +21,7 @@ agent:
|
|
|
21
21
|
|
|
22
22
|
| Field | Required | Description |
|
|
23
23
|
| ------------- | -------- | --------------------------------------------------------- |
|
|
24
|
-
| `model` | Yes | Model identifier
|
|
24
|
+
| `model` | Yes | Model identifier or variable reference |
|
|
25
25
|
| `system` | Yes | System prompt filename (without .md) |
|
|
26
26
|
| `input` | No | Variables to interpolate in system prompt |
|
|
27
27
|
| `tools` | No | List of tools the LLM can call |
|
|
@@ -67,6 +67,39 @@ agent:
|
|
|
67
67
|
|
|
68
68
|
> **Note**: Model IDs are passed directly to the provider SDK. Check the provider's documentation for the latest available models.
|
|
69
69
|
|
|
70
|
+
### Dynamic Model Selection
|
|
71
|
+
|
|
72
|
+
The model field can also reference an input variable, allowing consumers to choose the model when creating a session:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
input:
|
|
76
|
+
MODEL:
|
|
77
|
+
type: string
|
|
78
|
+
description: The LLM model to use
|
|
79
|
+
|
|
80
|
+
agent:
|
|
81
|
+
model: MODEL # Resolved from session input
|
|
82
|
+
system: system
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
When creating a session, pass the model:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const sessionId = await client.agentSessions.create('my-agent', {
|
|
89
|
+
MODEL: 'anthropic/claude-sonnet-4-5',
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This enables:
|
|
94
|
+
|
|
95
|
+
- **Multi-provider support** — Same agent works with different providers
|
|
96
|
+
- **A/B testing** — Test different models without protocol changes
|
|
97
|
+
- **User preferences** — Let users choose their preferred model
|
|
98
|
+
|
|
99
|
+
The model value is validated at runtime to ensure it's in the correct `provider/model-id` format.
|
|
100
|
+
|
|
101
|
+
> **Note**: When using dynamic models, provider-specific options (like `anthropic:`) may not apply if the model resolves to a different provider.
|
|
102
|
+
|
|
70
103
|
## System Prompt
|
|
71
104
|
|
|
72
105
|
The system prompt sets the agent's persona and instructions:
|
|
@@ -24,6 +24,24 @@ curl -H "Authorization: Bearer YOUR_API_KEY" \
|
|
|
24
24
|
|
|
25
25
|
API keys can be created in the Octavus Platform under your project's **API Keys** page.
|
|
26
26
|
|
|
27
|
+
## API Key Permissions
|
|
28
|
+
|
|
29
|
+
API keys have two permission scopes:
|
|
30
|
+
|
|
31
|
+
| Permission | Description | Used By |
|
|
32
|
+
| ------------ | -------------------------------------------------------- | ---------- |
|
|
33
|
+
| **Sessions** | Create and manage sessions, trigger agents, upload files | Server SDK |
|
|
34
|
+
| **Agents** | Create, update, and validate agent definitions | CLI |
|
|
35
|
+
|
|
36
|
+
Both permissions allow reading agent definitions (needed by CLI for sync and Server SDK for sessions).
|
|
37
|
+
|
|
38
|
+
**Recommended setup:** Use separate API keys for different purposes:
|
|
39
|
+
|
|
40
|
+
- **CLI key** with only "Agents" permission for CI/CD and development
|
|
41
|
+
- **Server key** with only "Sessions" permission for production applications
|
|
42
|
+
|
|
43
|
+
This limits the blast radius if a key is compromised.
|
|
44
|
+
|
|
27
45
|
## Response Format
|
|
28
46
|
|
|
29
47
|
All responses are JSON. Success responses return the data directly (not wrapped in a `data` field).
|
|
@@ -7,6 +7,8 @@ description: Session management API endpoints.
|
|
|
7
7
|
|
|
8
8
|
Sessions represent conversations with agents. They store conversation history, resources, and variables.
|
|
9
9
|
|
|
10
|
+
All session endpoints require an API key with the **Sessions** permission.
|
|
11
|
+
|
|
10
12
|
## Create Session
|
|
11
13
|
|
|
12
14
|
Create a new agent session.
|