@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.
Files changed (40) hide show
  1. package/dist/agUiExtras.js +1 -2
  2. package/dist/hooks.js +1 -2
  3. package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
  4. package/dist/runtime/AgUiThreadController.js +354 -68
  5. package/dist/runtime/AgUiThreadController.js.map +1 -1
  6. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  7. package/dist/runtime/adapter/conversions.js +10 -3
  8. package/dist/runtime/adapter/conversions.js.map +1 -1
  9. package/dist/runtime/adapter/replay-content.d.ts +9 -0
  10. package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
  11. package/dist/runtime/adapter/replay-content.js +185 -0
  12. package/dist/runtime/adapter/replay-content.js.map +1 -0
  13. package/dist/runtime/adapter/run-aggregator.js +1 -2
  14. package/dist/runtime/adapter/subscriber.js +1 -2
  15. package/dist/runtime/adapter/tool-approval.d.ts +2 -0
  16. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  17. package/dist/runtime/adapter/tool-approval.js +23 -6
  18. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  19. package/dist/runtime/agUiThreadState.d.ts +1 -1
  20. package/dist/runtime/agUiThreadState.d.ts.map +1 -1
  21. package/dist/runtime/agUiThreadState.js +3 -4
  22. package/dist/runtime/agUiThreadState.js.map +1 -1
  23. package/dist/runtime/event-parser.js +1 -2
  24. package/dist/runtime/interrupt-internals.js +1 -2
  25. package/dist/runtime/logger.js +1 -2
  26. package/dist/runtime/mcp-tool-result.js +1 -2
  27. package/dist/runtime/types.js +1 -2
  28. package/dist/useAgUiRuntime.js +1 -2
  29. package/package.json +5 -5
  30. package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
  31. package/src/runtime/AgUiThreadController.replay.test.ts +3932 -0
  32. package/src/runtime/AgUiThreadController.test.ts +79 -0
  33. package/src/runtime/AgUiThreadController.ts +725 -109
  34. package/src/runtime/adapter/conversions.content.test.ts +113 -1
  35. package/src/runtime/adapter/conversions.ts +18 -8
  36. package/src/runtime/adapter/replay-content.test.ts +235 -0
  37. package/src/runtime/adapter/replay-content.ts +306 -0
  38. package/src/runtime/adapter/tool-approval.ts +41 -3
  39. package/src/runtime/agUiThreadState.test.ts +21 -2
  40. 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 open = new Set(interrupts.map((interrupt) => interrupt.id));
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 iterateToolCallParts(content)) {
313
+ for (const part of calls) {
276
314
  const approval = part.approval;
277
315
  if (!approval || approval.approved === undefined) continue;
278
- if (!open.has(approval.id)) continue;
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({ owner, messageId: "assistant" });
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, messageId: "assistant" });
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: { owner: AbortController; messageId: string } | null;
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.messageId === action.messageId
84
+ state.pendingResume.messageIds.includes(action.messageId)
82
85
  ? state
83
86
  : {
84
87
  ...state,
85
- pendingResume: { owner: action.owner, messageId: action.messageId },
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