bs-agent 0.0.9 → 0.0.12

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/README.md CHANGED
@@ -1,420 +1,443 @@
1
1
  # @buildship/agent
2
2
 
3
- A React library for integrating BuildShip AI agents into your frontend
4
- applications with support for streaming responses, file handling, and
5
- client-side widgets.
6
-
7
- ## Features
8
-
9
- - **Real-time Streaming**: Built on Server-Sent Events (SSE) for live agent
10
- responses
11
- - **Session Management**: Automatic conversation persistence with localStorage
12
- - **File Support**: Upload and send files to your agents
13
- - **Client Tools/Widgets**: Render interactive components from agent responses
14
- - **TypeScript**: Full type safety with comprehensive TypeScript definitions
15
- - **Multi-Agent**: Support for multiple agents in a single application
16
-
17
- ## Installation
3
+ A type-safe TypeScript SDK for [BuildShip](https://buildship.com) agents with
4
+ streaming support.
5
+
6
+ - 🔄 **Streaming-first** — real-time text, reasoning, tool calls & handoffs via
7
+ SSE
8
+ - 🧩 **Client tools** — headless handlers, interactive widgets, pause/resume
9
+ - ⚛️ **React bindings** hooks & context for chat UIs with session management
10
+ - 💬 **Multi-turn** — session-based conversations with persistent history
11
+ - 🛑 **Abort** cancel any streaming request mid-flight
12
+ - 🐛 **Debug data** structured debug entries for every tool call, reasoning
13
+ step & handoff
14
+ - 📦 **Zero extra deps** native `fetch` + `ReadableStream`, only `zod` as a
15
+ dependency
16
+
17
+ ## Install
18
18
 
19
19
  ```bash
20
20
  npm install @buildship/agent
21
21
  ```
22
22
 
23
+ The package exposes two entry points:
24
+
25
+ ```ts
26
+ import { ... } from "@buildship/agent/core"; // Vanilla JS/TS — works anywhere
27
+ import { ... } from "@buildship/agent/react"; // React hooks, context & components
28
+ ```
29
+
30
+ ---
31
+
32
+ # Core (`@buildship/agent/core`)
33
+
34
+ The core module provides a class-based API that works in any JavaScript
35
+ environment — Node.js, browser, Edge Runtime, etc.
36
+
23
37
  ## Quick Start
24
38
 
25
- ```tsx
26
- import { AgentContextProvider, useAgentContext } from "@buildship/agent";
39
+ ```ts
40
+ import { BuildShipAgent, z } from "@buildship/agent/core";
27
41
 
28
- // 1. Wrap your app with the provider
29
- function App() {
30
- return (
31
- <AgentContextProvider>
32
- <YourApp />
33
- </AgentContextProvider>
34
- );
35
- }
42
+ const agent = new BuildShipAgent({
43
+ agentId: "YOUR_AGENT_ID",
44
+ accessKey: "YOUR_ACCESS_KEY", // optional
45
+ baseUrl: "https://your-project.buildship.run",
46
+ });
36
47
 
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
- );
48
+ // Simple one-shot
49
+ const session = await agent.execute("Hello!", {
50
+ onText: (text) => process.stdout.write(text),
51
+ onComplete: (fullText) => console.log("\nDone:", fullText),
52
+ onError: (err) => console.error(err),
53
+ });
54
+ ```
44
55
 
45
- const handleSend = () => {
46
- agent.handleSend("Hello, agent!");
47
- };
56
+ ## Multi-Turn Conversations
48
57
 
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
- }
58
+ ```ts
59
+ // First message returns a session
60
+ const session = await agent.execute("What is 2 + 2?", {
61
+ onText: (t) => process.stdout.write(t),
62
+ onComplete: () => console.log(),
63
+ });
64
+
65
+ // Continue with the same session ID
66
+ const continued = agent.session(session.getSessionId());
67
+ await continued.execute("Now multiply that by 3", {
68
+ onText: (t) => process.stdout.write(t),
69
+ onComplete: () => console.log(),
70
+ });
62
71
  ```
63
72
 
64
- ## Setup
73
+ ## Client Tools
65
74
 
66
- ### AgentContextProvider
75
+ Define tools the agent can invoke on the client side. Use **Zod schemas** for
76
+ type-safe parameter definitions:
67
77
 
68
- The `AgentContextProvider` must wrap your application to enable agent
69
- functionality. It manages:
78
+ ```ts
79
+ import { BuildShipAgent, z } from "@buildship/agent/core";
70
80
 
71
- - Global session state across all agents
72
- - Automatic localStorage persistence
73
- - Agent runner registry
81
+ const agent = new BuildShipAgent({ agentId: "..." });
74
82
 
