@assistant-ui/react-ag-ui 0.0.43 → 0.0.45

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.
Files changed (41) hide show
  1. package/dist/agUiExtras.d.ts +0 -1
  2. package/dist/agUiExtras.d.ts.map +1 -1
  3. package/dist/hooks.d.ts +15 -9
  4. package/dist/hooks.d.ts.map +1 -1
  5. package/dist/hooks.js +17 -8
  6. package/dist/hooks.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +26 -4
  10. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  11. package/dist/runtime/AgUiThreadRuntimeCore.js +296 -89
  12. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  13. package/dist/runtime/adapter/conversions.d.ts +0 -1
  14. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  15. package/dist/runtime/adapter/conversions.js +24 -5
  16. package/dist/runtime/adapter/conversions.js.map +1 -1
  17. package/dist/runtime/adapter/run-aggregator.d.ts +0 -1
  18. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  19. package/dist/runtime/adapter/run-aggregator.js +36 -7
  20. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  21. package/dist/runtime/adapter/subscriber.d.ts +0 -1
  22. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.d.ts +0 -1
  24. package/dist/runtime/event-parser.d.ts.map +1 -1
  25. package/dist/runtime/event-parser.js +18 -9
  26. package/dist/runtime/event-parser.js.map +1 -1
  27. package/dist/runtime/logger.d.ts.map +1 -1
  28. package/dist/runtime/types.d.ts +13 -1
  29. package/dist/runtime/types.d.ts.map +1 -1
  30. package/dist/useAgUiRuntime.d.ts +0 -1
  31. package/dist/useAgUiRuntime.d.ts.map +1 -1
  32. package/dist/useAgUiRuntime.js +8 -2
  33. package/dist/useAgUiRuntime.js.map +1 -1
  34. package/package.json +6 -6
  35. package/src/hooks.ts +29 -7
  36. package/src/index.ts +2 -0
  37. package/src/runtime/AgUiThreadRuntimeCore.ts +390 -108
  38. package/src/runtime/adapter/conversions.ts +71 -15
  39. package/src/runtime/adapter/run-aggregator.ts +69 -5
  40. package/src/runtime/types.ts +17 -0
  41. package/src/useAgUiRuntime.ts +8 -1
@@ -1,10 +1,18 @@
1
1
  "use client";
2
2
 
3
3
  import type { InputContent } from "@ag-ui/client";
4
- import type { ThreadMessageLike as CoreThreadMessageLike } from "@assistant-ui/core";
4
+ import type {
5
+ ThreadMessageLike as CoreThreadMessageLike,
6
+ ToolModelContentPart,
7
+ } from "@assistant-ui/core";
5
8
  import { getAutoStatus } from "@assistant-ui/core/internal";
6
9
  import { type Tool, toToolsJSONSchema } from "assistant-stream";
7
10
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
11
+ import {
12
+ AG_UI_METADATA_NAMESPACE,
13
+ type AgUiCustomMetadata,
14
+ } from "./run-aggregator";
15
+ import type { AgUiInterrupt } from "../types";
8
16
 
9
17
  export type { InputContent };
10
18
 
@@ -54,6 +62,7 @@ type ToolCallPart = {
54
62
  args?: ReadonlyJSONObject;
55
63
  result?: unknown;
56
64
  isError?: boolean;
65
+ modelContent?: readonly ToolModelContentPart[];
57
66
  unstable_toolMessageId?: string;
58
67
  };
59
68
 
@@ -440,6 +449,28 @@ function extractAssistantToolCalls(
440
449
  return parts;
441
450
  }
442
451
 
