@standardagents/react 0.10.1-dev.616ec2e → 0.10.1-next.bbd142a

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, manage files, and listen for custom events.
3
+ React hooks and components for Standard Agents - connect to AI agent threads with real-time updates, send messages, and listen for custom events.
4
4
 
5
5
  ## Installation
6
6
 
@@ -16,37 +16,41 @@ yarn add @standardagents/react
16
16
 
17
17
  ```tsx
18
18
  import {
19
- AgentBuilderProvider,
19
+ StandardAgentsProvider,
20
20
  ThreadProvider,
21
21
  useThread,
22
+ sendMessage,
22
23
  } from "@standardagents/react"
23
24
 
24
25
  function App() {
25
26
  return (
26
- <AgentBuilderProvider config={{ endpoint: "https://your-api.com" }}>
27
+ <StandardAgentsProvider config={{ endpoint: "https://your-api.com" }}>
27
28
  <ThreadProvider threadId="thread-123">
28
29
  <ChatInterface />
29
30
  </ThreadProvider>
30
- </AgentBuilderProvider>
31
+ </StandardAgentsProvider>
31
32
  )
32
33
  }
33
34
 
34
35
  function ChatInterface() {
35
- const { messages, sendMessage, status } = useThread()
36
+ // No thread ID needed - inherited from ThreadProvider context
37
+ const messages = useThread()
36
38
 
37
39
  const handleSend = async (text: string) => {
38
- await sendMessage({ role: "user", content: text })
40
+ await sendMessage("thread-123", {
41
+ role: "user",
42
+ content: text,
43
+ })
39
44
  }
40
45
 
41
46
  return (
42
47
  <div>
43
- <p>Status: {status}</p>
44
48
  {messages.map((msg) => (
45
49
  <div key={msg.id}>
46
50
  <strong>{msg.role}:</strong> {msg.content}
47
51
  </div>
48
52
  ))}
49
- <input onKeyDown={(e) => e.key === "Enter" && handleSend(e.currentTarget.value)} />
53
+ <input onSubmit={(e) => handleSend(e.currentTarget.value)} />
50
54
  </div>
51
55
  )
52
56
  }
@@ -65,247 +69,292 @@ All API requests and WebSocket connections will automatically include this token
65
69
 
66
70
  ## API Reference
67
71
 
68
- ### `AgentBuilderProvider`
72
+ ### `StandardAgentsProvider`
69
73
 
70
74
  Context provider that configures the Standard Agents client for all child components.
71
75
 
72
76
  **Props:**
73
77
 
74
- - `config.endpoint: string` - The API endpoint URL
78
+ - `config.endpoint: string` - The API endpoint URL (e.g., `https://api.example.com`)
75
79
 
76
80
  ```tsx
77
- <AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
81
+ <StandardAgentsProvider config={{ endpoint: "https://api.example.com" }}>
78
82
  {children}
79
- </AgentBuilderProvider>
83
+ </StandardAgentsProvider>
80
84
  ```
81
85
 
82
86
  ---
83
87
 
84
88
  ### `ThreadProvider`
85
89
 
86
- Context provider that establishes a WebSocket connection to a specific thread. Must be nested inside `AgentBuilderProvider`.
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`.
87
91
 
88
92
  **Props:**
89
93
 
90
94
  - `threadId: string` - The thread ID to connect to
91
95
  - `preload?: boolean` - Fetch existing messages on mount (default: `true`)
92
96
  - `live?: boolean` - Enable WebSocket for real-time updates (default: `true`)
93
- - `useWorkblocks?: boolean` - Transform tool calls into workblocks (default: `false`)
94
97
  - `depth?: number` - Message depth level for nested conversations (default: `0`)
95
98
  - `includeSilent?: boolean` - Include silent messages (default: `false`)
96
99
  - `endpoint?: string` - Override the endpoint from context
97
100
 
98
101
  ```tsx
99
- <AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
100
- <ThreadProvider threadId="thread-123" live={true}>
102
+ <StandardAgentsProvider config={{ endpoint: "https://api.example.com" }}>
103
+ <ThreadProvider threadId="thread-123" live={true} depth={0}>
101
104
  <YourComponents />
102
105
  </ThreadProvider>
103
- </AgentBuilderProvider>
106
+ </StandardAgentsProvider>
104
107
  ```
105
108
 
106
109
  ---
107
110
 
108
- ### `useThread()`
111
+ ### `useThread(options?)`
109
112
 
110
- Hook to access the full thread context. Must be used within a `ThreadProvider`.
113
+ Hook to subscribe to thread messages. Must be used within a `ThreadProvider`.
111
114
 
112
- **Returns:** `ThreadContextValue`
115
+ **Parameters:**
113
116
 
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
117
+ - `options?: UseThreadOptions` - Configuration options
118
+
119
+ **Options:**
120
+
121
+ - `useWorkblocks?: boolean` - Group tool calls into workblocks (default: `true`)
122
+
123
+ **Returns:** `ThreadMessage[]` - Array of messages or workblocks
130
124
 
131
125
  **Example:**
132
126
 
133
127
  ```tsx