75
- ```tsx
76
- import { AgentContextProvider } from "@buildship/agent";
83
+ // Fire-and-forget tool
84
+ agent.registerClientTool({
85
+ name: "show_notification",
86
+ description: "Display a notification to the user",
87
+ parameters: z.object({
88
+ title: z.string().describe("Notification title"),
89
+ message: z.string().describe("Notification body"),
90
+ }),
91
+ handler: (args) => {
92
+ showNotification(args.title, args.message);
93
+ },
94
+ });
77
95
 
78
- function App() {
79
- return (
80
- <AgentContextProvider>{/* Your app components */}</AgentContextProvider>
81
- );
82
- }
96
+ // Blocking tool — agent pauses until result is returned
97
+ agent.registerClientTool({
98
+ name: "get_location",
99
+ description: "Get the user's current location",
100
+ parameters: z.object({}),
101
+ await: true,
102
+ handler: async () => {
103
+ const pos = await navigator.geolocation.getCurrentPosition();
104
+ return { lat: pos.coords.latitude, lng: pos.coords.longitude };
105
+ },
106
+ });
83
107
  ```
84
108
 
85
- ## Basic Usage
109
+ ### Pause & Resume (Manual)
86
110
 
87
- ### Using the Agent Hook
111
+ For blocking tools without a handler, the agent pauses and you resume manually:
88
112
 
89
- The `useAgentContext` hook is the primary interface for interacting with agents:
113
+ ```ts
114
+ agent.registerClientTool({
115
+ name: "confirm_action",
116
+ description: "Ask the user to confirm an action",
117
+ parameters: z.object({
118
+ action: z.string().describe("The action to confirm"),
119
+ }),
120
+ await: true,
121
+ // No handler — you handle it manually
122
+ });
90
123
 
91
- ```tsx
92
- import { useAgentContext } from "@buildship/agent";
124
+ const session = await agent.execute("Delete my account", {
125
+ onText: (t) => process.stdout.write(t),
126
+ onPaused: (toolName, args) => {
127
+ console.log(`Agent paused for: ${toolName}`, args);
128
+ },
129
+ });
93
130
 
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)
131
+ if (session.isPaused()) {
132
+ const tool = session.getPausedTool();
133
+ // ... show confirmation UI, then resume:
134
+ await session.resume(
135
+ { confirmed: true },
136
+ {
137
+ onText: (t) => process.stdout.write(t),
138
+ },
99
139
  );
140
+ }
141
+ ```
100
142
 
101
- // Send a message
102
- const sendMessage = async () => {
103
- await agent.handleSend("What's the weather today?");
104
- };
143
+ ## Abort
105
144
 
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
- ))}
145
+ ```ts
146
+ const session = agent.session(sessionId);
115
147
 
116
- {agent.inProgress && <div>Agent is thinking...</div>}
148
+ session.execute("Write a long essay...", {
149
+ onText: (text) => {
150
+ process.stdout.write(text);
151
+ if (userCancelled) session.abort();
152
+ },
153
+ });
154
+ ```
117
155
 
118
- <button onClick={sendMessage} disabled={agent.inProgress}>
119
- Send
120
- </button>
121
- </div>
122
- );
156
+ ## Stream Callbacks
157
+
158
+ ```ts
159
+ interface StreamCallbacks {
160
+ /** Called for each text chunk from the agent. */
161
+ onText?: (text: string) => void;
162
+ /** Called for each reasoning chunk (models with chain-of-thought). */
163
+ onReasoning?: (delta: string, index: number) => void;
164
+ /** Called when control is handed off to a sub-agent. */
165
+ onAgentHandoff?: (agentName: string) => void;
166
+ /** Called when a tool execution starts. */
167
+ onToolStart?: (toolName: string, toolType: ToolType) => void;
168
+ /** Called when a tool execution completes. */
169
+ onToolEnd?: (toolName: string, result?: any, error?: string) => void;
170
+ /** Called when agent pauses for a blocking client tool. */
171
+ onPaused?: (toolName: string, args: any) => void;
172
+ /** Called when the stream completes successfully. */
173
+ onComplete?: (fullText: string) => void;
174
+ /** Called if an error occurs during streaming. */
175
+ onError?: (error: Error) => void;
176
+ /** Called for every raw SSE event. Useful for debug panels. */
177
+ onEvent?: (event: StreamEvent) => void;
123
178
  }
