@assistant-ui/react-ag-ui 0.0.56 → 0.0.57

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.
@@ -20,7 +20,10 @@ import type {
20
20
  ThreadMessage,
21
21
  ToolCallMessagePart,
22
22
  } from "@assistant-ui/core";
23
- import { MessageRepository } from "@assistant-ui/core/internal";
23
+ import {
24
+ createMessageRepositorySession,
25
+ invokeUserCallback,
26
+ } from "@assistant-ui/core/internal";
24
27
  import type {
25
28
  AbstractAgent,
26
29
  AgentSubscriber,
@@ -99,36 +102,17 @@ const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" } as const;
99
102
 
100
103
  type AgUiRuntimeCallbackName = "onError" | "onCancel";
101
104
 
102
- const reportCallbackError = (name: AgUiRuntimeCallbackName, error: unknown) => {
103
- console.error(`[react-ag-ui] ${name} callback threw an error`, error);
104
- };
105
-
106
- const invokeRuntimeCallback = <TArgs extends unknown[]>(
105
+ const invokeRuntimeCallback = <TArgs extends readonly unknown[]>(
107
106
  name: AgUiRuntimeCallbackName,
108
- callback: ((...args: TArgs) => void) | undefined,
107
+ callback: ((...args: TArgs) => unknown) | undefined,
109
108
  ...args: TArgs
110
- ) => {
111
- if (!callback) return;
112
-
113
- try {
114
- const result = callback(...args) as unknown;
115
- if (
116
- result !== null &&
117
- (typeof result === "object" || typeof result === "function") &&
118
- "then" in result &&
119
- typeof result.then === "function"
120
- ) {
121
- void Promise.resolve(result).catch((error) => {
122
- reportCallbackError(name, error);
123
- });
124
- }
125
- } catch (error) {
126
- reportCallbackError(name, error);
127
- }
109
+ ): void => {
110
+ void invokeUserCallback("react-ag-ui", name, callback, ...args);
128
111
  };
129
112
 
130
- // The aggregator only ever sends the interrupts it owns, so a shallow spread at
131
- // the custom level would drop sibling agui state such as opaqueReasoning.
113
+ // The aggregator sends only the agui keys it owns (interrupts and
114
+ // opaqueReasoning, each as a full replacement), so a shallow spread at the
115
+ // custom level would drop any sibling agui state a snapshot import attached.
132
116
  const mergeAgUiNamespace = (
133
117
  current: Record<string, unknown> | undefined,
134
118
  incoming: Record<string, unknown>,
@@ -158,8 +142,22 @@ export class AgUiThreadRuntimeCore {
158
142
  private readonly reportedErrors = new WeakSet<object>();
159
143
 
160
144
  private runtime: AssistantRuntime | undefined;
161
- private readonly repository = new MessageRepository();
162
- private exportedRepository: ExportedMessageRepository | undefined;
145
+ private readonly session = createMessageRepositorySession({
146
+ decorateExport: (exported, repository) => {
147
+ let parentId: string | null = null;
148
+ for (const message of repository.getMessages()) {
149
+ if (message.metadata.isOptimistic) {
150
+ exported.messages.push({ parentId, message });
151
+ }
152
+ parentId = message.id;
153
+ }
154
+
155
+ return {
156
+ ...exported,
157
+ headId: repository.headId,
158
+ };
159
+ },
160
+ });
163
161
  private isRunningFlag = false;
164
162
  private abortController: AbortController | null = null;
165
163
  // The agent that started the active run. updateOptions can swap this.agent
@@ -175,6 +173,7 @@ export class AgUiThreadRuntimeCore {
175
173
  private readonly historyWrites = new Map<string, Promise<void>>();
176
174
  private _isLoading = false;
177
175
  private _loadPromise: Promise<void> | undefined;
176
+ private _loadRequested = false;
178
177
  private pendingResumeMessageId: string | null = null;
179
178
  private pendingA2uiResume = false;
180
179
  private pendingA2uiAction: Record<string, unknown> | undefined;
@@ -198,8 +197,19 @@ export class AgUiThreadRuntimeCore {
198
197
  this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
199
198
  this.onError = options.onError;
200
199
  this.onCancel = options.onCancel;
200
+ const previousHistory = this.history;
201
201
  this.history = options.history;
202
202
  this.installResumeShim();
203
+
204
+ if (
205
+ this._loadRequested &&
206
+ !this._loadPromise &&
207
+ !previousHistory &&
208
+ options.history &&
209
+ this.session.getMessages().length === 0
210
+ ) {
211
+ void this.__internal_load();
212
+ }
203
213
  }
204
214
 
205
215
  attachRuntime(runtime: AssistantRuntime) {
@@ -214,97 +224,11 @@ export class AgUiThreadRuntimeCore {
214
224
  }
215
225
 
216
226
  getMessages(): readonly ThreadMessage[] {
217
- return this.repository.getMessages();
227
+ return this.session.getMessages();
218
228
  }
219
229
 
220
230
  getMessageRepository(): ExportedMessageRepository {
221
- if (this.exportedRepository) return this.exportedRepository;
222
-
223
- const exportedRepository = this.repository.export();
224
- let parentId: string | null = null;
225
- for (const message of this.repository.getMessages()) {
226
- if (message.metadata.isOptimistic) {
227
- exportedRepository.messages.push({ parentId, message });
228
- }
229
- parentId = message.id;
230
- }
231
-
232
- this.exportedRepository = {
233
- ...exportedRepository,
234
- headId: this.repository.headId,
235
- };
236
- return this.exportedRepository;
237
- }
238
-
239
- private tryGetMessage(messageId: string) {
240
- try {
241
- return this.repository.getMessage(messageId);
242
- } catch {
243
- return undefined;
244
- }
245
- }
246
-
247
- private tryGetMessages(
248
- messageId: string,
249
- ): readonly ThreadMessage[] | undefined {
250
- try {
251
- return this.repository.getMessages(messageId);
252
- } catch {
253
- return undefined;
254
- }
255
- }
256
-
257
- private hasMessage(messageId: string): boolean {
258
- return this.tryGetMessage(messageId) !== undefined;
259
- }
260
-
261
- private addOrUpdateMessage(
262
- parentId: string | null,
263
- message: ThreadMessage,
264
- ): void {
265
- this.repository.addOrUpdateMessage(parentId, message);
266
- this.exportedRepository = undefined;
267
- }
268
-
269
- private deleteMessage(
270
- messageId: string,
271
- replacementId?: string | null,
272
- ): void {
273
- this.repository.deleteMessage(messageId, replacementId);
274
- this.exportedRepository = undefined;
275
- }
276
-
277
- private tryDeleteMessage(messageId: string): boolean {
278
- if (!this.hasMessage(messageId)) return false;
279
- this.deleteMessage(messageId);
280
- return true;
281
- }
282
-
283
- private switchToBranch(messageId: string): void {
284
- this.repository.switchToBranch(messageId);
285
- this.exportedRepository = undefined;
286
- }
287
-
288
- private resetRepositoryHead(messageId: string | null): void {
289
- this.repository.resetHead(messageId);
290
- this.exportedRepository = undefined;
291
- }
292
-
293
- private clearRepository(): void {
294
- this.repository.clear();
295
- this.exportedRepository = undefined;
296
- }
297
-
298
- private updateMessage(
299
- messageId: string,
300
- updater: (message: ThreadMessage) => ThreadMessage,
301
- ): boolean {
302
- const item = this.tryGetMessage(messageId);
303
- if (!item) return false;
304
- const message = updater(item.message);
305
- if (message === item.message) return false;
306
- this.addOrUpdateMessage(item.parentId, message);
307
- return true;
231
+ return this.session.export();
308
232
  }
309
233
 
310
234
  getState(): ReadonlyJSONValue | undefined {
@@ -320,9 +244,11 @@ export class AgUiThreadRuntimeCore {
320
244
  }
321
245
 
322
246
  __internal_load(): Promise<void> {
247
+ this._loadRequested = true;
323
248
  if (this._loadPromise) return this._loadPromise;
249
+ if (!this.history) return Promise.resolve();
324
250
 
325
- const promise = this.history?.load() ?? Promise.resolve(null);
251
+ const promise = this.history.load();
326
252
 
327
253
  this._isLoading = true;
328
254
 
@@ -330,14 +256,21 @@ export class AgUiThreadRuntimeCore {
330
256
  .then(async (repo) => {
331
257
  if (!repo) return;
332
258
 
333
- this.applyExternalMessageRepository(repo);
259
+ this.session.applyExternalMessageRepository(repo);
260
+ this.assistantHistoryParents.clear();
261
+ this.snapshotHistoryIds.clear();
262
+ this.persistedHistoryIds.clear();
263
+ for (const { message } of repo.messages) {
264
+ this.persistedHistoryIds.add(message.id);
265
+ }
266
+ this.notifyUpdate();
334
267
 
335
268
  if (repo.state !== undefined) {
336
269
  this.loadExternalState(repo.state);
337
270
  }
338
271
 
339
272
  if (repo.unstable_resume) {
340
- const parentId = repo.headId ?? this.repository.headId;
273
+ const parentId = repo.headId ?? this.session.headId;
341
274
  const resumeStream = this.history?.resume?.bind(this.history);
342
275
  await this.startRun(
343
276
  parentId,
@@ -384,17 +317,17 @@ export class AgUiThreadRuntimeCore {
384
317
  }
385
318
 
386
319
  private appendEntry(message: AppendMessage): string {
387
- if (message.sourceId) this.tryDeleteMessage(message.sourceId);
320
+ if (message.sourceId) this.session.tryDeleteMessage(message.sourceId);
388
321
 
389
322
  const threadMessage = this.toThreadMessage(message);
390
323
  const parentId =
391
324
  message.parentId === null
392
325
  ? null
393
- : message.parentId && this.hasMessage(message.parentId)
326
+ : message.parentId && this.session.hasMessage(message.parentId)
394
327
  ? message.parentId
395
- : this.repository.headId;
396
- this.addOrUpdateMessage(parentId, threadMessage);
397
- this.switchToBranch(threadMessage.id);
328
+ : this.session.headId;
329
+ this.session.addOrUpdateMessage(parentId, threadMessage);
330
+ this.session.switchToBranch(threadMessage.id);
398
331
  this.notifyUpdate();
399
332
  this.recordHistoryEntry(parentId, threadMessage);
400
333
  return threadMessage.id;
@@ -619,9 +552,8 @@ export class AgUiThreadRuntimeCore {
619
552
 
620
553
  // Bound against the message the gates landed on, so this check claims a
621
554
  // batch only where the projection did.
622
- const gatedMessage = this.tryGetMessage(pending.messageId)?.message as
623
- | ThreadAssistantMessage
624
- | undefined;
555
+ const gatedMessage = this.session.tryGetMessage(pending.messageId)
556
+ ?.message as ThreadAssistantMessage | undefined;
625
557
  const gated = projectAgUiToolApprovals(
626
558
  pending.interrupts,
627
559
  new Set(
@@ -647,13 +579,16 @@ export class AgUiThreadRuntimeCore {
647
579
  pending.interrupts,
648
580
  );
649
581
 
650
- const recorded = this.updateMessage(pending.messageId, (message) => {
651
- if (message.role !== "assistant") return message;
652
- const assistant = message as ThreadAssistantMessage;
653
- const content = withToolApprovalDecision(assistant.content, options);
654
- if (content === assistant.content) return assistant;
655
- return { ...assistant, content };
656
- });
582
+ const recorded = this.session.updateMessage(
583
+ pending.messageId,
584
+ (message) => {
585
+ if (message.role !== "assistant") return message;
586
+ const assistant = message as ThreadAssistantMessage;
587
+ const content = withToolApprovalDecision(assistant.content, options);
588
+ if (content === assistant.content) return assistant;
589
+ return { ...assistant, content };
590
+ },
591
+ );
657
592
  if (!recorded) {
658
593
  throw new Error(
659
594
  `[agui] respondToToolApproval: approval "${options.approvalId}" is already decided`,
@@ -661,7 +596,7 @@ export class AgUiThreadRuntimeCore {
661
596
  }
662
597
  this.notifyUpdate();
663
598
 
664
- const assistant = this.tryGetMessage(pending.messageId)?.message as
599
+ const assistant = this.session.tryGetMessage(pending.messageId)?.message as
665
600
  | ThreadAssistantMessage
666
601
  | undefined;
667
602
  if (!assistant) return;
@@ -776,7 +711,7 @@ export class AgUiThreadRuntimeCore {
776
711
  if (typeof message === "string") {
777
712
  return {
778
713
  createdAt: new Date(),
779
- parentId: this.repository.headId,
714
+ parentId: this.session.headId,
780
715
  sourceId: null,
781
716
  runConfig: {},
782
717
  role: "user",
@@ -787,7 +722,7 @@ export class AgUiThreadRuntimeCore {
787
722
  }
788
723
  return {
789
724
  createdAt: message.createdAt ?? new Date(),
790
- parentId: message.parentId ?? this.repository.headId,
725
+ parentId: message.parentId ?? this.session.headId,
791
726
  sourceId: message.sourceId ?? null,
792
727
  role: message.role ?? "user",
793
728
  content: message.content,
@@ -802,7 +737,7 @@ export class AgUiThreadRuntimeCore {
802
737
  messageId: string,
803
738
  resume: readonly AgUiResumeEntry[],
804
739
  ): void {
805
- const touched = this.updateMessage(messageId, (message) => {
740
+ const touched = this.session.updateMessage(messageId, (message) => {
806
741
  if (message.role !== "assistant") return message;
807
742
  const assistant = message as ThreadAssistantMessage;
808
743
  if (
@@ -852,7 +787,7 @@ export class AgUiThreadRuntimeCore {
852
787
  }
853
788
 
854
789
  private cancelUnresolvedToolCalls(messageId: string): void {
855
- const updated = this.updateMessage(messageId, (message) => {
790
+ const updated = this.session.updateMessage(messageId, (message) => {
856
791
  if (message.role !== "assistant") return message;
857
792
  const assistant = message as ThreadAssistantMessage;
858
793
  const content = assistant.content.map((part) => {
@@ -869,7 +804,7 @@ export class AgUiThreadRuntimeCore {
869
804
  }
870
805
 
871
806
  addToolResult(options: AddToolResultOptions): void {
872
- const updated = this.updateMessage(options.messageId, (message) => {
807
+ const updated = this.session.updateMessage(options.messageId, (message) => {
873
808
  if (message.role !== "assistant") return message;
874
809
  const assistant = message as ThreadAssistantMessage;
875
810
  let matchedToolCall = false;
@@ -896,7 +831,7 @@ export class AgUiThreadRuntimeCore {
896
831
  sendA2uiAction(action: Record<string, unknown>): void {
897
832
  this.assertNoPendingInterrupts();
898
833
  this.maybeAutoCancelPendingToolCalls();
899
- const parentId = this.repository.headId;
834
+ const parentId = this.session.headId;
900
835
  if (parentId === null) {
901
836
  this.logger.debug(
902
837
  "[agui] sendA2uiAction: no messages to resume, dropping action",
@@ -934,7 +869,7 @@ export class AgUiThreadRuntimeCore {
934
869
  }
935
870
 
936
871
  private maybeCompleteAfterToolResults(messageId: string): boolean {
937
- const message = this.tryGetMessage(messageId)?.message;
872
+ const message = this.session.tryGetMessage(messageId)?.message;
938
873
  if (!message || message.role !== "assistant") return false;
939
874
  const assistant = message as ThreadAssistantMessage;
940
875
  if (
@@ -948,7 +883,7 @@ export class AgUiThreadRuntimeCore {
948
883
  );
949
884
  if (!allResolved) return false;
950
885
 
951
- const updated = this.updateMessage(messageId, (current) =>
886
+ const updated = this.session.updateMessage(messageId, (current) =>
952
887
  current.role === "assistant"
953
888
  ? {
954
889
  ...(current as ThreadAssistantMessage),
@@ -978,7 +913,7 @@ export class AgUiThreadRuntimeCore {
978
913
  this.assistantHistoryParents.clear();
979
914
 
980
915
  if (messages.length === 0) {
981
- this.clearRepository();
916
+ this.session.clear();
982
917
  } else {
983
918
  let expectedParentId: string | null = null;
984
919
  let lastAppliedId: string | null = null;
@@ -988,31 +923,31 @@ export class AgUiThreadRuntimeCore {
988
923
  for (const message of messages) {
989
924
  if (seen.has(message.id)) continue;
990
925
  seen.add(message.id);
991
- const existing = this.tryGetMessage(message.id);
926
+ const existing = this.session.tryGetMessage(message.id);
992
927
  if (existing && existing.parentId !== expectedParentId) {
993
928
  hardReplace = true;
994
929
  break;
995
930
  }
996
- this.addOrUpdateMessage(expectedParentId, message);
931
+ this.session.addOrUpdateMessage(expectedParentId, message);
997
932
  expectedParentId = message.id;
998
933
  lastAppliedId = message.id;
999
934
  }
1000
935
 
1001
936
  if (hardReplace) {
1002
- this.clearRepository();
937
+ this.session.clear();
1003
938
  expectedParentId = null;
1004
939
  lastAppliedId = null;
1005
940
  seen.clear();
1006
941
  for (const message of messages) {
1007
942
  if (seen.has(message.id)) continue;
1008
943
  seen.add(message.id);
1009
- this.addOrUpdateMessage(expectedParentId, message);
944
+ this.session.addOrUpdateMessage(expectedParentId, message);
1010
945
  expectedParentId = message.id;
1011
946
  lastAppliedId = message.id;
1012
947
  }
1013
948
  }
1014
949
 
1015
- this.resetRepositoryHead(lastAppliedId);
950
+ this.session.resetHead(lastAppliedId);
1016
951
  }
1017
952
 
1018
953
  this.snapshotHistoryIds.clear();
@@ -1022,71 +957,6 @@ export class AgUiThreadRuntimeCore {
1022
957
  this.notifyUpdate();
1023
958
  }
1024
959
 
1025
- private applyExternalMessageRepository(
1026
- loaded: ExportedMessageRepository,
1027
- ): void {
1028
- const headId = loaded.headId ?? loaded.messages.at(-1)?.message.id ?? null;
1029
- const ids = new Set<string>();
1030
- let degenerate = false;
1031
- for (const { message } of loaded.messages) {
1032
- if (ids.has(message.id)) {
1033
- degenerate = true;
1034
- break;
1035
- }
1036
- ids.add(message.id);
1037
- }
1038
- if (headId !== null && !ids.has(headId)) degenerate = true;
1039
-
1040
- if (!degenerate) {
1041
- this.clearRepository();
1042
- let pending = [...loaded.messages];
1043
- const importedIds = new Set<string>();
1044
-
1045
- while (pending.length > 0) {
1046
- const unresolved: typeof pending = [];
1047
- let progressed = false;
1048
- for (const item of pending) {
1049
- if (item.parentId !== null && !importedIds.has(item.parentId)) {
1050
- unresolved.push(item);
1051
- continue;
1052
- }
1053
- this.addOrUpdateMessage(item.parentId, item.message);
1054
- importedIds.add(item.message.id);
1055
- progressed = true;
1056
- }
1057
- if (!progressed) {
1058
- degenerate = true;
1059
- break;
1060
- }
1061
- pending = unresolved;
1062
- }
1063
- }
1064
-
1065
- if (degenerate) {
1066
- this.clearRepository();
1067
- let previousId: string | null = null;
1068
- for (const { message } of loaded.messages) {
1069
- const existing = this.tryGetMessage(message.id);
1070
- this.addOrUpdateMessage(
1071
- existing ? existing.parentId : previousId,
1072
- message,
1073
- );
1074
- previousId = message.id;
1075
- }
1076
- this.resetRepositoryHead(previousId);
1077
- } else {
1078
- this.resetRepositoryHead(headId);
1079
- }
1080
-
1081
- this.assistantHistoryParents.clear();
1082
- this.snapshotHistoryIds.clear();
1083
- this.persistedHistoryIds.clear();
1084
- for (const { message } of loaded.messages) {
1085
- this.persistedHistoryIds.add(message.id);
1086
- }
1087
- this.notifyUpdate();
1088
- }
1089
-
1090
960
  loadExternalState(state: ReadonlyJSONValue): void {
1091
961
  this.stateSnapshot = state;
1092
962
  this.notifyUpdate();
@@ -1115,22 +985,23 @@ export class AgUiThreadRuntimeCore {
1115
985
  ): Promise<void> {
1116
986
  const normalizedRunConfig = runConfig ?? {};
1117
987
  this.lastRunConfig = normalizedRunConfig;
1118
- const parent = parentId === null ? undefined : this.tryGetMessage(parentId);
988
+ const parent =
989
+ parentId === null ? undefined : this.session.tryGetMessage(parentId);
1119
990
  const shouldEagerlyInsertAssistant =
1120
991
  parentId !== null &&
1121
992
  parent !== undefined &&
1122
- parentId !== this.repository.headId;
993
+ parentId !== this.session.headId;
1123
994
  const historicalMessages = [
1124
995
  ...(shouldEagerlyInsertAssistant
1125
- ? (this.tryGetMessages(parentId) ?? this.repository.getMessages())
1126
- : this.repository.getMessages()),
996
+ ? (this.session.tryGetMessages(parentId) ?? this.session.getMessages())
997
+ : this.session.getMessages()),
1127
998
  ];
1128
999
  const runStartMessageIds = new Set(
1129
1000
  this.getMessageRepository().messages.map(({ message }) => message.id),
1130
1001
  );
1131
1002
 
1132
1003
  this.pendingError = null;
1133
- const assistantParentId = parent ? parentId : this.repository.headId;
1004
+ const assistantParentId = parent ? parentId : this.session.headId;
1134
1005
  let assistantMessageId: string | undefined;
1135
1006
  // A snapshot the preserve gate declines still evicts the in-flight
1136
1007
  // assistant; recreating under the cached id on the next content-bearing
@@ -1142,11 +1013,12 @@ export class AgUiThreadRuntimeCore {
1142
1013
  let assistantCollided = false;
1143
1014
  const ensureAssistant = (allowRecreate = false): string => {
1144
1015
  const cached = assistantMessageId;
1145
- if (cached !== undefined && this.tryGetMessage(cached)) return cached;
1016
+ if (cached !== undefined && this.session.tryGetMessage(cached))
1017
+ return cached;
1146
1018
  if (cached !== undefined && (assistantCollided || !allowRecreate)) {
1147
1019
  return cached;
1148
1020
  }
1149
- const repositoryHeadId = this.repository.headId;
1021
+ const repositoryHeadId = this.session.headId;
1150
1022
  const shouldUseSelectedParent =
1151
1023
  cached === undefined ||
1152
1024
  (shouldEagerlyInsertAssistant &&
@@ -1157,7 +1029,7 @@ export class AgUiThreadRuntimeCore {
1157
1029
  const parentId =
1158
1030
  shouldUseSelectedParent &&
1159
1031
  assistantParentId &&
1160
- this.hasMessage(assistantParentId)
1032
+ this.session.hasMessage(assistantParentId)
1161
1033
  ? assistantParentId
1162
1034
  : repositoryHeadId;
1163
1035
  const created = this.insertAssistantPlaceholder(parentId, cached);
@@ -1181,32 +1053,51 @@ export class AgUiThreadRuntimeCore {
1181
1053
  }
1182
1054
  };
1183
1055
 
1056
+ const adoptServerMessageId = (
1057
+ serverId: string,
1058
+ startNewMessage: boolean,
1059
+ ) => {
1060
+ if (startNewMessage && assistantMessageId !== undefined) {
1061
+ applyUpdate({ status: { type: "complete", reason: "unknown" } });
1062
+ const previousId = assistantMessageId;
1063
+ assistantMessageId = undefined;
1064
+ if (this.session.tryGetMessage(previousId)) {
1065
+ const created = this.insertAssistantPlaceholder(previousId);
1066
+ assistantMessageId = created;
1067
+ this.markPendingAssistantHistory(created, previousId);
1068
+ }
1069
+ }
1070
+ const placeholder = ensureAssistant(true);
1071
+ if (placeholder === serverId) return;
1072
+ const reassigned = this.reassignAssistantId(placeholder, serverId);
1073
+ // A collision drops the placeholder before revealing the existing
1074
+ // server message as the current head. Only messages introduced during
1075
+ // this run can replace the placeholder; regeneration must not rewrite
1076
+ // a previous branch when the server incorrectly reuses its id.
1077
+ const adoptsVisibleCollision =
1078
+ !reassigned &&
1079
+ !runStartMessageIds.has(serverId) &&
1080
+ this.session.headId === serverId;
1081
+ if (reassigned || adoptsVisibleCollision) {
1082
+ assistantMessageId = serverId;
1083
+ if (adoptsVisibleCollision) {
1084
+ const parentId =
1085
+ this.session.tryGetMessage(serverId)?.parentId ?? null;
1086
+ this.markPendingAssistantHistory(serverId, parentId);
1087
+ }
1088
+ } else {
1089
+ assistantCollided = true;
1090
+ }
1091
+ };
1092
+
1184
1093
  const aggregator = new RunAggregator({
1185
1094
  showThinking: this.showThinking,
1186
1095
  logger: this.logger,
1187
1096
  emit: applyUpdate,
1188
1097
  onServerMessageId: (serverId) => {
1189
- const placeholder = ensureAssistant(true);
1190
- if (placeholder === serverId) return;
1191
- const reassigned = this.reassignAssistantId(placeholder, serverId);
1192
- // A collision drops the placeholder before revealing the existing
1193
- // server message as the current head. Only messages introduced during
1194
- // this run can replace the placeholder; regeneration must not rewrite
1195
- // a previous branch when the server incorrectly reuses its id.
1196
- const adoptsVisibleCollision =
1197
- !reassigned &&
1198
- !runStartMessageIds.has(serverId) &&
1199
- this.repository.headId === serverId;
1200
- if (reassigned || adoptsVisibleCollision) {
1201
- assistantMessageId = serverId;
1202
- if (adoptsVisibleCollision) {
1203
- const parentId = this.tryGetMessage(serverId)?.parentId ?? null;
1204
- this.markPendingAssistantHistory(serverId, parentId);
1205
- }
1206
- } else {
1207
- assistantCollided = true;
1208
- }
1098
+ adoptServerMessageId(serverId, false);
1209
1099
  },
1100
+ onTextMessageStart: (serverId) => adoptServerMessageId(serverId, true),
1210
1101
  });
1211
1102
  const dispatch = (event: AgUiEvent) =>
1212
1103
  this.handleEvent(aggregator, event, assistantMessageId);
@@ -1319,7 +1210,7 @@ export class AgUiThreadRuntimeCore {
1319
1210
  return;
1320
1211
  }
1321
1212
  this.maybeAutoCancelPendingToolCalls();
1322
- const parentId = this.repository.headId;
1213
+ const parentId = this.session.headId;
1323
1214
  if (parentId !== null) {
1324
1215
  this.startResumeRun(parentId);
1325
1216
  } else {
@@ -1361,7 +1252,7 @@ export class AgUiThreadRuntimeCore {
1361
1252
  unstable_threadId: ctx.threadId,
1362
1253
  unstable_parentId: ctx.parentId,
1363
1254
  unstable_getMessage: () => {
1364
- const message = this.tryGetMessage(currentId())?.message;
1255
+ const message = this.session.tryGetMessage(currentId())?.message;
1365
1256
  if (!message) {
1366
1257
  throw new Error(
1367
1258
  "[agui] resume stream requested the assistant message before it existed",
@@ -1388,7 +1279,7 @@ export class AgUiThreadRuntimeCore {
1388
1279
  }
1389
1280
 
1390
1281
  if (ctx.abortSignal.aborted) return;
1391
- const current = this.tryGetMessage(currentId())?.message;
1282
+ const current = this.session.tryGetMessage(currentId())?.message;
1392
1283
  if (!current || current.status?.type === "running") {
1393
1284
  ctx.applyUpdate({ status: { type: "complete", reason: "unknown" } });
1394
1285
  }
@@ -1402,7 +1293,7 @@ export class AgUiThreadRuntimeCore {
1402
1293
  ) {
1403
1294
  const threadId = this.agent.threadId || "main";
1404
1295
  const messages = toAgUiMessages(
1405
- historyMessages ?? this.repository.getMessages(),
1296
+ historyMessages ?? this.session.getMessages(),
1406
1297
  );
1407
1298
  const context = this.runtime?.thread.getModelContext();
1408
1299
  const input = {
@@ -1481,34 +1372,34 @@ export class AgUiThreadRuntimeCore {
1481
1372
  custom: {},
1482
1373
  },
1483
1374
  };
1484
- this.addOrUpdateMessage(parentId ?? this.repository.headId, assistant);
1485
- this.switchToBranch(id);
1375
+ this.session.addOrUpdateMessage(parentId ?? this.session.headId, assistant);
1376
+ this.session.switchToBranch(id);
1486
1377
  this.notifyUpdate();
1487
1378
  return id;
1488
1379
  }
1489
1380
 
1490
1381
  private reassignAssistantId(oldId: string, newId: string): boolean {
1491
1382
  if (oldId === newId) return true;
1492
- const oldItem = this.tryGetMessage(oldId);
1383
+ const oldItem = this.session.tryGetMessage(oldId);
1493
1384
  if (!oldItem) return false;
1494
1385
 
1495
- const collidesWithExisting = this.hasMessage(newId);
1386
+ const collidesWithExisting = this.session.hasMessage(newId);
1496
1387
 
1497
1388
  if (collidesWithExisting) {
1498
1389
  this.logger.debug?.(
1499
1390
  "[agui] reassignAssistantId: server id already present in messages or repository, dropping placeholder",
1500
1391
  { oldId, newId },
1501
1392
  );
1502
- this.deleteMessage(oldId, oldItem.parentId ?? null);
1393
+ this.session.deleteMessage(oldId, oldItem.parentId ?? null);
1503
1394
  } else {
1504
1395
  const { isOptimistic: _, ...metadata } = oldItem.message.metadata;
1505
- this.addOrUpdateMessage(oldItem.parentId, {
1396
+ this.session.addOrUpdateMessage(oldItem.parentId, {
1506
1397
  ...oldItem.message,
1507
1398
  id: newId,
1508
1399
  metadata,
1509
1400
  } as ThreadMessage);
1510
- this.switchToBranch(newId);
1511
- this.tryDeleteMessage(oldId);
1401
+ this.session.switchToBranch(newId);
1402
+ this.session.tryDeleteMessage(oldId);
1512
1403
  }
1513
1404
 
1514
1405
  const pendingParent = this.assistantHistoryParents.get(oldId);
@@ -1558,7 +1449,7 @@ export class AgUiThreadRuntimeCore {
1558
1449
  update: ChatModelRunResult,
1559
1450
  ): string {
1560
1451
  let latestStatus: MessageStatus | undefined;
1561
- const touched = this.updateMessage(messageId, (message) => {
1452
+ const touched = this.session.updateMessage(messageId, (message) => {
1562
1453
  if (message.role !== "assistant") return message;
1563
1454
  const assistant = message as ThreadAssistantMessage;
1564
1455
  const metadata = update.metadata
@@ -1727,7 +1618,7 @@ export class AgUiThreadRuntimeCore {
1727
1618
  messageId: string,
1728
1619
  event: Extract<AgUiEvent, { type: "TOOL_CALL_RESULT" }>,
1729
1620
  ): void {
1730
- const updated = this.updateMessage(messageId, (message) => {
1621
+ const updated = this.session.updateMessage(messageId, (message) => {
1731
1622
  if (message.role !== "assistant") return message;
1732
1623
  const assistant = message as ThreadAssistantMessage;
1733
1624
  const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
@@ -1814,7 +1705,7 @@ export class AgUiThreadRuntimeCore {
1814
1705
  ? serverHash
1815
1706
  : undefined;
1816
1707
  const result = event.content["result"];
1817
- const updated = this.updateMessage(messageId, (message) => {
1708
+ const updated = this.session.updateMessage(messageId, (message) => {
1818
1709
  if (message.role !== "assistant") return message;
1819
1710
  const assistant = message as ThreadAssistantMessage;
1820
1711
  let matchedToolCall = false;
@@ -1866,7 +1757,7 @@ export class AgUiThreadRuntimeCore {
1866
1757
  ) {
1867
1758
  try {
1868
1759
  const activeMessage = activeAssistantId
1869
- ? this.tryGetMessage(activeAssistantId)?.message
1760
+ ? this.session.tryGetMessage(activeAssistantId)?.message
1870
1761
  : undefined;
1871
1762
  const activeAssistant =
1872
1763
  activeMessage?.role === "assistant" ? activeMessage : undefined;
@@ -1899,7 +1790,7 @@ export class AgUiThreadRuntimeCore {
1899
1790
  }
1900
1791
  this.applyExternalMessages(converted);
1901
1792
  if (activeAssistant !== undefined) {
1902
- const activeItem = this.tryGetMessage(activeAssistant.id);
1793
+ const activeItem = this.session.tryGetMessage(activeAssistant.id);
1903
1794
  if (activeItem) {
1904
1795
  if (preservesActiveAssistant) {
1905
1796
  this.snapshotHistoryIds.delete(activeAssistant.id);
@@ -1951,7 +1842,7 @@ export class AgUiThreadRuntimeCore {
1951
1842
  if (!history) return;
1952
1843
  const parentId = this.assistantHistoryParents.get(messageId);
1953
1844
  if (parentId === undefined) return;
1954
- const message = this.tryGetMessage(messageId)?.message;
1845
+ const message = this.session.tryGetMessage(messageId)?.message;
1955
1846
  if (!message || message.role !== "assistant") return;
1956
1847
  if (!this.isPersistableStatus(message.status)) return;
1957
1848
  const wasPersisted = this.persistedHistoryIds.has(messageId);