@agents24/chat-react 0.5.32 → 0.5.34

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.
@@ -3,7 +3,7 @@ import {
3
3
  createMcpPkceProof,
4
4
  validateMcpCallbackResult,
5
5
  waitForMcpOauthComplete
6
- } from "../chunk-ZCNHLFRB.js";
6
+ } from "../chunk-QG5XSK2Z.js";
7
7
  import {
8
8
  SUPPORTED_INPUT_MIME_TYPES,
9
9
  attachmentFileAccept,
@@ -18,10 +18,155 @@ import {
18
18
  // src/runtime/use-agent-chat-runtime.ts
19
19
  import {
20
20
  useAgentAttachments,
21
- useAgentChat,
22
- useAgentMcp
21
+ useAgentChat
23
22
  } from "@agents24/react";
23
+ import * as React2 from "react";
24
+
25
+ // src/runtime/use-agent-chat-hitl.ts
26
+ import { useAgentMcp } from "@agents24/react";
24
27
  import * as React from "react";
28
+ function detach(operation) {
29
+ operation.abort.abort();
30
+ operation.popup?.close();
31
+ }
32
+ function useAgentChatHitl(options) {
33
+ const mcp = useAgentMcp();
34
+ const latest = React.useRef(options);
35
+ latest.current = options;
36
+ const mounted = React.useRef(true);
37
+ const operations = React.useRef(/* @__PURE__ */ new Map());
38
+ const [states, setStates] = React.useState({});
39
+ React.useEffect(() => {
40
+ mounted.current = true;
41
+ const active = operations.current;
42
+ return () => {
43
+ mounted.current = false;
44
+ for (const operation of active.values()) detach(operation);
45
+ active.clear();
46
+ };
47
+ }, []);
48
+ React.useEffect(() => {
49
+ for (const [key, operation] of operations.current) {
50
+ if (operation.isCurrent()) continue;
51
+ detach(operation);
52
+ operations.current.delete(key);
53
+ setStates((current) => {
54
+ const next = { ...current };
55
+ delete next[key];
56
+ return next;
57
+ });
58
+ }
59
+ });
60
+ React.useEffect(() => {
61
+ setStates({});
62
+ }, [options.client, options.controller.activeThreadId]);
63
+ const onHitlAction = React.useCallback(async (part, response) => {
64
+ const captured = latest.current;
65
+ const { controller, client, bootstrap, selectionGeneration } = captured;
66
+ const key = part.interruptId || part.id;
67
+ if (operations.current.has(key)) return;
68
+ const generation = selectionGeneration.current;
69
+ const threadId = controller.activeThreadId;
70
+ const runId = controller.messages.find((message) => message.parts.includes(part))?.runId || controller.activeRunId;
71
+ const operation = {
72
+ abort: new AbortController(),
73
+ phase: part.hitlKind === "mcp_auth" && response.action === "connect" ? "connecting" : "resuming",
74
+ isCurrent() {
75
+ const current = latest.current;
76
+ return mounted.current && !operation.abort.signal.aborted && current.client === client && current.selectionGeneration.current === generation && current.controller.activeThreadId === threadId && (!current.controller.activeRunId || current.controller.activeRunId === runId) && !["cancelling", "cancelled", "failed", "completed"].includes(current.controller.runState) && (operation.phase === "resuming" || current.controller.messages.some((message) => message.runId === runId && message.parts.some((candidate) => candidate.type === "hitl" && candidate.interruptId === part.interruptId && candidate.status === "pending")));
77
+ }
78
+ };
79
+ if (!operation.isCurrent()) return;
80
+ captured.clearOperationError();
81
+ operations.current.set(key, operation);
82
+ setStates((current) => ({ ...current, [key]: { action: response.action, status: operation.phase } }));
83
+ try {
84
+ if (operation.phase === "connecting") {
85
+ if (!runId) throw new Error("MCP authorization is missing its paused run.");
86
+ if (!bootstrap) throw new Error("MCP authorization requires runtime bootstrap.");
87
+ if (!captured.mcpRedirectUri) throw new Error("mcpRedirectUri is required for MCP Connect.");
88
+ const serverId = String(part.presentation.server_id || "").trim();
89
+ if (!serverId) throw new Error("MCP authorization is missing its server identity.");
90
+ const serverEmbed = bootstrap.authentication.modes.includes("server_embed");
91
+ const popupNonce = browserChatCrypto().randomUUID();
92
+ const popup = window.open("about:blank", "agents24-mcp", "popup,width=560,height=720");
93
+ if (!popup) throw new Error("Allow popups to connect this MCP account.");
94
+ operation.popup = popup;
95
+ popup.document.title = "Connecting to MCP";
96
+ popup.document.body.textContent = "Opening secure connection\u2026";
97
+ const proof = await createMcpPkceProof();
98
+ if (!operation.isCurrent()) return;
99
+ const started = await mcp.startAuthorization({
100
+ runId,
101
+ serverId,
102
+ interruptId: part.interruptId,
103
+ redirectUri: captured.mcpRedirectUri,
104
+ popupNonce,
105
+ codeChallenge: proof.challenge,
106
+ signal: operation.abort.signal
107
+ });
108
+ if (!operation.isCurrent()) return;
109
+ if (!started.callback_origin) throw new Error("MCP authorization did not return a callback origin.");
110
+ const completionPromise = waitForMcpOauthComplete({
111
+ popup,
112
+ callbackOrigin: started.callback_origin,
113
+ popupNonce,
114
+ serverId,
115
+ runId,
116
+ interruptId: part.interruptId,
117
+ signal: operation.abort.signal,
118
+ requireConnectionId: serverEmbed || Boolean(captured.completeMcpAuthorization)
119
+ });
120
+ void completionPromise.catch(() => void 0);
121
+ popup.location.replace(started.authorization_url);
122
+ const completion = await completionPromise;
123
+ if (!operation.isCurrent()) return;
124
+ if (captured.completeMcpAuthorization) {
125
+ await captured.completeMcpAuthorization({ completion, codeVerifier: proof.verifier, serverId });
126
+ } else if (serverEmbed) {
127
+ validateMcpCallbackResult({ server_id: completion.serverId, connection_id: completion.connectionId || "" }, serverId);
128
+ } else {
129
+ const redeemed = await mcp.redeemCallback({
130
+ code: completion.code,
131
+ codeVerifier: proof.verifier,
132
+ signal: operation.abort.signal
133
+ });
134
+ if (!operation.isCurrent()) return;
135
+ validateMcpCallbackResult(redeemed, serverId);
136
+ }
137
+ popup.close();
138
+ }
139
+ if (!operation.isCurrent()) return;
140
+ operation.phase = "resuming";
141
+ setStates((current) => ({ ...current, [key]: { action: response.action, status: "resuming" } }));
142
+ await controller.resumeHitl(part, response);
143
+ } catch (error) {
144
+ if (!operation.isCurrent()) return;
145
+ captured.onError(error);
146
+ setStates((current) => ({ ...current, [key]: {
147
+ action: response.action,
148
+ status: "error",
149
+ error: error instanceof Error ? error.message : String(error)
150
+ } }));
151
+ throw error;
152
+ } finally {
153
+ const ownsState = operation.isCurrent();
154
+ detach(operation);
155
+ if (operations.current.get(key) === operation) {
156
+ operations.current.delete(key);
157
+ if (ownsState) setStates((current) => {
158
+ if (current[key]?.status === "error") return current;
159
+ const next = { ...current };
160
+ delete next[key];
161
+ return next;
162
+ });
163
+ }
164
+ }
165
+ }, [mcp]);
166
+ return { onHitlAction, getHitlActionState: (part) => states[part.interruptId || part.id] };
167
+ }
168
+
169
+ // src/runtime/use-agent-chat-runtime.ts
25
170
  function agentChatRuntimeStateFromError(error) {
26
171
  const typed = error;
27
172
  if (typed?.kind === "authentication") return "expired";
@@ -60,9 +205,6 @@ function modelsFromEffectivePolicy(policy) {
60
205
  models
61
206
  };
62
207
  }
63
- function hitlKey(part) {
64
- return part.interruptId || part.id;
65
- }
66
208
  function useAgentChatRuntime(options) {
67
209
  const {
68
210
  activeThreadId,
@@ -78,43 +220,39 @@ function useAgentChatRuntime(options) {
78
220
  activeThreadId === void 0 ? { client } : { activeThreadId, client }
79
221
  );
80
222
  const attachments = useAgentAttachments();
81
- const mcp = useAgentMcp();
82
- const [bootstrap, setBootstrap] = React.useState(null);
83
- const bootstrapRef = React.useRef(bootstrap);
223
+ const [bootstrap, setBootstrap] = React2.useState(null);
224
+ const bootstrapRef = React2.useRef(bootstrap);
84
225
  bootstrapRef.current = bootstrap;
85
- const [effectivePolicy, setEffectivePolicy] = React.useState(null);
86
- const [isBootstrapping, setIsBootstrapping] = React.useState(true);
87
- const [bootstrapError, setBootstrapError] = React.useState(null);
88
- const [localOperationError, setLocalOperationError] = React.useState(null);
89
- const [selectedModelId, setSelectedModelId] = React.useState(modelId ?? null);
90
- const selectedModelIdRef = React.useRef(selectedModelId);
226
+ const [effectivePolicy, setEffectivePolicy] = React2.useState(null);
227
+ const [isBootstrapping, setIsBootstrapping] = React2.useState(true);
228
+ const [bootstrapError, setBootstrapError] = React2.useState(null);
229
+ const [localOperationError, setLocalOperationError] = React2.useState(null);
230
+ const [selectedModelId, setSelectedModelId] = React2.useState(modelId ?? null);
231
+ const selectedModelIdRef = React2.useRef(selectedModelId);
91
232
  selectedModelIdRef.current = selectedModelId;
92
- const [selectedAgentId, setSelectedAgentId] = React.useState(null);
93
- const selectedAgentIdRef = React.useRef(null);
233
+ const [selectedAgentId, setSelectedAgentId] = React2.useState(null);
234
+ const selectedAgentIdRef = React2.useRef(null);
94
235
  selectedAgentIdRef.current = selectedAgentId;
95
- const [isChangingAgent, setIsChangingAgent] = React.useState(false);
96
- const agentChangePromiseRef = React.useRef(null);
97
- const [isSubmitting, setIsSubmitting] = React.useState(false);
98
- const submitPromiseRef = React.useRef(null);
99
- const [hitlStates, setHitlStates] = React.useState({});
100
- const hitlStatesRef = React.useRef(hitlStates);
101
- hitlStatesRef.current = hitlStates;
102
- const mountedRef = React.useRef(true);
103
- const clientRef = React.useRef(client);
236
+ const [isChangingAgent, setIsChangingAgent] = React2.useState(false);
237
+ const agentChangePromiseRef = React2.useRef(null);
238
+ const [isSubmitting, setIsSubmitting] = React2.useState(false);
239
+ const submitPromiseRef = React2.useRef(null);
240
+ const mountedRef = React2.useRef(true);
241
+ const clientRef = React2.useRef(client);
104
242
  clientRef.current = client;
105
- const bootstrapPromiseRef = React.useRef(null);
106
- React.useEffect(() => {
243
+ const bootstrapPromiseRef = React2.useRef(null);
244
+ React2.useEffect(() => {
107
245
  mountedRef.current = true;
108
246
  return () => {
109
247
  mountedRef.current = false;
110
248
  };
111
249
  }, []);
112
- const previousControlledThreadRef = React.useRef(activeThreadId);
113
- const threadSelectionGenerationRef = React.useRef(0);
114
- const controlledSyncTargetRef = React.useRef(null);
115
- const previousControllerThreadRef = React.useRef(controller.activeThreadId);
116
- const pendingThreadTransitionRef = React.useRef(null);
117
- const lastReportedThreadRef = React.useRef(activeThreadId ?? controller.activeThreadId);
250
+ const previousControlledThreadRef = React2.useRef(activeThreadId);
251
+ const threadSelectionGenerationRef = React2.useRef(0);
252
+ const controlledSyncTargetRef = React2.useRef(null);
253
+ const previousControllerThreadRef = React2.useRef(controller.activeThreadId);
254
+ const pendingThreadTransitionRef = React2.useRef(null);
255
+ const lastReportedThreadRef = React2.useRef(activeThreadId ?? controller.activeThreadId);
118
256
  if (activeThreadId !== previousControlledThreadRef.current) {
119
257
  threadSelectionGenerationRef.current += 1;
120
258
  previousControlledThreadRef.current = activeThreadId;
@@ -123,12 +261,12 @@ function useAgentChatRuntime(options) {
123
261
  lastReportedThreadRef.current = activeThreadId;
124
262
  }
125
263
  }
126
- const reportActiveThread = React.useCallback((threadId, reason) => {
264
+ const reportActiveThread = React2.useCallback((threadId, reason) => {
127
265
  if (lastReportedThreadRef.current === threadId) return;
128
266
  lastReportedThreadRef.current = threadId;
129
267
  onActiveThreadIdChange?.({ threadId, reason });
130
268
  }, [onActiveThreadIdChange]);
131
- const retryBootstrap = React.useCallback(() => {
269
+ const retryBootstrap = React2.useCallback(() => {
132
270
  if (bootstrapPromiseRef.current?.client === client) return bootstrapPromiseRef.current.promise;
133
271
  const requestClient = client;
134
272
  const request = (async () => {
@@ -166,7 +304,7 @@ function useAgentChatRuntime(options) {
166
304
  });
167
305
  return request;
168
306
  }, [client, controller.refreshThreads, modelId]);
169
- const refreshEffectivePolicy = React.useCallback(async () => {
307
+ const refreshEffectivePolicy = React2.useCallback(async () => {
170
308
  if (bootstrap?.features.agents) {
171
309
  const refreshed = await client.bootstrap({ agentId: selectedAgentIdRef.current || void 0 });
172
310
  if (!mountedRef.current || clientRef.current !== client) return;
@@ -183,22 +321,18 @@ function useAgentChatRuntime(options) {
183
321
  setSelectedModelId((current) => available.models.some((item) => item.id === current) ? current : available.defaultModelId);
184
322
  }
185
323
  }, [bootstrap, client, modelId]);
186
- React.useEffect(() => {
324
+ React2.useEffect(() => {
187
325
  void retryBootstrap();
188
326
  }, [retryBootstrap]);
189
- React.useEffect(() => {
327
+ React2.useEffect(() => {
190
328
  if (modelId !== void 0) setSelectedModelId(modelId);
191
329
  }, [modelId]);
192
- React.useEffect(() => {
193
- hitlStatesRef.current = {};
194
- setHitlStates({});
195
- }, [client, controller.activeThreadId]);
196
- React.useEffect(() => {
330
+ React2.useEffect(() => {
197
331
  if (controlledSyncTargetRef.current?.value === null && controller.activeThreadId) {
198
332
  controller.startNewThread();
199
333
  }
200
334
  }, [activeThreadId, controller.activeThreadId, controller.startNewThread]);
201
- React.useEffect(() => {
335
+ React2.useEffect(() => {
202
336
  const previous = previousControllerThreadRef.current;
203
337
  const current = controller.activeThreadId;
204
338
  previousControllerThreadRef.current = current;
@@ -211,11 +345,11 @@ function useAgentChatRuntime(options) {
211
345
  if (pendingTransition && current === pendingTransition.value) return;
212
346
  if (previous === null && current !== null) reportActiveThread(current, "created");
213
347
  }, [activeThreadId, controller.activeThreadId, reportActiveThread]);
214
- const clearOperationError = React.useCallback(() => {
348
+ const clearOperationError = React2.useCallback(() => {
215
349
  setLocalOperationError(null);
216
350
  controller.clearOperationError();
217
351
  }, [controller.clearOperationError]);
218
- const submit = React.useCallback((input) => {
352
+ const submit = React2.useCallback((input) => {
219
353
  if (submitPromiseRef.current) return submitPromiseRef.current;
220
354
  clearOperationError();
221
355
  setIsSubmitting(true);
@@ -256,86 +390,24 @@ function useAgentChatRuntime(options) {
256
390
  void request.then(clearRequest, clearRequest);
257
391
  return request;
258
392
  }, [bootstrap, clearOperationError, controller.activeThreadId, controller.submit, refreshEffectivePolicy]);
259
- const completeMcp = React.useCallback(async (part) => {
260
- if (!mcpRedirectUri) throw new Error("mcpRedirectUri is required for MCP Connect.");
261
- const serverId = String(part.presentation.server_id || "").trim();
262
- if (!serverId) throw new Error("MCP authorization is missing its server identity.");
263
- const runId = controller.activeRunId || controller.messages.find((message) => message.parts.includes(part))?.runId;
264
- if (!runId) throw new Error("MCP authorization is missing its paused run.");
265
- const popupNonce = browserChatCrypto().randomUUID();
266
- const proof = await createMcpPkceProof();
267
- const popup = window.open("about:blank", "agents24-mcp", "popup,width=560,height=720");
268
- if (!popup) throw new Error("Allow popups to connect this MCP account.");
269
- popup.document.title = "Connecting to MCP";
270
- popup.document.body.textContent = "Opening secure connection\u2026";
271
- try {
272
- const started = await mcp.startAuthorization({
273
- runId,
274
- serverId,
275
- interruptId: part.interruptId,
276
- redirectUri: mcpRedirectUri,
277
- popupNonce,
278
- codeChallenge: proof.challenge
279
- });
280
- if (!started.callback_origin) throw new Error("MCP authorization did not return a callback origin.");
281
- popup.location.replace(started.authorization_url);
282
- const completion = await waitForMcpOauthComplete({
283
- popup,
284
- callbackOrigin: started.callback_origin,
285
- popupNonce,
286
- requireConnectionId: Boolean(completeMcpAuthorization),
287
- serverId
288
- });
289
- if (completeMcpAuthorization) {
290
- await completeMcpAuthorization({ completion, codeVerifier: proof.verifier, serverId });
291
- } else {
292
- const redeemed = await mcp.redeemCallback({ code: completion.code, codeVerifier: proof.verifier });
293
- validateMcpCallbackResult(redeemed, serverId);
294
- }
295
- } finally {
296
- popup.close();
297
- }
298
- }, [completeMcpAuthorization, controller.activeRunId, controller.messages, mcp, mcpRedirectUri]);
299
- const onHitlAction = React.useCallback(async (part, response) => {
300
- const action = response.action;
301
- const key = hitlKey(part);
302
- const current = hitlStatesRef.current[key];
303
- if (current?.status === "connecting" || current?.status === "resuming") return;
304
- clearOperationError();
305
- const pending = {
306
- action,
307
- status: part.hitlKind === "mcp_auth" && action === "connect" ? "connecting" : "resuming"
308
- };
309
- hitlStatesRef.current = { ...hitlStatesRef.current, [key]: pending };
310
- setHitlStates(hitlStatesRef.current);
311
- try {
312
- if (part.hitlKind === "mcp_auth" && action === "connect") await completeMcp(part);
313
- await controller.resumeHitl(part, response);
314
- setHitlStates((states) => {
315
- const next = { ...states };
316
- delete next[key];
317
- hitlStatesRef.current = next;
318
- return next;
319
- });
320
- } catch (error) {
321
- setLocalOperationError(error);
322
- const failedState = {
323
- ...hitlStatesRef.current,
324
- [key]: { action, error: agentChatRuntimeErrorMessage(error), status: "error" }
325
- };
326
- hitlStatesRef.current = failedState;
327
- setHitlStates(failedState);
328
- throw error;
329
- }
330
- }, [clearOperationError, completeMcp, controller.resumeHitl]);
331
- const newThread = React.useCallback(() => {
393
+ const { onHitlAction, getHitlActionState } = useAgentChatHitl({
394
+ client,
395
+ bootstrap,
396
+ controller,
397
+ selectionGeneration: threadSelectionGenerationRef,
398
+ completeMcpAuthorization,
399
+ mcpRedirectUri,
400
+ clearOperationError,
401
+ onError: setLocalOperationError
402
+ });
403
+ const newThread = React2.useCallback(() => {
332
404
  threadSelectionGenerationRef.current += 1;
333
405
  clearOperationError();
334
406
  pendingThreadTransitionRef.current = null;
335
407
  reportActiveThread(null, "new");
336
408
  controller.startNewThread();
337
409
  }, [clearOperationError, controller.startNewThread, reportActiveThread]);
338
- const openThread = React.useCallback(async (threadId) => {
410
+ const openThread = React2.useCallback(async (threadId) => {
339
411
  const requestGeneration = ++threadSelectionGenerationRef.current;
340
412
  clearOperationError();
341
413
  pendingThreadTransitionRef.current = { reason: "opened", value: threadId };
@@ -365,7 +437,7 @@ function useAgentChatRuntime(options) {
365
437
  if (requestGeneration === threadSelectionGenerationRef.current) pendingThreadTransitionRef.current = null;
366
438
  }
367
439
  }, [clearOperationError, client, controller.openThread, reportActiveThread]);
368
- const deleteThread = React.useCallback(async (threadId) => {
440
+ const deleteThread = React2.useCallback(async (threadId) => {
369
441
  clearOperationError();
370
442
  const wasActive = controller.activeThreadId === threadId;
371
443
  if (wasActive) threadSelectionGenerationRef.current += 1;
@@ -382,7 +454,7 @@ function useAgentChatRuntime(options) {
382
454
  }
383
455
  }
384
456
  }, [clearOperationError, controller.activeThreadId, controller.deleteThread, reportActiveThread]);
385
- const changeModel = React.useCallback((nextModelId) => {
457
+ const changeModel = React2.useCallback((nextModelId) => {
386
458
  if (modelId === void 0) setSelectedModelId(nextModelId);
387
459
  onModelChange?.(nextModelId);
388
460
  if (controller.activeThreadId && selectedAgentId && client.threads.setRuntimeSelection) {
@@ -393,7 +465,7 @@ function useAgentChatRuntime(options) {
393
465
  }).catch(setLocalOperationError);
394
466
  }
395
467
  }, [client.threads, controller.activeThreadId, modelId, onModelChange, selectedAgentId]);
396
- const resolveAttachmentContent = React.useCallback(async (attachment, signal) => {
468
+ const resolveAttachmentContent = React2.useCallback(async (attachment, signal) => {
397
469
  const attachmentId = typeof attachment.id === "string" ? attachment.id.trim() : "";
398
470
  if (!attachmentId) throw new Error("Attachment content identity is unavailable.");
399
471
  const access = await attachments.createContentAccess({
@@ -403,7 +475,7 @@ function useAgentChatRuntime(options) {
403
475
  });
404
476
  return { url: access.url, validUntil: access.valid_until || null };
405
477
  }, [attachments.createContentAccess]);
406
- const changeAgent = React.useCallback((nextAgentId) => {
478
+ const changeAgent = React2.useCallback((nextAgentId) => {
407
479
  if (agentChangePromiseRef.current) return agentChangePromiseRef.current;
408
480
  if (nextAgentId === selectedAgentIdRef.current || !canChangeAgentRuntime(controller.runState, controller.activeRunId)) return Promise.resolve();
409
481
  clearOperationError();
@@ -470,7 +542,7 @@ function useAgentChatRuntime(options) {
470
542
  deleteThread,
471
543
  errorMessage: agentChatRuntimeErrorMessage(fatalError || operationError),
472
544
  fatalError,
473
- getHitlActionState: (part) => hitlStates[hitlKey(part)],
545
+ getHitlActionState,
474
546
  isBootstrapping,
475
547
  isChangingAgent,
476
548
  isSubmitting: isSubmitting || controller.isSubmitting,
@@ -489,11 +561,11 @@ function useAgentChatRuntime(options) {
489
561
  }
490
562
 
491
563
  // src/runtime/use-delegation-tasks.ts
492
- import * as React2 from "react";
564
+ import * as React3 from "react";
493
565
  function useDelegationTasks({ threadId, source, pollIntervalMs = 2e3 }) {
494
- const [state, setState] = React2.useState({ threadId: null, tasks: [], error: null, loading: false });
495
- const [revision, refresh] = React2.useReducer((value) => value + 1, 0);
496
- React2.useEffect(() => {
566
+ const [state, setState] = React3.useState({ threadId: null, tasks: [], error: null, loading: false });
567
+ const [revision, refresh] = React3.useReducer((value) => value + 1, 0);
568
+ React3.useEffect(() => {
497
569
  if (!threadId) {
498
570
  setState({ threadId: null, tasks: [], error: null, loading: false });
499
571
  return;