@standardagents/react 0.10.0 → 0.10.1-dev.616ec2e

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,6 +1,6 @@
1
1
  # @standardagents/react
2
2
 
3
- React hooks and components for Standard Agents - connect to AI agent threads with real-time updates, send messages, and listen for custom events.
3
+ React hooks and components for Standard Agents - connect to AI agent threads with real-time updates, send messages, manage files, and listen for custom events.
4
4
 
5
5
  ## Installation
6
6
 
@@ -16,41 +16,37 @@ yarn add @standardagents/react
16
16
 
17
17
  ```tsx
18
18
  import {
19
- StandardAgentsProvider,
19
+ AgentBuilderProvider,
20
20
  ThreadProvider,
21
21
  useThread,
22
- sendMessage,
23
22
  } from "@standardagents/react"
24
23
 
25
24
  function App() {
26
25
  return (
27
- <StandardAgentsProvider config={{ endpoint: "https://your-api.com" }}>
26
+ <AgentBuilderProvider config={{ endpoint: "https://your-api.com" }}>
28
27
  <ThreadProvider threadId="thread-123">
29
28
  <ChatInterface />
30
29
  </ThreadProvider>
31
- </StandardAgentsProvider>
30
+ </AgentBuilderProvider>
32
31
  )
33
32
  }
34
33
 
35
34
  function ChatInterface() {
36
- // No thread ID needed - inherited from ThreadProvider context
37
- const messages = useThread()
35
+ const { messages, sendMessage, status } = useThread()
38
36
 
39
37
  const handleSend = async (text: string) => {
40
- await sendMessage("thread-123", {
41
- role: "user",
42
- content: text,
43
- })
38
+ await sendMessage({ role: "user", content: text })
44
39
  }
45
40
 
46
41
  return (
47
42
  <div>
43
+ <p>Status: {status}</p>
48
44
  {messages.map((msg) => (
49
45
  <div key={msg.id}>
50
46
  <strong>{msg.role}:</strong> {msg.content}
51
47
  </div>
52
48
  ))}
53
- <input onSubmit={(e) => handleSend(e.currentTarget.value)} />
49
+ <input onKeyDown={(e) => e.key === "Enter" && handleSend(e.currentTarget.value)} />
54
50
  </div>
55
51
  )
56
52
  }
@@ -69,292 +65,247 @@ All API requests and WebSocket connections will automatically include this token
69
65
 
70
66
  ## API Reference
71
67
 
72
- ### `StandardAgentsProvider`
68
+ ### `AgentBuilderProvider`
73
69
 
74
70
  Context provider that configures the Standard Agents client for all child components.
75
71
 
76
72
  **Props:**
77
73
 
78
- - `config.endpoint: string` - The API endpoint URL (e.g., `https://api.example.com`)
74
+ - `config.endpoint: string` - The API endpoint URL
79
75
 
80
76
  ```tsx
81
- <StandardAgentsProvider config={{ endpoint: "https://api.example.com" }}>
77
+ <AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
82
78
  {children}
83
- </StandardAgentsProvider>
79
+ </AgentBuilderProvider>
84
80
  ```
85
81
 
86
82
  ---
87
83
 
88
84
  ### `ThreadProvider`
89
85
 
90
- Context provider that establishes a WebSocket connection to a specific thread. Must be nested inside `StandardAgentsProvider`. Provides thread context to child hooks like `useThread` and `onThreadEvent`.
86
+ Context provider that establishes a WebSocket connection to a specific thread. Must be nested inside `AgentBuilderProvider`.
91
87
 
92
88
  **Props:**
93
89
 
94
90
  - `threadId: string` - The thread ID to connect to
95
91
  - `preload?: boolean` - Fetch existing messages on mount (default: `true`)
96
92
  - `live?: boolean` - Enable WebSocket for real-time updates (default: `true`)
93
+ - `useWorkblocks?: boolean` - Transform tool calls into workblocks (default: `false`)
97
94
  - `depth?: number` - Message depth level for nested conversations (default: `0`)
98
95
  - `includeSilent?: boolean` - Include silent messages (default: `false`)
99
96
  - `endpoint?: string` - Override the endpoint from context
