@assistant-ui/react-ag-ui 0.0.69 → 0.0.71

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 (30) hide show
  1. package/README.md +1 -1
  2. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +6 -3
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  4. package/dist/runtime/AgUiThreadRuntimeCore.js +77 -10
  5. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  6. package/dist/runtime/adapter/conversions.d.ts +5 -0
  7. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  8. package/dist/runtime/adapter/conversions.js +104 -15
  9. package/dist/runtime/adapter/conversions.js.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.d.ts +3 -0
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +4 -2
  13. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  14. package/dist/runtime/types.d.ts +18 -2
  15. package/dist/runtime/types.d.ts.map +1 -1
  16. package/dist/runtime/types.js.map +1 -1
  17. package/dist/useAgUiRuntime.d.ts.map +1 -1
  18. package/dist/useAgUiRuntime.js +4 -0
  19. package/dist/useAgUiRuntime.js.map +1 -1
  20. package/package.json +5 -5
  21. package/src/runtime/AgUiThreadRuntimeCore.interrupt-cancel.test.ts +202 -0
  22. package/src/runtime/AgUiThreadRuntimeCore.ts +143 -8
  23. package/src/runtime/adapter/conversions.metadata.test.ts +400 -0
  24. package/src/runtime/adapter/conversions.ts +170 -28
  25. package/src/runtime/adapter/run-aggregator.approval.test.ts +4 -4
  26. package/src/runtime/adapter/run-aggregator.ts +13 -2
  27. package/src/runtime/types.ts +20 -1
  28. package/src/useAgUiRuntime.context.test.tsx +227 -0
  29. package/src/useAgUiRuntime.external-store.test.tsx +39 -0
  30. package/src/useAgUiRuntime.ts +4 -0
@@ -69,6 +69,7 @@ export type AgUiMessage =
69
69
  role: "user";
70
70
  content: string | InputContent[];
71
71
  name?: string;
72
+ metadata?: Record<string, unknown>;
72
73
  }
73
74
  | {
74
75
  id: string;
@@ -76,18 +77,21 @@ export type AgUiMessage =
76
77
  content: string;
77
78
  name?: string;
78
79
  toolCalls?: AgUiToolCall[];
80
+ metadata?: Record<string, unknown>;
79
81
  }
80
82
  | {
81
83
  id: string;
82
84
  role: "system" | "developer";
83
85
  content: string;
84
86
  name?: string;
87
+ metadata?: Record<string, unknown>;
85
88
  }
86
89
  | {
87
90
  id: string;
88
91
  role: "reasoning";
89
92
  content: string;
90
93
  encryptedValue?: string;
94
+ metadata?: Record<string, unknown>;
91
95
  }
92
96
  | {
93
97
  id: string;
@@ -95,6 +99,7 @@ export type AgUiMessage =
95
99
  content: string;
96
100
  toolCallId: string;
97
101
  error?: string;
102
+ metadata?: Record<string, unknown>;
98
103
  };
99
104
 
100
105
  type CoreToolCallPart = Extract<
@@ -117,6 +122,108 @@ const getString = (record: Record<string, unknown>, key: string) => {
117
122
  return typeof value === "string" ? value : undefined;
118
123
  };
119
124
 