452
+ function validateInterrupts(value: unknown): AgUiInterrupt[] | undefined {
453
+ if (!Array.isArray(value)) return undefined;
454
+ const valid = value.filter(
455
+ (entry): entry is AgUiInterrupt =>
456
+ isObject(entry) &&
457
+ typeof entry.id === "string" &&
458
+ typeof entry.reason === "string",
459
+ );
460
+ return valid.length > 0 ? valid : undefined;
461
+ }
462
+
463
+ function readPersistedInterrupts(
464
+ metadata: unknown,
465
+ ): AgUiInterrupt[] | undefined {
466
+ if (!isObject(metadata)) return undefined;
467
+ const custom = metadata.custom;
468
+ if (!isObject(custom)) return undefined;
469
+ const namespaced = custom[AG_UI_METADATA_NAMESPACE];
470
+ if (!isObject(namespaced)) return undefined;
471
+ return validateInterrupts(namespaced.interrupts);
472
+ }
473
+
443
474
  function toAssistantSnapshotMessage(
444
475
  rawMessage: Record<string, unknown>,
445
476
  ): CoreThreadMessageLike {
@@ -450,11 +481,23 @@ function toAssistantSnapshotMessage(
450
481
  ...toolCallParts,
451
482
  ];
452
483
  const messageName = getString(rawMessage, "name");
484
+ const interrupts = readPersistedInterrupts(rawMessage.metadata);
453
485
  return {
454
486
  id: getString(rawMessage, "id") ?? generateId(),
455
487
  role: "assistant",
456
488
  content: assistantContent.length > 0 ? assistantContent : "",
457
489
  ...(messageName !== undefined ? { name: messageName } : {}),
490
+ ...(interrupts
491
+ ? {
492
+ metadata: {
493
+ custom: {
494
+ [AG_UI_METADATA_NAMESPACE]: {
495
+ interrupts,
496
+ } satisfies AgUiCustomMetadata,
497
+ },
498
+ },
499
+ }
500
+ : {}),
458
501
  };
459
502
  }
460
503
 