100
97
 
101
98
  ```tsx
102
- <StandardAgentsProvider config={{ endpoint: "https://api.example.com" }}>
103
- <ThreadProvider threadId="thread-123" live={true} depth={0}>
99
+ <AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
100
+ <ThreadProvider threadId="thread-123" live={true}>
104
101
  <YourComponents />
105
102
  </ThreadProvider>
106
- </StandardAgentsProvider>
103
+ </AgentBuilderProvider>
107
104
  ```
108
105
 
109
106
  ---
110
107
 
111
- ### `useThread(options?)`
108
+ ### `useThread()`
112
109
 
113
- Hook to subscribe to thread messages. Must be used within a `ThreadProvider`.
110
+ Hook to access the full thread context. Must be used within a `ThreadProvider`.
114
111
 
115
- **Parameters:**
116
-
117
- - `options?: UseThreadOptions` - Configuration options
118
-
119
- **Options:**
120
-
121
- - `useWorkblocks?: boolean` - Group tool calls into workblocks (default: `true`)
112
+ **Returns:** `ThreadContextValue`
122
113
 
123
- **Returns:** `ThreadMessage[]` - Array of messages or workblocks
114
+ - `threadId: string` - The thread ID
115
+ - `messages: Message[]` - Array of messages
116
+ - `workblocks: ThreadMessage[]` - Messages transformed to workblocks (if `useWorkblocks` is true)
117
+ - `status: ConnectionStatus` - WebSocket connection status (`"connecting"` | `"connected"` | `"disconnected"` | `"reconnecting"`)
118
+ - `loading: boolean` - Whether messages are loading (alias: `isLoading`)
119
+ - `error: Error | null` - Any error that occurred
120
+ - `options: ThreadProviderOptions` - Options passed to the provider
121
+ - `sendMessage: (payload: SendMessagePayload) => Promise<Message>` - Send a message
122
+ - `stopExecution: () => Promise<void>` - Stop current execution
123
+ - `onEvent: <T>(eventType, listener) => () => void` - Subscribe to custom events (alias: `subscribeToEvent`)
124
+ - `files: ThreadFile[]` - All files (pending + committed)
125
+ - `addFiles: (files: File[] | FileList) => void` - Upload files
126
+ - `removeFile: (id: string) => void` - Remove a pending file
127
+ - `getFileUrl: (file: ThreadFile) => string` - Get file URL
128
+ - `getThumbnailUrl: (file: ThreadFile) => string` - Get thumbnail URL
129
+ - `getPreviewUrl: (file: ThreadFile) => string | null` - Get preview URL
124
130
 
125
131
  **Example:**
126
132
 
127
133
  ```tsx
128
- function ThreadView() {
129
- // Thread ID inherited from ThreadProvider context
130
- const messages = useThread()
134
+ function ChatView() {
135
+ const {
136
+ messages,
137
+ sendMessage,
138
+ stopExecution,
139
+ status,
140
+ isLoading,
141
+ files,
142
+ addFiles,
143
+ } = useThread()
131
144
 
132
145
  return (
133
146
  <div>
134
- {messages.map((item) => {
135
- if (item.type === "workblock") {
136
- return <WorkblockView key={item.id} workblock={item} />
137
- }
138
- return <MessageView key={item.id} message={item} />
139
- })}
140
- </div>
141
- )
142
- }
147
+ <p>Status: {status}</p>
148
+ {isLoading && <p>Loading...</p>}
143
149
 
144
- // Disable workblocks transformation
145
- function RawMessagesView() {
146
- const messages = useThread({ useWorkblocks: false })
147
- // Returns raw messages without workblock grouping
148
- }
149
- ```
150
-
151
- ---
152
-
153
- ### `useThreadId()`
154
-
155
- Hook to get the current thread ID from context. Must be used within a `ThreadProvider`.
150
+ {messages.map((msg) => (
151
+ <div key={msg.id}>
152
+ <strong>{msg.role}:</strong> {msg.content}
153
+ </div>
154
+ ))}
156
155
 
157
- **Returns:** `string` - The thread ID
156
+ <input
157
+ type="file"
158
+ multiple
159
+ onChange={(e) => e.target.files && addFiles(e.target.files)}
160
+ />
158
161
 
