@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.
- package/README.md +16 -0
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +3 -4
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +29 -43
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +34 -11
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +13 -3
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +326 -119
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +9 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +4 -1
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +3 -1
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +11 -12
- package/dist/runtime/adapter/tool-approval.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +176 -45
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +42 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +1 -0
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/runtime/AgUiThreadRuntimeCore.ts +40 -60
- package/src/runtime/adapter/conversions.ts +87 -17
- package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
- package/src/runtime/adapter/run-aggregator.ts +510 -139
- package/src/runtime/adapter/subscriber.ts +10 -1
- package/src/runtime/adapter/tool-approval.test.ts +75 -0
- package/src/runtime/adapter/tool-approval.ts +17 -12
- package/src/runtime/event-parser.ts +216 -36
- package/src/runtime/types.ts +70 -12
- package/src/useAgUiRuntime.approval.test.tsx +34 -24
- package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
- 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
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
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 ?? [])
|
|
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.
|
|
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
|
|
427
|
-
if (
|
|
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
|
-
|
|
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
|
|
446
|
-
if (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(
|
|
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
|
-
|
|
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 (
|
|
905
|
+
for (const part of iterateToolCallParts(previous)) if (isResolvedToolCall(part)) resolved.set(part.toolCallId, part);
|
|
918
906
|
if (resolved.size === 0) return next;
|
|
919
|
-
|
|
920
|
-
|
|
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
|
|
1000
|
-
if (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
|
|
1044
|
-
if (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,
|