@assistant-ui/react-ag-ui 0.0.57 → 0.0.58

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 (40) hide show
  1. package/README.md +16 -0
  2. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +3 -4
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  4. package/dist/runtime/AgUiThreadRuntimeCore.js +29 -43
  5. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  6. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  7. package/dist/runtime/adapter/conversions.js +34 -11
  8. package/dist/runtime/adapter/conversions.js.map +1 -1
  9. package/dist/runtime/adapter/run-aggregator.d.ts +13 -3
  10. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  11. package/dist/runtime/adapter/run-aggregator.js +326 -119
  12. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  13. package/dist/runtime/adapter/subscriber.d.ts +9 -0
  14. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  15. package/dist/runtime/adapter/subscriber.js +4 -1
  16. package/dist/runtime/adapter/subscriber.js.map +1 -1
  17. package/dist/runtime/adapter/tool-approval.d.ts +3 -1
  18. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  19. package/dist/runtime/adapter/tool-approval.js +11 -12
  20. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  21. package/dist/runtime/event-parser.d.ts.map +1 -1
  22. package/dist/runtime/event-parser.js +176 -45
  23. package/dist/runtime/event-parser.js.map +1 -1
  24. package/dist/runtime/types.d.ts +42 -1
  25. package/dist/runtime/types.d.ts.map +1 -1
  26. package/dist/useAgUiRuntime.js +1 -0
  27. package/dist/useAgUiRuntime.js.map +1 -1
  28. package/package.json +9 -9
  29. package/src/runtime/AgUiThreadRuntimeCore.ts +40 -60
  30. package/src/runtime/adapter/conversions.ts +87 -17
  31. package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
  32. package/src/runtime/adapter/run-aggregator.ts +510 -139
  33. package/src/runtime/adapter/subscriber.ts +10 -1
  34. package/src/runtime/adapter/tool-approval.test.ts +75 -0
  35. package/src/runtime/adapter/tool-approval.ts +17 -12
  36. package/src/runtime/event-parser.ts +216 -36
  37. package/src/runtime/types.ts +70 -12
  38. package/src/useAgUiRuntime.approval.test.tsx +34 -24
  39. package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
  40. package/src/useAgUiRuntime.ts +1 -0
package/README.md CHANGED
@@ -33,6 +33,22 @@ export function Provider({ children }: { children: React.ReactNode }) {
33
33
  }