159
- **Example:**
162
+ <div>
163
+ {files.map((file) => (
164
+ <span key={file.id}>{file.name} ({file.status})</span>
165
+ ))}
166
+ </div>
160
167
 
161
- ```tsx
162
- function CurrentThreadId() {
163
- const threadId = useThreadId()
164
- return <span>Thread: {threadId}</span>
168
+ <button onClick={() => sendMessage({ role: "user", content: "Hello!" })}>Send</button>
169
+ <button onClick={stopExecution}>Stop</button>
170
+ </div>
171
+ )
165
172
  }
166
173
  ```
167
174
 
168
175
  ---
169
176
 
170
- ### `sendMessage(threadId, payload, options?)`
177
+ ### `useThreadEvent<T>(eventType)`
171
178
 
172
- Send a message to a thread. This is a standalone function that works outside of React components.
179
+ Hook to listen for custom events emitted by the agent. Must be used within a `ThreadProvider`.
173
180
 
174
181
  **Parameters:**
175
182
 
176
- - `threadId: string` - The thread ID
177
- - `payload: SendMessagePayload` - The message to send
178
- - `role: 'user' | 'assistant'` - Message role
179
- - `content: string | null` - Message content
180
- - `silent?: boolean` - Silent message (not shown to user, default: `false`)
181
- - `options?: { endpoint?: string }` - Override endpoint
183
+ - `eventType: string` - The custom event type to listen for
182
184
 
183
- **Returns:** `Promise<Message>` - The created message
185
+ **Returns:** `T | null` - The latest event value, or `null` if no event received yet
184
186
 
185
187
  **Example:**
186
188
 
187
189
  ```tsx
188
- import { sendMessage, useThreadId } from "@standardagents/react"
189
-
190
- function SendButton() {
191
- const threadId = useThreadId()
190
+ function TodoProgress() {
191
+ const todos = useThreadEvent<{ todos: string[]; completed: number }>("todo-updated")
192
192
 
193
- const handleSend = async () => {
194
- await sendMessage(threadId, {
195
- role: "user",
196
- content: "Hello, agent!",
197
- })
198
- }
193
+ if (!todos) return <div>Waiting for updates...</div>
199
194
 
200
- return <button onClick={handleSend}>Send</button>
195
+ return (
196
+ <div>
197
+ <p>Progress: {todos.completed} / {todos.todos.length}</p>
198
+ <ul>
199
+ {todos.todos.map((todo, i) => (
200
+ <li key={i}>{todo}</li>
201
+ ))}
202
+ </ul>
203
+ </div>
204
+ )
201
205
  }
202
-
203
- // Send a silent message (for context injection)
204
- await sendMessage("thread-123", {
205
- role: "user",
206
- content: "Additional context",
207
- silent: true,
208
- })
209
-
210
- // Send an assistant message (for injecting responses)
211
- await sendMessage("thread-123", {
212
- role: "assistant",
213
- content: "Custom assistant response",
214
- })
215
206
  ```
216
207
 
217
208
  ---
218
209
 
219
- ### `stopThread(threadId, options?)`
210
+ ### `onThreadEvent<T>(eventType, callback)`
220
211
 
221
- Cancel an in-flight thread execution.
212
+ Hook to listen for custom events with a callback. Must be used within a `ThreadProvider`.
222
213
 
223
214
  **Parameters:**
224
215
 
225
- - `threadId: string` - The thread ID
226
- - `options?: { endpoint?: string }` - Override endpoint
227
-
228
- **Returns:** `Promise<void>`
216
+ - `eventType: string` - The custom event type to listen for
217
+ - `callback: (data: T) => void` - Called when event is received
229
218
 
230
219
  **Example:**
231
220
 
