buildship-agent 1.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/LICENSE +21 -0
- package/README.md +901 -0
- package/dist/index.cjs +1145 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +221 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.js +1116 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
# super-agent
|
|
2
|
+
|
|
3
|
+
A React library for building AI agent applications with streaming responses, interactive tools, and session management. Works with any AI agent backend that supports Server-Sent Events (SSE).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Real-time Streaming**: Built on Server-Sent Events (SSE) for live agent
|
|
8
|
+
responses
|
|
9
|
+
- **Session Management**: Automatic conversation persistence with localStorage
|
|
10
|
+
- **File Support**: Upload and send files to your agents
|
|
11
|
+
- **Client Tools/Widgets**: Render interactive components from agent responses
|
|
12
|
+
- **Interactive Tools**: Pause agent execution and collect user input with
|
|
13
|
+
`requiresResult` tools
|
|
14
|
+
- **TypeScript**: Full type safety with comprehensive TypeScript definitions
|
|
15
|
+
- **Multi-Agent**: Support for multiple agents in a single application
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install super-agent
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { AgentContextProvider, useAgentContext } from "super-agent";
|
|
27
|
+
|
|
28
|
+
// 1. Wrap your app with the provider
|
|
29
|
+
function App() {
|
|
30
|
+
return (
|
|
31
|
+
<AgentContextProvider>
|
|
32
|
+
<YourApp />
|
|
33
|
+
</AgentContextProvider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Use the agent in your components
|
|
38
|
+
function ChatComponent() {
|
|
39
|
+
const agent = useAgentContext(
|
|
40
|
+
"your-agent-id",
|
|
41
|
+
"https://your-agent-url.com",
|
|
42
|
+
"your-access-key",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const handleSend = () => {
|
|
46
|
+
agent.handleSend("Hello, agent!");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div>
|
|
51
|
+
{agent.messages.map((msg, idx) => (
|
|
52
|
+
<div key={idx}>
|
|
53
|
+
<strong>{msg.role}:</strong> {msg.content}
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
<button onClick={handleSend} disabled={agent.inProgress}>
|
|
57
|
+
Send Message
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Setup
|
|
65
|
+
|
|
66
|
+
### AgentContextProvider
|
|
67
|
+
|
|
68
|
+
The `AgentContextProvider` must wrap your application to enable agent
|
|
69
|
+
functionality. It manages:
|
|
70
|
+
|
|
71
|
+
- Global session state across all agents
|
|
72
|
+
- Automatic localStorage persistence
|
|
73
|
+
- Agent runner registry
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { AgentContextProvider } from "super-agent";
|
|
77
|
+
|
|
78
|
+
function App() {
|
|
79
|
+
return (
|
|
80
|
+
<AgentContextProvider>{/* Your app components */}</AgentContextProvider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Basic Usage
|
|
86
|
+
|
|
87
|
+
### Using the Agent Hook
|
|
88
|
+
|
|
89
|
+
The `useAgentContext` hook is the primary interface for interacting with agents:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useAgentContext } from "super-agent";
|
|
93
|
+
|
|
94
|
+
function AgentChat() {
|
|
95
|
+
const agent = useAgentContext(
|
|
96
|
+
"my-agent-id", // Unique identifier for your agent
|
|
97
|
+
"https://agent-url.com", // Your agent's endpoint URL
|
|
98
|
+
"your-access-key", // Access key for authenticated requests (sent as Bearer token)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Send a message
|
|
102
|
+
const sendMessage = async () => {
|
|
103
|
+
await agent.handleSend("What's the weather today?");
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Display messages
|
|
107
|
+
return (
|
|
108
|
+
<div>
|
|
109
|
+
{agent.messages.map((message, idx) => (
|
|
110
|
+
<div key={idx}>
|
|
111
|
+
<strong>{message.role === "user" ? "You" : "Agent"}:</strong>
|
|
112
|
+
<p>{message.content}</p>
|
|
113
|
+
</div>
|
|
114
|
+
))}
|
|
115
|
+
|
|
116
|
+
{agent.inProgress && <div>Agent is thinking...</div>}
|
|
117
|
+
|
|
118
|
+
<button onClick={sendMessage} disabled={agent.inProgress}>
|
|
119
|
+
Send
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### AgentRunner API
|
|
127
|
+
|
|
128
|
+
The `useAgentContext` hook returns an `AgentRunner` object with:
|
|
129
|
+
|
|
130
|
+
| Property | Type | Description |
|
|
131
|
+
| ---------------------- | ------------------------------- | ------------------------------------------- |
|
|
132
|
+
| `messages` | `Message[]` | Array of conversation messages |
|
|
133
|
+
| `inProgress` | `boolean` | Whether the agent is currently processing |
|
|
134
|
+
| `isPaused` | `boolean` | Whether agent is paused waiting for results |
|
|
135
|
+
| `pendingToolCalls` | `PendingToolCall[]` | Tools waiting for user input |
|
|
136
|
+
| `sessionId` | `string` | Current conversation session ID |
|
|
137
|
+
| `sessions` | `Session[]` | All sessions for this agent |
|
|
138
|
+
| `debugData` | `Record<string, DebugDataType>` | Debug information indexed by executionId |
|
|
139
|
+
| `handleSend` | `Function` | Send a message to the agent |
|
|
140
|
+
| `submitToolResults` | `Function` | Submit results from interactive tools |
|
|
141
|
+
| `switchSession` | `Function` | Switch to a different session |
|
|
142
|
+
| `deleteSession` | `Function` | Delete a session |
|
|
143
|
+
| `addOptimisticMessage` | `Function` | Add message to UI without sending |
|
|
144
|
+
| `abort` | `Function` | Cancel current agent execution |
|
|
145
|
+
|
|
146
|
+
### Session Management
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
function SessionList() {
|
|
150
|
+
const agent = useAgentContext("agent-id", "agent-url", "access-key");
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
{agent.sessions.map((session) => (
|
|
155
|
+
<div key={session.id}>
|
|
156
|
+
<button onClick={() => agent.switchSession(session.id)}>
|
|
157
|
+
{session.name || "Untitled Session"}
|
|
158
|
+
</button>
|
|
159
|
+
<button onClick={() => agent.deleteSession(session.id)}>
|
|
160
|
+
Delete
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
|
|
165
|
+
{/* Create new session */}
|
|
166
|
+
<button onClick={() => agent.switchSession()}>New Session</button>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## File Handling
|
|
173
|
+
|
|
174
|
+
To send files to your agent:
|
|
175
|
+
|
|
176
|
+
### 1. Upload Files to Storage
|
|
177
|
+
|
|
178
|
+
First, upload files to a publicly accessible URL (e.g., Firebase Storage, AWS
|
|
179
|
+
S3):
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
async function uploadFiles(files: File[]): Promise<Record<string, string>> {
|
|
183
|
+
// Upload to your storage provider
|
|
184
|
+
const fileMap: Record<string, string> = {};
|
|
185
|
+
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
const url = await uploadToStorage(file); // Your upload logic
|
|
188
|
+
const fileId = file.name.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
|
|
189
|
+
fileMap[fileId] = url;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return fileMap;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 2. Send Files with Message
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
function FileUpload() {
|
|
200
|
+
const agent = useAgentContext("agent-id", "agent-url", "access-key");
|
|
201
|
+
const [files, setFiles] = useState<File[]>([]);
|
|
202
|
+
|
|
203
|
+
const handleSendWithFiles = async () => {
|
|
204
|
+
// Upload files and get URL mapping
|
|
205
|
+
const fileMap = await uploadFiles(files);
|
|
206
|
+
|
|
207
|
+
// Create input with file references
|
|
208
|
+
const fileIds = Object.keys(fileMap).join(", ");
|
|
209
|
+
const input = `Analyze these files: ${fileIds}`;
|
|
210
|
+
|
|
211
|
+
// Send to agent with file context
|
|
212
|
+
await agent.handleSend(input, {
|
|
213
|
+
context: {
|
|
214
|
+
mapped_file_ids_with_url: fileMap,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div>
|
|
221
|
+
<input
|
|
222
|
+
type="file"
|
|
223
|
+
multiple
|
|
224
|
+
onChange={(e) => setFiles(Array.from(e.target.files || []))}
|
|
225
|
+
/>
|
|
226
|
+
<button onClick={handleSendWithFiles}>Send with Files</button>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### File Context Structure
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
{
|
|
236
|
+
context: {
|
|
237
|
+
mapped_file_ids_with_url: {
|
|
238
|
+
"file_name_pdf": "https://storage.url/file1.pdf",
|
|
239
|
+
"image_png": "https://storage.url/image.png"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Client Tools / Widgets
|
|
246
|
+
|
|
247
|
+
Client tools allow your agent to render interactive widgets directly in the chat
|
|
248
|
+
interface. There are two types of client tools:
|
|
249
|
+
|
|
250
|
+
- **Fire-and-forget tools** (default): Agent calls the tool and continues
|
|
251
|
+
immediately without waiting for a result
|
|
252
|
+
- **Interactive tools** (`requiresResult: true`): Agent pauses execution and
|
|
253
|
+
waits for user input before continuing
|
|
254
|
+
|
|
255
|
+
### 1. Define Tool Configuration
|
|
256
|
+
|
|
257
|
+
Create tool configurations with Zod schemas:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { z } from "zod";
|
|
261
|
+
|
|
262
|
+
export const chartToolConfig = {
|
|
263
|
+
name: "render_chart",
|
|
264
|
+
description: "Renders a bar chart with the provided data",
|
|
265
|
+
schema: z.object({
|
|
266
|
+
title: z.string().describe("Chart title"),
|
|
267
|
+
data: z
|
|
268
|
+
.array(
|
|
269
|
+
z.object({
|
|
270
|
+
label: z.string(),
|
|
271
|
+
value: z.number(),
|
|
272
|
+
}),
|
|
273
|
+
)
|
|
274
|
+
.describe("Data points for the chart"),
|
|
275
|
+
}),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Create your widget component
|
|
279
|
+
export function ChartWidget({ title, data }) {
|
|
280
|
+
return (
|
|
281
|
+
<div>
|
|
282
|
+
<h3>{title}</h3>
|
|
283
|
+
{/* Your chart rendering logic */}
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 2. Create Widget Registry
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
import type { ComponentType } from "react";
|
|
293
|
+
import type { ClientToolDefinition } from "super-agent";
|
|
294
|
+
import { z } from "zod";
|
|
295
|
+
|
|
296
|
+
// Import all your widgets
|
|
297
|
+
import { ChartWidget, chartToolConfig } from "./widgets/chart";
|
|
298
|
+
import { MapWidget, mapToolConfig } from "./widgets/map";
|
|
299
|
+
|
|
300
|
+
const allConfigs = [chartToolConfig, mapToolConfig];
|
|
301
|
+
|
|
302
|
+
// Registry for rendering widgets
|
|
303
|
+
export const widgetRegistry: Record<string, ComponentType<any>> = {
|
|
304
|
+
[chartToolConfig.name]: ChartWidget,
|
|
305
|
+
[mapToolConfig.name]: MapWidget,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Convert to agent tool definitions
|
|
309
|
+
export function getToolDefinitions(): ClientToolDefinition[] {
|
|
310
|
+
return allConfigs.map((config) => {
|
|
311
|
+
const parameters = z.toJSONSchema(config.schema);
|
|
312
|
+
|
|
313
|
+
// Remove $schema property for LLM compatibility
|
|
314
|
+
if (
|
|
315
|
+
parameters &&
|
|
316
|
+
typeof parameters === "object" &&
|
|
317
|
+
"$schema" in parameters
|
|
318
|
+
) {
|
|
319
|
+
const { $schema, ...rest } = parameters as any;
|
|
320
|
+
return {
|
|
321
|
+
name: config.name,
|
|
322
|
+
description: config.description,
|
|
323
|
+
parameters: rest,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
name: config.name,
|
|
329
|
+
description: config.description,
|
|
330
|
+
parameters,
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 3. Pass Tools to Agent
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import { getToolDefinitions } from "./widget-registry";
|
|
340
|
+
|
|
341
|
+
function ChatWithWidgets() {
|
|
342
|
+
const agent = useAgentContext("agent-id", "agent-url", "access-key");
|
|
343
|
+
const tools = getToolDefinitions();
|
|
344
|
+
|
|
345
|
+
const handleSend = async (input: string) => {
|
|
346
|
+
await agent.handleSend(input, {
|
|
347
|
+
clientTools: tools,
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div>
|
|
353
|
+
<button onClick={() => handleSend("Show me a chart")}>
|
|
354
|
+
Ask for Chart
|
|
355
|
+
</button>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### 4. Render Widgets in Messages
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
import { widgetRegistry } from "./widget-registry";
|
|
365
|
+
|
|
366
|
+
function MessageDisplay() {
|
|
367
|
+
const agent = useAgentContext("agent-id", "agent-url", "access-key");
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<div>
|
|
371
|
+
{agent.messages.map((message) => (
|
|
372
|
+
<div key={message.executionId}>
|
|
373
|
+
{/* Render message parts (text + widgets) */}
|
|
374
|
+
{message.parts?.map((part, idx) => {
|
|
375
|
+
if (part.type === "text") {
|
|
376
|
+
return <p key={idx}>{part.text}</p>;
|
|
377
|
+
} else if (part.type === "widget") {
|
|
378
|
+
const Widget = widgetRegistry[part.toolName];
|
|
379
|
+
if (!Widget) return null;
|
|
380
|
+
return <Widget key={idx} {...part.inputs} />;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
})}
|
|
384
|
+
|
|
385
|
+
{/* Fallback: render plain content if no parts */}
|
|
386
|
+
{!message.parts && <p>{message.content}</p>}
|
|
387
|
+
</div>
|
|
388
|
+
))}
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Interactive Client Tools (Pause & Resume)
|
|
395
|
+
|
|
396
|
+
Interactive client tools allow agents to pause execution and wait for user input
|
|
397
|
+
before continuing. This is useful for scenarios like:
|
|
398
|
+
|
|
399
|
+
- Flight/hotel selection from search results
|
|
400
|
+
- Form inputs and confirmations
|
|
401
|
+
- Date/time pickers
|
|
402
|
+
- File selection
|
|
403
|
+
- Custom decision points
|
|
404
|
+
|
|
405
|
+
### How It Works
|
|
406
|
+
|
|
407
|
+
1. Agent calls a tool with `requiresResult: true`
|
|
408
|
+
2. Agent execution **pauses** (`isPaused` becomes `true`)
|
|
409
|
+
3. Frontend renders UI and collects user input
|
|
410
|
+
4. Frontend calls `submitToolResults()` with user's selection
|
|
411
|
+
5. Agent **resumes** execution with the result and continues
|
|
412
|
+
|
|
413
|
+
### Complete Example: Flight Booking
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
import { useState, useEffect } from "react";
|
|
417
|
+
import { useAgentContext } from "super-agent";
|
|
418
|
+
import type { ClientToolDefinition } from "super-agent";
|
|
419
|
+
|
|
420
|
+
// Define interactive tool
|
|
421
|
+
const FLIGHT_PICKER_TOOL: ClientToolDefinition = {
|
|
422
|
+
name: "show_flight_picker",
|
|
423
|
+
description: "Show flight picker UI and get user's flight selection",
|
|
424
|
+
parameters: {
|
|
425
|
+
type: "object",
|
|
426
|
+
properties: {
|
|
427
|
+
origin: { type: "string", description: "Origin airport code" },
|
|
428
|
+
destination: { type: "string", description: "Destination airport code" },
|
|
429
|
+
date: { type: "string", description: "Travel date" },
|
|
430
|
+
flights: {
|
|
431
|
+
type: "array",
|
|
432
|
+
description: "Available flights",
|
|
433
|
+
items: {
|
|
434
|
+
type: "object",
|
|
435
|
+
properties: {
|
|
436
|
+
flightNumber: { type: "string" },
|
|
437
|
+
airline: { type: "string" },
|
|
438
|
+
price: { type: "number" },
|
|
439
|
+
departureTime: { type: "string" },
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
required: ["origin", "destination", "flights"],
|
|
445
|
+
},
|
|
446
|
+
requiresResult: true, // ✅ Agent will pause and wait
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Flight picker component
|
|
450
|
+
function FlightPickerModal({ flights, onSelect, onCancel }) {
|
|
451
|
+
return (
|
|
452
|
+
<div className="modal">
|
|
453
|
+
<h3>Select a Flight</h3>
|
|
454
|
+
{flights.map((flight) => (
|
|
455
|
+
<div key={flight.flightNumber} className="flight-option">
|
|
456
|
+
<div>
|
|
457
|
+
{flight.airline} {flight.flightNumber}
|
|
458
|
+
</div>
|
|
459
|
+
<div>Departure: {flight.departureTime}</div>
|
|
460
|
+
<div>${flight.price}</div>
|
|
461
|
+
<button onClick={() => onSelect(flight)}>Select</button>
|
|
462
|
+
</div>
|
|
463
|
+
))}
|
|
464
|
+
<button onClick={onCancel}>Cancel</button>
|
|
465
|
+
</div>
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Main chat component
|
|
470
|
+
function FlightBookingChat() {
|
|
471
|
+
const agent = useAgentContext(
|
|
472
|
+
"flight-agent",
|
|
473
|
+
"https://api.example.com/executeAgent/flight-agent",
|
|
474
|
+
"your-access-key",
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const [showFlightPicker, setShowFlightPicker] = useState(false);
|
|
478
|
+
const [currentToolCall, setCurrentToolCall] = useState(null);
|
|
479
|
+
|
|
480
|
+
// Monitor for pending tool calls
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
if (agent.isPaused && agent.pendingToolCalls.length > 0) {
|
|
483
|
+
const toolCall = agent.pendingToolCalls[0];
|
|
484
|
+
|
|
485
|
+
if (toolCall.toolName === "show_flight_picker") {
|
|
486
|
+
setCurrentToolCall(toolCall);
|
|
487
|
+
setShowFlightPicker(true);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}, [agent.isPaused, agent.pendingToolCalls]);
|
|
491
|
+
|
|
492
|
+
const handleFlightSelect = async (selectedFlight) => {
|
|
493
|
+
if (!currentToolCall) return;
|
|
494
|
+
|
|
495
|
+
// Submit the result back to the agent
|
|
496
|
+
await agent.submitToolResults([
|
|
497
|
+
{
|
|
498
|
+
callId: currentToolCall.callId,
|
|
499
|
+
result: selectedFlight,
|
|
500
|
+
},
|
|
501
|
+
]);
|
|
502
|
+
|
|
503
|
+
// Close the modal
|
|
504
|
+
setShowFlightPicker(false);
|
|
505
|
+
setCurrentToolCall(null);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const handleCancel = async () => {
|
|
509
|
+
if (!currentToolCall) return;
|
|
510
|
+
|
|
511
|
+
// Submit null/cancel result
|
|
512
|
+
await agent.submitToolResults([
|
|
513
|
+
{
|
|
514
|
+
callId: currentToolCall.callId,
|
|
515
|
+
result: { cancelled: true },
|
|
516
|
+
},
|
|
517
|
+
]);
|
|
518
|
+
|
|
519
|
+
setShowFlightPicker(false);
|
|
520
|
+
setCurrentToolCall(null);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const handleSendMessage = async (input: string) => {
|
|
524
|
+
await agent.handleSend(input, {
|
|
525
|
+
clientTools: [FLIGHT_PICKER_TOOL],
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<div>
|
|
531
|
+
{/* Messages */}
|
|
532
|
+
<div className="messages">
|
|
533
|
+
{agent.messages.map((msg, idx) => (
|
|
534
|
+
<div key={idx} className={msg.role}>
|
|
535
|
+
{msg.content}
|
|
536
|
+
</div>
|
|
537
|
+
))}
|
|
538
|
+
|
|
539
|
+
{agent.isPaused && <div className="status">Waiting for input...</div>}
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
{/* Flight Picker Modal */}
|
|
543
|
+
{showFlightPicker && currentToolCall && (
|
|
544
|
+
<FlightPickerModal
|
|
545
|
+
flights={currentToolCall.inputs.flights}
|
|
546
|
+
onSelect={handleFlightSelect}
|
|
547
|
+
onCancel={handleCancel}
|
|
548
|
+
/>
|
|
549
|
+
)}
|
|
550
|
+
|
|
551
|
+
{/* Input */}
|
|
552
|
+
<input
|
|
553
|
+
type="text"
|
|
554
|
+
placeholder="Ask to book a flight..."
|
|
555
|
+
onKeyPress={(e) => {
|
|
556
|
+
if (e.key === "Enter") {
|
|
557
|
+
handleSendMessage(e.currentTarget.value);
|
|
558
|
+
e.currentTarget.value = "";
|
|
559
|
+
}
|
|
560
|
+
}}
|
|
561
|
+
disabled={agent.inProgress}
|
|
562
|
+
/>
|
|
563
|
+
</div>
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### submitToolResults Options
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
agent.submitToolResults(
|
|
572
|
+
toolResults: ClientToolResult[],
|
|
573
|
+
options?: {
|
|
574
|
+
input?: string; // Optional follow-up message
|
|
575
|
+
context?: object; // Additional context
|
|
576
|
+
additionalHeaders?: Record<string, string>;
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Example: Multiple Tool Results
|
|
582
|
+
|
|
583
|
+
If the agent calls multiple tools that require results:
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
function MultiToolHandler() {
|
|
587
|
+
const agent = useAgentContext("agent-id", "agent-url", "access-key");
|
|
588
|
+
|
|
589
|
+
const handleSubmitAll = async () => {
|
|
590
|
+
// Submit results for all pending tools at once
|
|
591
|
+
const results = agent.pendingToolCalls.map((toolCall) => ({
|
|
592
|
+
callId: toolCall.callId,
|
|
593
|
+
result: getResultForTool(toolCall.toolName, toolCall.inputs),
|
|
594
|
+
}));
|
|
595
|
+
|
|
596
|
+
await agent.submitToolResults(results);
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
<div>
|
|
601
|
+
{agent.isPaused && (
|
|
602
|
+
<div>
|
|
603
|
+
<h3>Pending Actions ({agent.pendingToolCalls.length})</h3>
|
|
604
|
+
{agent.pendingToolCalls.map((toolCall) => (
|
|
605
|
+
<div key={toolCall.callId}>
|
|
606
|
+
<strong>{toolCall.toolName}</strong>
|
|
607
|
+
<pre>{JSON.stringify(toolCall.inputs, null, 2)}</pre>
|
|
608
|
+
</div>
|
|
609
|
+
))}
|
|
610
|
+
<button onClick={handleSubmitAll}>Submit All</button>
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Example: Resume with Follow-up Message
|
|
619
|
+
|
|
620
|
+
```tsx
|
|
621
|
+
const handleSubmitWithMessage = async (result: any) => {
|
|
622
|
+
await agent.submitToolResults(
|
|
623
|
+
[{ callId: currentToolCall.callId, result }],
|
|
624
|
+
{
|
|
625
|
+
input: "Great! Now book the hotel too.", // Optional follow-up
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
};
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Fire-and-Forget Tools (No Pause)
|
|
632
|
+
|
|
633
|
+
For tools that don't need user input, simply omit `requiresResult`:
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
const NOTIFICATION_TOOL: ClientToolDefinition = {
|
|
637
|
+
name: "show_notification",
|
|
638
|
+
description: "Display a notification to the user",
|
|
639
|
+
parameters: {
|
|
640
|
+
type: "object",
|
|
641
|
+
properties: {
|
|
642
|
+
message: { type: "string" },
|
|
643
|
+
type: { type: "string", enum: ["info", "success", "warning", "error"] },
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
// No requiresResult - agent continues immediately
|
|
647
|
+
};
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Best Practices for Interactive Tools
|
|
651
|
+
|
|
652
|
+
1. **Clear Tool Descriptions**: Help the LLM understand when to use the tool
|
|
653
|
+
|
|
654
|
+
```tsx
|
|
655
|
+
{
|
|
656
|
+
name: "show_flight_picker",
|
|
657
|
+
description: "Show a flight selection UI when the user asks to book or search for flights. Use this AFTER searching for available flights.",
|
|
658
|
+
// ...
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
2. **Handle Cancellation**: Always provide a way for users to cancel
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
await agent.submitToolResults([
|
|
666
|
+
{
|
|
667
|
+
callId: toolCall.callId,
|
|
668
|
+
result: { cancelled: true, reason: "User cancelled" },
|
|
669
|
+
},
|
|
670
|
+
]);
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
3. **Loading States**: Show appropriate UI while paused
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
{
|
|
677
|
+
agent.isPaused && (
|
|
678
|
+
<div className="loading">Waiting for your selection...</div>
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
4. **Error Handling**: Wrap submitToolResults in try-catch
|
|
684
|
+
|
|
685
|
+
```tsx
|
|
686
|
+
try {
|
|
687
|
+
await agent.submitToolResults([{ callId, result }]);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
console.error("Failed to submit tool result:", error);
|
|
690
|
+
// Show error to user
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
5. **Type Safety**: Type your tool inputs and results
|
|
695
|
+
```tsx
|
|
696
|
+
type FlightPickerInputs = {
|
|
697
|
+
origin: string;
|
|
698
|
+
destination: string;
|
|
699
|
+
flights: Flight[];
|
|
700
|
+
};
|
|
701
|
+
type FlightPickerResult = Flight | { cancelled: true };
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
## API Reference
|
|
705
|
+
|
|
706
|
+
### handleSend Options
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
agent.handleSend(input: string, options?: {
|
|
710
|
+
// Custom context data for the agent
|
|
711
|
+
context?: Record<string, unknown>;
|
|
712
|
+
|
|
713
|
+
// Don't add user message to UI (for optimistic updates)
|
|
714
|
+
skipUserMessage?: boolean;
|
|
715
|
+
|
|
716
|
+
// Additional HTTP headers
|
|
717
|
+
additionalHeaders?: Record<string, string>;
|
|
718
|
+
|
|
719
|
+
// Server-side tool definitions
|
|
720
|
+
tools?: ClientToolDefinition[];
|
|
721
|
+
|
|
722
|
+
// Client-side widget definitions
|
|
723
|
+
clientTools?: ClientToolDefinition[];
|
|
724
|
+
})
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Types
|
|
728
|
+
|
|
729
|
+
#### Message
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
type Message = {
|
|
733
|
+
role: "user" | "agent";
|
|
734
|
+
content: string; // Full text content
|
|
735
|
+
parts?: MessagePart[]; // Structured parts (text + widgets)
|
|
736
|
+
executionId?: string; // Links to debug data
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
type MessagePart =
|
|
740
|
+
| { type: "text"; text: string; firstSequence: number; lastSequence: number }
|
|
741
|
+
| {
|
|
742
|
+
type: "widget";
|
|
743
|
+
toolName: string;
|
|
744
|
+
callId: string;
|
|
745
|
+
inputs: any;
|
|
746
|
+
sequence: number;
|
|
747
|
+
};
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
#### Session
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
type Session = {
|
|
754
|
+
id: string;
|
|
755
|
+
createdAt: number;
|
|
756
|
+
updatedAt: number;
|
|
757
|
+
messages: Message[];
|
|
758
|
+
name?: string;
|
|
759
|
+
};
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
#### ClientToolDefinition
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
type ClientToolDefinition = {
|
|
766
|
+
name: string; // Unique tool identifier
|
|
767
|
+
description: string; // Human-readable description for LLM
|
|
768
|
+
parameters: unknown; // JSON Schema object
|
|
769
|
+
requiresResult?: boolean; // If true, agent pauses and waits for user input
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
type ClientToolResult = {
|
|
773
|
+
callId: string; // ID from the client_tool_call event
|
|
774
|
+
result: any; // User's input or selection
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
type PendingToolCall = {
|
|
778
|
+
toolName: string;
|
|
779
|
+
callId: string;
|
|
780
|
+
inputs: any;
|
|
781
|
+
requiresResult: boolean;
|
|
782
|
+
};
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
## Advanced Features
|
|
786
|
+
|
|
787
|
+
### Debug Data
|
|
788
|
+
|
|
789
|
+
Access detailed execution information:
|
|
790
|
+
|
|
791
|
+
```tsx
|
|
792
|
+
function DebugView() {
|
|
793
|
+
const agent = useAgentContext("agent-id", "agent-url", "access-key");
|
|
794
|
+
|
|
795
|
+
return (
|
|
796
|
+
<div>
|
|
797
|
+
{Object.entries(agent.debugData).map(([executionId, debug]) => (
|
|
798
|
+
<details key={executionId}>
|
|
799
|
+
<summary>Execution {executionId}</summary>
|
|
800
|
+
<pre>{JSON.stringify(debug, null, 2)}</pre>
|
|
801
|
+
</details>
|
|
802
|
+
))}
|
|
803
|
+
</div>
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Custom Headers
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
await agent.handleSend("message", {
|
|
812
|
+
additionalHeaders: {
|
|
813
|
+
"X-Custom-Header": "value",
|
|
814
|
+
Authorization: "Bearer token",
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Abort Requests
|
|
820
|
+
|
|
821
|
+
```tsx
|
|
822
|
+
function CancellableRequest() {
|
|
823
|
+
const agent = useAgentContext("agent-id", "agent-url", "access-key");
|
|
824
|
+
|
|
825
|
+
return (
|
|
826
|
+
<div>
|
|
827
|
+
<button onClick={() => agent.handleSend("Long task...")}>Start</button>
|
|
828
|
+
<button onClick={() => agent.abort()} disabled={!agent.inProgress}>
|
|
829
|
+
Cancel
|
|
830
|
+
</button>
|
|
831
|
+
</div>
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### Optimistic Updates
|
|
837
|
+
|
|
838
|
+
```tsx
|
|
839
|
+
function OptimisticMessage() {
|
|
840
|
+
const agent = useAgentContext("agent-id", "agent-url", "access-key");
|
|
841
|
+
|
|
842
|
+
const handleSend = async (input: string) => {
|
|
843
|
+
// Add message to UI immediately
|
|
844
|
+
agent.addOptimisticMessage(input);
|
|
845
|
+
|
|
846
|
+
// Send to agent without adding duplicate
|
|
847
|
+
await agent.handleSend(input, { skipUserMessage: true });
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
return <button onClick={() => handleSend("Hello")}>Send</button>;
|
|
851
|
+
}
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
## Storage and Persistence
|
|
855
|
+
|
|
856
|
+
The library automatically persists conversations to localStorage:
|
|
857
|
+
|
|
858
|
+
- **Sessions**: Stored under `buildship:agent:conversations`
|
|
859
|
+
- **Debug Data**: Stored under `buildship:agent:debug`
|
|
860
|
+
|
|
861
|
+
Sessions are automatically synced across browser tabs and survive page
|
|
862
|
+
refreshes.
|
|
863
|
+
|
|
864
|
+
## TypeScript Support
|
|
865
|
+
|
|
866
|
+
The package is written in TypeScript and exports all types:
|
|
867
|
+
|
|
868
|
+
```tsx
|
|
869
|
+
import type {
|
|
870
|
+
Message,
|
|
871
|
+
Session,
|
|
872
|
+
ClientToolDefinition,
|
|
873
|
+
ClientToolResult,
|
|
874
|
+
PendingToolCall,
|
|
875
|
+
AgentRunner,
|
|
876
|
+
DebugDataType,
|
|
877
|
+
MessagePart,
|
|
878
|
+
} from "super-agent";
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
## Best Practices
|
|
882
|
+
|
|
883
|
+
1. **Single Provider**: Only use one `AgentContextProvider` at the root of your
|
|
884
|
+
app
|
|
885
|
+
2. **Tool Descriptions**: Write clear, detailed descriptions for client tools to
|
|
886
|
+
help the LLM understand when to use them
|
|
887
|
+
3. **Interactive Tools**: Use `requiresResult: true` only when you truly need to
|
|
888
|
+
pause execution for user input. For simple notifications or displays, use
|
|
889
|
+
fire-and-forget tools
|
|
890
|
+
4. **File URLs**: Ensure file URLs are publicly accessible or use signed URLs
|
|
891
|
+
with sufficient expiration
|
|
892
|
+
5. **Error Handling**: Wrap `handleSend` and `submitToolResults` calls in
|
|
893
|
+
try-catch blocks for error handling
|
|
894
|
+
6. **Widget Registry**: Keep your widget registry centralized for easier
|
|
895
|
+
maintenance
|
|
896
|
+
7. **Pending Tool Management**: Always handle `isPaused` state and provide clear
|
|
897
|
+
UI for users when the agent is waiting for input
|
|
898
|
+
|
|
899
|
+
## License
|
|
900
|
+
|
|
901
|
+
MIT
|