@@ -608,19 +651,29 @@ export function fromAgUiMessages(
608
651
 
609
652
  for (let i = 0; i < converted.length; i++) {
610
653
  const message = converted[i]!;
611
- if (message.role !== "assistant" || !Array.isArray(message.content)) {
612
- continue;
613
- }
614
- const hasPendingToolCall = message.content.some(
615
- (part) =>
616
- isObject(part) &&
617
- part.type === "tool-call" &&
618
- part.result === undefined,
619
- );
620
- if (hasPendingToolCall) {
654
+ if (message.role !== "assistant") continue;
655
+
656
+ const hasInterrupt =
657
+ readPersistedInterrupts(message.metadata) !== undefined;
658
+ const hasPendingToolCall =
659
+ Array.isArray(message.content) &&
660
+ message.content.some(
661
+ (part) =>
662
+ isObject(part) &&
663
+ part.type === "tool-call" &&
664
+ part.result === undefined,
665
+ );
666
+
667
+ if (hasInterrupt || hasPendingToolCall) {
621
668
  converted[i] = {
622
669
  ...message,
623
- status: getAutoStatus(false, false, false, true, undefined),
670
+ status: getAutoStatus(
671
+ false,
672
+ false,
673
+ hasInterrupt,
674
+ hasPendingToolCall,
675
+ undefined,
676
+ ),
624
677
  };
625
678
  }
626
679
  }
@@ -666,10 +719,13 @@ function convertAssistantMessage(
666
719
  for (const { id: toolCallId, part } of toolCalls) {
667
720
  if (part.result === undefined) continue;
668
721
 
722
+ const modelText = extractText(part.modelContent);
669
723
  const resultContent =
670
- typeof part.result === "string"
671
- ? part.result
672
- : JSON.stringify(part.result);
724
+ modelText.length > 0
725
+ ? modelText
726
+ : typeof part.result === "string"
727
+ ? part.result
728
+ : JSON.stringify(part.result);
673
729
 
674
730
  const toolMessage: AgUiMessage = {
675
731
  id: part.unstable_toolMessageId ?? `${toolCallId}:tool`,
@@ -6,6 +6,7 @@ import {
6
6
  type MessageTiming,
7
7
  type ThreadAssistantMessagePart,
8
8
  type ToolCallMessagePart,
9
+ type ToolModelContentPart,
9
10
  } from "@assistant-ui/core";
10
11
  import type { AgUiEvent, AgUiInterrupt } from "../types";
11
12
  import type { Logger } from "../logger";
@@ -28,10 +29,16 @@ type ToolCallState = {
28
29
  parentMessageId?: string;
29
30
  toolMessageId?: string;
30
31
  mcpAppResourceUri?: string;
32
+ mcpAppServerId?: string;
33
+ modelContent?: ToolModelContentPart[];
34
+ snapshotResultApplied: boolean;
31
35
  };
32
36
 
33
37
  const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
34
38
 
39
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
40
+ !!value && typeof value === "object" && !Array.isArray(value);
41
+
35
42
  export type RunAggregatorOptions = {
36
43
  showThinking: boolean;
37
44
  logger: Logger;
@@ -236,8 +243,13 @@ export class RunAggregator {
236
243
  }
237
244
  case "ACTIVITY_SNAPSHOT": {
238
245
  if (event.activityType !== MCP_APPS_ACTIVITY_TYPE) break;
239
- const id = this.lastResolvedToolCallId;
240
- const entry = id ? this.toolCalls.get(id) : undefined;
246
+ const toolCallId = event.content.toolCallId;
247
+ const entry =
248
+ typeof toolCallId === "string"
249
+ ? this.toolCalls.get(toolCallId)
250
+ : this.lastResolvedToolCallId
251
+ ? this.toolCalls.get(this.lastResolvedToolCallId)
252
+ : undefined;
241
253
  const resourceUri = event.content.resourceUri;
242
254
  if (
243
255
  entry &&
@@ -245,6 +257,35 @@ export class RunAggregator {
245
257
  isMcpAppUri(resourceUri)
246
258
  ) {
247
259
  entry.mcpAppResourceUri = resourceUri;
260
+ const id = event.content.serverId;
261
+ const hash = event.content.serverHash;
262
+ if (typeof id === "string" && id.length > 0) {
263
+ entry.mcpAppServerId = id;
264
+ } else if (typeof hash === "string" && hash.length > 0) {
265
+ entry.mcpAppServerId = hash;
266
+ }
267
+ const result = event.content.result;
268
+ if (isPlainObject(result)) {
269
+ if (
270
+ entry.result !== undefined &&
271
+ entry.modelContent === undefined
272
+ ) {
273
+ entry.modelContent = [
274
+ {
275
+ type: "text",
276
+ text:
277
+ typeof entry.result === "string"
278
+ ? entry.result
279
+ : JSON.stringify(entry.result),
280
+ },
281
+ ];
282
+ }
283
+ entry.result = result;
284
+ entry.snapshotResultApplied = true;
285
+ if (typeof result.isError === "boolean") {
286
+ entry.isError = result.isError;
287
+ }
288
+ }
248
289
  this.emit();
249
290
  }
250
291
  break;
@@ -343,6 +384,7 @@ export class RunAggregator {
343
384
  parsedArgs: undefined,
344
385
  result: undefined,
345
386
  isError: undefined,
387
+ snapshotResultApplied: false,
346
388
  };
347
389
  if (parentMessageId) {
348
390
  state.parentMessageId = parentMessageId;
@@ -411,6 +453,7 @@ export class RunAggregator {
411
453
  parsedArgs: undefined,
412
454
  result: undefined,
413
455
  isError: undefined,
456
+ snapshotResultApplied: false,
414
457
  };
415
458
  this.toolCalls.set(id, entry);
416
459
  }
@@ -421,8 +464,17 @@ export class RunAggregator {
421
464
  ) {
422
465
  this.partOrder.push({ kind: "tool-call", toolCallId: id });
423
466
  }
424
- entry.result = tryParseJSON(content);
425
- entry.isError = isError;
467
+ if (entry.snapshotResultApplied) {
468
+ if (entry.modelContent === undefined) {
469
+ entry.modelContent = [{ type: "text", text: content }];
470
+ }
471
+ if (entry.isError === undefined) {
472
+ entry.isError = isError;
473
+ }
474
+ } else {
475
+ entry.result = tryParseJSON(content);
476
+ entry.isError = isError;
477
+ }
426
478
  if (toolMessageId) {
427
479
  entry.toolMessageId = toolMessageId;
428
480
  }
@@ -460,9 +512,21 @@ export class RunAggregator {
460
512
  args: (entry.parsedArgs ?? {}) as any,
461
513
  argsText: entry.argsText,
462
514
  ...(entry.result !== undefined ? { result: entry.result } : {}),
515
+ ...(entry.modelContent !== undefined
516
+ ? { modelContent: entry.modelContent }
517
+ : {}),
463
518
  ...(entry.isError !== undefined ? { isError: entry.isError } : {}),
464
519
  ...(entry.mcpAppResourceUri
465
- ? { mcp: { app: { resourceUri: entry.mcpAppResourceUri } } }
520
+ ? {
521
+ mcp: {
522
+ app: {
523
+ resourceUri: entry.mcpAppResourceUri,
524
+ ...(entry.mcpAppServerId
525
+ ? { serverId: entry.mcpAppServerId }
526
+ : {}),
527
+ },
528
+ },
529
+ }
466
530
  : {}),
467
531
  ...(entry.parentMessageId ? { parentId: entry.parentMessageId } : {}),
468
532
  ...(entry.toolMessageId
@@ -59,6 +59,17 @@ export type UseAgUiRuntimeOptions = ExternalStoreSharedOptions & {
59
59
  agent: AbstractAgent;
60
60
  logger?: Partial<Logger>;
61
61
  showThinking?: boolean;
62
+ /**
63
+ * When the user sends, edits, or reloads a message while client-side tool
64
+ * calls are still pending, automatically cancel the unresolved tool calls
65
+ * with an error result so the agent's tool-call accounting stays
66
+ * consistent. Pending AG-UI interrupts are exempt: they still reject the
67
+ * run and must be answered with `useAgUiSubmitInterruptResponses` or
68
+ * discarded with `useAgUiSteerAway`. When disabled, `useAgUiSteerAway`
69
+ * remains the explicit way to cancel pending tool calls.
70
+ * Defaults to `true`.
71
+ */
72
+ autoCancelPendingToolCalls?: boolean | undefined;
62
73
  onError?: (e: Error) => void;
63
74
  onCancel?: () => void;
64
75
  adapters?: UseAgUiRuntimeAdapters;
@@ -95,6 +106,12 @@ export type AgUiRuntimeExtras = {
95
106
  message: CreateAppendMessage,
96
107
  responses?: readonly AgUiResumeEntry[],
97
108
  ) => Promise<void>;
109
+ state: ReadonlyJSONValue | undefined;
110
+ setState: (
111
+ next:
112
+ | ReadonlyJSONValue
113
+ | ((prev: ReadonlyJSONValue | undefined) => ReadonlyJSONValue),
114
+ ) => void;
98
115
  };
99
116
 
100
117
  export type AgUiRunFinishedOutcome =
@@ -58,6 +58,7 @@ export function useAgUiRuntime(
58
58
  agent: options.agent,
59
59
  logger,
60
60
  showThinking: options.showThinking ?? true,
61
+ autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,
61
62
  ...(options.onError && { onError: options.onError }),
62
63
  ...(options.onCancel && { onCancel: options.onCancel }),
63
64
  ...(historyAdapter && { history: historyAdapter }),
@@ -70,6 +71,7 @@ export function useAgUiRuntime(
70
71
  agent: options.agent,
71
72
  logger,
72
73
  showThinking: options.showThinking ?? true,
74
+ autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,
73
75
  ...(options.onError && { onError: options.onError }),
74
76
  ...(options.onCancel && { onCancel: options.onCancel }),
75
77
  ...(historyAdapter && { history: historyAdapter }),
@@ -95,6 +97,7 @@ export function useAgUiRuntime(
95
97
  ? async () => {
96
98
  await onSwitchToNewThread();
97
99
  core.applyExternalMessages([]);
100
+ core.resetState();
98
101
  }
99
102
  : undefined,
100
103
  onSwitchToThread: onSwitchToThread
@@ -106,6 +109,8 @@ export function useAgUiRuntime(
106
109
  core.applyExternalMessages(result.messages);
107
110
  if (result.state !== undefined) {
108
111
  core.loadExternalState(result.state);
112
+ } else {
113
+ core.resetState();
109
114
  }
110
115
  if (result.unstable_resume) {
111
116
  void core.resumeInFlightRun(result.messages);
@@ -136,7 +141,7 @@ export function useAgUiRuntime(
136
141
  return {
137
142
  ...shared,
138
143
  isLoading: core.isLoading,
139
- messages: core.getMessages(),
144
+ messageRepository: core.getMessageRepository(),
140
145
  state: core.getState(),
141
146
  isRunning: core.isRunning() || hasExecutingTools,
142
147
  extras: agUiExtras.provide({
@@ -145,6 +150,8 @@ export function useAgUiRuntime(
145
150
  submitInterruptResponses: (responses) =>
146
151
  core.submitInterruptResponses(responses),
147
152
  steerAway: (message, responses) => core.steerAway(message, responses),
153
+ state: core.getState(),
154
+ setState: (next) => core.setState(next),
148
155
  }),
149
156
  unstable_enableToolInvocations: true,
150
157
  setToolStatuses,