@assistant-ui/react-ag-ui 0.1.0 → 0.1.1
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/dist/agUiExtras.js +1 -2
- package/dist/hooks.js +1 -2
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +354 -68
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +10 -3
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/replay-content.d.ts +9 -0
- package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
- package/dist/runtime/adapter/replay-content.js +185 -0
- package/dist/runtime/adapter/replay-content.js.map +1 -0
- package/dist/runtime/adapter/run-aggregator.js +1 -2
- package/dist/runtime/adapter/subscriber.js +1 -2
- package/dist/runtime/adapter/tool-approval.d.ts +2 -0
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +23 -6
- package/dist/runtime/adapter/tool-approval.js.map +1 -1
- package/dist/runtime/agUiThreadState.d.ts +1 -1
- package/dist/runtime/agUiThreadState.d.ts.map +1 -1
- package/dist/runtime/agUiThreadState.js +3 -4
- package/dist/runtime/agUiThreadState.js.map +1 -1
- package/dist/runtime/event-parser.js +1 -2
- package/dist/runtime/interrupt-internals.js +1 -2
- package/dist/runtime/logger.js +1 -2
- package/dist/runtime/mcp-tool-result.js +1 -2
- package/dist/runtime/types.js +1 -2
- package/dist/useAgUiRuntime.js +1 -2
- package/package.json +5 -5
- package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
- package/src/runtime/AgUiThreadController.replay.test.ts +3932 -0
- package/src/runtime/AgUiThreadController.test.ts +79 -0
- package/src/runtime/AgUiThreadController.ts +725 -109
- package/src/runtime/adapter/conversions.content.test.ts +113 -1
- package/src/runtime/adapter/conversions.ts +18 -8
- package/src/runtime/adapter/replay-content.test.ts +235 -0
- package/src/runtime/adapter/replay-content.ts +306 -0
- package/src/runtime/adapter/tool-approval.ts +41 -3
- package/src/runtime/agUiThreadState.test.ts +21 -2
- package/src/runtime/agUiThreadState.ts +12 -3
|
@@ -228,6 +228,40 @@ export const projectAgUiToolApprovals = (
|
|
|
228
228
|
);
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
+
/** A recorded decision authorizes one exact call and gate, not a reused id. */
|
|
232
|
+
export const preserveToolApprovalDecision = (
|
|
233
|
+
previous: ToolCallMessagePart | undefined,
|
|
234
|
+
next: ToolCallMessagePart,
|
|
235
|
+
): ToolApproval | undefined => {
|
|
236
|
+
const prior = previous?.approval;
|
|
237
|
+
let approval = next.approval;
|
|
238
|
+
if (!approval && prior) {
|
|
239
|
+
const {
|
|
240
|
+
approved: _approved,
|
|
241
|
+
reason: _reason,
|
|
242
|
+
resolution: _resolution,
|
|
243
|
+
...pending
|
|
244
|
+
} = prior;
|
|
245
|
+
approval = pending;
|
|
246
|
+
}
|
|
247
|
+
if (!approval) return undefined;
|
|
248
|
+
// AG-UI may omit argument events for a no-argument call; both forms mean {}.
|
|
249
|
+
if (
|
|
250
|
+
!previous ||
|
|
251
|
+
prior?.id !== approval.id ||
|
|
252
|
+
previous.toolCallId !== next.toolCallId ||
|
|
253
|
+
previous.toolName !== next.toolName ||
|
|
254
|
+
(previous.argsText || "{}") !== (next.argsText || "{}")
|
|
255
|
+
)
|
|
256
|
+
return approval;
|
|
257
|
+
return {
|
|
258
|
+
...approval,
|
|
259
|
+
...(prior.approved !== undefined ? { approved: prior.approved } : {}),
|
|
260
|
+
...(prior.reason !== undefined ? { reason: prior.reason } : {}),
|
|
261
|
+
...(prior.resolution !== undefined ? { resolution: prior.resolution } : {}),
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
|
|
231
265
|
/**
|
|
232
266
|
* Records a decision on the gated tool call. The decision is held on the part
|
|
233
267
|
* until every gate in the batch is answered, because AG-UI resumes a run with
|
|
@@ -266,16 +300,20 @@ export const buildToolApprovalResume = (
|
|
|
266
300
|
content: readonly ThreadAssistantMessagePart[],
|
|
267
301
|
interrupts: readonly AgUiInterrupt[],
|
|
268
302
|
): AgUiResumeEntry[] | null => {
|
|
269
|
-
const
|
|
303
|
+
const calls = Array.from(iterateToolCallParts(content));
|
|
304
|
+
const gates = projectAgUiToolApprovals(
|
|
305
|
+
interrupts,
|
|
306
|
+
new Set(calls.map((part) => part.toolCallId)),
|
|
307
|
+
);
|
|
270
308
|
// Decisions accumulate in a `Map` rather than in an object literal, because an
|
|
271
309
|
// interrupt id of `__proto__` would otherwise write to the prototype chain
|
|
272
310
|
// instead of recording a response, and `constructor` or `toString` would
|
|
273
311
|
// report themselves as answered while undecided.
|
|
274
312
|
const responses = new Map<string, { status: "resolved"; payload: unknown }>();
|
|
275
|
-
for (const part of
|
|
313
|
+
for (const part of calls) {
|
|
276
314
|
const approval = part.approval;
|
|
277
315
|
if (!approval || approval.approved === undefined) continue;
|
|
278
|
-
if (
|
|
316
|
+
if (gates.get(part.toolCallId)?.id !== approval.id) continue;
|
|
279
317
|
responses.set(approval.id, {
|
|
280
318
|
status: "resolved",
|
|
281
319
|
payload: {
|
|
@@ -115,7 +115,10 @@ describe("reduceAgUiThreadState", () => {
|
|
|
115
115
|
owner,
|
|
116
116
|
messageId: "assistant",
|
|
117
117
|
});
|
|
118
|
-
expect(withResume.pendingResume).toEqual({
|
|
118
|
+
expect(withResume.pendingResume).toEqual({
|
|
119
|
+
owner,
|
|
120
|
+
messageIds: ["assistant"],
|
|
121
|
+
});
|
|
119
122
|
expect(
|
|
120
123
|
reduceAgUiThreadState(withResume, {
|
|
121
124
|
type: "resumeDeferred",
|
|
@@ -123,6 +126,22 @@ describe("reduceAgUiThreadState", () => {
|
|
|
123
126
|
messageId: "assistant",
|
|
124
127
|
}),
|
|
125
128
|
).toBe(withResume);
|
|
129
|
+
const withTwoOwners = reduceAgUiThreadState(withResume, {
|
|
130
|
+
type: "resumeDeferred",
|
|
131
|
+
owner,
|
|
132
|
+
messageId: "assistant-next",
|
|
133
|
+
});
|
|
134
|
+
expect(withTwoOwners.pendingResume).toEqual({
|
|
135
|
+
owner,
|
|
136
|
+
messageIds: ["assistant", "assistant-next"],
|
|
137
|
+
});
|
|
138
|
+
expect(
|
|
139
|
+
reduceAgUiThreadState(withTwoOwners, {
|
|
140
|
+
type: "resumeDeferred",
|
|
141
|
+
owner: new AbortController(),
|
|
142
|
+
messageId: "new-run",
|
|
143
|
+
}).pendingResume?.messageIds,
|
|
144
|
+
).toEqual(["new-run"]);
|
|
126
145
|
const withOwner = reduceAgUiThreadState(withResume, {
|
|
127
146
|
type: "a2uiResumeDeferred",
|
|
128
147
|
owner,
|
|
@@ -189,7 +208,7 @@ describe("reduceAgUiThreadState", () => {
|
|
|
189
208
|
});
|
|
190
209
|
expect(cleared.pendingA2uiResumeOwner).toBeNull();
|
|
191
210
|
expect(cleared.pendingA2uiAction).toBeUndefined();
|
|
192
|
-
expect(cleared.pendingResume).toEqual({ owner,
|
|
211
|
+
expect(cleared.pendingResume).toEqual({ owner, messageIds: ["assistant"] });
|
|
193
212
|
expect(
|
|
194
213
|
reduceAgUiThreadState(cleared, { type: "a2uiContinuationsCleared" }),
|
|
195
214
|
).toBe(cleared);
|
|
@@ -8,7 +8,10 @@ export type AgUiThreadState = {
|
|
|
8
8
|
historyMutationGeneration: number;
|
|
9
9
|
stateSnapshot: ReadonlyJSONValue | undefined;
|
|
10
10
|
lastRunConfig: RunConfig | undefined;
|
|
11
|
-
pendingResume: {
|
|
11
|
+
pendingResume: {
|
|
12
|
+
owner: AbortController;
|
|
13
|
+
messageIds: readonly string[];
|
|
14
|
+
} | null;
|
|
12
15
|
pendingA2uiResumeOwner: AbortController | null;
|
|
13
16
|
pendingA2uiAction: Record<string, unknown> | undefined;
|
|
14
17
|
};
|
|
@@ -78,11 +81,17 @@ export const reduceAgUiThreadState = (
|
|
|
78
81
|
: { ...state, lastRunConfig: action.runConfig };
|
|
79
82
|
case "resumeDeferred":
|
|
80
83
|
return state.pendingResume?.owner === action.owner &&
|
|
81
|
-
state.pendingResume.
|
|
84
|
+
state.pendingResume.messageIds.includes(action.messageId)
|
|
82
85
|
? state
|
|
83
86
|
: {
|
|
84
87
|
...state,
|
|
85
|
-
pendingResume: {
|
|
88
|
+
pendingResume: {
|
|
89
|
+
owner: action.owner,
|
|
90
|
+
messageIds:
|
|
91
|
+
state.pendingResume?.owner === action.owner
|
|
92
|
+
? [...state.pendingResume.messageIds, action.messageId]
|
|
93
|
+
: [action.messageId],
|
|
94
|
+
},
|
|
86
95
|
};
|
|
87
96
|
case "resumeCleared":
|
|
88
97
|
return state.pendingResume === null
|