134
- function ChatView() {
135
- const {
136
- messages,
137
- sendMessage,
138
- stopExecution,
139
- status,
140
- isLoading,
141
- files,
142
- addFiles,
143
- } = useThread()
128
+ function ThreadView() {
129
+ // Thread ID inherited from ThreadProvider context
130
+ const messages = useThread()
144
131
 
145
132
  return (
146
133
  <div>
147
- <p>Status: {status}</p>
148
- {isLoading && <p>Loading...</p>}
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
+ }
149
143
 
150
- {messages.map((msg) => (
151
- <div key={msg.id}>
152
- <strong>{msg.role}:</strong> {msg.content}
153
- </div>
154
- ))}
144
+ // Disable workblocks transformation
145
+ function RawMessagesView() {
146
+ const messages = useThread({ useWorkblocks: false })
147
+ // Returns raw messages without workblock grouping
148
+ }
149
+ ```
150
+
151
+ ---
155
152
 
156
- <input
157
- type="file"
158
- multiple
159
- onChange={(e) => e.target.files && addFiles(e.target.files)}
160
- />
153
+ ### `useThreadId()`
161
154
 
162
- <div>
163
- {files.map((file) => (
164
- <span key={file.id}>{file.name} ({file.status})</span>
165
- ))}
166
- </div>
155
+ Hook to get the current thread ID from context. Must be used within a `ThreadProvider`.
167
156
 
168
- <button onClick={() => sendMessage({ role: "user", content: "Hello!" })}>Send</button>
169
- <button onClick={stopExecution}>Stop</button>
170
- </div>
171
- )
157
+ **Returns:** `string` - The thread ID
158
+
159
+ **Example:**
160
+
161
+ ```tsx
162
+ function CurrentThreadId() {
163
+ const threadId = useThreadId()
164
+ return <span>Thread: {threadId}</span>
172
165
  }
173
166
  ```
174
167
 
175
168
  ---
176
169
 
177
- ### `useThreadEvent<T>(eventType)`
170
+ ### `sendMessage(threadId, payload, options?)`
178
171
 
179
- Hook to listen for custom events emitted by the agent. Must be used within a `ThreadProvider`.
172
+ Send a message to a thread. This is a standalone function that works outside of React components.
180
173
 
181
174
  **Parameters:**
182
175
 
183
- - `eventType: string` - The custom event type to listen for
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
184
182
 
185
- **Returns:** `T | null` - The latest event value, or `null` if no event received yet
183
+ **Returns:** `Promise<Message>` - The created message
186
184
 
187
185
  **Example:**
188
186
 
189
187
  ```tsx
190
- function TodoProgress() {
191
- const todos = useThreadEvent<{ todos: string[]; completed: number }>("todo-updated")
188
+ import { sendMessage, useThreadId } from "@standardagents/react"
192
189
 
193
- if (!todos) return <div>Waiting for updates...</div>
190
+ function SendButton() {
191
+ const threadId = useThreadId()
194
192
 
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
- )
193
+ const handleSend = async () => {
194
+ await sendMessage(threadId, {
195
+ role: "user",
196
+ content: "Hello, agent!",
197
+ })
198
+ }
199
+
200
+ return <button onClick={handleSend}>Send</button>
205
201
  }
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
+ })
206
215
  ```
207
216
 
208
217
  ---
209
218
 
210
- ### `onThreadEvent<T>(eventType, callback)`
219
+ ### `stopThread(threadId, options?)`
211
220
 
212
- Hook to listen for custom events with a callback. Must be used within a `ThreadProvider`.
221
+ Cancel an in-flight thread execution.
213
222
 
214
223
  **Parameters:**
215
224
 
216
- - `eventType: string` - The custom event type to listen for
217
- - `callback: (data: T) => void` - Called when event is received
225
+ - `threadId: string` - The thread ID
226
+ - `options?: { endpoint?: string }` - Override endpoint
227
+
228
+ **Returns:** `Promise<void>`
218
229
 
219
230
  **Example:**
220
231
 
221
232
  ```tsx
222
- function Notifications() {
223
- onThreadEvent<{ message: string }>("notification", (data) => {
224
- alert(data.message)
225
- })
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
+ }
226
249
 
227
- return null
250
+ return (
251
+ <button onClick={handleStop} disabled={stopping}>
252
+ {stopping ? "Stopping..." : "Stop Execution"}
253
+ </button>
254
+ )
228
255
  }
229
256
  ```
230
257
 
231
258
  ---
232
259
 
233
- ## File Management
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:**
234
269
 
235
- The `useThread()` hook provides file management capabilities:
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:**
236
275
 
237
276
  ```tsx
238
- function FileUploader() {
239
- const { files, addFiles, removeFile, getFileUrl, getPreviewUrl } = useThread()
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>
240
286
 
241
287
  return (
242
288
  <div>
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
- ))}
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>
264
297
  </div>
265
298
  )
266
299
  }
267
300
  ```
268
301
 
269
- ### File States
302
+ **Backend Event Emission:**
270
303
 
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
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
+ ```
275
314
 
276
315
  ---
