@assistant-ui/react-ag-ui 0.1.2 → 0.1.3
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/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +31 -4
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +8 -0
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/package.json +5 -4
- package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +180 -1
- package/src/runtime/AgUiThreadController.test.ts +133 -1
- package/src/runtime/AgUiThreadController.ts +70 -7
- package/src/runtime/adapter/run-aggregator.ts +11 -0
- package/src/useAgUiRuntime.cloud-run-leases.test.tsx +384 -0
- package/src/useAgUiRuntime.state.test.tsx +87 -0
|
@@ -58,9 +58,12 @@ const answerRecord = {
|
|
|
58
58
|
],
|
|
59
59
|
};
|
|
60
60
|
const otherRecord = { id: "other", role: "assistant", content: "Next step." };
|
|
61
|
-
const
|
|
61
|
+
const taskCallEvents = [
|
|
62
62
|
{ type: "TOOL_CALL_START", toolCallId: "task", toolCallName: "task" },
|
|
63
63
|
{ type: "TOOL_CALL_END", toolCallId: "task" },
|
|
64
|
+
];
|
|
65
|
+
const taskAndSubagentEvents = [
|
|
66
|
+
...taskCallEvents,
|
|
64
67
|
{
|
|
65
68
|
type: "SUBAGENT_STARTED",
|
|
66
69
|
subagentRunId: "sub",
|
|
@@ -106,7 +109,53 @@ function statuses(core: AgUiThreadController) {
|
|
|
106
109
|
return { answer: answer.status?.type, subagent: nested?.status?.type };
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
function taskPartFor(core: AgUiThreadController, messageId: string) {
|
|
113
|
+
const message = core.getMessages().find(({ id }) => id === messageId);
|
|
114
|
+
if (message?.role !== "assistant") return undefined;
|
|
115
|
+
return message.content.find(
|
|
116
|
+
(part) => part.type === "tool-call" && part.toolCallId === "task",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
describe("AgUiThreadController snapshot head moves", () => {
|
|
121
|
+
it("reopens a settled snapshot-only assistant when a later snapshot adds a tool call", async () => {
|
|
122
|
+
const answer = {
|
|
123
|
+
id: "m1",
|
|
124
|
+
role: "assistant",
|
|
125
|
+
content: "Let me ask.",
|
|
126
|
+
};
|
|
127
|
+
const core = createController((wire) => [
|
|
128
|
+
start,
|
|
129
|
+
{ type: "MESSAGES_SNAPSHOT", messages: [...wire.messages, answer] },
|
|
130
|
+
{
|
|
131
|
+
type: "MESSAGES_SNAPSHOT",
|
|
132
|
+
messages: [
|
|
133
|
+
...wire.messages,
|
|
134
|
+
{
|
|
135
|
+
...answer,
|
|
136
|
+
toolCalls: [
|
|
137
|
+
{
|
|
138
|
+
id: "ask",
|
|
139
|
+
type: "function",
|
|
140
|
+
function: { name: "ask_user", arguments: "{}" },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
finished,
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
await core.append(userMessage);
|
|
150
|
+
|
|
151
|
+
expect(
|
|
152
|
+
core.getMessages().find(({ id }) => id === "m1")?.status,
|
|
153
|
+
).toMatchObject({
|
|
154
|
+
type: "requires-action",
|
|
155
|
+
reason: "tool-calls",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
110
159
|
it("settles the moved-off answer when the snapshot has no tool call", async () => {
|
|
111
160
|
const core = createController((wire) => [
|
|
112
161
|
start,
|
|
@@ -143,6 +192,136 @@ describe("AgUiThreadController snapshot head moves", () => {
|
|
|
143
192
|
});
|
|
144
193
|
});
|
|
145
194
|
|
|
195
|
+
it("keeps a moved-off answer settled and owns the result after repeated snapshots", async () => {
|
|
196
|
+
const core = createController((wire) => [
|
|
197
|
+
start,
|
|
198
|
+
...answerText,
|
|
199
|
+
...taskCallEvents,
|
|
200
|
+
snapshot(wire, otherRecord),
|
|
201
|
+
snapshot(wire, otherRecord),
|
|
202
|
+
taskResult,
|
|
203
|
+
finished,
|
|
204
|
+
]);
|
|
205
|
+
await core.append(userMessage);
|
|
206
|
+
|
|
207
|
+
expect(core.isRunning()).toBe(false);
|
|
208
|
+
expect(statuses(core).answer).toBe("complete");
|
|
209
|
+
expect(
|
|
210
|
+
core.getMessages().find(({ id }) => id === "other")?.status?.type,
|
|
211
|
+
).toBe("complete");
|
|
212
|
+
expect(taskPartFor(core, "answer")).toMatchObject({ result: "done" });
|
|
213
|
+
expect(taskPartFor(core, "other")).toBeUndefined();
|
|
214
|
+
expect(
|
|
215
|
+
core.getMessages().find(({ id }) => id === "answer")?.content,
|
|
216
|
+
).toEqual([
|
|
217
|
+
{ type: "text", text: "Let me check." },
|
|
218
|
+
{
|
|
219
|
+
type: "tool-call",
|
|
220
|
+
toolCallId: "task",
|
|
221
|
+
toolName: "task",
|
|
222
|
+
args: {},
|
|
223
|
+
argsText: "{}",
|
|
224
|
+
result: "done",
|
|
225
|
+
unstable_toolMessageId: "tool-task",
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
expect(
|
|
229
|
+
core.getMessages().find(({ id }) => id === "other")?.content,
|
|
230
|
+
).toEqual([{ type: "text", text: "Next step." }]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("preserves full content and settled statuses with a sub-agent after repeated snapshots", async () => {
|
|
234
|
+
const core = createController((wire) => [
|
|
235
|
+
start,
|
|
236
|
+
...answerText,
|
|
237
|
+
...taskAndSubagentEvents,
|
|
238
|
+
snapshot(wire, otherRecord),
|
|
239
|
+
snapshot(wire, otherRecord),
|
|
240
|
+
subFinished,
|
|
241
|
+
taskResult,
|
|
242
|
+
finished,
|
|
243
|
+
]);
|
|
244
|
+
await core.append(userMessage);
|
|
245
|
+
|
|
246
|
+
expect(core.isRunning()).toBe(false);
|
|
247
|
+
expect(statuses(core)).toEqual({
|
|
248
|
+
answer: "complete",
|
|
249
|
+
subagent: "complete",
|
|
250
|
+
});
|
|
251
|
+
expect(
|
|
252
|
+
core.getMessages().find(({ id }) => id === "other")?.status?.type,
|
|
253
|
+
).toBe("complete");
|
|
254
|
+
expect(
|
|
255
|
+
core.getMessages().find(({ id }) => id === "answer")?.content,
|
|
256
|
+
).toEqual([
|
|
257
|
+
{ type: "text", text: "Let me check." },
|
|
258
|
+
{
|
|
259
|
+
type: "tool-call",
|
|
260
|
+
toolCallId: "task",
|
|
261
|
+
toolName: "task",
|
|
262
|
+
args: {},
|
|
263
|
+
argsText: "{}",
|
|
264
|
+
result: "done",
|
|
265
|
+
unstable_toolMessageId: "tool-task",
|
|
266
|
+
messages: [
|
|
267
|
+
expect.objectContaining({
|
|
268
|
+
id: "sub",
|
|
269
|
+
role: "assistant",
|
|
270
|
+
status: { type: "complete", reason: "unknown" },
|
|
271
|
+
content: [{ type: "text", text: "working" }],
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
]);
|
|
276
|
+
expect(
|
|
277
|
+
core.getMessages().find(({ id }) => id === "other")?.content,
|
|
278
|
+
).toEqual([{ type: "text", text: "Next step." }]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it.each([false, true])(
|
|
282
|
+
"preserves current text when the moved result arrives during streaming: %s",
|
|
283
|
+
async (duringStreaming) => {
|
|
284
|
+
const core = createController((wire) => [
|
|
285
|
+
start,
|
|
286
|
+
...answerText,
|
|
287
|
+
...taskCallEvents,
|
|
288
|
+
snapshot(wire, otherRecord),
|
|
289
|
+
snapshot(wire, otherRecord),
|
|
290
|
+
...(!duringStreaming ? [taskResult] : []),
|
|
291
|
+
{ type: "TEXT_MESSAGE_START", messageId: "other", role: "assistant" },
|
|
292
|
+
{ type: "TEXT_MESSAGE_CONTENT", messageId: "other", delta: " Before." },
|
|
293
|
+
...(duringStreaming ? [taskResult] : []),
|
|
294
|
+
{ type: "TEXT_MESSAGE_CONTENT", messageId: "other", delta: " After." },
|
|
295
|
+
{ type: "TEXT_MESSAGE_END", messageId: "other" },
|
|
296
|
+
finished,
|
|
297
|
+
]);
|
|
298
|
+
await core.append(userMessage);
|
|
299
|
+
|
|
300
|
+
expect(core.isRunning()).toBe(false);
|
|
301
|
+
expect(statuses(core).answer).toBe("complete");
|
|
302
|
+
expect(
|
|
303
|
+
core.getMessages().find(({ id }) => id === "answer")?.content,
|
|
304
|
+
).toEqual([
|
|
305
|
+
{ type: "text", text: "Let me check." },
|
|
306
|
+
{
|
|
307
|
+
type: "tool-call",
|
|
308
|
+
toolCallId: "task",
|
|
309
|
+
toolName: "task",
|
|
310
|
+
args: {},
|
|
311
|
+
argsText: "{}",
|
|
312
|
+
result: "done",
|
|
313
|
+
unstable_toolMessageId: "tool-task",
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
expect(core.getMessages().find(({ id }) => id === "other")).toMatchObject(
|
|
317
|
+
{
|
|
318
|
+
status: { type: "complete" },
|
|
319
|
+
content: [{ type: "text", text: "Next step. Before. After." }],
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
146
325
|
it("does not show a complete answer with a running sub-agent after a result snapshot", async () => {
|
|
147
326
|
const core = createController((wire) => [
|
|
148
327
|
start,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import type
|
|
2
|
+
import { HttpAgent, type AbstractAgent } from "@ag-ui/client";
|
|
3
3
|
import type {
|
|
4
4
|
AppendMessage,
|
|
5
5
|
ThreadAssistantMessage,
|
|
@@ -540,6 +540,25 @@ describe("AgUiThreadController activity deltas", () => {
|
|
|
540
540
|
});
|
|
541
541
|
|
|
542
542
|
describe("AgUiThreadController run state", () => {
|
|
543
|
+
const createStateAgent = (sentStates: unknown[]) =>
|
|
544
|
+
new HttpAgent({
|
|
545
|
+
url: "http://agent.test/agent",
|
|
546
|
+
initialState: { fromInitialState: true },
|
|
547
|
+
fetch: async (_url, init) => {
|
|
548
|
+
const body = JSON.parse(String(init?.body));
|
|
549
|
+
sentStates.push(body.state);
|
|
550
|
+
const events = [
|
|
551
|
+
{ type: "RUN_STARTED", threadId: body.threadId, runId: body.runId },
|
|
552
|
+
{ type: "STATE_SNAPSHOT", snapshot: { a: 1 } },
|
|
553
|
+
{ type: "RUN_FINISHED", threadId: body.threadId, runId: body.runId },
|
|
554
|
+
];
|
|
555
|
+
return new Response(
|
|
556
|
+
events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""),
|
|
557
|
+
{ headers: { "content-type": "text/event-stream" } },
|
|
558
|
+
);
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
|
|
543
562
|
type Subscriber = {
|
|
544
563
|
onStateSnapshotEvent?: (payload: {
|
|
545
564
|
event: { type: "STATE_SNAPSHOT"; snapshot: { count: number } };
|
|
@@ -577,6 +596,119 @@ describe("AgUiThreadController run state", () => {
|
|
|
577
596
|
expect(runAgent.mock.calls[2]?.[0].state).toBeNull();
|
|
578
597
|
expect(agent.state).toBeNull();
|
|
579
598
|
});
|
|
599
|
+
|
|
600
|
+
it("isolates mounted threads sharing one agent through runs and setState", async () => {
|
|
601
|
+
const sentStates: unknown[] = [];
|
|
602
|
+
const agent = createStateAgent(sentStates);
|
|
603
|
+
const { core: threadA } = createCore(undefined, agent);
|
|
604
|
+
const { core: threadB } = createCore(undefined, agent);
|
|
605
|
+
|
|
606
|
+
await threadA.append({ ...userMessage("A"), startRun: true });
|
|
607
|
+
expect(threadA.getState()).toEqual({ a: 1 });
|
|
608
|
+
expect.soft(threadB.getState()).toEqual({ fromInitialState: true });
|
|
609
|
+
|
|
610
|
+
threadA.setState({ x: 1 });
|
|
611
|
+
await flush();
|
|
612
|
+
expect(threadA.getState()).toEqual({ x: 1 });
|
|
613
|
+
expect.soft(threadB.getState()).toEqual({ fromInitialState: true });
|
|
614
|
+
|
|
615
|
+
await threadB.append({ ...userMessage("B"), startRun: true });
|
|
616
|
+
expect
|
|
617
|
+
.soft(sentStates)
|
|
618
|
+
.toEqual([{ fromInitialState: true }, { fromInitialState: true }]);
|
|
619
|
+
expect.soft(threadA.getState()).toEqual({ x: 1 });
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("starts a later thread from the shared agent's initial state after a run", async () => {
|
|
623
|
+
const sentStates: unknown[] = [];
|
|
624
|
+
const agent = createStateAgent(sentStates);
|
|
625
|
+
const { core: threadA } = createCore(undefined, agent);
|
|
626
|
+
|
|
627
|
+
await threadA.append({ ...userMessage("A"), startRun: true });
|
|
628
|
+
expect(agent.state).toEqual({ a: 1 });
|
|
629
|
+
|
|
630
|
+
const { core: threadB } = createCore(undefined, agent);
|
|
631
|
+
expect.soft(threadB.getState()).toEqual({ fromInitialState: true });
|
|
632
|
+
await threadB.append({ ...userMessage("B"), startRun: true });
|
|
633
|
+
expect(sentStates).toEqual([
|
|
634
|
+
{ fromInitialState: true },
|
|
635
|
+
{ fromInitialState: true },
|
|
636
|
+
]);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("copies the initial state before the agent state is mutated", () => {
|
|
640
|
+
const agent = new HttpAgent({
|
|
641
|
+
url: "http://agent.test/agent",
|
|
642
|
+
initialState: { nested: { count: 0 } },
|
|
643
|
+
});
|
|
644
|
+
const { core } = createCore(undefined, agent);
|
|
645
|
+
|
|
646
|
+
agent.state.nested.count = 1;
|
|
647
|
+
expect(core.getState()).toEqual({ nested: { count: 0 } });
|
|
648
|
+
|
|
649
|
+
const { core: laterThread } = createCore(undefined, agent);
|
|
650
|
+
expect(laterThread.getState()).toEqual({ nested: { count: 0 } });
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("records an undefined initial state without adopting later agent state", () => {
|
|
654
|
+
const agent = {} as AbstractAgent;
|
|
655
|
+
createCore(undefined, agent);
|
|
656
|
+
agent.state = { fromPreviousThread: true };
|
|
657
|
+
|
|
658
|
+
const { core } = createCore(undefined, agent);
|
|
659
|
+
expect(core.getState()).toBeUndefined();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("leaves the state undefined for an agent without initialState", () => {
|
|
663
|
+
const agent = new HttpAgent({ url: "http://agent.test/agent" });
|
|
664
|
+
const { core } = createCore(undefined, agent);
|
|
665
|
+
|
|
666
|
+
expect(agent.state).toEqual({});
|
|
667
|
+
expect(core.getState()).toBeUndefined();
|
|
668
|
+
|
|
669
|
+
const swapped = new HttpAgent({ url: "http://agent.test/other" });
|
|
670
|
+
core.updateOptions({
|
|
671
|
+
agent: swapped,
|
|
672
|
+
logger: makeLogger(),
|
|
673
|
+
showThinking: true,
|
|
674
|
+
});
|
|
675
|
+
expect(core.getState()).toBeUndefined();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("seeds a new agent's initial state on swap when the thread has no snapshot", () => {
|
|
679
|
+
const { core } = createCore();
|
|
680
|
+
const agent = new HttpAgent({
|
|
681
|
+
url: "http://agent.test/agent",
|
|
682
|
+
initialState: { a: 1 },
|
|
683
|
+
});
|
|
684
|
+
const listener = vi.fn();
|
|
685
|
+
core.getSnapshot();
|
|
686
|
+
core.subscribe(listener);
|
|
687
|
+
|
|
688
|
+
core.updateOptions({ agent, logger: makeLogger(), showThinking: true });
|
|
689
|
+
|
|
690
|
+
expect(agent.state).toEqual({ a: 1 });
|
|
691
|
+
expect(core.getState()).toEqual({ a: 1 });
|
|
692
|
+
expect(core.getSnapshot().state).toEqual({ a: 1 });
|
|
693
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("preserves the thread snapshot on swap and records the new agent's initial state", async () => {
|
|
697
|
+
const { core } = createCore();
|
|
698
|
+
core.setState({ fromThread: true });
|
|
699
|
+
const sentStates: unknown[] = [];
|
|
700
|
+
const agent = createStateAgent(sentStates);
|
|
701
|
+
|
|
702
|
+
core.updateOptions({ agent, logger: makeLogger(), showThinking: true });
|
|
703
|
+
|
|
704
|
+
expect(agent.state).toEqual({ fromInitialState: true });
|
|
705
|
+
expect(core.getState()).toEqual({ fromThread: true });
|
|
706
|
+
await core.append({ ...userMessage("after swap"), startRun: true });
|
|
707
|
+
expect(sentStates).toEqual([{ fromThread: true }]);
|
|
708
|
+
|
|
709
|
+
const { core: laterThread } = createCore(undefined, agent);
|
|
710
|
+
expect(laterThread.getState()).toEqual({ fromInitialState: true });
|
|
711
|
+
});
|
|
580
712
|
});
|
|
581
713
|
|
|
582
714
|
describe("AgUiThreadController setState", () => {
|
|
@@ -82,6 +82,26 @@ import {
|
|
|
82
82
|
withToolApprovalDecision,
|
|
83
83
|
} from "./adapter/tool-approval";
|
|
84
84
|
|
|
85
|
+
// The SDK does not retain initialState.
|
|
86
|
+
const agentInitialStates = new WeakMap<
|
|
87
|
+
AbstractAgent,
|
|
88
|
+
ReadonlyJSONValue | undefined
|
|
89
|
+
>();
|
|
90
|
+
|
|
91
|
+
const getAgentInitialState = (agent: AbstractAgent) => {
|
|
92
|
+
if (!agentInitialStates.has(agent)) {
|
|
93
|
+
const state = agent.state;
|
|
94
|
+
agentInitialStates.set(
|
|
95
|
+
agent,
|
|
96
|
+
state === undefined ||
|
|
97
|
+
(isPlainObject(state) && Object.keys(state).length === 0)
|
|
98
|
+
? undefined
|
|
99
|
+
: (structuredClone(state) as ReadonlyJSONValue),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return agentInitialStates.get(agent);
|
|
103
|
+
};
|
|
104
|
+
|
|
85
105
|
// AbstractAgent.runAgent declares two parameters. HttpAgent ignores a third and
|
|
86
106
|
// is cancelled through agent.abortRun(); the run options stay for subclasses
|
|
87
107
|
// that inherit the base no-op abortRun and have no other cancellation hook.
|
|
@@ -386,6 +406,13 @@ export class AgUiThreadController {
|
|
|
386
406
|
|
|
387
407
|
constructor(options: CoreOptions) {
|
|
388
408
|
this.agent = options.agent;
|
|
409
|
+
const initialState = getAgentInitialState(this.agent);
|
|
410
|
+
if (initialState !== undefined) {
|
|
411
|
+
this.dispatch({
|
|
412
|
+
type: "stateSnapshot",
|
|
413
|
+
snapshot: initialState,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
389
416
|
this.logger = options.logger;
|
|
390
417
|
this.showThinking = options.showThinking;
|
|
391
418
|
this.joinStrategy = options.joinStrategy;
|
|
@@ -421,7 +448,18 @@ export class AgUiThreadController {
|
|
|
421
448
|
}
|
|
422
449
|
|
|
423
450
|
updateOptions(options: CoreOptions) {
|
|
451
|
+
const agentChanged = this.agent !== options.agent;
|
|
424
452
|
this.agent = options.agent;
|
|
453
|
+
if (agentChanged) {
|
|
454
|
+
const initialState = getAgentInitialState(this.agent);
|
|
455
|
+
if (
|
|
456
|
+
this.state.stateSnapshot === undefined &&
|
|
457
|
+
initialState !== undefined
|
|
458
|
+
) {
|
|
459
|
+
this.dispatch({ type: "stateSnapshot", snapshot: initialState });
|
|
460
|
+
this.publishUpdate();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
425
463
|
this.logger = options.logger;
|
|
426
464
|
this.showThinking = options.showThinking;
|
|
427
465
|
this.joinStrategy = options.joinStrategy;
|
|
@@ -2775,7 +2813,7 @@ export class AgUiThreadController {
|
|
|
2775
2813
|
update.content as ThreadAssistantMessage["content"],
|
|
2776
2814
|
)
|
|
2777
2815
|
: assistant.content;
|
|
2778
|
-
const calls =
|
|
2816
|
+
const calls = Array.from(iterateToolCallParts(content));
|
|
2779
2817
|
// A retained historical call needs existing or current stream authority.
|
|
2780
2818
|
const pending =
|
|
2781
2819
|
pendingToolCallIds ??
|
|
@@ -2789,7 +2827,11 @@ export class AgUiThreadController {
|
|
|
2789
2827
|
: undefined);
|
|
2790
2828
|
if (replay && latestStatus?.type !== "running" && pending)
|
|
2791
2829
|
replay.admittedToolCallIds = pending;
|
|
2792
|
-
if (
|
|
2830
|
+
if (
|
|
2831
|
+
replay ||
|
|
2832
|
+
(latestStatus?.type === "requires-action" &&
|
|
2833
|
+
latestStatus.reason === "interrupt")
|
|
2834
|
+
) {
|
|
2793
2835
|
// Project gates against the content the user can actually answer.
|
|
2794
2836
|
const interrupts =
|
|
2795
2837
|
latestStatus?.type === "requires-action" &&
|
|
@@ -2805,10 +2847,10 @@ export class AgUiThreadController {
|
|
|
2805
2847
|
new Set(calls.map((part) => part.toolCallId)),
|
|
2806
2848
|
);
|
|
2807
2849
|
const originalCalls = new Map(
|
|
2808
|
-
Array.from(
|
|
2809
|
-
|
|
2810
|
-
part,
|
|
2811
|
-
|
|
2850
|
+
Array.from(
|
|
2851
|
+
iterateToolCallParts(replay?.originalContent ?? assistant.content),
|
|
2852
|
+
(part) => [part.toolCallId, part],
|
|
2853
|
+
),
|
|
2812
2854
|
);
|
|
2813
2855
|
content = mapToolCallPartsDeep(content, (part) => {
|
|
2814
2856
|
const approval = approvals.get(part.toolCallId);
|
|
@@ -3301,6 +3343,9 @@ export class AgUiThreadController {
|
|
|
3301
3343
|
}
|
|
3302
3344
|
}
|
|
3303
3345
|
activeAssistantId = nextActiveAssistantId;
|
|
3346
|
+
if (activeAssistantId && joinStrategy !== "concat-content") {
|
|
3347
|
+
aggregator.rebaseMessage(activeAssistantId);
|
|
3348
|
+
}
|
|
3304
3349
|
if (hasNewPendingToolCallAnswer && head) {
|
|
3305
3350
|
this.updateAssistantMessage(head.id, {
|
|
3306
3351
|
status: { type: "running" },
|
|
@@ -3592,10 +3637,28 @@ export class AgUiThreadController {
|
|
|
3592
3637
|
previous?.status?.type === "running" &&
|
|
3593
3638
|
snapshot.status?.type === "requires-action" &&
|
|
3594
3639
|
snapshot.status.reason === "tool-calls";
|
|
3640
|
+
const previousToolCallIds = new Set(
|
|
3641
|
+
Array.from(
|
|
3642
|
+
iterateToolCallParts(previous?.content ?? []),
|
|
3643
|
+
(call) => call.toolCallId,
|
|
3644
|
+
),
|
|
3645
|
+
);
|
|
3646
|
+
const preservesSettledStatus =
|
|
3647
|
+
convertedMessage.id !== activeAssistantId &&
|
|
3648
|
+
this.isTerminalStatus(previous?.status) &&
|
|
3649
|
+
snapshot.status?.type === "requires-action" &&
|
|
3650
|
+
snapshot.status.reason === "tool-calls" &&
|
|
3651
|
+
Array.from(iterateToolCallParts(snapshot.content)).every(
|
|
3652
|
+
(call) =>
|
|
3653
|
+
isResolvedToolCall(call) ||
|
|
3654
|
+
previousToolCallIds.has(call.toolCallId),
|
|
3655
|
+
);
|
|
3595
3656
|
converted[index] = previous
|
|
3596
3657
|
? {
|
|
3597
3658
|
...snapshot,
|
|
3598
|
-
...(preservesRunningStatus
|
|
3659
|
+
...(preservesRunningStatus || preservesSettledStatus
|
|
3660
|
+
? { status: previous.status }
|
|
3661
|
+
: {}),
|
|
3599
3662
|
content: this.mergeSnapshotAssistantContent(
|
|
3600
3663
|
previous.content,
|
|
3601
3664
|
snapshot.content,
|
|
@@ -267,6 +267,17 @@ export class RunAggregator {
|
|
|
267
267
|
return this.toolCalls.has(toolCallId);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
rebaseMessage(messageId: string): void {
|
|
271
|
+
const snapshotText = this.pendingSnapshotText.get(messageId);
|
|
272
|
+
this.resetMessageParts();
|
|
273
|
+
this.lastTextMessageId = messageId;
|
|
274
|
+
this.reportedServerMessageId = messageId;
|
|
275
|
+
if (snapshotText !== undefined) {
|
|
276
|
+
this.pendingSnapshotText.set(messageId, snapshotText);
|
|
277
|
+
}
|
|
278
|
+
this.startTextMessage(ROOT_SCOPE, messageId);
|
|
279
|
+
}
|
|
280
|
+
|
|
270
281
|
handle(event: AgUiEvent): void {
|
|
271
282
|
switch (event.type) {
|
|
272
283
|
case "RUN_STARTED": {
|