232
221
  ```tsx
233
- import { stopThread, useThreadId } from "@standardagents/react"
234
-
235
- function StopButton() {
236
- const threadId = useThreadId()
237
- const [stopping, setStopping] = useState(false)
238
-
239
- const handleStop = async () => {
240
- setStopping(true)
241
- try {
242
- await stopThread(threadId)
243
- } catch (error) {
244
- console.error("Failed to stop thread:", error)
245
- } finally {
246
- setStopping(false)
247
- }
248
- }
222
+ function Notifications() {
223
+ onThreadEvent<{ message: string }>("notification", (data) => {
224
+ alert(data.message)
225
+ })
249
226
 
250
- return (
251
- <button onClick={handleStop} disabled={stopping}>
252
- {stopping ? "Stopping..." : "Stop Execution"}
253
- </button>
254
- )
227
+ return null
255
228
  }
256
229
  ```
257
230
 
258
231
  ---
259
232
 
260
- ### `onThreadEvent<T>(eventType)`
261
-
262
- Hook to listen for custom events emitted by the agent via WebSocket. Must be used within a `ThreadProvider`.
263
-
264
- **Parameters:**
265
-
266
- - `eventType: string` - The custom event type to listen for
267
-
268
- **Type Parameter:**
233
+ ## File Management
269
234
 
270
- - `T` - The expected shape of the event data
271
-
272
- **Returns:** `T | null` - The latest event value, or `null` if no event received yet
273
-
274
- **Example:**
235
+ The `useThread()` hook provides file management capabilities:
275
236
 
276
237
  ```tsx
277
- import { onThreadEvent } from "@standardagents/react"
278
-
279
- function TodoProgress() {
280
- // Thread ID inherited from ThreadProvider context
281
- const todos = onThreadEvent<{ todos: string[]; completed: number }>(
282
- "todo-updated"
283
- )
284
-
285
- if (!todos) return <div>Waiting for updates...</div>
238
+ function FileUploader() {
239
+ const { files, addFiles, removeFile, getFileUrl, getPreviewUrl } = useThread()
286
240
 
287
241
  return (
288
242
  <div>
289
- <p>
290
- Progress: {todos.completed} / {todos.todos.length}
291
- </p>
292
- <ul>
293
- {todos.todos.map((todo, i) => (
294
- <li key={i}>{todo}</li>
295
- ))}
296
- </ul>
243
+ <input
244
+ type="file"
245
+ multiple
246
+ accept="image/*,.pdf,.txt"
247
+ onChange={(e) => e.target.files && addFiles(e.target.files)}
248
+ />
249
+
250
+ {files.map((file) => (
251
+ <div key={file.id}>
252
+ {file.isImage && file.status !== 'uploading' && (
253
+ <img src={getPreviewUrl(file) || ''} alt={file.name} />
254
+ )}
255
+ <span>{file.name}</span>
256
+ <span>{file.status}</span>
257
+ {file.status === 'uploading' && <span>Uploading...</span>}
258
+ {file.status === 'error' && <span>Error: {file.error}</span>}
259
+ {file.status !== 'committed' && (
260
+ <button onClick={() => removeFile(file.id)}>Remove</button>
261
+ )}
262
+ </div>
263
+ ))}
297
264
  </div>
298
265
  )
299
266
  }
300
267
  ```
301
268
 
302
- **Backend Event Emission:**
269
+ ### File States
303
270
 
304
- ```typescript
305
- // In your agent's tool or hook (using @standardagents/builder)
306
- import { emitThreadEvent } from "@standardagents/builder"
307
-
308
- // Emit an event to all connected clients
309
- emitThreadEvent(flow, "todo-updated", {
310
- todos: ["Task 1", "Task 2"],
311
- completed: 1,
312
- })
313
- ```
271
+ - `uploading` - File is being uploaded
272
+ - `ready` - Upload complete, file ready to attach to message
273
+ - `committed` - File is attached to a sent message
274
+ - `error` - Upload failed
314
275
 
315
276
  ---
316
277
 
317
- ## Message Types
318
-
319
- ### Regular Message
278
+ ## Types
320
279
 
321
280
  ```typescript
322
281
  interface Message {
323
282
  id: string
324
283
  role: "user" | "assistant" | "system" | "tool"
325
284
  content: string | null
326
- created_at: number // microseconds
327
- reasoning_content?: string | null
328
- silent?: boolean
329
- depth?: number
285
+ created_at: number
286
+ attachments?: string // JSON array of AttachmentRef
330
287
  }
331
- ```
332
-
333
- ### Workblock
334
288
 