124
179
  ```
125
180
 
126
- ### AgentRunner API
181
+ ## Core API Reference
127
182
 
128
- The `useAgentContext` hook returns an `AgentRunner` object with:
183
+ ### `BuildShipAgent`
129
184
 
130
- | Property | Type | Description |
131
- | ---------------------- | ------------------------------- | ----------------------------------------- |
132
- | `messages` | `Message[]` | Array of conversation messages |
133
- | `inProgress` | `boolean` | Whether the agent is currently processing |
134
- | `sessionId` | `string` | Current conversation session ID |
135
- | `sessions` | `Session[]` | All sessions for this agent |
136
- | `debugData` | `Record<string, DebugDataType>` | Debug information indexed by executionId |
137
- | `handleSend` | `Function` | Send a message to the agent |
138
- | `switchSession` | `Function` | Switch to a different session |
139
- | `deleteSession` | `Function` | Delete a session |
140
- | `addOptimisticMessage` | `Function` | Add message to UI without sending |
141
- | `abort` | `Function` | Cancel current agent execution |
185
+ | Method | Description |
186
+ | --------------------------------------- | ---------------------------------------------------------- |
187
+ | `execute(message, callbacks, context?)` | Start a new conversation. Returns `AgentSession`. |
188
+ | `session(sessionId)` | Continue an existing conversation. Returns `AgentSession`. |
189
+ | `registerClientTool(tool)` | Register a client-side tool. |
190
+ | `unregisterClientTool(name)` | Remove a registered tool. |
142
191
 
143
- ### Session Management
192
+ ### `AgentSession`
144
193
 
145
- ```tsx
146
- function SessionList() {
147
- const agent = useAgentContext("agent-id", "agent-url", "access-key");
194
+ | Method | Description |
195
+ | --------------------------------------- | ---------------------------------------------------- |
196
+ | `execute(message, callbacks, context?)` | Send a message in this session. |
197
+ | `resume(result, callbacks)` | Resume after a blocking tool pause. |
198
+ | `isPaused()` | Check if waiting for a tool result. |
199
+ | `getPausedTool()` | Get paused tool info (`{ callId, toolName, args }`). |
200
+ | `getSessionId()` | Get the session ID. |
201
+ | `abort()` | Cancel the current stream. |
148
202
 
149
- return (
150
- <div>
151
- {agent.sessions.map((session) => (
152
- <div key={session.id}>
153
- <button onClick={() => agent.switchSession(session.id)}>
154
- {session.name || "Untitled Session"}
155
- </button>
156
- <button onClick={() => agent.deleteSession(session.id)}>
157
- Delete
158
- </button>
159
- </div>
160
- ))}
203
+ ### Stream Events
161
204
 
162
- {/* Create new session */}
163
- <button onClick={() => agent.switchSession()}>New Session</button>
164
- </div>
165
- );
166
- }
167
- ```
205
+ All events share a `meta` object with `executionId` and `sequence`.
168
206
 
169
- ## File Handling
207
+ | Event Type | Description | Data |
208
+ | ----------------- | ---------------------------------- | ----------------------------------------------------------------- |
209
+ | `text_delta` | Text chunk from the agent | `string` |
210
+ | `reasoning_delta` | Chain-of-thought reasoning chunk | `{ delta: string, index: number }` |
211
+ | `tool_call_start` | A tool execution started | `{ callId, toolName, toolType, inputs?, serverName?, paused? }` |
212
+ | `tool_call_end` | A tool execution completed | `{ callId, toolName, toolType, result?, error?, executionTime? }` |
213
+ | `agent_handoff` | Control transferred to a sub-agent | `{ agentName: string }` |
170
214
 
171
- To send files to your agent:
215
+ ### Tool Types
172
216
 
173
- ### 1. Upload Files to Storage
217
+ ```ts
218
+ type ToolType = "flow" | "node" | "mcp" | "client" | "builtin" | "agent";
219
+ ```
174
220
 
175
- First, upload files to a publicly accessible URL (e.g., Firebase Storage, AWS
176
- S3):
221
+ ---
177
222
 
178
- ```tsx
179
- async function uploadFiles(files: File[]): Promise<Record<string, string>> {
180
- // Upload to your storage provider
181
- const fileMap: Record<string, string> = {};
223
+ # React (`@buildship/agent/react`)
182
224
 
183
- for (const file of files) {
184
- const url = await uploadToStorage(file); // Your upload logic
185
- const fileId = file.name.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
186
- fileMap[fileId] = url;
187
- }
225
+ The React module provides hooks, context providers, and components for building
226
+ chat UIs with full session management, client tool support, and debug panels.
188
227
 
189
- return fileMap;
190
- }
191
- ```
228
+ ## Setup
192
229
 
193
- ### 2. Send Files with Message
230
+ Wrap your app (or the chat area) with `AgentContextProvider`:
194
231
 
195
232
  ```tsx