34
34
  ```
35
35
 
36
+ ## Subagents
37
+
38
+ An AG-UI backend that runs subagents (the agents-as-tools pattern) emits `SUBAGENT_STARTED` / `SUBAGENT_FINISHED` / `SUBAGENT_ERROR` plus a `subagentRunId` on the events each subagent produces. This adapter groups that activity into one nested assistant message per subagent run and attaches it to the spawning tool call as `ToolCallMessagePart.messages`, joined on `SUBAGENT_STARTED.parentToolCallId`. `PartPrimitive.Messages` renders it without extra wiring, and the subagent's name, description, result, and error code ride on that message's `agui` metadata.
39
+
40
+ A subagent that names no reachable spawning call (`parentToolCallId` is optional, may name a call this run never saw, or two runs may name each other) has nowhere to nest, so its output renders in the parent thread instead of being dropped. This matches the downgrade the protocol's own pre-subagent compatibility middleware performs.
41
+
42
+ A subagent's frontend-executed tool calls work the same way the root agent's do: a call nested on `ToolCallMessagePart.messages` is reachable by `getPendingToolCalls()`, resolves through `addToolResult`, and its result rides the resume as a `tool` record on the spawning assistant record, the same flattened wire shape these calls had before subagent attribution.
43
+
44
+ Approval gates cover nested calls too: a gate that names a subagent-scoped call projects onto the nested part, the frontend tool stays unexecuted while the gate is open, decisions recorded through `respondToToolApproval` land on the nested part, and an undecided gate's result is never exported to the backend.
45
+
46
+ Two limitations are worth knowing before you rely on this:
47
+
48
+ - **Nested structure does not survive a reload.** Thread restore reads the flattened wire shape, so a restored subagent tool call comes back as a root-level part rather than nested under its spawning call. Results and decisions are preserved; only the nesting is not.
49
+
50
+ - **Nested human-in-the-loop is not wired up.** A `SUBAGENT_FINISHED` with a `suspended` outcome marks the nested message `requires-action`, and its `interruptIds` are preserved on the message metadata, but there is no resume path that answers them yet.
51
+
36
52
  ## See also
37
53
 
38
54
  - `@assistant-ui/react-a2a` for the A2A v1.0 protocol.
@@ -89,9 +89,9 @@ declare class AgUiThreadRuntimeCore {
89
89
  */
90
90
  private resumeWithResponses;
91
91
  /**
92
- * The core seams that reach `respondToToolApproval` discard the promise they
93
- * receive, so a rejected decision would otherwise surface only as an
94
- * unhandled rejection. A failure raised by the resumed run itself was already
92
+ * A rejected decision reaches the caller through the approval seam, and
93
+ * `onError` in addition, because a consumer watching only the runtime would
94
+ * otherwise miss it. A failure raised by the resumed run itself was already
95
95
  * reported by `startRun` before it rethrew, so reporting it here again would
96
96
  * give the consumer two notifications for one failure.
97
97
  */
@@ -117,7 +117,6 @@ declare class AgUiThreadRuntimeCore {
117
117
  private startRun;
118
118
  private consumeResumeStream;
119
119
  private buildRunInput;
120
- private installResumeShim;
121
120
  private setRunning;
122
121
  private finishRun;
123
122
  private insertAssistantPlaceholder;
@@ -1 +1 @@
1
- {"version":3,"file":"AgUiThreadRuntimeCore.d.ts","names":[],"sources":["../../src/runtime/AgUiThreadRuntimeCore.ts"],"mappings":";;;;;;KA8EK,YAAY,YAAY;KACxB,gBACH,SAAS,wBACN,eAAe;KACf;EACH;EACA;EACA,WAAW;EACX,SAAS;;KAGN;EACH,OAAO;EACP,QAAQ;EACR;EACA;EACA,WAAW,OAAO;EAClB;EACA,UAAU;EACV;;cAoCW;UACH;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;UAET;mBACS;UAgBT;UACA;UAGA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UACA;UACA;EAEI,YAAA,SAAS;EAYrB,cAAc,SAAS,KAAK;EAsB5B,cAAc,SAAS;EAIvB;EAOA,wBAAwB;EAIxB,wBAAwB;EAIxB,YAAY;EAIZ;MAII;EAIJ,mBAAmB;EAsDb,OAAO,SAAS,gBAAgB;UAW9B;UAQA;EAiBF,KAAK,SAAS,gBAAgB;EAI9B,OACJ,yBACA;IAAU,YAAY;MACrB;EAMG,UAAU;EAcV,OAAO,QAAQ,kBAAkB;EAUjC,kBAAkB,mBAAmB,kBAAkB;UAwBrD;UAOA;EAgBR;IACE;IACA,qBAAqB;;EAavB;IACE;IACA;;EAcI,yBACJ,oBAAoB,oBACnB;;;;;;;UAuCW;;;;;;;;EAed,YAAY;UAYJ;EAwBF,sBACJ,SAAS,+BACR;EAsEG,UACJ,SAAS,qBACT,qBAAqB,oBACpB;UAyCK;UAkBA;UAiCA;UA0BA;EAmCR,yBAAyB;UAkBjB;EAiBR,cAAc,SAAS;EAyBvB,eAAe,QAAQ;UA4Bf;UAYA;UA6BA;EAUR,sBAAsB,mBAAmB;EAkDzC,kBAAkB,OAAO;EAKzB,SACE,MACI,sBACE,MAAM,kCAAkC;EAOhD;UAKc;UAyPA;UA2DN;UAkCA;UAqBA;UAKA;UAQA;UAyBA;UAkEA;UAiDA;UA4BA;UA6BA;UAgEA;UA2EA;UA8DA;UAuDA;UAQA;UAIA;UAKA;UAMA;UAQA;UAsDA;UAqBA"}
1
+ {"version":3,"file":"AgUiThreadRuntimeCore.d.ts","names":[],"sources":["../../src/runtime/AgUiThreadRuntimeCore.ts"],"mappings":";;;;;;KA8EK,YAAY,YAAY;KACxB,gBACH,SAAS,wBACN,eAAe;KACf;EACH;EACA;EACA,WAAW;EACX,SAAS;;KAGN;EACH,OAAO;EACP,QAAQ;EACR;EACA;EACA,WAAW,OAAO;EAClB;EACA,UAAU;EACV;;cAoCW;UACH;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;UAET;mBACS;UAgBT;UACA;UAGA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UACA;UACA;EAEI,YAAA,SAAS;EAWrB,cAAc,SAAS,KAAK;EAoB5B,cAAc,SAAS;EAIvB;EAOA,wBAAwB;EAIxB,wBAAwB;EAIxB,YAAY;EAIZ;MAII;EAIJ,mBAAmB;EAsDb,OAAO,SAAS,gBAAgB;UAW9B;UAQA;EAiBF,KAAK,SAAS,gBAAgB;EAI9B,OACJ,yBACA;IAAU,YAAY;MACrB;EAMG,UAAU;EAcV,OAAO,QAAQ,kBAAkB;EAUjC,kBAAkB,mBAAmB,kBAAkB;UAwBrD;UAOA;EAgBR;IACE;IACA,qBAAqB;;EAavB;IACE;IACA;;EAaI,yBACJ,oBAAoB,oBACnB;;;;;;;UAuCW;;;;;;;;EAed,YAAY;UAYJ;EAwBF,sBACJ,SAAS,+BACR;EAsEG,UACJ,SAAS,qBACT,qBAAqB,oBACpB;UAyCK;UAkBA;UAiCA;UA0BA;EAmCR,yBAAyB;UAiBjB;EAiBR,cAAc,SAAS;EA+BvB,eAAe,QAAQ;UA4Bf;UAYA;UAiCA;EAUR,sBAAsB,mBAAmB;EAkDzC,kBAAkB,OAAO;EAKzB,SACE,MACI,sBACE,MAAM,kCAAkC;EAOhD;UAKc;UAyPA;UA2DN;UAkCA;UAKA;UAQA;UAyBA;UAkEA;UAiDA;UA0BA;UA6BA;UAgEA;UA0EA;UA6DA;UAuDA;UAQA;UAIA;UAKA;UAMA;UAQA;UAsDA;UAqBA"}
@@ -5,14 +5,13 @@ import { AG_UI_METADATA_NAMESPACE, RunAggregator, isPlainObject, tryParseJSON }
5
5
  import { fromAgUiMessages, toAgUiMessages, toAgUiTools } from "./adapter/conversions.js";
6
6
  import { createAgUiSubscriber } from "./adapter/subscriber.js";
7
7
  import { fromThreadMessageLike, generateId, isMcpAppUri } from "@assistant-ui/core";
8
- import { createMessageRepositorySession, invokeUserCallback } from "@assistant-ui/core/internal";
8
+ import { createMessageRepositorySession, invokeUserCallback, iterateToolCallParts, mapToolCallPartsDeep } from "@assistant-ui/core/internal";
9
9
  import jsonpatch from "fast-json-patch";
10
10
  //#region src/runtime/AgUiThreadRuntimeCore.ts
11
11
  const optimisticPrefix = "__optimistic__";
12
12
  const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
13
13
  const isOptimisticId = (id) => id.startsWith(optimisticPrefix);
14
14
  const isResolvedToolCall = (part) => part.type === "tool-call" && "result" in part && part.result !== void 0;
15
- const symbolResumeShim = Symbol("agui-resume-shim");
16
15
  const FALLBACK_USER_STATUS = {
17
16
  type: "complete",
18
17
  reason: "unknown"
@@ -83,7 +82,6 @@ var AgUiThreadRuntimeCore = class {
83
82
  this.onCancel = options.onCancel;
84
83
  this.history = options.history;
85
84
  this.notifyUpdate = options.notifyUpdate;
86
- this.installResumeShim();
87
85
  }
88
86
  updateOptions(options) {
89
87
  this.agent = options.agent;
@@ -94,7 +92,6 @@ var AgUiThreadRuntimeCore = class {
94
92
  this.onCancel = options.onCancel;
95
93
  const previousHistory = this.history;
96
94
  this.history = options.history;
97
- this.installResumeShim();
98
95
  if (this._loadRequested && !this._loadPromise && !previousHistory && options.history && this.session.getMessages().length === 0) this.__internal_load();
99
96
  }
100
97
  attachRuntime(runtime) {
@@ -234,8 +231,7 @@ var AgUiThreadRuntimeCore = class {
234
231
  const assistant = this.findRequiresActionAssistant("tool-calls");
235
232
  if (!assistant) return null;
236
233
  const toolCallIds = [];
237
- for (const part of assistant.content) {
238
- if (part.type !== "tool-call") continue;
234
+ for (const part of iterateToolCallParts(assistant.content)) {
239
235
  if (isResolvedToolCall(part)) continue;
240
236
  toolCallIds.push(part.toolCallId);
241
237
  }
@@ -266,9 +262,9 @@ var AgUiThreadRuntimeCore = class {
266
262
  await this.startRun(messageId, this.lastRunConfig, resume);
267
263
  }
268
264
  /**
269
- * The core seams that reach `respondToToolApproval` discard the promise they
270
- * receive, so a rejected decision would otherwise surface only as an
271
- * unhandled rejection. A failure raised by the resumed run itself was already
265
+ * A rejected decision reaches the caller through the approval seam, and
266
+ * `onError` in addition, because a consumer watching only the runtime would
267
+ * otherwise miss it. A failure raised by the resumed run itself was already
272
268
  * reported by `startRun` before it rethrew, so reporting it here again would
273
269
  * give the consumer two notifications for one failure.
274
270
  */
@@ -293,7 +289,7 @@ var AgUiThreadRuntimeCore = class {
293
289
  const pending = this.getPendingInterrupts();
294
290
  if (!pending) throw new Error("[agui] respondToToolApproval: no pending interrupts on this thread");
295
291
  const gatedMessage = this.session.tryGetMessage(pending.messageId)?.message;
296
- if (![...projectAgUiToolApprovals(pending.interrupts, new Set((gatedMessage?.content ?? []).filter((part) => part.type === "tool-call").map((part) => part.toolCallId))).values()].some((approval) => approval.id === options.approvalId)) throw new Error(`[agui] respondToToolApproval: no pending tool-call interrupt for approval id "${options.approvalId}"`);
292
+ if (![...projectAgUiToolApprovals(pending.interrupts, new Set(Array.from(iterateToolCallParts(gatedMessage?.content ?? [])).map((part) => part.toolCallId))).values()].some((approval) => approval.id === options.approvalId)) throw new Error(`[agui] respondToToolApproval: no pending tool-call interrupt for approval id "${options.approvalId}"`);
297
293
  this.assertInterruptsAnswerable("respondToToolApproval", pending.interrupts);
298
294
  if (!this.session.updateMessage(pending.messageId, (message) => {
299
295
  if (message.role !== "assistant") return message;
@@ -411,8 +407,8 @@ var AgUiThreadRuntimeCore = class {
411
407
  for (let index = messages.length - 1; index >= 0; index--) {
412
408
  const message = messages[index];
413
409
  if (!message || message.role !== "assistant") continue;
414
- for (const part of message.content) {
415
- if (part.type !== "tool-call" || part.toolCallId !== toolCallId) continue;
410
+ for (const part of iterateToolCallParts(message.content)) {
411
+ if (part.toolCallId !== toolCallId) continue;
416
412
  if (!isResolvedToolCall(part)) return message.id;
417
413
  fallbackMessageId ??= message.id;
418
414
  }
@@ -423,8 +419,8 @@ var AgUiThreadRuntimeCore = class {
423
419
  if (this.session.updateMessage(messageId, (message) => {
424
420
  if (message.role !== "assistant") return message;
425
421
  const assistant = message;
426
- const content = assistant.content.map((part) => {
427
- if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
422
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
423
+ if (isResolvedToolCall(part)) return part;
428
424
  return {
429
425
  ...part,
430
426
  result: { error: "Tool call cancelled by user" },
@@ -438,12 +434,14 @@ var AgUiThreadRuntimeCore = class {
438
434
  })) this.notifyUpdate();
439
435
  }
440
436
  addToolResult(options) {
441
- if (!this.session.updateMessage(options.messageId, (message) => {
437
+ const sessionMessageId = this.session.tryGetMessage(options.messageId) ? options.messageId : this.findMessageIdForToolCall(options.toolCallId);
438
+ if (sessionMessageId === void 0) return;
439
+ if (!this.session.updateMessage(sessionMessageId, (message) => {
442
440
  if (message.role !== "assistant") return message;
443
441
  const assistant = message;
444
442
  let matchedToolCall = false;
445
- const content = assistant.content.map((part) => {
446
- if (part.type !== "tool-call" || part.toolCallId !== options.toolCallId) return part;
443
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
444
+ if (part.toolCallId !== options.toolCallId) return part;
447
445
  matchedToolCall = true;
448
446
  return {
449
447
  ...part,
@@ -459,7 +457,7 @@ var AgUiThreadRuntimeCore = class {
459
457
  };
460
458
  })) return;
461
459
  this.notifyUpdate();
462
- this.maybeResumeAfterToolResults(options.messageId);
460
+ this.maybeResumeAfterToolResults(sessionMessageId);
463
461
  }
464
462
  sendA2uiAction(action) {
465
463
  this.assertNoPendingInterrupts();
@@ -492,7 +490,12 @@ var AgUiThreadRuntimeCore = class {
492
490
  if (!message || message.role !== "assistant") return false;
493
491
  const assistant = message;
494
492
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "tool-calls") return false;
495
- if (!assistant.content.every((part) => part.type !== "tool-call" || isResolvedToolCall(part))) return false;
493
+ let allResolved = true;
494
+ for (const part of iterateToolCallParts(assistant.content)) if (!isResolvedToolCall(part)) {
495
+ allResolved = false;
496
+ break;
497
+ }
498
+ if (!allResolved) return false;
496
499
  if (!this.session.updateMessage(messageId, (current) => current.role === "assistant" ? {
497
500
  ...current,
498
501
  status: {
@@ -792,21 +795,6 @@ var AgUiThreadRuntimeCore = class {
792
795
  this.pendingA2uiAction = void 0;
793
796
  return input;
794
797
  }
795
- installResumeShim() {
796
- const agent = this.agent;
797
- if (agent[symbolResumeShim]) return;
798
- agent[symbolResumeShim] = true;
799
- const original = Object.hasOwn(agent, "prepareRunAgentInput") ? agent.prepareRunAgentInput : Object.getPrototypeOf(agent)?.prepareRunAgentInput;
800
- if (typeof original !== "function") return;
801
- agent.prepareRunAgentInput = function(params) {
802
- const input = original.call(this, params);
803
- if (params?.resume !== void 0 && input && typeof input === "object") return {
804
- ...input,
805
- resume: params.resume
806
- };
807
- return input;
808
- };
809
- }
810
798
  setRunning(running) {
811
799
  this.isRunningFlag = running;
812
800
  this.notifyUpdate();
@@ -914,14 +902,12 @@ var AgUiThreadRuntimeCore = class {
914
902
  }
915
903
  preserveToolResults(previous, next) {
916
904
  const resolved = /* @__PURE__ */ new Map();
917
- for (const part of previous) if (part.type === "tool-call" && isResolvedToolCall(part)) resolved.set(part.toolCallId, part);
905
+ for (const part of iterateToolCallParts(previous)) if (isResolvedToolCall(part)) resolved.set(part.toolCallId, part);
918
906
  if (resolved.size === 0) return next;
919
- let changed = false;
920
- const merged = next.map((part) => {
921
- if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
907
+ const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
908
+ if (isResolvedToolCall(part)) return part;
922
909
  const prior = resolved.get(part.toolCallId);
923
910
  if (!prior) return part;
924
- changed = true;
925
911
  return {
926
912
  ...part,
927
913
  result: prior.result,
@@ -996,8 +982,8 @@ var AgUiThreadRuntimeCore = class {
996
982
  const assistant = message;
997
983
  const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
998
984
  let matchedToolCall = false;
999
- const content = assistant.content.map((part) => {
1000
- if (part.type !== "tool-call" || part.toolCallId !== event.toolCallId) return part;
985
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
986
+ if (part.toolCallId !== event.toolCallId) return part;
1001
987
  matchedToolCall = true;
1002
988
  if (part.mcp?.app !== void 0 && isPlainObject(part.result) && Array.isArray(part.result["content"])) return {
1003
989
  ...part,
@@ -1040,8 +1026,8 @@ var AgUiThreadRuntimeCore = class {
1040
1026
  if (message.role !== "assistant") return message;
1041
1027
  const assistant = message;
1042
1028
  let matchedToolCall = false;
1043
- const content = assistant.content.map((part) => {
1044
- if (part.type !== "tool-call" || part.toolCallId !== toolCallId) return part;
1029
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
1030
+ if (part.toolCallId !== toolCallId) return part;
1045
1031
  matchedToolCall = true;
1046
1032
  return {
1047
1033
  ...part,