335
- When `useWorkblocks: true`, tool calls and results are grouped:
336
-
337
- ```typescript
338
- interface WorkMessage {
339
- type: "workblock"
340
- id: string
341
- content: string | null
342
- reasoning_content?: string | null
343
- status: "pending" | "completed"
344
- workItems: WorkItem[]
345
- created_at: number
346
- depth?: number
289
+ interface SendMessagePayload {
290
+ role: "user" | "assistant" | "system"
291
+ content: string
292
+ silent?: boolean
293
+ attachments?: string[] // Paths of files to attach
347
294
  }
348
295
 
349
- interface WorkItem {
296
+ interface ThreadFile {
350
297
  id: string
351
- type: "tool_call" | "tool_result"
352
298
  name: string
353
- input?: string
354
- content?: string
355
- status: "success" | "error" | null
356
- tool_call_id?: string
299
+ mimeType: string
300
+ size: number
301
+ isImage: boolean
302
+ status: "uploading" | "ready" | "committed" | "error"
303
+ error?: string
304
+ path?: string
305
+ localPreviewUrl: string | null
357
306
  }
307
+
308
+ type ConnectionStatus = "connecting" | "connected" | "disconnected" | "reconnecting"
358
309
  ```
359
310
 
360
311
  ---
@@ -362,74 +313,53 @@ interface WorkItem {
362
313
  ## Complete Example
363
314
 
364
315
  ```tsx
316
+ import { useState, useEffect } from "react"
365
317
  import {
366
- StandardAgentsProvider,
318
+ AgentBuilderProvider,
367
319
  ThreadProvider,
368
320
  useThread,
369
- useThreadId,
370
- sendMessage,
371
- stopThread,
372
- onThreadEvent,
321
+ useThreadEvent,
373
322
  } from "@standardagents/react"
374
- import { useState, useEffect } from "react"
375
323
 
376
324
  function App() {
377
- // Set auth token
378
325
  useEffect(() => {
379
326
  localStorage.setItem("standardagents_auth_token", "your-token")
380
327
  }, [])
381
328
 
382
329
  return (
383
- <StandardAgentsProvider config={{ endpoint: "https://api.example.com" }}>
384
- <ThreadProvider threadId="thread-123" live={true}>
330
+ <AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
331
+ <ThreadProvider threadId="thread-123">
385
332
  <AgentChat />
386
333
  </ThreadProvider>
387
- </StandardAgentsProvider>
334
+ </AgentBuilderProvider>
388
335
  )
389
336
  }
390
337
 
391
338
  function AgentChat() {
392
339
  const [input, setInput] = useState("")
393
- const [isExecuting, setIsExecuting] = useState(false)
394
340
 
395
- // Get thread ID from context
396
- const threadId = useThreadId()
341
+ const {
342
+ messages,
343
+ sendMessage,
344
+ stopExecution,
345
+ status,
346
+ isLoading,
347
+ files,
348
+ addFiles,
349
+ removeFile,
350
+ getPreviewUrl,
351
+ } = useThread()
397
352
 
398
- // Subscribe to thread messages (no threadId needed)
399
- const messages = useThread({ useWorkblocks: true })
400
-
401
- // Listen for custom progress events (no threadId needed)
402
- const progress = onThreadEvent<{ step: string; percent: number }>("progress")
353
+ const progress = useThreadEvent<{ step: string; percent: number }>("progress")
403
354
 
404
355
  const handleSend = async () => {
405
356
  if (!input.trim()) return
406
-
407
- setIsExecuting(true)
408
- try {
409
- await sendMessage(threadId, {
410
- role: "user",
411
- content: input,
412
- })
413
- setInput("")
414
- } catch (error) {
415
- console.error("Failed to send message:", error)
416
- } finally {
417
- setIsExecuting(false)
418
- }
419
- }
420
-
421
- const handleStop = async () => {
422
- try {
423
- await stopThread(threadId)
424
- setIsExecuting(false)
425
- } catch (error) {
426
- console.error("Failed to stop thread:", error)
427
- }
357
+ await sendMessage({ role: "user", content: input })
358
+ setInput("")
428
359
  }
429
360
 
430
361
  return (
431
362
  <div>
432
- {/* Progress indicator */}
433
363
  {progress && (
434
364
  <div>
435
365
  <p>{progress.step}</p>
@@ -437,43 +367,39 @@ function AgentChat() {
437
367
  </div>
438
368
  )}
439
369
 
440
- {/* Message list */}
441
- <div>
442
- {messages.map((item) => {
443
- if (item.type === "workblock") {
444
- return (
445
- <div key={item.id}>
446
- <p>Executing tools...</p>
447
- {item.workItems.map((work) => (
448
- <div key={work.id}>
449
- {work.type === "tool_call" && `🔧 ${work.name}`}
450
- {work.type === "tool_result" && `✅ ${work.status}`}
451
- </div>
452
- ))}
453
- </div>
454
- )
455
- }
456
-
457
- return (
458
- <div key={item.id}>
459
- <strong>{item.role}:</strong> {item.content}
460
- </div>
461
- )
462
- })}
463
- </div>
370
+ {isLoading && <p>Loading messages...</p>}
371
+
372
+ {messages.map((msg) => (
373
+ <div key={msg.id}>
374
+ <strong>{msg.role}:</strong> {msg.content}
375
+ </div>
376
+ ))}
377
+
378
+ <input
379
+ type="file"
380
+ multiple
381
+ onChange={(e) => e.target.files && addFiles(e.target.files)}
382
+ />
383
+
384
+ {files.filter(f => f.status !== 'committed').map((file) => (
385
+ <div key={file.id}>
386
+ {file.isImage && getPreviewUrl(file) && (
387
+ <img src={getPreviewUrl(file)!} alt={file.name} width={50} />
388
+ )}
389
+ <span>{file.name} ({file.status})</span>
390
+ <button onClick={() => removeFile(file.id)}>x</button>
391
+ </div>
392
+ ))}
464
393
 
465
- {/* Input */}
466
394
  <div>
467
395
  <input
468
396
  value={input}
469
397
  onChange={(e) => setInput(e.target.value)}
470
398
  onKeyDown={(e) => e.key === "Enter" && handleSend()}
471
- disabled={isExecuting}
472
399
  />
473
- <button onClick={handleSend} disabled={isExecuting}>
474
- Send
475
- </button>
476
- {isExecuting && <button onClick={handleStop}>Stop</button>}
400
+ <button onClick={handleSend}>Send</button>
401
+ <button onClick={stopExecution}>Stop</button>
402
+ <span>Status: {status}</span>
477
403
  </div>
478
404
  </div>
479
405
  )
@@ -484,61 +410,14 @@ function AgentChat() {
484
410
 
485
411
  ## TypeScript Support
486
412
 
487
- The package is written in TypeScript and includes full type definitions.
413
+ The package includes full TypeScript definitions:
488
414
 
489
415
  ```tsx
490
416
  import type {
491
417
  Message,
492
- WorkMessage,
493
- ThreadMessage,
494
418
  SendMessagePayload,
495
- UseThreadOptions,
496
- ThreadProviderOptions,
419
+ ThreadFile,
420
+ ConnectionStatus,
421
+ ThreadContextValue,
497
422
  } from "@standardagents/react"
498
423
  ```
499
-
500
- ---
501
-
502
- ## Local Development
503
-
504
- ### Building the Package
505
-
506
- ```bash
507
- # Install dependencies
508
- pnpm install
509
-
510
- # Build package
511
- pnpm build
512
-
513
- # Watch mode (rebuilds on changes)
514
- pnpm dev
515
-
516
- # Type check
517
- pnpm typecheck
518
-
519
- # Run tests
520
- pnpm test
521
- ```
522
-
523
- ### Linking for Local Development
524
-
525
- Using `file:` protocol in your app's `package.json`:
526
-
527
- ```json
528
- {
529
- "dependencies": {
530
- "@standardagents/react": "file:../path/to/packages/react"
531
- }
532
- }
533
- ```
534
-
535
- Or using `pnpm link`:
536
-
537
- ```bash
538
- # In this package
539
- cd packages/react
540
- pnpm link --global
541
-
542
- # In your app
543
- pnpm link --global @standardagents/react
544
- ```