@assistant-ui/react-ag-ui 0.1.7 → 0.1.9
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 +10 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +86 -32
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +10 -0
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +9 -4
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/types.d.ts +6 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +6 -2
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/AgUiThreadController.connect.test.ts +372 -0
- package/src/runtime/AgUiThreadController.notifications.test.ts +126 -0
- package/src/runtime/AgUiThreadController.ts +158 -36
- package/src/runtime/adapter/conversions.subagent-messages.test.ts +117 -0
- package/src/runtime/adapter/conversions.ts +40 -5
- package/src/runtime/types.ts +6 -0
- package/src/useAgUiRuntime.connect.test.tsx +169 -0
- package/src/useAgUiRuntime.external-store.test.tsx +46 -1
- package/src/useAgUiRuntime.ts +8 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { HttpAgent } from "@ag-ui/client";
|
|
2
|
+
import { expect, it, vi } from "vitest";
|
|
3
|
+
import { AgUiThreadController } from "./AgUiThreadController";
|
|
4
|
+
import type { AgUiSubscriber } from "./adapter/subscriber";
|
|
5
|
+
import { makeLogger } from "./logger";
|
|
6
|
+
|
|
7
|
+
it("notifies subscribers synchronously when a delta callback cancels the run", async () => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
let subscriber!: AgUiSubscriber;
|
|
10
|
+
let finishRun!: () => void;
|
|
11
|
+
const agent = {
|
|
12
|
+
runAgent: (_input: unknown, current: AgUiSubscriber) => {
|
|
13
|
+
subscriber = current;
|
|
14
|
+
return new Promise<void>((resolve) => {
|
|
15
|
+
finishRun = resolve;
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
abortRun: vi.fn(),
|
|
19
|
+
} as unknown as HttpAgent;
|
|
20
|
+
const runningStates: boolean[] = [];
|
|
21
|
+
let runningStatesAtCancelReturn: boolean[] = [];
|
|
22
|
+
const error = vi.fn(() => {
|
|
23
|
+
void controller.cancel();
|
|
24
|
+
runningStatesAtCancelReturn = [...runningStates];
|
|
25
|
+
});
|
|
26
|
+
const controller = new AgUiThreadController({
|
|
27
|
+
agent,
|
|
28
|
+
logger: makeLogger({ error }),
|
|
29
|
+
showThinking: true,
|
|
30
|
+
});
|
|
31
|
+
const pending = controller.reload(null);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
subscriber.onTextMessageStartEvent?.({
|
|
35
|
+
event: { type: "TEXT_MESSAGE_START", messageId: "answer" },
|
|
36
|
+
});
|
|
37
|
+
controller.subscribe(() => {
|
|
38
|
+
runningStates.push(controller.getSnapshot().isRunning);
|
|
39
|
+
});
|
|
40
|
+
subscriber.onTextMessageContentEvent?.({
|
|
41
|
+
event: {
|
|
42
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
43
|
+
messageId: "answer",
|
|
44
|
+
delta: "partial",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
expect(runningStates).toEqual([]);
|
|
48
|
+
expect(controller.getSnapshot().isRunning).toBe(true);
|
|
49
|
+
|
|
50
|
+
subscriber.onStateDeltaEvent?.({
|
|
51
|
+
event: {
|
|
52
|
+
type: "STATE_DELTA",
|
|
53
|
+
delta: [{ op: "replace", path: "/missing", value: 1 }],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(error).toHaveBeenCalledOnce();
|
|
58
|
+
expect(runningStatesAtCancelReturn.length).toBeGreaterThan(0);
|
|
59
|
+
expect(runningStatesAtCancelReturn.at(-1)).toBe(false);
|
|
60
|
+
expect(controller.getSnapshot().isRunning).toBe(false);
|
|
61
|
+
vi.runAllTimers();
|
|
62
|
+
expect(runningStates).toEqual(runningStatesAtCancelReturn);
|
|
63
|
+
} finally {
|
|
64
|
+
finishRun();
|
|
65
|
+
await pending;
|
|
66
|
+
vi.useRealTimers();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("notifies once for a text delta burst while the stream stays open", async () => {
|
|
71
|
+
vi.useFakeTimers();
|
|
72
|
+
let subscriber!: AgUiSubscriber;
|
|
73
|
+
let finishRun!: () => void;
|
|
74
|
+
const agent = {
|
|
75
|
+
runAgent: (_input: unknown, current: AgUiSubscriber) => {
|
|
76
|
+
subscriber = current;
|
|
77
|
+
return new Promise<void>((resolve) => {
|
|
78
|
+
finishRun = resolve;
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
abortRun: vi.fn(),
|
|
82
|
+
} as unknown as HttpAgent;
|
|
83
|
+
const controller = new AgUiThreadController({
|
|
84
|
+
agent,
|
|
85
|
+
logger: makeLogger(),
|
|
86
|
+
showThinking: true,
|
|
87
|
+
});
|
|
88
|
+
const pending = controller.reload(null);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
subscriber.onTextMessageStartEvent?.({
|
|
92
|
+
event: { type: "TEXT_MESSAGE_START", messageId: "answer" },
|
|
93
|
+
});
|
|
94
|
+
const listener = vi.fn(() => controller.getSnapshot());
|
|
95
|
+
controller.subscribe(listener);
|
|
96
|
+
for (const delta of ["hello", " ", "world"]) {
|
|
97
|
+
subscriber.onTextMessageContentEvent?.({
|
|
98
|
+
event: { type: "TEXT_MESSAGE_CONTENT", messageId: "answer", delta },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
expect(listener).not.toHaveBeenCalled();
|
|
103
|
+
vi.runAllTimers();
|
|
104
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
105
|
+
expect(listener.mock.results[0]?.value).toMatchObject({
|
|
106
|
+
isRunning: true,
|
|
107
|
+
messageRepository: {
|
|
108
|
+
messages: [
|
|
109
|
+
{
|
|
110
|
+
message: {
|
|
111
|
+
role: "assistant",
|
|
112
|
+
content: [{ type: "text", text: "hello world" }],
|
|
113
|
+
status: { type: "running" },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
vi.runAllTimers();
|
|
120
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
121
|
+
} finally {
|
|
122
|
+
finishRun();
|
|
123
|
+
await pending;
|
|
124
|
+
vi.useRealTimers();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
@@ -218,6 +218,28 @@ const isActivityPart = (part: ThreadAssistantMessage["content"][number]) =>
|
|
|
218
218
|
type ResumeStream = (
|
|
219
219
|
options: ChatModelRunOptions,
|
|
220
220
|
) => AsyncGenerator<ChatModelRunResult, void, unknown>;
|
|
221
|
+
/** A run already in flight on the agent, joined through `agent.connectAgent()`. */
|
|
222
|
+
type ResumeSource = ResumeStream | "connect";
|
|
223
|
+
|
|
224
|
+
// connectAgent() swallows the base class's not-implemented error and resolves
|
|
225
|
+
// with no events, so only an agent that overrides connect() can rejoin a run.
|
|
226
|
+
// AbstractAgent extends nothing, so the prototype owning connect() is the base
|
|
227
|
+
// exactly when its own prototype is Object.prototype. Checking ownership instead
|
|
228
|
+
// of function identity holds when the app loads its own copy of @ag-ui/client.
|
|
229
|
+
const implementsConnect = (agent: AbstractAgent): boolean => {
|
|
230
|
+
if (typeof agent.connectAgent !== "function") return false;
|
|
231
|
+
if (Object.prototype.hasOwnProperty.call(agent, "connect")) return true;
|
|
232
|
+
for (
|
|
233
|
+
let proto: object | null = Object.getPrototypeOf(agent);
|
|
234
|
+
proto !== null;
|
|
235
|
+
proto = Object.getPrototypeOf(proto)
|
|
236
|
+
) {
|
|
237
|
+
if (Object.prototype.hasOwnProperty.call(proto, "connect")) {
|
|
238
|
+
return Object.getPrototypeOf(proto) !== Object.prototype;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
};
|
|
221
243
|
type ResumeRunConfig = {
|
|
222
244
|
parentId: string | null;
|
|
223
245
|
sourceId: string | null;
|
|
@@ -240,6 +262,7 @@ type CoreOptions = {
|
|
|
240
262
|
logger: Logger;
|
|
241
263
|
showThinking: boolean;
|
|
242
264
|
joinStrategy?: UseAgUiRuntimeOptions["joinStrategy"];
|
|
265
|
+
joinAdjacentAssistantMessages?: boolean | undefined;
|
|
243
266
|
getRunContext?: UseAgUiRuntimeOptions["getRunContext"];
|
|
244
267
|
resumeTranscript?: AgUiResumeTranscript | undefined;
|
|
245
268
|
autoCancelPendingToolCalls?: boolean | undefined;
|
|
@@ -377,6 +400,7 @@ export class AgUiThreadController {
|
|
|
377
400
|
private agent: AbstractAgent;
|
|
378
401
|
private logger: Logger;
|
|
379
402
|
private showThinking: boolean;
|
|
403
|
+
private joinAdjacentAssistantMessages: boolean;
|
|
380
404
|
private joinStrategy: UseAgUiRuntimeOptions["joinStrategy"];
|
|
381
405
|
private getRunContext: UseAgUiRuntimeOptions["getRunContext"];
|
|
382
406
|
private resumeTranscript: AgUiResumeTranscript;
|
|
@@ -384,6 +408,7 @@ export class AgUiThreadController {
|
|
|
384
408
|
private onError: ((error: Error) => void) | undefined;
|
|
385
409
|
private onCancel: (() => void) | undefined;
|
|
386
410
|
private readonly listeners = new Set<() => void>();
|
|
411
|
+
private pendingUpdateNotification: ReturnType<typeof setTimeout> | undefined;
|
|
387
412
|
private snapshot:
|
|
388
413
|
| {
|
|
389
414
|
isLoading: boolean;
|
|
@@ -445,6 +470,8 @@ export class AgUiThreadController {
|
|
|
445
470
|
}
|
|
446
471
|
this.logger = options.logger;
|
|
447
472
|
this.showThinking = options.showThinking;
|
|
473
|
+
this.joinAdjacentAssistantMessages =
|
|
474
|
+
options.joinAdjacentAssistantMessages ?? false;
|
|
448
475
|
this.joinStrategy = options.joinStrategy;
|
|
449
476
|
this.getRunContext = options.getRunContext;
|
|
450
477
|
this.resumeTranscript = options.resumeTranscript ?? "full";
|
|
@@ -472,8 +499,20 @@ export class AgUiThreadController {
|
|
|
472
499
|
this.state = reduceAgUiThreadState(this.state, action);
|
|
473
500
|
}
|
|
474
501
|
|
|
475
|
-
private publishUpdate(): void {
|
|
502
|
+
private publishUpdate(deferNotification = false): void {
|
|
476
503
|
this.snapshot = undefined;
|
|
504
|
+
if (deferNotification) {
|
|
505
|
+
if (this.pendingUpdateNotification !== undefined) return;
|
|
506
|
+
this.pendingUpdateNotification = setTimeout(() => {
|
|
507
|
+
this.pendingUpdateNotification = undefined;
|
|
508
|
+
for (const listener of this.listeners) listener();
|
|
509
|
+
}, 0);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (this.pendingUpdateNotification !== undefined) {
|
|
513
|
+
clearTimeout(this.pendingUpdateNotification);
|
|
514
|
+
this.pendingUpdateNotification = undefined;
|
|
515
|
+
}
|
|
477
516
|
for (const listener of this.listeners) listener();
|
|
478
517
|
}
|
|
479
518
|
|
|
@@ -492,6 +531,8 @@ export class AgUiThreadController {
|
|
|
492
531
|
}
|
|
493
532
|
this.logger = options.logger;
|
|
494
533
|
this.showThinking = options.showThinking;
|
|
534
|
+
this.joinAdjacentAssistantMessages =
|
|
535
|
+
options.joinAdjacentAssistantMessages ?? false;
|
|
495
536
|
this.joinStrategy = options.joinStrategy;
|
|
496
537
|
this.getRunContext = options.getRunContext;
|
|
497
538
|
this.resumeTranscript = options.resumeTranscript ?? "full";
|
|
@@ -517,9 +558,22 @@ export class AgUiThreadController {
|
|
|
517
558
|
|
|
518
559
|
detachRuntime() {
|
|
519
560
|
this.runtime = undefined;
|
|
520
|
-
|
|
561
|
+
try {
|
|
562
|
+
this.leave();
|
|
563
|
+
} catch (error) {
|
|
521
564
|
this.logger.error("[agui] failed to cancel run during teardown", error);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Stops following the active run because the thread is being left (a thread
|
|
570
|
+
* switch or teardown), as opposed to cancel(), which the user asked for.
|
|
571
|
+
*/
|
|
572
|
+
leave(): void {
|
|
573
|
+
this.dispatch({
|
|
574
|
+
type: "historyMutationAdvanced",
|
|
522
575
|
});
|
|
576
|
+
this.abortActiveRun(true);
|
|
523
577
|
}
|
|
524
578
|
|
|
525
579
|
getMessages(): readonly ThreadMessage[] {
|
|
@@ -582,12 +636,11 @@ export class AgUiThreadController {
|
|
|
582
636
|
.then(async (repo) => {
|
|
583
637
|
if (repo?.unstable_resume) {
|
|
584
638
|
const parentId = repo.headId ?? this.session.headId;
|
|
585
|
-
const resumeStream = this.history?.resume?.bind(this.history);
|
|
586
639
|
await this.startRun(
|
|
587
640
|
parentId,
|
|
588
641
|
this.state.lastRunConfig,
|
|
589
642
|
undefined,
|
|
590
|
-
|
|
643
|
+
this.getResumeSource(),
|
|
591
644
|
);
|
|
592
645
|
}
|
|
593
646
|
})
|
|
@@ -739,16 +792,26 @@ export class AgUiThreadController {
|
|
|
739
792
|
// synchronously, so a callback that starts another run must keep the
|
|
740
793
|
// controller it installed, and a throw from a subclass's abortRun would
|
|
741
794
|
// otherwise strand the thread as running.
|
|
742
|
-
private abortActiveRun(): void {
|
|
795
|
+
private abortActiveRun(leaveRunning = false): void {
|
|
743
796
|
const controller = this.abortController;
|
|
744
797
|
if (!controller) return;
|
|
745
798
|
try {
|
|
746
|
-
(this.activeRunAgent ?? this.agent)
|
|
799
|
+
this.stopAgent(this.activeRunAgent ?? this.agent, leaveRunning);
|
|
747
800
|
} finally {
|
|
748
801
|
controller.abort();
|
|
749
802
|
}
|
|
750
803
|
}
|
|
751
804
|
|
|
805
|
+
private stopAgent(agent: AbstractAgent, leaveRunning: boolean): void {
|
|
806
|
+
if (leaveRunning && implementsConnect(agent)) {
|
|
807
|
+
void agent.detachActiveRun().catch((error: unknown) => {
|
|
808
|
+
this.logger.error?.("[agui] agent detachActiveRun failed", error);
|
|
809
|
+
});
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
agent.abortRun();
|
|
813
|
+
}
|
|
814
|
+
|
|
752
815
|
// Starting a run supersedes the active one, and reports whether the caller
|
|
753
816
|
// still owns the thread afterwards: the local abort runs onCancel
|
|
754
817
|
// synchronously, so a callback that starts its own run keeps it. abortRun is a
|
|
@@ -785,11 +848,11 @@ export class AgUiThreadController {
|
|
|
785
848
|
async resumeInFlightRun(messages: readonly ThreadMessage[]): Promise<void> {
|
|
786
849
|
const historyImport = this.waitForHistoryImport();
|
|
787
850
|
if (historyImport && !(await historyImport)) return;
|
|
788
|
-
// Without a resume
|
|
789
|
-
const
|
|
790
|
-
if (!
|
|
851
|
+
// Without a resume source startRun would re-run the agent from scratch.
|
|
852
|
+
const resumeSource = this.getResumeSource();
|
|
853
|
+
if (!resumeSource) {
|
|
791
854
|
const error = new Error(
|
|
792
|
-
"[agui] unstable_resume requires a ThreadHistoryAdapter with a resume() method; skipping resume after thread switch",
|
|
855
|
+
"[agui] unstable_resume requires a ThreadHistoryAdapter with a resume() method or an agent that implements connect(); skipping resume after thread switch",
|
|
793
856
|
);
|
|
794
857
|
this.logger.error?.(error.message);
|
|
795
858
|
invokeRuntimeCallback("onError", this.onError, error);
|
|
@@ -801,13 +864,19 @@ export class AgUiThreadController {
|
|
|
801
864
|
parentId,
|
|
802
865
|
this.state.lastRunConfig,
|
|
803
866
|
undefined,
|
|
804
|
-
|
|
867
|
+
resumeSource,
|
|
805
868
|
);
|
|
806
869
|
} catch {
|
|
807
870
|
// startRun already reported via onError; don't reject the switch.
|
|
808
871
|
}
|
|
809
872
|
}
|
|
810
873
|
|
|
874
|
+
private getResumeSource(): ResumeSource | undefined {
|
|
875
|
+
const resumeStream = this.history?.resume?.bind(this.history);
|
|
876
|
+
if (resumeStream) return resumeStream;
|
|
877
|
+
return implementsConnect(this.agent) ? "connect" : undefined;
|
|
878
|
+
}
|
|
879
|
+
|
|
811
880
|
private assertNoPendingInterrupts(): void {
|
|
812
881
|
if (!this.getPendingInterrupts()) return;
|
|
813
882
|
throw new Error(
|
|
@@ -1664,7 +1733,7 @@ export class AgUiThreadController {
|
|
|
1664
1733
|
this.activeRunAgent = null;
|
|
1665
1734
|
this.setRunning(false);
|
|
1666
1735
|
try {
|
|
1667
|
-
(activeRunAgent ?? this.agent)
|
|
1736
|
+
this.stopAgent(activeRunAgent ?? this.agent, true);
|
|
1668
1737
|
} catch (error) {
|
|
1669
1738
|
this.logger.error?.("[agui] agent abortRun failed", error);
|
|
1670
1739
|
} finally {
|
|
@@ -1679,10 +1748,12 @@ export class AgUiThreadController {
|
|
|
1679
1748
|
parentId: string | null,
|
|
1680
1749
|
runConfig?: RunConfig,
|
|
1681
1750
|
resume?: ResumeDispatch,
|
|
1682
|
-
|
|
1751
|
+
resumeSource?: ResumeSource,
|
|
1683
1752
|
replayExistingMessages = false,
|
|
1684
1753
|
): Promise<void> {
|
|
1685
1754
|
if (this.isThreadSwitching?.()) return;
|
|
1755
|
+
const resumeStream =
|
|
1756
|
+
typeof resumeSource === "function" ? resumeSource : undefined;
|
|
1686
1757
|
// A default AG-UI run supersedes the active run; the hook's opt-in message
|
|
1687
1758
|
// queue serializes sends instead. append supersedes earlier, before it links
|
|
1688
1759
|
// its message; this covers the entry points that start a run without one.
|
|
@@ -1778,6 +1849,7 @@ export class AgUiThreadController {
|
|
|
1778
1849
|
let frameOwnerIds = new Set<string>();
|
|
1779
1850
|
let restrictedPendingToolCallIds: ReadonlySet<string> | undefined;
|
|
1780
1851
|
let settlingRun = false;
|
|
1852
|
+
let deferUpdateNotifications = false;
|
|
1781
1853
|
let latestSnapshot: ThreadAssistantMessage | undefined;
|
|
1782
1854
|
let snapshotToolCallIds = new Set<string>();
|
|
1783
1855
|
const snapshotResultIds = new Set<string>();
|
|
@@ -1813,7 +1885,11 @@ export class AgUiThreadController {
|
|
|
1813
1885
|
this.session.hasMessage(assistantParentId)
|
|
1814
1886
|
? assistantParentId
|
|
1815
1887
|
: repositoryHeadId;
|
|
1816
|
-
const created = this.insertAssistantPlaceholder(
|
|
1888
|
+
const created = this.insertAssistantPlaceholder(
|
|
1889
|
+
parentId,
|
|
1890
|
+
cached,
|
|
1891
|
+
deferUpdateNotifications,
|
|
1892
|
+
);
|
|
1817
1893
|
assistantMessageId = created;
|
|
1818
1894
|
this.markPendingAssistantHistory(created, parentId);
|
|
1819
1895
|
return created;
|
|
@@ -2061,6 +2137,7 @@ export class AgUiThreadController {
|
|
|
2061
2137
|
},
|
|
2062
2138
|
replayContexts.get(ownerId),
|
|
2063
2139
|
interrupt ? new Set<string>() : restrictedPendingToolCallIds,
|
|
2140
|
+
deferUpdateNotifications,
|
|
2064
2141
|
);
|
|
2065
2142
|
}
|
|
2066
2143
|
const { content: _content, ...controlUpdate } = update;
|
|
@@ -2078,6 +2155,7 @@ export class AgUiThreadController {
|
|
|
2078
2155
|
primaryUpdate,
|
|
2079
2156
|
replayContexts.get(messageId),
|
|
2080
2157
|
restrictedPendingToolCallIds,
|
|
2158
|
+
deferUpdateNotifications,
|
|
2081
2159
|
);
|
|
2082
2160
|
if (messageId === frameMessageId && resolved !== assistantMessageId) {
|
|
2083
2161
|
assistantMessageId = resolved;
|
|
@@ -2151,7 +2229,11 @@ export class AgUiThreadController {
|
|
|
2151
2229
|
const previousId = assistantMessageId;
|
|
2152
2230
|
assistantMessageId = undefined;
|
|
2153
2231
|
if (!adoptsReplay && this.session.tryGetMessage(previousId)) {
|
|
2154
|
-
const created = this.insertAssistantPlaceholder(
|
|
2232
|
+
const created = this.insertAssistantPlaceholder(
|
|
2233
|
+
previousId,
|
|
2234
|
+
undefined,
|
|
2235
|
+
deferUpdateNotifications,
|
|
2236
|
+
);
|
|
2155
2237
|
assistantMessageId = created;
|
|
2156
2238
|
this.markPendingAssistantHistory(created, previousId);
|
|
2157
2239
|
}
|
|
@@ -2165,7 +2247,11 @@ export class AgUiThreadController {
|
|
|
2165
2247
|
replayEnded = true;
|
|
2166
2248
|
const placeholder = ensureAssistant(true);
|
|
2167
2249
|
if (placeholder === serverId) return;
|
|
2168
|
-
const reassigned = this.reassignAssistantId(
|
|
2250
|
+
const reassigned = this.reassignAssistantId(
|
|
2251
|
+
placeholder,
|
|
2252
|
+
serverId,
|
|
2253
|
+
deferUpdateNotifications,
|
|
2254
|
+
);
|
|
2169
2255
|
// A collision drops the placeholder before revealing the existing
|
|
2170
2256
|
// server message as the current head. Only messages introduced during
|
|
2171
2257
|
// this run can replace the placeholder; regeneration must not rewrite
|
|
@@ -2238,13 +2324,19 @@ export class AgUiThreadController {
|
|
|
2238
2324
|
(settlingRun && heldMessages.has(messageId));
|
|
2239
2325
|
if (held) heldMessages.set(messageId, new Set(unresolved));
|
|
2240
2326
|
else heldMessages.delete(messageId);
|
|
2241
|
-
this.updateAssistantMessage(
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
? {
|
|
2246
|
-
:
|
|
2247
|
-
|
|
2327
|
+
this.updateAssistantMessage(
|
|
2328
|
+
messageId,
|
|
2329
|
+
{
|
|
2330
|
+
...update,
|
|
2331
|
+
...(content !== undefined ? { content } : {}),
|
|
2332
|
+
status: held
|
|
2333
|
+
? { type: "running" }
|
|
2334
|
+
: { type: "complete", reason: "unknown" },
|
|
2335
|
+
},
|
|
2336
|
+
undefined,
|
|
2337
|
+
undefined,
|
|
2338
|
+
deferUpdateNotifications,
|
|
2339
|
+
);
|
|
2248
2340
|
const current = this.session.tryGetMessage(messageId)?.message;
|
|
2249
2341
|
if (current?.role === "assistant")
|
|
2250
2342
|
retainedSnapshots.set(messageId, { ...snapshot, message: current });
|
|
@@ -2262,7 +2354,13 @@ export class AgUiThreadController {
|
|
|
2262
2354
|
!runStartMessageIds.has(head.id) &&
|
|
2263
2355
|
!aggregator.isRetained(head.id)
|
|
2264
2356
|
)
|
|
2265
|
-
this.updateAssistantMessage(
|
|
2357
|
+
this.updateAssistantMessage(
|
|
2358
|
+
head.id,
|
|
2359
|
+
{ status: { type: "running" } },
|
|
2360
|
+
undefined,
|
|
2361
|
+
undefined,
|
|
2362
|
+
deferUpdateNotifications,
|
|
2363
|
+
);
|
|
2266
2364
|
};
|
|
2267
2365
|
|
|
2268
2366
|
const abortController = new AbortController();
|
|
@@ -2477,6 +2575,13 @@ export class AgUiThreadController {
|
|
|
2477
2575
|
if (settlingRun) persistRunAssistants(true);
|
|
2478
2576
|
const beforeSnapshot =
|
|
2479
2577
|
event.type === "MESSAGES_SNAPSHOT" ? this.session.getMessages() : [];
|
|
2578
|
+
const previousDeferredNotifications = deferUpdateNotifications;
|
|
2579
|
+
deferUpdateNotifications =
|
|
2580
|
+
event.type === "TEXT_MESSAGE_CONTENT" ||
|
|
2581
|
+
event.type === "REASONING_MESSAGE_CONTENT" ||
|
|
2582
|
+
event.type === "TOOL_CALL_ARGS" ||
|
|
2583
|
+
event.type === "ACTIVITY_DELTA" ||
|
|
2584
|
+
event.type === "STATE_DELTA";
|
|
2480
2585
|
try {
|
|
2481
2586
|
const nextAssistantMessageId = this.handleEvent(
|
|
2482
2587
|
aggregator,
|
|
@@ -2569,6 +2674,7 @@ export class AgUiThreadController {
|
|
|
2569
2674
|
}
|
|
2570
2675
|
if (settlingRun) persistRunAssistants(false);
|
|
2571
2676
|
} finally {
|
|
2677
|
+
deferUpdateNotifications = previousDeferredNotifications;
|
|
2572
2678
|
settlingRun = false;
|
|
2573
2679
|
}
|
|
2574
2680
|
};
|
|
@@ -2638,9 +2744,13 @@ export class AgUiThreadController {
|
|
|
2638
2744
|
} catch {
|
|
2639
2745
|
// ignore
|
|
2640
2746
|
}
|
|
2641
|
-
|
|
2642
|
-
runAgentInstance.
|
|
2643
|
-
|
|
2747
|
+
if (resumeSource === "connect") {
|
|
2748
|
+
await runAgentInstance.connectAgent(input, subscriber);
|
|
2749
|
+
} else {
|
|
2750
|
+
const runAgent: RunAgentWithRunOptions =
|
|
2751
|
+
runAgentInstance.runAgent.bind(runAgentInstance);
|
|
2752
|
+
await runAgent(input, subscriber, { signal: abortSignal });
|
|
2753
|
+
}
|
|
2644
2754
|
}
|
|
2645
2755
|
} catch (error) {
|
|
2646
2756
|
// HttpAgent rethrows a failure it already passed to the subscriber's
|
|
@@ -2962,6 +3072,7 @@ export class AgUiThreadController {
|
|
|
2962
3072
|
private insertAssistantPlaceholder(
|
|
2963
3073
|
parentId: string | null,
|
|
2964
3074
|
id: string = generateOptimisticId(),
|
|
3075
|
+
deferNotification = false,
|
|
2965
3076
|
): string {
|
|
2966
3077
|
const assistant: ThreadAssistantMessage = {
|
|
2967
3078
|
id,
|
|
@@ -2980,11 +3091,15 @@ export class AgUiThreadController {
|
|
|
2980
3091
|
};
|
|
2981
3092
|
this.session.addOrUpdateMessage(parentId ?? this.session.headId, assistant);
|
|
2982
3093
|
this.session.switchToBranch(id);
|
|
2983
|
-
this.publishUpdate();
|
|
3094
|
+
this.publishUpdate(deferNotification);
|
|
2984
3095
|
return id;
|
|
2985
3096
|
}
|
|
2986
3097
|
|
|
2987
|
-
private reassignAssistantId(
|
|
3098
|
+
private reassignAssistantId(
|
|
3099
|
+
oldId: string,
|
|
3100
|
+
newId: string,
|
|
3101
|
+
deferNotification = false,
|
|
3102
|
+
): boolean {
|
|
2988
3103
|
if (oldId === newId) return true;
|
|
2989
3104
|
const oldItem = this.session.tryGetMessage(oldId);
|
|
2990
3105
|
if (!oldItem) return false;
|
|
@@ -3045,7 +3160,7 @@ export class AgUiThreadController {
|
|
|
3045
3160
|
);
|
|
3046
3161
|
}
|
|
3047
3162
|
|
|
3048
|
-
this.publishUpdate();
|
|
3163
|
+
this.publishUpdate(deferNotification);
|
|
3049
3164
|
return !collidesWithExisting;
|
|
3050
3165
|
}
|
|
3051
3166
|
|
|
@@ -3054,6 +3169,7 @@ export class AgUiThreadController {
|
|
|
3054
3169
|
update: ChatModelRunResult,
|
|
3055
3170
|
replay?: ReplayContext,
|
|
3056
3171
|
pendingToolCallIds?: ReadonlySet<string>,
|
|
3172
|
+
deferNotification = false,
|
|
3057
3173
|
): string {
|
|
3058
3174
|
let latestStatus: MessageStatus | undefined;
|
|
3059
3175
|
const touched = this.session.updateMessage(messageId, (message) => {
|
|
@@ -3177,10 +3293,10 @@ export class AgUiThreadController {
|
|
|
3177
3293
|
latestStatus !== undefined && latestStatus.type !== "running";
|
|
3178
3294
|
if (isSettled && isOptimisticId(messageId)) {
|
|
3179
3295
|
const stableId = generateId();
|
|
3180
|
-
this.reassignAssistantId(messageId, stableId);
|
|
3296
|
+
this.reassignAssistantId(messageId, stableId, deferNotification);
|
|
3181
3297
|
resolvedMessageId = stableId;
|
|
3182
3298
|
} else {
|
|
3183
|
-
this.publishUpdate();
|
|
3299
|
+
this.publishUpdate(deferNotification);
|
|
3184
3300
|
}
|
|
3185
3301
|
if (this.isPersistableStatus(latestStatus)) {
|
|
3186
3302
|
this.persistAssistantHistory(resolvedMessageId);
|
|
@@ -3546,7 +3662,7 @@ export class AgUiThreadController {
|
|
|
3546
3662
|
type: "stateSnapshot",
|
|
3547
3663
|
snapshot: result.newDocument as ReadonlyJSONValue,
|
|
3548
3664
|
});
|
|
3549
|
-
this.updateActiveAssistantState(activeAssistantId);
|
|
3665
|
+
this.updateActiveAssistantState(activeAssistantId, true);
|
|
3550
3666
|
} catch (error) {
|
|
3551
3667
|
this.logger.error?.("[agui] failed to apply state delta", error);
|
|
3552
3668
|
}
|
|
@@ -3658,7 +3774,10 @@ export class AgUiThreadController {
|
|
|
3658
3774
|
return activeAssistantId;
|
|
3659
3775
|
}
|
|
3660
3776
|
|
|
3661
|
-
private updateActiveAssistantState(
|
|
3777
|
+
private updateActiveAssistantState(
|
|
3778
|
+
messageId: string | undefined,
|
|
3779
|
+
deferNotification = false,
|
|
3780
|
+
): void {
|
|
3662
3781
|
const current =
|
|
3663
3782
|
messageId === undefined
|
|
3664
3783
|
? undefined
|
|
@@ -3680,7 +3799,7 @@ export class AgUiThreadController {
|
|
|
3680
3799
|
};
|
|
3681
3800
|
});
|
|
3682
3801
|
}
|
|
3683
|
-
this.publishUpdate();
|
|
3802
|
+
this.publishUpdate(deferNotification);
|
|
3684
3803
|
}
|
|
3685
3804
|
|
|
3686
3805
|
private applyCrossRunToolResult(
|
|
@@ -3860,7 +3979,10 @@ export class AgUiThreadController {
|
|
|
3860
3979
|
activeMessage?.role === "assistant" ? activeMessage : undefined;
|
|
3861
3980
|
const normalized = fromAgUiSnapshotMessages(
|
|
3862
3981
|
rawMessages,
|
|
3863
|
-
{
|
|
3982
|
+
{
|
|
3983
|
+
showThinking: this.showThinking,
|
|
3984
|
+
joinAdjacentAssistantMessages: this.joinAdjacentAssistantMessages,
|
|
3985
|
+
},
|
|
3864
3986
|
this.subagentParentToolCallIds,
|
|
3865
3987
|
);
|
|
3866
3988
|
const converted: ThreadMessage[] = [];
|