196
- function FileUpload() {
197
- const agent = useAgentContext("agent-id", "agent-url", "access-key");
198
- const [files, setFiles] = useState<File[]>([]);
199
-
200
- const handleSendWithFiles = async () => {
201
- // Upload files and get URL mapping
202
- const fileMap = await uploadFiles(files);
203
-
204
- // Create input with file references
205
- const fileIds = Object.keys(fileMap).join(", ");
206
- const input = `Analyze these files: ${fileIds}`;
207
-
208
- // Send to agent with file context
209
- await agent.handleSend(input, {
210
- context: {
211
- mapped_file_ids_with_url: fileMap,
212
- },
213
- });
214
- };
233
+ import { AgentContextProvider } from "@buildship/agent/react";
215
234
 
235
+ function App() {
216
236
  return (
217
- <div>
218
- <input
219
- type="file"
220
- multiple
221
- onChange={(e) => setFiles(Array.from(e.target.files || []))}
222
- />
223
- <button onClick={handleSendWithFiles}>Send with Files</button>
224
- </div>
237
+ <AgentContextProvider>
238
+ <ChatPage />
239
+ </AgentContextProvider>
225
240
  );
226
241
  }
227
242
  ```
228
243
 
229
- ### File Context Structure
244
+ ## `useAgent` Hook
230
245
 
231
- ```typescript
232
- {
233
- context: {
234
- mapped_file_ids_with_url: {
235
- "file_name_pdf": "https://storage.url/file1.pdf",
236
- "image_png": "https://storage.url/image.png"
237
- }
238
- }
239
- }
240
- ```
241
-
242
- ## Client Tools / Widgets
243
-
244
- Client tools allow your agent to render interactive widgets directly in the chat
245
- interface.
246
-
247
- ### 1. Define Tool Configuration
248
-
249
- Create tool configurations with Zod schemas:
246
+ The main hook for interacting with an agent. Manages messages, streaming,
247
+ sessions, and debug data.
250
248
 
251
249
  ```tsx
252
- import { z } from "zod";
253
-
254
- export const chartToolConfig = {
255
- name: "render_chart",
256
- description: "Renders a bar chart with the provided data",
257
- schema: z.object({
258
- title: z.string().describe("Chart title"),
259
- data: z
260
- .array(
261
- z.object({
262
- label: z.string(),
263
- value: z.number(),
264
- }),
265
- )
266
- .describe("Data points for the chart"),
267
- }),
268
- };
250
+ import { useAgent } from "@buildship/agent/react";
251
+
252
+ function ChatPage() {
253
+ const {
254
+ messages, // Message[] full conversation history
255
+ inProgress, // boolean — true while streaming
256
+ handleSend, // (input, options?) => Promise — send a message
257
+ resumeTool, // (callId, result) => Promise — resume a paused tool
258
+ abort, // () => void — cancel the current stream
259
+ sessionId, // string — current session ID
260
+ sessions, // Session[] — all sessions for this agent
261
+ switchSession, // (sessionId?) => void — switch to a session (or create new)
262
+ deleteSession, // (sessionId) => void — delete a session
263
+ addOptimisticMessage, // (input) => void — add a user message immediately
264
+ debugData, // Record<string, DebugDataType> — debug entries by execution ID
265
+ } = useAgent(
266
+ "agent-id",
267
+ "https://your-project.buildship.run/executeAgent/AGENT_ID",
268
+ "access-key",
269
+ );
269
270
 
270
- // Create your widget component
271
- export function ChartWidget({ title, data }) {
272
271
  return (
273
272
  <div>
274
- <h3>{title}</h3>
275
- {/* Your chart rendering logic */}
273
+ {messages.map((msg, i) => (
274
+ <div key={i} className={msg.role}>
275
+ {msg.content}
276
+ </div>
277
+ ))}
278
+
279
+ <button onClick={() => handleSend("Hello!")} disabled={inProgress}>
280
+ Send
281
+ </button>
276
282
  </div>
277
283
  );
278
284
  }
279
285
  ```
280
286
 
281
- ### 2. Create Widget Registry
287
+ ### `handleSend` Options
282
288
 
283
- ```tsx
284
- import type { ComponentType } from "react";
285
- import type { ClientToolDefinition } from "@buildship/agent";
286
- import { z } from "zod";
289
+ ```ts
290
+ handleSend(input: string, options?: {
291
+ context?: Record<string, unknown>; // Additional context passed to the agent
292
+ skipUserMessage?: boolean; // Don't add a user message to the conversation
293
+ });
294
+ ```
287
295
 
288
- // Import all your widgets
289
- import { ChartWidget, chartToolConfig } from "./widgets/chart";
290
- import { MapWidget, mapToolConfig } from "./widgets/map";
296
+ ## `useAgentContext` Hook
291
297
 
292
- const allConfigs = [chartToolConfig, mapToolConfig];
298
+ An alternative to `useAgent` for multi-agent setups. Initializes agents
299
+ declaratively and shares state through context.
293
300
 
294
- // Registry for rendering widgets
295
- export const widgetRegistry: Record<string, ComponentType<any>> = {
296
- [chartToolConfig.name]: ChartWidget,
297
- [mapToolConfig.name]: MapWidget,
298
- };
301
+ ```tsx
302
+ import { useAgentContext } from "@buildship/agent/react";
299
303
 
300
- // Convert to agent tool definitions
301
- export function getToolDefinitions(): ClientToolDefinition[] {
302
- return allConfigs.map((config) => {
303
- const parameters = z.toJSONSchema(config.schema);
304
-
305
- // Remove $schema property for LLM compatibility
306
- if (
307
- parameters &&
308
- typeof parameters === "object" &&
309
- "$schema" in parameters
310
- ) {
311
- const { $schema, ...rest } = parameters as any;
312
- return {
313
- name: config.name,
314
- description: config.description,
315
- parameters: rest,
316
- };
317
- }
318
-
319
- return {
320
- name: config.name,
321
- description: config.description,
322
- parameters,
323
- };
324
- });
304
+ function ChatPage() {
305
+ const agent = useAgentContext(
306
+ "agent-id",
307
+ "https://your-project.buildship.run/executeAgent/AGENT_ID",
308
+ "access-key",
309
+ );
310
+
311
+ // Same return shape as useAgent
312
+ const { messages, handleSend, inProgress, sessions, ... } = agent;
325
313
  }
326
314
  ```
327
315
 
328
- ### 3. Pass Tools to Agent
316
+ ## Client Tools (React)
329
317
 
330
- ```tsx
331
- import { getToolDefinitions } from "./widget-registry";
318
+ ### `useClientTool` — Headless Tools
332
319
 
333
- function ChatWithWidgets() {
334
- const agent = useAgentContext("agent-id", "agent-url", "access-key");
335
- const tools = getToolDefinitions();
320
+ Register a tool that runs code without rendering any UI:
336
321
 
337
- const handleSend = async (input: string) => {
338
- await agent.handleSend(input, {
339
- clientTools: tools,
340
- });
341
- };
322
+ ```tsx
323
+ import { useClientTool } from "@buildship/agent/react";
324
+ import { z } from "@buildship/agent/core";
325
+
326
+ function ChatPage() {
327
+ // Fire-and-forget — runs handler, result is discarded
328
+ useClientTool("agent-id", {
329
+ name: "show_notification",
330
+ description: "Display a notification to the user",
331
+ parameters: z.object({
332
+ title: z.string(),
333
+ message: z.string(),
334
+ }),
335
+ handler: (inputs) => {
336
+ toast(inputs.title, inputs.message);
337
+ },
338
+ });
342
339
 
343
- return (
344
- <div>
345
- <button onClick={() => handleSend("Show me a chart")}>
346
- Ask for Chart
347
- </button>
348
- </div>
349
- );
340
+ // Blocking tool — agent pauses, handler result is sent back
341
+ useClientTool("agent-id", {
342
+ name: "get_location",
343
+ description: "Get the user's current GPS location",
344
+ parameters: z.object({}),
345
+ await: true,
346
+ handler: async () => {
347
+ const pos = await getCurrentPosition();
348
+ return { lat: pos.coords.latitude, lng: pos.coords.longitude };
349
+ },
350
+ });
351
+
352
+ // ...
350
353
  }
351
354
  ```
352
355
 
353
- ### 4. Render Widgets in Messages
356
+ ### `useClientTool` Widget Tools
354
357
 
355
- ```tsx
356
- import { widgetRegistry } from "./widget-registry";
358
+ Register a tool that renders interactive UI inline in the conversation:
357
359
 
358
- function MessageDisplay() {
359
- const agent = useAgentContext("agent-id", "agent-url", "access-key");
360
+ ```tsx
361
+ import { useClientTool, ToolRenderer } from "@buildship/agent/react";
362
+ import { z } from "@buildship/agent/core";
363
+
364
+ function ChatPage() {
365
+ const { messages } = useAgent("agent-id", agentUrl);
366
+
367
+ // Register a widget tool with a render function
368
+ useClientTool("agent-id", {
369
+ name: "feedback_form",
370
+ description: "Collects user feedback",
371
+ parameters: z.object({
372
+ question: z.string().describe("The feedback question"),
373
+ }),
374
+ await: true, // Agent pauses until user submits
375
+ render: ({ inputs, submit, status, result }) => (
376
+ <div>
377
+ <p>{inputs.question}</p>
378
+ {status === "pending" ? (
379
+ <button onClick={() => submit({ answer: "Great!" })}>Submit</button>
380
+ ) : (
381
+ <p>✅ Submitted: {JSON.stringify(result)}</p>
382
+ )}
383
+ </div>
384
+ ),
385
+ });
360
386
 
387
+ // Render messages with embedded widgets
361
388
  return (
362
389
  <div>
363
- {agent.messages.map((message) => (
364
- <div key={message.executionId}>
365
- {/* Render message parts (text + widgets) */}
366
- {message.parts?.map((part, idx) => {
367
- if (part.type === "text") {
368
- return <p key={idx}>{part.text}</p>;
369
- } else if (part.type === "widget") {
370
- const Widget = widgetRegistry[part.toolName];
371
- if (!Widget) return null;
372
- return <Widget key={idx} {...part.inputs} />;
373
- }
374
- return null;
375
- })}
376
-
377
- {/* Fallback: render plain content if no parts */}
378
- {!message.parts && <p>{message.content}</p>}
379
- </div>
380
- ))}
390
+ {messages.map((msg) =>
391
+ msg.parts?.map((part) => {
392
+ if (part.type === "text") {
393
+ return <p key={part.firstSequence}>{part.text}</p>;
394
+ }
395
+ if (part.type === "widget") {
396
+ return (
397
+ <ToolRenderer key={part.callId} agentId="agent-id" part={part} />
398
+ );
399
+ }
400
+ return null;
401
+ }),
402
+ )}
381
403
  </div>
382
404
  );
383
405
  }
384
406
  ```
385
407
 
386
- ## API Reference
387
-
388
- ### handleSend Options
408
+ ### `ClientToolConfig`
389
409
 
390
- ```typescript
391
- agent.handleSend(input: string, options?: {
392
- // Custom context data for the agent
393
- context?: Record<string, unknown>;
394
-
395
- // Don't add user message to UI (for optimistic updates)
396
- skipUserMessage?: boolean;
397
-
398
- // Additional HTTP headers
399
- additionalHeaders?: Record<string, string>;
410
+ ```ts
411
+ interface ClientToolConfig {
412
+ name: string; // Must match the tool name the agent knows
413
+ description: string; // Description of what the tool does
414
+ parameters: ZodSchema | Record<string, any>; // Zod schema or raw JSON Schema
415
+ await?: boolean; // If true, agent pauses until result
416
+ handler?: (inputs: any) => any | Promise<any>; // For headless tools
417
+ render?: (props: ClientToolRenderProps) => any; // For widget tools
418
+ }
419
+ ```
400
420
 
401
- // Server-side tool definitions
402
- tools?: ClientToolDefinition[];
421
+ ### `ClientToolRenderProps`
403
422
 
404
- // Client-side widget definitions
405
- clientTools?: ClientToolDefinition[];
406
- })
423
+ ```ts
424
+ interface ClientToolRenderProps<T = any> {
425
+ inputs: T; // Parsed inputs from the agent
426
+ submit: (result: any) => void; // Submit a result (only for await: true tools)
427
+ status: "pending" | "submitted"; // Widget status
428
+ result?: any; // Persisted result after submission
429
+ }
407
430
  ```
408
431
 
409
- ### Types
432
+ ## Messages & Parts
410
433
 
411
- #### Message
434
+ Messages can contain rich, interleaved content via `parts`:
412
435
 
413
- ```typescript
436
+ ```ts
414
437
  type Message = {
415
438
  role: "user" | "agent";
416
439
  content: string; // Full text content
417
- parts?: MessagePart[]; // Structured parts (text + widgets)
440
+ parts?: MessagePart[]; // Rich content (text + widgets)
418
441
  executionId?: string; // Links to debug data
419
442
  };
420
443
 
@@ -425,13 +448,39 @@ type MessagePart =
425
448
  toolName: string;
426
449
  callId: string;
427
450
  inputs: any;
428
- sequence: number;
451
+ paused?: boolean;
452
+ status?: "pending" | "submitted";
453
+ result?: any;
429
454
  };
430
455
  ```
431
456
 
432
- #### Session
457
+ > **Tip:** When rendering messages, iterate over `msg.parts` instead of
458
+ > `msg.content` to get both text and widgets interleaved in the correct order.
459
+
460
+ ## Sessions
461
+
462
+ Sessions are automatically persisted to local storage and synced across tabs.
463
+
464
+ ```tsx
465
+ const { sessions, switchSession, deleteSession, sessionId } = useAgent(...);
466
+
467
+ // List all sessions
468
+ sessions.map((s) => (
469
+ <button key={s.id} onClick={() => switchSession(s.id)}>
470
+ {s.name} ({s.messages.length} messages)
471
+ </button>
472
+ ));
473
+
474
+ // Create a new session
475
+ switchSession(); // No argument = new session
476
+
477
+ // Delete a session
478
+ deleteSession(sessionId);
479
+ ```
480
+
481
+ ### Session Type
433
482
 
434
- ```typescript
483
+ ```ts
435
484
  type Session = {
436
485
  id: string;
437
486
  createdAt: number;
@@ -441,123 +490,160 @@ type Session = {
441
490
  };
442
491
  ```
443
492
 
444
- #### ClientToolDefinition
445
-
446
- ```typescript
447
- type ClientToolDefinition = {
448
- name: string; // Unique tool identifier
449
- description: string; // Human-readable description for LLM
450
- parameters: unknown; // JSON Schema object
451
- };
452
- ```
453
-
454
- ## Advanced Features
493
+ ## Debug Data
455
494
 
456
- ### Debug Data
457
-
458
- Access detailed execution information:
495
+ Debug data captures structured information about every tool call, reasoning
496
+ step, and agent handoff during execution. It's stored by `executionId` (matching
497
+ the user message it belongs to).
459
498
 
460
499
  ```tsx
461
- function DebugView() {
462
- const agent = useAgentContext("agent-id", "agent-url", "access-key");
500
+ const { debugData, messages } = useAgent(...);
463
501
 
464
- return (
465
- <div>
466
- {Object.entries(agent.debugData).map(([executionId, debug]) => (
467
- <details key={executionId}>
468
- <summary>Execution {executionId}</summary>
469
- <pre>{JSON.stringify(debug, null, 2)}</pre>
470
- </details>
471
- ))}
472
- </div>
473
- );
474
- }
502
+ // Get debug data for a specific message
503
+ const messageDebug = debugData[message.executionId];
504
+
505
+ // Each entry is one of:
506
+ type DebugDataType = Array<ToolExecutionItem | ReasoningItem | HandoffItem>;
475
507
  ```
476
508
 
477
- ### Custom Headers
509
+ ### Debug Entry Types
510
+
511
+ ```ts
512
+ type ToolExecutionItem = {
513
+ itemType: "tool_call";
514
+ toolName: string;
515
+ callId: string;
516
+ toolType: ToolType; // "flow" | "node" | "mcp" | "client" | "builtin" | "agent"
517
+ status: "progress" | "complete" | "error";
518
+ inputs?: unknown;
519
+ output?: unknown;
520
+ error?: string;
521
+ serverName?: string; // For MCP tools
522
+ };
478
523
 
479
- ```tsx
480
- await agent.handleSend("message", {
481
- additionalHeaders: {
482
- "X-Custom-Header": "value",
483
- Authorization: "Bearer token",
484
- },
485
- });
524
+ type ReasoningItem = {
525
+ itemType: "reasoning";
526
+ reasoning: string;
527
+ index?: number;
528
+ };
529
+
530
+ type HandoffItem = {
531
+ itemType: "handoff";
532
+ agentName: string;
533
+ };
486
534
  ```
487
535
 
488
- ### Abort Requests
536
+ ## React API Reference
489
537
 
490
- ```tsx
491
- function CancellableRequest() {
492
- const agent = useAgentContext("agent-id", "agent-url", "access-key");
538
+ ### Hooks
493
539
 
494
- return (
495
- <div>
496
- <button onClick={() => agent.handleSend("Long task...")}>Start</button>
497
- <button onClick={() => agent.abort()} disabled={!agent.inProgress}>
498
- Cancel
499
- </button>
500
- </div>
501
- );
502
- }
503
- ```
540
+ | Hook | Description |
541
+ | ------------------------------------------ | ------------------------------------------------ |
542
+ | `useAgent(agentId, agentUrl, accessKey?)` | Main hook — messages, streaming, sessions, debug |
543
+ | `useAgentContext(agentId, agentUrl, key?)` | Context-based alternative for multi-agent setups |
544
+ | `useClientTool(agentId, config)` | Register a client tool (headless or widget) |
504
545
 
505
- ### Optimistic Updates
546
+ ### Components
506
547
 
507
- ```tsx
508
- function OptimisticMessage() {
509
- const agent = useAgentContext("agent-id", "agent-url", "access-key");
548
+ | Component | Description |
549
+ | ------------------------------------------- | -------------------------------------------------- |
550
+ | `<AgentContextProvider>` | Provides shared agent state (sessions, debug data) |
551
+ | `<ToolRenderer agentId={id} part={part} />` | Renders a widget tool from a message part |
510
552
 
511
- const handleSend = async (input: string) => {
512
- // Add message to UI immediately
513
- agent.addOptimisticMessage(input);
553
+ ### Utilities
514
554
 
515
- // Send to agent without adding duplicate
516
- await agent.handleSend(input, { skipUserMessage: true });
517
- };
555
+ | Export | Description |
556
+ | ------------------------------------- | -------------------------------------------------------------------- |
557
+ | `tryParseJSON(value)` | Safely parse a JSON string, returns parsed object or original string |
558
+ | `updateAgentMessageParts(msg, event)` | Append/merge parts into an agent message |
518
559
 
519
- return <button onClick={() => handleSend("Hello")}>Send</button>;
520
- }
521
- ```
560
+ ## Full Example
522
561
 
523
- ## Storage and Persistence
562
+ ```tsx
563
+ import {
564
+ AgentContextProvider,
565
+ useAgent,
566
+ useClientTool,
567
+ ToolRenderer,
568
+ } from "@buildship/agent/react";
569
+ import { z } from "@buildship/agent/core";
524
570
 
525
- The library automatically persists conversations to localStorage:
571
+ const AGENT_ID = "my-agent";
572
+ const AGENT_URL = "https://my-project.buildship.run/executeAgent/my-agent";
526
573
 
527
- - **Sessions**: Stored under `buildship:agent:conversations`
528
- - **Debug Data**: Stored under `buildship:agent:debug`
574
+ function App() {
575
+ return (
576
+ <AgentContextProvider>
577
+ <Chat />
578
+ </AgentContextProvider>
579
+ );
580
+ }
529
581
 
530
- Sessions are automatically synced across browser tabs and survive page
531
- refreshes.
582
+ function Chat() {
583
+ const { messages, handleSend, inProgress, resumeTool, abort, debugData } =
584
+ useAgent(AGENT_ID, AGENT_URL);
585
+ const [input, setInput] = useState("");
586
+
587
+ // Register a widget tool
588
+ useClientTool(AGENT_ID, {
589
+ name: "poll",
590
+ description: "Ask the user to vote on options",
591
+ parameters: z.object({
592
+ question: z.string(),
593
+ options: z.array(z.string()),
594
+ }),
595
+ await: true,
596
+ render: ({ inputs, submit, status }) => (
597
+ <div>
598
+ <p>{inputs.question}</p>
599
+ {status === "pending" ? (
600
+ inputs.options.map((opt) => (
601
+ <button key={opt} onClick={() => submit({ vote: opt })}>
602
+ {opt}
603
+ </button>
604
+ ))
605
+ ) : (
606
+ <p>✅ Vote recorded</p>
607
+ )}
608
+ </div>
609
+ ),
610
+ });
532
611
 
533
- ## TypeScript Support
612
+ const send = () => {
613
+ if (!input.trim()) return;
614
+ handleSend(input);
615
+ setInput("");
616
+ };
534
617
 
535
- The package is written in TypeScript and exports all types:
618
+ return (
619
+ <div>
620
+ {messages.map((msg, i) => (
621
+ <div key={i}>
622
+ <strong>{msg.role}:</strong>
623
+ {msg.parts?.map((part) =>
624
+ part.type === "text" ? (
625
+ <span key={part.firstSequence}>{part.text}</span>
626
+ ) : (
627
+ <ToolRenderer key={part.callId} agentId={AGENT_ID} part={part} />
628
+ ),
629
+ ) ?? msg.content}
630
+ </div>
631
+ ))}
536
632
 
537
- ```tsx
538
- import type {
539
- Message,
540
- Session,
541
- ClientToolDefinition,
542
- AgentRunner,
543
- DebugDataType,
544
- MessagePart,
545
- } from "@buildship/agent";
633
+ <input
634
+ value={input}
635
+ onChange={(e) => setInput(e.target.value)}
636
+ onKeyDown={(e) => e.key === "Enter" && send()}
637
+ />
638
+ <button onClick={send} disabled={inProgress}>
639
+ Send
640
+ </button>
641
+ {inProgress && <button onClick={abort}>Stop</button>}
642
+ </div>
643
+ );
644
+ }
546
645
  ```
547
646
 
548
- ## Best Practices
549
-
550
- 1. **Single Provider**: Only use one `AgentContextProvider` at the root of your
551
- app
552
- 2. **Tool Descriptions**: Write clear, detailed descriptions for client tools to
553
- help the LLM understand when to use them
554
- 3. **File URLs**: Ensure file URLs are publicly accessible or use signed URLs
555
- with sufficient expiration
556
- 4. **Error Handling**: Wrap `handleSend` calls in try-catch blocks for error
557
- handling
558
- 5. **Widget Registry**: Keep your widget registry centralized for easier
559
- maintenance
560
-
561
647
  ## License
562
648
 
563
649
  MIT