125
+ function toAgUiWireMetadata(
126
+ metadata: unknown,
127
+ ): Record<string, unknown> | undefined {
128
+ return isObject(metadata) ? { ...metadata } : undefined;
129
+ }
130
+
131
+ function readLegacyAgUiNamespace(metadata: unknown): Record<string, unknown> {
132
+ if (!isObject(metadata) || !isObject(metadata.custom)) return {};
133
+ const namespaced = metadata.custom[AG_UI_METADATA_NAMESPACE];
134
+ if (!isObject(namespaced)) return {};
135
+ return "interrupts" in namespaced
136
+ ? { interrupts: namespaced.interrupts }
137
+ : {};
138
+ }
139
+
140
+ function withoutPersistedInterrupts(
141
+ wire: Record<string, unknown>,
142
+ ): Record<string, unknown> {
143
+ if (!isObject(wire.custom)) return wire;
144
+ const namespaced = wire.custom[AG_UI_METADATA_NAMESPACE];
145
+ if (!isObject(namespaced) || !("interrupts" in namespaced)) return wire;
146
+
147
+ const { interrupts: _interrupts, ...remainingNamespace } = namespaced;
148
+ const { [AG_UI_METADATA_NAMESPACE]: _namespace, ...remainingCustom } =
149
+ wire.custom;
150
+ if (Object.keys(remainingNamespace).length > 0) {
151
+ remainingCustom[AG_UI_METADATA_NAMESPACE] = remainingNamespace;
152
+ }
153
+ const { custom: _custom, ...remainingWire } = wire;
154
+ return Object.keys(remainingCustom).length > 0
155
+ ? { ...remainingWire, custom: remainingCustom }
156
+ : remainingWire;
157
+ }
158
+
159
+ function toSnapshotCustom(
160
+ metadata: unknown,
161
+ state: Record<string, unknown> = {},
162
+ ): Record<string, unknown> {
163
+ const wire = withoutPersistedInterrupts(toAgUiWireMetadata(metadata) ?? {});
164
+ const needsWireSlot =
165
+ isObject(metadata) &&
166
+ (Object.keys(metadata).length === 0 || AG_UI_METADATA_NAMESPACE in wire);
167
+ const namespaced = {
168
+ ...readLegacyAgUiNamespace(metadata),
169
+ ...state,
170
+ ...(needsWireSlot ? { messageMetadata: wire } : {}),
171
+ };
172
+ return {
173
+ ...wire,
174
+ ...(Object.keys(namespaced).length > 0
175
+ ? { [AG_UI_METADATA_NAMESPACE]: namespaced }
176
+ : {}),
177
+ };
178
+ }
179
+
180
+ function toAgUiMessageMetadata(
181
+ metadata: unknown,
182
+ ): Record<string, unknown> | undefined {
183
+ const custom = readCustomMetadata(metadata);
184
+ const namespaced = readNamespacedMetadata(metadata);
185
+ if (isObject(namespaced.messageMetadata)) {
186
+ return { ...namespaced.messageMetadata };
187
+ }
188
+ const { [AG_UI_METADATA_NAMESPACE]: _state, ...wire } = custom;
189
+ return Object.keys(wire).length > 0 ? wire : undefined;
190
+ }
191
+
192
+ function withAgUiMessageMetadata(metadata: unknown): {
193
+ metadata?: Record<string, unknown>;
194
+ } {
195
+ const messageMetadata = toAgUiMessageMetadata(metadata);
196
+ return messageMetadata !== undefined ? { metadata: messageMetadata } : {};
197
+ }
198
+
199
+ function withAgUiPartMessageMetadata(
200
+ providerMetadata: PartProviderMetadata | undefined,
201
+ messageMetadata: Record<string, unknown> | undefined,
202
+ ): PartProviderMetadata {
203
+ if (!messageMetadata) return providerMetadata ?? {};
204
+
205
+ const existing = providerMetadata ?? {};
206
+ const namespaced = isObject(existing[AG_UI_METADATA_NAMESPACE])
207
+ ? existing[AG_UI_METADATA_NAMESPACE]
208
+ : {};
209
+ return {
210
+ ...existing,
211
+ [AG_UI_METADATA_NAMESPACE]: {
212
+ ...namespaced,
213
+ messageMetadata: messageMetadata as ReadonlyJSONObject,
214
+ },
215
+ };
216
+ }
217
+
218
+ function readAgUiPartMessageMetadata(
219
+ providerMetadata: unknown,
220
+ ): Record<string, unknown> | undefined {
221
+ const namespaced = readAgUiNamespace(providerMetadata);
222
+ return namespaced && isObject(namespaced.messageMetadata)
223
+ ? toAgUiWireMetadata(namespaced.messageMetadata)
224
+ : undefined;
225
+ }
226
+
120
227
  const getToolCallId = (record: Record<string, unknown>) =>