277
316
 
278
- ## Types
317
+ ## Message Types
318
+
319
+ ### Regular Message
279
320
 
280
321
  ```typescript
281
322
  interface Message {
282
323
  id: string
283
324
  role: "user" | "assistant" | "system" | "tool"
284
325
  content: string | null
285
- created_at: number
286
- attachments?: string // JSON array of AttachmentRef
326
+ created_at: number // microseconds
327
+ reasoning_content?: string | null
328
+ silent?: boolean
329
+ depth?: number
287
330
  }
331
+ ```
288
332
 
289
- interface SendMessagePayload {
290
- role: "user" | "assistant" | "system"
291
- content: string
292
- silent?: boolean
293
- attachments?: string[] // Paths of files to attach
333
+ ### Workblock
334
+
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
294
347
  }
295
348
 
296
- interface ThreadFile {
349
+ interface WorkItem {
297
350
  id: string
351
+ type: "tool_call" | "tool_result"
298
352
  name: 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
353
+ input?: string
354
+ content?: string
355
+ status: "success" | "error" | null
356
+ tool_call_id?: string
306
357
  }
307
-
308
- type ConnectionStatus = "connecting" | "connected" | "disconnected" | "reconnecting"
309
358
  ```
310
359
 
311
360
  ---
@@ -313,53 +362,74 @@ type ConnectionStatus = "connecting" | "connected" | "disconnected" | "reconnect
313
362
  ## Complete Example
314
363
 
315
364
  ```tsx
316
- import { useState, useEffect } from "react"
317
365
  import {
318
- AgentBuilderProvider,
366
+ StandardAgentsProvider,
319
367
  ThreadProvider,
320
368
  useThread,
321
- useThreadEvent,
369
+ useThreadId,
370
+ sendMessage,
371
+ stopThread,
372
+ onThreadEvent,
322
373
  } from "@standardagents/react"
374
+ import { useState, useEffect } from "react"
323
375
 
324
376
  function App() {
377
+ // Set auth token
325
378
  useEffect(() => {
326
379
  localStorage.setItem("standardagents_auth_token", "your-token")
327
380
  }, [])
328
381
 
329
382
  return (
330
- <AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
331
- <ThreadProvider threadId="thread-123">
383
+ <StandardAgentsProvider config={{ endpoint: "https://api.example.com" }}>
384
+ <ThreadProvider threadId="thread-123" live={true}>
332
385
  <AgentChat />
333
386
  </ThreadProvider>
334
- </AgentBuilderProvider>
387
+ </StandardAgentsProvider>
335
388
  )
336
389
  }
337
390
 
338
391
  function AgentChat() {
339
392
  const [input, setInput] = useState("")
393
+ const [isExecuting, setIsExecuting] = useState(false)
340
394
 
341
- const {
342
- messages,
343
- sendMessage,
344
- stopExecution,
345
- status,
346
- isLoading,
347
- files,
348
- addFiles,
349
- removeFile,
350
- getPreviewUrl,
351
- } = useThread()
395
+ // Get thread ID from context
396
+ const threadId = useThreadId()
352
397
 
353
- const progress = useThreadEvent<{ step: string; percent: number }>("progress")
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")
354
403
 
355
404
  const handleSend = async () => {
356
405
  if (!input.trim()) return
357
- await sendMessage({ role: "user", content: input })
358
- setInput("")
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
+ }
359
428
  }
360
429
 
361
430
  return (
362
431
  <div>
432
+ {/* Progress indicator */}
363
433
  {progress && (
364
434
  <div>
365
435
  <p>{progress.step}</p>
@@ -367,39 +437,43 @@ function AgentChat() {
367
437
  </div>
368
438
  )}
369
439
 
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
- ))}
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>
393
464
 
465
+ {/* Input */}
394
466
  <div>
395
467
  <input
396
468
  value={input}
397
469
  onChange={(e) => setInput(e.target.value)}
398
470
  onKeyDown={(e) => e.key === "Enter" && handleSend()}
471
+ disabled={isExecuting}
399
472
  />
400
- <button onClick={handleSend}>Send</button>
401
- <button onClick={stopExecution}>Stop</button>
402
- <span>Status: {status}</span>
473
+ <button onClick={handleSend} disabled={isExecuting}>
474
+ Send
475
+ </button>
476
+ {isExecuting && <button onClick={handleStop}>Stop</button>}
403
477
  </div>
404
478
  </div>
405
479
  )
@@ -410,14 +484,61 @@ function AgentChat() {
410
484
 
411
485
  ## TypeScript Support
412
486
 
413
- The package includes full TypeScript definitions:
487
+ The package is written in TypeScript and includes full type definitions.
414
488
 
415
489
  ```tsx
416
490
  import type {
417
491
  Message,
492
+ WorkMessage,
493
+ ThreadMessage,
418
494
  SendMessagePayload,
419
- ThreadFile,
420
- ConnectionStatus,
421
- ThreadContextValue,
495
+ UseThreadOptions,
496
+ ThreadProviderOptions,
422
497
  } from "@standardagents/react"
423
498
  ```
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
+ ```