121
228
  getString(record, "toolCallId") ?? getString(record, "tool_call_id");
122
229
 
@@ -132,6 +239,7 @@ const readAgUiReasoningMeta = (providerMetadata: unknown) => {
132
239
  return {
133
240
  encryptedValue: getString(namespaced, "encryptedValue"),
134
241
  reasoningId: getString(namespaced, "reasoningId"),
242
+ messageMetadata: toAgUiWireMetadata(namespaced.messageMetadata),
135
243
  };
136
244
  };
137
245
 
@@ -600,8 +708,14 @@ function readOpaqueReasoning(metadata: unknown): AgUiOpaqueReasoning[] {
600
708
  const id = getString(entry, "id");
601
709
  const encryptedValue = getString(entry, "encryptedValue");
602
710
  if (!id?.trim() || !encryptedValue?.trim()) return [];
711
+ const metadata = toAgUiWireMetadata(entry.metadata);
603
712
  return [
604
- { id, encryptedValue, ...(entry.after === true ? { after: true } : {}) },
713
+ {
714
+ id,
715
+ encryptedValue,
716
+ ...(metadata ? { metadata } : {}),
717
+ ...(entry.after === true ? { after: true } : {}),
718
+ },
605
719
  ];
606
720
  });
607
721
  }
@@ -631,7 +745,8 @@ function toAssistantSnapshotMessage(
631
745
  rawMessage: Record<string, unknown>,
632
746
  ): CoreThreadMessageLike {
633
747
  const text = extractText(rawMessage.content);
634
- const interrupts = readPersistedInterrupts(rawMessage.metadata);
748
+ const custom = toSnapshotCustom(rawMessage.metadata);
749
+ const interrupts = readPersistedInterrupts({ custom });
635
750
  const restoredToolCalls = extractAssistantToolCalls(rawMessage);
636
751
  const approvals = projectAgUiToolApprovals(
637
752
  interrupts,
@@ -652,21 +767,24 @@ function toAssistantSnapshotMessage(
652
767
  ...toolCallParts,
653
768
  ];
654
769
  const messageName = getString(rawMessage, "name");
770
+ const namespaced = isObject(custom[AG_UI_METADATA_NAMESPACE])
771
+ ? { ...custom[AG_UI_METADATA_NAMESPACE] }
772
+ : {};
773
+ if (interrupts) namespaced.interrupts = interrupts;
774
+ else delete namespaced.interrupts;
775
+ const messageCustom = { ...custom };
776
+ if (Object.keys(namespaced).length > 0) {
777
+ messageCustom[AG_UI_METADATA_NAMESPACE] = namespaced;
778
+ } else {
779
+ delete messageCustom[AG_UI_METADATA_NAMESPACE];
780
+ }
655
781
  return {
656
782
  id: getString(rawMessage, "id") ?? generateId(),
657
783
  role: "assistant",
658
784
  content: assistantContent.length > 0 ? assistantContent : "",
659
785
  ...(messageName !== undefined ? { name: messageName } : {}),
660
- ...(interrupts
661
- ? {
662
- metadata: {
663
- custom: {
664
- [AG_UI_METADATA_NAMESPACE]: {
665
- interrupts,
666
- } satisfies AgUiCustomMetadata,
667
- },
668
- },
669
- }
786
+ ...(Object.keys(messageCustom).length > 0
787
+ ? { metadata: { custom: messageCustom } }
670
788
  : {}),
671
789
  };
672
790
  }
@@ -676,6 +794,10 @@ function toUserOrSystemSnapshotMessage(
676
794
  rawMessage: Record<string, unknown>,
677
795
  ): CoreThreadMessageLike {
678
796
  const messageName = getString(rawMessage, "name");
797
+ const custom = toSnapshotCustom(
798
+ rawMessage.metadata,
799
+ role === "developer" ? { role: "developer" } : {},
800
+ );
679
801
  const attachments =
680
802
  role === "user" ? toSnapshotAttachments(rawMessage.content) : [];
681
803
  return {
@@ -686,17 +808,7 @@ function toUserOrSystemSnapshotMessage(
686
808
  content: extractText(rawMessage.content),
687
809
  ...(messageName !== undefined ? { name: messageName } : {}),
688
810
  ...(attachments.length > 0 ? { attachments } : {}),
689
- ...(role === "developer"
690
- ? {
691
- metadata: {
692
- custom: {
693
- [AG_UI_METADATA_NAMESPACE]: {
694
- role: "developer",
695
- } satisfies AgUiCustomMetadata,
696
- },
697
- },
698
- }
699
- : {}),
811
+ ...(Object.keys(custom).length > 0 ? { metadata: { custom } } : {}),
700
812
  };
701
813
  }
702
814
 
@@ -941,6 +1053,7 @@ export function fromAgUiMessages(
941
1053
  const mcpAppUri = readMcpAppResourceUri(mcpResult?._meta);
942
1054
  const mcpApp =
943
1055
  mcpAppUri !== undefined ? { resourceUri: mcpAppUri } : undefined;
1056
+ const messageMetadata = toAgUiWireMetadata(rawMessage.metadata);
944
1057
 
945
1058
  let updated = false;
946
1059
  for (
@@ -974,6 +1087,14 @@ export function fromAgUiMessages(
974
1087
  ? { unstable_toolMessageId: toolMessageId }
975
1088
  : {}),
976
1089
  ...(mcpApp ? { mcp: { app: mcpApp } } : {}),
1090
+ ...(messageMetadata
1091
+ ? {
1092
+ providerMetadata: withAgUiPartMessageMetadata(
1093
+ (part as ToolCallPart).providerMetadata,
1094
+ messageMetadata,
1095
+ ),
1096
+ }
1097
+ : {}),
977
1098
  };
978
1099
  const updatedContent = message.content.map((contentPart, index) =>
979
1100
  index === partIndex ? updatedPart : contentPart,
@@ -1010,6 +1131,14 @@ export function fromAgUiMessages(
1010
1131
  ? { unstable_toolMessageId: toolMessageId }
1011
1132
  : {}),
1012
1133
  ...(mcpApp ? { mcp: { app: mcpApp } } : {}),
1134
+ ...(messageMetadata
1135
+ ? {
1136
+ providerMetadata: withAgUiPartMessageMetadata(
1137
+ undefined,
1138
+ messageMetadata,
1139
+ ),
1140
+ }
1141
+ : {}),
1013
1142
  },
1014
1143
  ],
1015
1144
  });
@@ -1108,6 +1237,7 @@ export function fromAgUiMessages(
1108
1237
  if (role === "reasoning") {
1109
1238
  const text = extractText(rawMessage.content);
1110
1239
  const encryptedValue = getString(rawMessage, "encryptedValue");
1240
+ const messageMetadata = toAgUiWireMetadata(rawMessage.metadata);
1111
1241
  if (text.trim().length === 0) {
1112
1242
  // showThinking hides reasoning; this record is never rendered anyway,
1113
1243
  // and dropping it would deny the provider the payload it needs to
@@ -1120,6 +1250,7 @@ export function fromAgUiMessages(
1120
1250
  opaqueReasoning.push({
1121
1251
  id: opaqueId,
1122
1252
  encryptedValue,
1253
+ ...(messageMetadata ? { metadata: messageMetadata } : {}),
1123
1254
  anchor: converted.length,
1124
1255
  });
1125
1256
  }
@@ -1135,6 +1266,7 @@ export function fromAgUiMessages(
1135
1266
  opaqueReasoning.push({
1136
1267
  id: hiddenId,
1137
1268
  encryptedValue,
1269
+ ...(messageMetadata ? { metadata: messageMetadata } : {}),
1138
1270
  anchor: converted.length,
1139
1271
  });
1140
1272
  }
@@ -1149,6 +1281,9 @@ export function fromAgUiMessages(
1149
1281
  const meta = {
1150
1282
  ...(reasoningId !== undefined ? { reasoningId } : {}),
1151
1283
  ...(encryptedValue !== undefined ? { encryptedValue } : {}),
1284
+ ...(messageMetadata
1285
+ ? { messageMetadata: messageMetadata as ReadonlyJSONObject }
1286
+ : {}),
1152
1287
  };
1153
1288
  pendingReasoning.push({
1154
1289
  id: reasoningId,
@@ -1265,9 +1400,8 @@ function convertAssistantMessage(
1265
1400
  if (!isObject(part) || part.type !== "reasoning") continue;
1266
1401
  const text = getString(part, "text") ?? "";
1267
1402
  if (text.trim().length === 0) continue;
1268
- const { encryptedValue, reasoningId } = readAgUiReasoningMeta(
1269
- part.providerMetadata,
1270
- );
1403
+ const { encryptedValue, reasoningId, messageMetadata } =
1404
+ readAgUiReasoningMeta(part.providerMetadata);
1271
1405
  converted.push({
1272
1406
  // The wire id is what a signature was issued against, so it wins over a
1273
1407
  // synthesized one whenever the run carried it.
@@ -1279,12 +1413,12 @@ function convertAssistantMessage(
1279
1413
  role: "reasoning",
1280
1414
  content: text,
1281
1415
  ...(encryptedValue !== undefined ? { encryptedValue } : {}),
1416
+ ...(messageMetadata ? { metadata: messageMetadata } : {}),
1282
1417
  });
1283
1418
  reasoningIndex++;
1284
1419
  }
1285
1420
 
1286
- // Drop assistant messages with no text or tool calls (e.g. an imported
1287
- // reasoning-only entry) so they are not re-sent as a blank assistant turn.
1421
+ // Some providers reject a replayed blank assistant turn, even when it carries metadata.
1288
1422
  if (shellIsDropped) {
1289
1423
  return;
1290
1424
  }
@@ -1319,6 +1453,7 @@ function convertAssistantMessage(
1319
1453
  role: "assistant",
1320
1454
  content,
1321
1455
  ...(message.name ? { name: message.name } : {}),
1456
+ ...withAgUiMessageMetadata(message.metadata),
1322
1457
  ...(toolCalls.length + nestedToolCalls.length > 0
1323
1458
  ? {
1324
1459
  toolCalls: [...toolCalls, ...nestedToolCalls].map(
@@ -1356,6 +1491,7 @@ function emitToolResult(
1356
1491
  : typeof part.result === "string"
1357
1492
  ? part.result
1358
1493
  : JSON.stringify(part.result);
1494
+ const messageMetadata = readAgUiPartMessageMetadata(part.providerMetadata);
1359
1495
 
1360
1496
  converted.push({
1361
1497
  id: part.unstable_toolMessageId ?? `${toolCallId}:tool`,
@@ -1363,6 +1499,7 @@ function emitToolResult(
1363
1499
  content: resultContent,
1364
1500
  toolCallId,
1365
1501
  ...(part.isError ? { error: resultContent } : {}),
1502
+ ...(messageMetadata ? { metadata: messageMetadata } : {}),
1366
1503
  });
1367
1504
  }
1368
1505
 
@@ -1379,6 +1516,7 @@ function convertToolMessage(
1379
1516
  content,
1380
1517
  toolCallId,
1381
1518
  ...(typeof message.error === "string" ? { error: message.error } : {}),
1519
+ ...withAgUiMessageMetadata(message.metadata),
1382
1520
  });
1383
1521
  }
1384
1522
 
@@ -1398,6 +1536,7 @@ export function toAgUiMessages(
1398
1536
  role: "reasoning",
1399
1537
  content: "",
1400
1538
  encryptedValue: entry.encryptedValue,
1539
+ ...(entry.metadata ? { metadata: entry.metadata } : {}),
1401
1540
  });
1402
1541
  for (const entry of opaqueReasoning) {
1403
1542
  if (entry.after !== true) converted.push(toOpaqueRecord(entry));
@@ -1426,6 +1565,7 @@ export function toAgUiMessages(
1426
1565
  role: "user",
1427
1566
  content: buildUserContent(message),
1428
1567
  ...(message.name ? { name: message.name } : {}),
1568
+ ...withAgUiMessageMetadata(message.metadata),
1429
1569
  });
1430
1570
  flushTrailingOpaqueReasoning();
1431
1571
  continue;
@@ -1449,6 +1589,7 @@ export function toAgUiMessages(
1449
1589
  role: wireRole,
1450
1590
  content: extractText(message.content),
1451
1591
  ...(message.name ? { name: message.name } : {}),
1592
+ ...withAgUiMessageMetadata(message.metadata),
1452
1593
  });
1453
1594
  flushTrailingOpaqueReasoning();
1454
1595
  continue;
@@ -1459,6 +1600,7 @@ export function toAgUiMessages(
1459
1600
  id: message.id,
1460
1601
  role: "reasoning",
1461
1602
  content: extractText(message.content),
1603
+ ...withAgUiMessageMetadata(message.metadata),
1462
1604
  });
1463
1605
  flushTrailingOpaqueReasoning();
1464
1606
  continue;
@@ -85,16 +85,16 @@ describe("RunAggregator tool approval projection", () => {
85
85
  ).toEqual([GATE]);
86
86
  });
87
87
 
88
- it("drops the gate when the run is then cancelled, keeping the interrupts", () => {
88
+ it("keeps the gate when the finished run is then cancelled", () => {
89
89
  const { aggregator, last } = gatedAggregator();
90
90
 
91
91
  aggregator.handle({ type: "RUN_CANCELLED" } as any);
92
92
 
93
93
  expect(last().status).toMatchObject({
94
- type: "incomplete",
95
- reason: "cancelled",
94
+ type: "requires-action",
95
+ reason: "interrupt",
96
96
  });
97
- expect(approvalOf(last())).toBeUndefined();
97
+ expect(approvalOf(last())).toBeDefined();
98
98
  expect(
99
99
  (last().metadata?.custom as Record<string, any> | undefined)?.agui
100
100
  ?.interrupts,
@@ -11,6 +11,7 @@ import {
11
11
  type ToolCallMessagePart,
12
12
  type ToolModelContentPart,
13
13
  } from "@assistant-ui/core";
14
+ import type { JoinStrategy } from "@assistant-ui/core/react";
14
15
  import {
15
16
  applyA2uiOperations,
16
17
  surfaceToPresentToolCall,
@@ -176,6 +177,7 @@ export const isPlainObject = (
176
177
 
177
178
  export type RunAggregatorOptions = {
178
179
  showThinking: boolean;
180
+ joinStrategy?: JoinStrategy | undefined;
179
181
  logger: Logger;
180
182
  emit: Emit;
181
183
  onServerMessageId?: (messageId: string) => void;
@@ -195,6 +197,7 @@ export type RunAggregatorOptions = {
195
197
  export class RunAggregator {
196
198
  private readonly emitUpdate: Emit;
197
199
  private readonly showThinking: boolean;
200
+ private readonly joinStrategy: JoinStrategy | undefined;
198
201
  private readonly logger: Logger;
199
202
  private readonly onServerMessageId: ((messageId: string) => void) | undefined;
200
203
  private readonly onTextMessageStart:
@@ -259,6 +262,7 @@ export class RunAggregator {
259
262
  constructor(options: RunAggregatorOptions) {
260
263
  this.emitUpdate = options.emit;
261
264
  this.showThinking = options.showThinking;
265
+ this.joinStrategy = options.joinStrategy;
262
266
  this.logger = options.logger;
263
267
  this.onServerMessageId = options.onServerMessageId;
264
268
  this.onTextMessageStart = options.onTextMessageStart;
@@ -319,8 +323,14 @@ export class RunAggregator {
319
323
  }
320
324
  case "RUN_CANCELLED": {
321
325
  // A cancel can land after RUN_FINISHED while the response is still
322
- // closing, and the answer it finished is already persisted as complete.
323
- if (this.status?.type === "complete") break;
326
+ // closing: the finished answer stays complete, and an interrupt the run
327
+ // already delivered stays answerable.
328
+ if (
329
+ this.status?.type === "complete" ||
330
+ (this.status?.type === "requires-action" &&
331
+ this.status.reason === "interrupt")
332
+ )
333
+ break;
324
334
  this.status = { type: "incomplete", reason: "cancelled" };
325
335
  this.closeOpenSubagentRuns(this.status);
326
336
  this.emit();
@@ -845,6 +855,7 @@ export class RunAggregator {
845
855
 
846
856
  private beginDistinctTextMessage(messageId: string | undefined): void {
847
857
  if (
858
+ this.joinStrategy === "concat-content" ||
848
859
  !messageId ||
849
860
  this.lastTextMessageId === undefined ||
850
861
  this.lastTextMessageId === messageId ||
@@ -6,11 +6,13 @@ import type {
6
6
  ExternalStoreThreadListAdapter,
7
7
  FeedbackAdapter,
8
8
  RealtimeVoiceAdapter,
9
+ RunConfig,
9
10
  SpeechSynthesisAdapter,
10
11
  ThreadHistoryAdapter,
11
12
  ThreadMessage,
12
13
  } from "@assistant-ui/core";
13
- import type { AbstractAgent } from "@ag-ui/client";
14
+ import type { JoinStrategy } from "@assistant-ui/core/react";
15
+ import type { AbstractAgent, Context, Message } from "@ag-ui/client";
14
16
  import type { Logger } from "./logger";
15
17
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
16
18
  import type { McpToolCallResult } from "./mcp-tool-result";
@@ -22,6 +24,7 @@ export const A2UI_SURFACE_ACTIVITY_TYPE = "a2ui-surface";
22
24
  export type AgUiOpaqueReasoning = {
23
25
  id: string;
24
26
  encryptedValue: string;
27
+ metadata?: Record<string, unknown>;
25
28
  after?: boolean;
26
29
  };
27
30
 
@@ -80,6 +83,22 @@ export type UseAgUiRuntimeOptions = ExternalStoreSharedOptions & {
80
83
  agent: AbstractAgent;
81
84
  logger?: Partial<Logger>;
82
85
  showThinking?: boolean;
86
+ joinStrategy?: JoinStrategy;
87
+ /**
88
+ * Return context entries for each run. `messages` is the exact AG-UI
89
+ * transcript sent in the run input, after transcript selection and message
90
+ * conversion. On resume, it follows `resumeTranscript`: "full" sends the
91
+ * whole thread, while "appended" sends messages after the interrupted
92
+ * assistant message, falling back to the whole thread if that message is
93
+ * missing. `runConfig` is the run config passed while building that input.
94
+ * If the getter throws, the error is reported and the run continues without host entries.
95
+ */
96
+ getRunContext?:
97
+ | ((input: {
98
+ messages: readonly Message[];
99
+ runConfig: RunConfig | undefined;
100
+ }) => readonly Context[])
101
+ | undefined;
83
102
  /**
84
103
  * What `messages` carries on a resume run, meaning a `RunAgentInput` that
85
104
  * also carries `resume`. The AG-UI interrupt spec leaves this undefined: its