@assistant-ui/react-ag-ui 0.0.54 → 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.
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +25 -15
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +257 -210
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +13 -3
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +77 -39
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +23 -2
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +155 -41
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +3 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +9 -0
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +56 -0
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -0
- package/dist/runtime/adapter/tool-approval.js +244 -0
- package/dist/runtime/adapter/tool-approval.js.map +1 -0
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +2 -1
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/interrupt-internals.d.ts +8 -0
- package/dist/runtime/interrupt-internals.d.ts.map +1 -0
- package/dist/runtime/interrupt-internals.js +25 -0
- package/dist/runtime/interrupt-internals.js.map +1 -0
- package/dist/useAgUiRuntime.js +14 -9
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +8 -2
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +105 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +436 -272
- package/src/runtime/adapter/conversions.content.test.ts +61 -3
- package/src/runtime/adapter/conversions.ts +123 -57
- package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
- package/src/runtime/adapter/run-aggregator.ts +234 -47
- package/src/runtime/adapter/subscriber.ts +14 -0
- package/src/runtime/adapter/tool-approval.test.ts +681 -0
- package/src/runtime/adapter/tool-approval.ts +366 -0
- package/src/runtime/event-parser.ts +10 -1
- package/src/runtime/interrupt-internals.ts +35 -0
- package/src/useAgUiRuntime.approval.test.tsx +885 -0
- package/src/useAgUiRuntime.history.test.tsx +76 -0
- package/src/useAgUiRuntime.ts +15 -9
|
@@ -14,12 +14,16 @@ import type {
|
|
|
14
14
|
ChatModelRunResult,
|
|
15
15
|
ExportedMessageRepository,
|
|
16
16
|
MessageStatus,
|
|
17
|
+
RespondToToolApprovalOptions,
|
|
17
18
|
ThreadAssistantMessage,
|
|
18
19
|
ThreadHistoryAdapter,
|
|
19
20
|
ThreadMessage,
|
|
20
21
|
ToolCallMessagePart,
|
|
21
22
|
} from "@assistant-ui/core";
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
createMessageRepositorySession,
|
|
25
|
+
invokeUserCallback,
|
|
26
|
+
} from "@assistant-ui/core/internal";
|
|
23
27
|
import type {
|
|
24
28
|
AbstractAgent,
|
|
25
29
|
AgentSubscriber,
|
|
@@ -45,6 +49,12 @@ import {
|
|
|
45
49
|
toAgUiTools,
|
|
46
50
|
} from "./adapter/conversions";
|
|
47
51
|
import { createAgUiSubscriber } from "./adapter/subscriber";
|
|
52
|
+
import {
|
|
53
|
+
buildToolApprovalResume,
|
|
54
|
+
projectAgUiToolApprovals,
|
|
55
|
+
withSettledToolApprovals,
|
|
56
|
+
withToolApprovalDecision,
|
|
57
|
+
} from "./adapter/tool-approval";
|
|
48
58
|
|
|
49
59
|
// AbstractAgent.runAgent declares two parameters. HttpAgent ignores a third and
|
|
50
60
|
// is cancelled through agent.abortRun(); the run options stay for subclasses
|
|
@@ -92,36 +102,17 @@ const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" } as const;
|
|
|
92
102
|
|
|
93
103
|
type AgUiRuntimeCallbackName = "onError" | "onCancel";
|
|
94
104
|
|
|
95
|
-
const
|
|
96
|
-
console.error(`[react-ag-ui] ${name} callback threw an error`, error);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const invokeRuntimeCallback = <TArgs extends unknown[]>(
|
|
105
|
+
const invokeRuntimeCallback = <TArgs extends readonly unknown[]>(
|
|
100
106
|
name: AgUiRuntimeCallbackName,
|
|
101
|
-
callback: ((...args: TArgs) =>
|
|
107
|
+
callback: ((...args: TArgs) => unknown) | undefined,
|
|
102
108
|
...args: TArgs
|
|
103
|
-
) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const result = callback(...args) as unknown;
|
|
108
|
-
if (
|
|
109
|
-
result !== null &&
|
|
110
|
-
(typeof result === "object" || typeof result === "function") &&
|
|
111
|
-
"then" in result &&
|
|
112
|
-
typeof result.then === "function"
|
|
113
|
-
) {
|
|
114
|
-
void Promise.resolve(result).catch((error) => {
|
|
115
|
-
reportCallbackError(name, error);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
} catch (error) {
|
|
119
|
-
reportCallbackError(name, error);
|
|
120
|
-
}
|
|
109
|
+
): void => {
|
|
110
|
+
void invokeUserCallback("react-ag-ui", name, callback, ...args);
|
|
121
111
|
};
|
|
122
112
|
|
|
123
|
-
// The aggregator only
|
|
124
|
-
//
|
|
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.
|
|
125
116
|
const mergeAgUiNamespace = (
|
|
126
117
|
current: Record<string, unknown> | undefined,
|
|
127
118
|
incoming: Record<string, unknown>,
|
|
@@ -148,10 +139,25 @@ export class AgUiThreadRuntimeCore {
|
|
|
148
139
|
private onError: ((error: Error) => void) | undefined;
|
|
149
140
|
private onCancel: (() => void) | undefined;
|
|
150
141
|
private readonly notifyUpdate: () => void;
|
|
142
|
+
private readonly reportedErrors = new WeakSet<object>();
|
|
151
143
|
|
|
152
144
|
private runtime: AssistantRuntime | undefined;
|
|
153
|
-
private readonly
|
|
154
|
-
|
|
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
|
+
});
|
|
155
161
|
private isRunningFlag = false;
|
|
156
162
|
private abortController: AbortController | null = null;
|
|
157
163
|
// The agent that started the active run. updateOptions can swap this.agent
|
|
@@ -162,9 +168,12 @@ export class AgUiThreadRuntimeCore {
|
|
|
162
168
|
private history: ThreadHistoryAdapter | undefined;
|
|
163
169
|
private lastRunConfig: RunConfig | undefined;
|
|
164
170
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
165
|
-
private readonly
|
|
171
|
+
private readonly snapshotHistoryIds = new Set<string>();
|
|
172
|
+
private readonly persistedHistoryIds = new Set<string>();
|
|
173
|
+
private readonly historyWrites = new Map<string, Promise<void>>();
|
|
166
174
|
private _isLoading = false;
|
|
167
175
|
private _loadPromise: Promise<void> | undefined;
|
|
176
|
+
private _loadRequested = false;
|
|
168
177
|
private pendingResumeMessageId: string | null = null;
|
|
169
178
|
private pendingA2uiResume = false;
|
|
170
179
|
private pendingA2uiAction: Record<string, unknown> | undefined;
|
|
@@ -188,8 +197,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
188
197
|
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
189
198
|
this.onError = options.onError;
|
|
190
199
|
this.onCancel = options.onCancel;
|
|
200
|
+
const previousHistory = this.history;
|
|
191
201
|
this.history = options.history;
|
|
192
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
|
+
}
|
|
193
213
|
}
|
|
194
214
|
|
|
195
215
|
attachRuntime(runtime: AssistantRuntime) {
|
|
@@ -198,100 +218,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
198
218
|
|
|
199
219
|
detachRuntime() {
|
|
200
220
|
this.runtime = undefined;
|
|
221
|
+
void this.cancel().catch((error) => {
|
|
222
|
+
this.logger.error("[agui] failed to cancel run during teardown", error);
|
|
223
|
+
});
|
|
201
224
|
}
|
|
202
225
|
|
|
203
226
|
getMessages(): readonly ThreadMessage[] {
|
|
204
|
-
return this.
|
|
227
|
+
return this.session.getMessages();
|
|
205
228
|
}
|
|
206
229
|
|
|
207
230
|
getMessageRepository(): ExportedMessageRepository {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const exportedRepository = this.repository.export();
|
|
211
|
-
let parentId: string | null = null;
|
|
212
|
-
for (const message of this.repository.getMessages()) {
|
|
213
|
-
if (message.metadata.isOptimistic) {
|
|
214
|
-
exportedRepository.messages.push({ parentId, message });
|
|
215
|
-
}
|
|
216
|
-
parentId = message.id;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
this.exportedRepository = {
|
|
220
|
-
...exportedRepository,
|
|
221
|
-
headId: this.repository.headId,
|
|
222
|
-
};
|
|
223
|
-
return this.exportedRepository;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private tryGetMessage(messageId: string) {
|
|
227
|
-
try {
|
|
228
|
-
return this.repository.getMessage(messageId);
|
|
229
|
-
} catch {
|
|
230
|
-
return undefined;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private tryGetMessages(
|
|
235
|
-
messageId: string,
|
|
236
|
-
): readonly ThreadMessage[] | undefined {
|
|
237
|
-
try {
|
|
238
|
-
return this.repository.getMessages(messageId);
|
|
239
|
-
} catch {
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private hasMessage(messageId: string): boolean {
|
|
245
|
-
return this.tryGetMessage(messageId) !== undefined;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private addOrUpdateMessage(
|
|
249
|
-
parentId: string | null,
|
|
250
|
-
message: ThreadMessage,
|
|
251
|
-
): void {
|
|
252
|
-
this.repository.addOrUpdateMessage(parentId, message);
|
|
253
|
-
this.exportedRepository = undefined;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private deleteMessage(
|
|
257
|
-
messageId: string,
|
|
258
|
-
replacementId?: string | null,
|
|
259
|
-
): void {
|
|
260
|
-
this.repository.deleteMessage(messageId, replacementId);
|
|
261
|
-
this.exportedRepository = undefined;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private tryDeleteMessage(messageId: string): boolean {
|
|
265
|
-
if (!this.hasMessage(messageId)) return false;
|
|
266
|
-
this.deleteMessage(messageId);
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private switchToBranch(messageId: string): void {
|
|
271
|
-
this.repository.switchToBranch(messageId);
|
|
272
|
-
this.exportedRepository = undefined;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private resetRepositoryHead(messageId: string | null): void {
|
|
276
|
-
this.repository.resetHead(messageId);
|
|
277
|
-
this.exportedRepository = undefined;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private clearRepository(): void {
|
|
281
|
-
this.repository.clear();
|
|
282
|
-
this.exportedRepository = undefined;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private updateMessage(
|
|
286
|
-
messageId: string,
|
|
287
|
-
updater: (message: ThreadMessage) => ThreadMessage,
|
|
288
|
-
): boolean {
|
|
289
|
-
const item = this.tryGetMessage(messageId);
|
|
290
|
-
if (!item) return false;
|
|
291
|
-
const message = updater(item.message);
|
|
292
|
-
if (message === item.message) return false;
|
|
293
|
-
this.addOrUpdateMessage(item.parentId, message);
|
|
294
|
-
return true;
|
|
231
|
+
return this.session.export();
|
|
295
232
|
}
|
|
296
233
|
|
|
297
234
|
getState(): ReadonlyJSONValue | undefined {
|
|
@@ -307,9 +244,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
307
244
|
}
|
|
308
245
|
|
|
309
246
|
__internal_load(): Promise<void> {
|
|
247
|
+
this._loadRequested = true;
|
|
310
248
|
if (this._loadPromise) return this._loadPromise;
|
|
249
|
+
if (!this.history) return Promise.resolve();
|
|
311
250
|
|
|
312
|
-
const promise = this.history
|
|
251
|
+
const promise = this.history.load();
|
|
313
252
|
|
|
314
253
|
this._isLoading = true;
|
|
315
254
|
|
|
@@ -317,14 +256,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
317
256
|
.then(async (repo) => {
|
|
318
257
|
if (!repo) return;
|
|
319
258
|
|
|
320
|
-
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();
|
|
321
267
|
|
|
322
268
|
if (repo.state !== undefined) {
|
|
323
269
|
this.loadExternalState(repo.state);
|
|
324
270
|
}
|
|
325
271
|
|
|
326
272
|
if (repo.unstable_resume) {
|
|
327
|
-
const parentId = repo.headId ?? this.
|
|
273
|
+
const parentId = repo.headId ?? this.session.headId;
|
|
328
274
|
const resumeStream = this.history?.resume?.bind(this.history);
|
|
329
275
|
await this.startRun(
|
|
330
276
|
parentId,
|
|
@@ -371,17 +317,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
371
317
|
}
|
|
372
318
|
|
|
373
319
|
private appendEntry(message: AppendMessage): string {
|
|
374
|
-
if (message.sourceId) this.tryDeleteMessage(message.sourceId);
|
|
320
|
+
if (message.sourceId) this.session.tryDeleteMessage(message.sourceId);
|
|
375
321
|
|
|
376
322
|
const threadMessage = this.toThreadMessage(message);
|
|
377
323
|
const parentId =
|
|
378
324
|
message.parentId === null
|
|
379
325
|
? null
|
|
380
|
-
: message.parentId && this.hasMessage(message.parentId)
|
|
326
|
+
: message.parentId && this.session.hasMessage(message.parentId)
|
|
381
327
|
? message.parentId
|
|
382
|
-
: this.
|
|
383
|
-
this.addOrUpdateMessage(parentId, threadMessage);
|
|
384
|
-
this.switchToBranch(threadMessage.id);
|
|
328
|
+
: this.session.headId;
|
|
329
|
+
this.session.addOrUpdateMessage(parentId, threadMessage);
|
|
330
|
+
this.session.switchToBranch(threadMessage.id);
|
|
385
331
|
this.notifyUpdate();
|
|
386
332
|
this.recordHistoryEntry(parentId, threadMessage);
|
|
387
333
|
return threadMessage.id;
|
|
@@ -526,34 +472,144 @@ export class AgUiThreadRuntimeCore {
|
|
|
526
472
|
);
|
|
527
473
|
}
|
|
528
474
|
|
|
475
|
+
this.assertInterruptsAnswerable(
|
|
476
|
+
"submitInterruptResponses",
|
|
477
|
+
pending.interrupts,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
await this.resumeWithResponses(
|
|
481
|
+
pending.messageId,
|
|
482
|
+
openIds.map((id) => responsesById.get(id)!),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Resumes with an already validated resume array. Answerability is checked
|
|
488
|
+
* once per submission: re-checking here would let a clock crossing reject a
|
|
489
|
+
* decision this runtime has already recorded, stranding the gate decided and
|
|
490
|
+
* unretryable.
|
|
491
|
+
*/
|
|
492
|
+
private async resumeWithResponses(
|
|
493
|
+
messageId: string,
|
|
494
|
+
resume: AgUiResumeEntry[],
|
|
495
|
+
): Promise<void> {
|
|
496
|
+
this.clearPendingInterrupts(messageId, resume);
|
|
497
|
+
await this.startRun(messageId, this.lastRunConfig, resume);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The core seams that reach `respondToToolApproval` discard the promise they
|
|
502
|
+
* receive, so a rejected decision would otherwise surface only as an
|
|
503
|
+
* unhandled rejection. A failure raised by the resumed run itself was already
|
|
504
|
+
* reported by `startRun` before it rethrew, so reporting it here again would
|
|
505
|
+
* give the consumer two notifications for one failure.
|
|
506
|
+
*/
|
|
507
|
+
reportError(error: unknown): void {
|
|
508
|
+
if (this.reportedErrors.has(error as object)) {
|
|
509
|
+
this.reportedErrors.delete(error as object);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
invokeRuntimeCallback(
|
|
513
|
+
"onError",
|
|
514
|
+
this.onError,
|
|
515
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private assertInterruptsAnswerable(
|
|
520
|
+
method: "submitInterruptResponses" | "respondToToolApproval",
|
|
521
|
+
interrupts: readonly AgUiInterrupt[],
|
|
522
|
+
): void {
|
|
529
523
|
const now = Date.now();
|
|
530
|
-
for (const interrupt of
|
|
524
|
+
for (const interrupt of interrupts) {
|
|
531
525
|
if (!interrupt.expiresAt) continue;
|
|
532
526
|
const expiry = new Date(interrupt.expiresAt).getTime();
|
|
533
527
|
if (Number.isNaN(expiry)) {
|
|
534
528
|
throw new Error(
|
|
535
|
-
`[agui]
|
|
529
|
+
`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`,
|
|
536
530
|
);
|
|
537
531
|
}
|
|
538
532
|
if (expiry <= now) {
|
|
539
533
|
throw new Error(
|
|
540
|
-
`[agui]
|
|
534
|
+
`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`,
|
|
541
535
|
);
|
|
542
536
|
}
|
|
543
537
|
}
|
|
538
|
+
if (this.isRunningFlag) {
|
|
539
|
+
throw new Error(`[agui] ${method}: a run is already in progress`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
544
542
|
|
|
545
|
-
|
|
546
|
-
|
|
543
|
+
async respondToToolApproval(
|
|
544
|
+
options: RespondToToolApprovalOptions,
|
|
545
|
+
): Promise<void> {
|
|
546
|
+
const pending = this.getPendingInterrupts();
|
|
547
|
+
if (!pending) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
"[agui] respondToToolApproval: no pending interrupts on this thread",
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Bound against the message the gates landed on, so this check claims a
|
|
554
|
+
// batch only where the projection did.
|
|
555
|
+
const gatedMessage = this.session.tryGetMessage(pending.messageId)
|
|
556
|
+
?.message as ThreadAssistantMessage | undefined;
|
|
557
|
+
const gated = projectAgUiToolApprovals(
|
|
558
|
+
pending.interrupts,
|
|
559
|
+
new Set(
|
|
560
|
+
(gatedMessage?.content ?? [])
|
|
561
|
+
.filter((part) => part.type === "tool-call")
|
|
562
|
+
.map((part) => part.toolCallId),
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
const isGated = [...gated.values()].some(
|
|
566
|
+
(approval) => approval.id === options.approvalId,
|
|
547
567
|
);
|
|
568
|
+
if (!isGated) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`[agui] respondToToolApproval: no pending tool-call interrupt for approval id "${options.approvalId}"`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
548
573
|
|
|
549
|
-
|
|
574
|
+
// The decision is recorded only once the batch is known to be answerable:
|
|
575
|
+
// a rejected submission would otherwise leave the gate decided and
|
|
576
|
+
// unretryable, because a second click reports it as already decided.
|
|
577
|
+
this.assertInterruptsAnswerable(
|
|
578
|
+
"respondToToolApproval",
|
|
579
|
+
pending.interrupts,
|
|
580
|
+
);
|
|
581
|
+
|
|
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
|
+
);
|
|
592
|
+
if (!recorded) {
|
|
550
593
|
throw new Error(
|
|
551
|
-
|
|
594
|
+
`[agui] respondToToolApproval: approval "${options.approvalId}" is already decided`,
|
|
552
595
|
);
|
|
553
596
|
}
|
|
597
|
+
this.notifyUpdate();
|
|
598
|
+
|
|
599
|
+
const assistant = this.session.tryGetMessage(pending.messageId)?.message as
|
|
600
|
+
| ThreadAssistantMessage
|
|
601
|
+
| undefined;
|
|
602
|
+
if (!assistant) return;
|
|
603
|
+
|
|
604
|
+
// AG-UI resumes a run with one response per open interrupt, so the run
|
|
605
|
+
// stays paused until every gate in the batch has been answered.
|
|
606
|
+
const resume = buildToolApprovalResume(
|
|
607
|
+
assistant.content,
|
|
608
|
+
pending.interrupts,
|
|
609
|
+
);
|
|
610
|
+
if (!resume) return;
|
|
554
611
|
|
|
555
|
-
this.
|
|
556
|
-
await this.startRun(pending.messageId, this.lastRunConfig, resume);
|
|
612
|
+
await this.resumeWithResponses(pending.messageId, resume);
|
|
557
613
|
}
|
|
558
614
|
|
|
559
615
|
async steerAway(
|
|
@@ -595,7 +651,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
595
651
|
}
|
|
596
652
|
|
|
597
653
|
const normalized = this.toAppendMessage(message);
|
|
598
|
-
this.clearPendingInterrupts(pending.messageId);
|
|
654
|
+
this.clearPendingInterrupts(pending.messageId, resume);
|
|
599
655
|
const threadMessageId = this.appendEntry(normalized);
|
|
600
656
|
await this.startRun(threadMessageId, normalized.runConfig, resume);
|
|
601
657
|
}
|
|
@@ -655,7 +711,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
655
711
|
if (typeof message === "string") {
|
|
656
712
|
return {
|
|
657
713
|
createdAt: new Date(),
|
|
658
|
-
parentId: this.
|
|
714
|
+
parentId: this.session.headId,
|
|
659
715
|
sourceId: null,
|
|
660
716
|
runConfig: {},
|
|
661
717
|
role: "user",
|
|
@@ -666,7 +722,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
666
722
|
}
|
|
667
723
|
return {
|
|
668
724
|
createdAt: message.createdAt ?? new Date(),
|
|
669
|
-
parentId: message.parentId ?? this.
|
|
725
|
+
parentId: message.parentId ?? this.session.headId,
|
|
670
726
|
sourceId: message.sourceId ?? null,
|
|
671
727
|
role: message.role ?? "user",
|
|
672
728
|
content: message.content,
|
|
@@ -677,8 +733,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
677
733
|
} as AppendMessage;
|
|
678
734
|
}
|
|
679
735
|
|
|
680
|
-
private clearPendingInterrupts(
|
|
681
|
-
|
|
736
|
+
private clearPendingInterrupts(
|
|
737
|
+
messageId: string,
|
|
738
|
+
resume: readonly AgUiResumeEntry[],
|
|
739
|
+
): void {
|
|
740
|
+
const touched = this.session.updateMessage(messageId, (message) => {
|
|
682
741
|
if (message.role !== "assistant") return message;
|
|
683
742
|
const assistant = message as ThreadAssistantMessage;
|
|
684
743
|
if (
|
|
@@ -699,6 +758,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
699
758
|
}
|
|
700
759
|
return {
|
|
701
760
|
...assistant,
|
|
761
|
+
content: withSettledToolApprovals(assistant.content, resume),
|
|
702
762
|
status: { type: "complete" as const, reason: "unknown" as const },
|
|
703
763
|
metadata: { ...assistant.metadata, custom: newCustom },
|
|
704
764
|
};
|
|
@@ -727,7 +787,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
727
787
|
}
|
|
728
788
|
|
|
729
789
|
private cancelUnresolvedToolCalls(messageId: string): void {
|
|
730
|
-
const updated = this.updateMessage(messageId, (message) => {
|
|
790
|
+
const updated = this.session.updateMessage(messageId, (message) => {
|
|
731
791
|
if (message.role !== "assistant") return message;
|
|
732
792
|
const assistant = message as ThreadAssistantMessage;
|
|
733
793
|
const content = assistant.content.map((part) => {
|
|
@@ -744,7 +804,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
744
804
|
}
|
|
745
805
|
|
|
746
806
|
addToolResult(options: AddToolResultOptions): void {
|
|
747
|
-
const updated = this.updateMessage(options.messageId, (message) => {
|
|
807
|
+
const updated = this.session.updateMessage(options.messageId, (message) => {
|
|
748
808
|
if (message.role !== "assistant") return message;
|
|
749
809
|
const assistant = message as ThreadAssistantMessage;
|
|
750
810
|
let matchedToolCall = false;
|
|
@@ -771,7 +831,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
771
831
|
sendA2uiAction(action: Record<string, unknown>): void {
|
|
772
832
|
this.assertNoPendingInterrupts();
|
|
773
833
|
this.maybeAutoCancelPendingToolCalls();
|
|
774
|
-
const parentId = this.
|
|
834
|
+
const parentId = this.session.headId;
|
|
775
835
|
if (parentId === null) {
|
|
776
836
|
this.logger.debug(
|
|
777
837
|
"[agui] sendA2uiAction: no messages to resume, dropping action",
|
|
@@ -809,7 +869,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
809
869
|
}
|
|
810
870
|
|
|
811
871
|
private maybeCompleteAfterToolResults(messageId: string): boolean {
|
|
812
|
-
const message = this.tryGetMessage(messageId)?.message;
|
|
872
|
+
const message = this.session.tryGetMessage(messageId)?.message;
|
|
813
873
|
if (!message || message.role !== "assistant") return false;
|
|
814
874
|
const assistant = message as ThreadAssistantMessage;
|
|
815
875
|
if (
|
|
@@ -823,7 +883,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
823
883
|
);
|
|
824
884
|
if (!allResolved) return false;
|
|
825
885
|
|
|
826
|
-
const updated = this.updateMessage(messageId, (current) =>
|
|
886
|
+
const updated = this.session.updateMessage(messageId, (current) =>
|
|
827
887
|
current.role === "assistant"
|
|
828
888
|
? {
|
|
829
889
|
...(current as ThreadAssistantMessage),
|
|
@@ -853,7 +913,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
853
913
|
this.assistantHistoryParents.clear();
|
|
854
914
|
|
|
855
915
|
if (messages.length === 0) {
|
|
856
|
-
this.
|
|
916
|
+
this.session.clear();
|
|
857
917
|
} else {
|
|
858
918
|
let expectedParentId: string | null = null;
|
|
859
919
|
let lastAppliedId: string | null = null;
|
|
@@ -863,100 +923,36 @@ export class AgUiThreadRuntimeCore {
|
|
|
863
923
|
for (const message of messages) {
|
|
864
924
|
if (seen.has(message.id)) continue;
|
|
865
925
|
seen.add(message.id);
|
|
866
|
-
const existing = this.tryGetMessage(message.id);
|
|
926
|
+
const existing = this.session.tryGetMessage(message.id);
|
|
867
927
|
if (existing && existing.parentId !== expectedParentId) {
|
|
868
928
|
hardReplace = true;
|
|
869
929
|
break;
|
|
870
930
|
}
|
|
871
|
-
this.addOrUpdateMessage(expectedParentId, message);
|
|
931
|
+
this.session.addOrUpdateMessage(expectedParentId, message);
|
|
872
932
|
expectedParentId = message.id;
|
|
873
933
|
lastAppliedId = message.id;
|
|
874
934
|
}
|
|
875
935
|
|
|
876
936
|
if (hardReplace) {
|
|
877
|
-
this.
|
|
937
|
+
this.session.clear();
|
|
878
938
|
expectedParentId = null;
|
|
879
939
|
lastAppliedId = null;
|
|
880
940
|
seen.clear();
|
|
881
941
|
for (const message of messages) {
|
|
882
942
|
if (seen.has(message.id)) continue;
|
|
883
943
|
seen.add(message.id);
|
|
884
|
-
this.addOrUpdateMessage(expectedParentId, message);
|
|
944
|
+
this.session.addOrUpdateMessage(expectedParentId, message);
|
|
885
945
|
expectedParentId = message.id;
|
|
886
946
|
lastAppliedId = message.id;
|
|
887
947
|
}
|
|
888
948
|
}
|
|
889
949
|
|
|
890
|
-
this.
|
|
950
|
+
this.session.resetHead(lastAppliedId);
|
|
891
951
|
}
|
|
892
952
|
|
|
893
|
-
this.
|
|
953
|
+
this.snapshotHistoryIds.clear();
|
|
894
954
|
for (const { message } of this.getMessageRepository().messages) {
|
|
895
|
-
this.
|
|
896
|
-
}
|
|
897
|
-
this.notifyUpdate();
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
private applyExternalMessageRepository(
|
|
901
|
-
loaded: ExportedMessageRepository,
|
|
902
|
-
): void {
|
|
903
|
-
const headId = loaded.headId ?? loaded.messages.at(-1)?.message.id ?? null;
|
|
904
|
-
const ids = new Set<string>();
|
|
905
|
-
let degenerate = false;
|
|
906
|
-
for (const { message } of loaded.messages) {
|
|
907
|
-
if (ids.has(message.id)) {
|
|
908
|
-
degenerate = true;
|
|
909
|
-
break;
|
|
910
|
-
}
|
|
911
|
-
ids.add(message.id);
|
|
912
|
-
}
|
|
913
|
-
if (headId !== null && !ids.has(headId)) degenerate = true;
|
|
914
|
-
|
|
915
|
-
if (!degenerate) {
|
|
916
|
-
this.clearRepository();
|
|
917
|
-
let pending = [...loaded.messages];
|
|
918
|
-
const importedIds = new Set<string>();
|
|
919
|
-
|
|
920
|
-
while (pending.length > 0) {
|
|
921
|
-
const unresolved: typeof pending = [];
|
|
922
|
-
let progressed = false;
|
|
923
|
-
for (const item of pending) {
|
|
924
|
-
if (item.parentId !== null && !importedIds.has(item.parentId)) {
|
|
925
|
-
unresolved.push(item);
|
|
926
|
-
continue;
|
|
927
|
-
}
|
|
928
|
-
this.addOrUpdateMessage(item.parentId, item.message);
|
|
929
|
-
importedIds.add(item.message.id);
|
|
930
|
-
progressed = true;
|
|
931
|
-
}
|
|
932
|
-
if (!progressed) {
|
|
933
|
-
degenerate = true;
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
pending = unresolved;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
if (degenerate) {
|
|
941
|
-
this.clearRepository();
|
|
942
|
-
let previousId: string | null = null;
|
|
943
|
-
for (const { message } of loaded.messages) {
|
|
944
|
-
const existing = this.tryGetMessage(message.id);
|
|
945
|
-
this.addOrUpdateMessage(
|
|
946
|
-
existing ? existing.parentId : previousId,
|
|
947
|
-
message,
|
|
948
|
-
);
|
|
949
|
-
previousId = message.id;
|
|
950
|
-
}
|
|
951
|
-
this.resetRepositoryHead(previousId);
|
|
952
|
-
} else {
|
|
953
|
-
this.resetRepositoryHead(headId);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
this.assistantHistoryParents.clear();
|
|
957
|
-
this.recordedHistoryIds.clear();
|
|
958
|
-
for (const { message } of loaded.messages) {
|
|
959
|
-
this.recordedHistoryIds.add(message.id);
|
|
955
|
+
this.snapshotHistoryIds.add(message.id);
|
|
960
956
|
}
|
|
961
957
|
this.notifyUpdate();
|
|
962
958
|
}
|
|
@@ -989,37 +985,53 @@ export class AgUiThreadRuntimeCore {
|
|
|
989
985
|
): Promise<void> {
|
|
990
986
|
const normalizedRunConfig = runConfig ?? {};
|
|
991
987
|
this.lastRunConfig = normalizedRunConfig;
|
|
992
|
-
const parent =
|
|
988
|
+
const parent =
|
|
989
|
+
parentId === null ? undefined : this.session.tryGetMessage(parentId);
|
|
993
990
|
const shouldEagerlyInsertAssistant =
|
|
994
991
|
parentId !== null &&
|
|
995
992
|
parent !== undefined &&
|
|
996
|
-
parentId !== this.
|
|
993
|
+
parentId !== this.session.headId;
|
|
997
994
|
const historicalMessages = [
|
|
998
995
|
...(shouldEagerlyInsertAssistant
|
|
999
|
-
? (this.tryGetMessages(parentId) ?? this.
|
|
1000
|
-
: this.
|
|
996
|
+
? (this.session.tryGetMessages(parentId) ?? this.session.getMessages())
|
|
997
|
+
: this.session.getMessages()),
|
|
1001
998
|
];
|
|
999
|
+
const runStartMessageIds = new Set(
|
|
1000
|
+
this.getMessageRepository().messages.map(({ message }) => message.id),
|
|
1001
|
+
);
|
|
1002
1002
|
|
|
1003
1003
|
this.pendingError = null;
|
|
1004
|
-
const assistantParentId = parent ? parentId : this.
|
|
1004
|
+
const assistantParentId = parent ? parentId : this.session.headId;
|
|
1005
1005
|
let assistantMessageId: string | undefined;
|
|
1006
1006
|
// A snapshot the preserve gate declines still evicts the in-flight
|
|
1007
1007
|
// assistant; recreating under the cached id on the next content-bearing
|
|
1008
|
-
// emit keeps both the stream and the message identity. Status-only emits
|
|
1009
|
-
// and server-id collisions must not
|
|
1008
|
+
// emit keeps both the stream and the message identity. Status-only emits,
|
|
1009
|
+
// data-only content, and off-branch server-id collisions must not
|
|
1010
|
+
// recreate: the snapshot already carries this turn's assistant, so
|
|
1011
|
+
// resurrecting for custom-event data would leave a trailing data-only
|
|
1012
|
+
// duplicate.
|
|
1010
1013
|
let assistantCollided = false;
|
|
1011
1014
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
1012
1015
|
const cached = assistantMessageId;
|
|
1013
|
-
if (cached !== undefined && this.tryGetMessage(cached))
|
|
1016
|
+
if (cached !== undefined && this.session.tryGetMessage(cached))
|
|
1017
|
+
return cached;
|
|
1014
1018
|
if (cached !== undefined && (assistantCollided || !allowRecreate)) {
|
|
1015
1019
|
return cached;
|
|
1016
1020
|
}
|
|
1021
|
+
const repositoryHeadId = this.session.headId;
|
|
1022
|
+
const shouldUseSelectedParent =
|
|
1023
|
+
cached === undefined ||
|
|
1024
|
+
(shouldEagerlyInsertAssistant &&
|
|
1025
|
+
(repositoryHeadId === null ||
|
|
1026
|
+
runStartMessageIds.has(repositoryHeadId)));
|
|
1027
|
+
// Branch runs keep their selected parent unless the snapshot advanced to
|
|
1028
|
+
// a message introduced during this run.
|
|
1017
1029
|
const parentId =
|
|
1018
|
-
|
|
1030
|
+
shouldUseSelectedParent &&
|
|
1019
1031
|
assistantParentId &&
|
|
1020
|
-
this.hasMessage(assistantParentId)
|
|
1032
|
+
this.session.hasMessage(assistantParentId)
|
|
1021
1033
|
? assistantParentId
|
|
1022
|
-
:
|
|
1034
|
+
: repositoryHeadId;
|
|
1023
1035
|
const created = this.insertAssistantPlaceholder(parentId, cached);
|
|
1024
1036
|
assistantMessageId = created;
|
|
1025
1037
|
this.markPendingAssistantHistory(created, parentId);
|
|
@@ -1029,10 +1041,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
1029
1041
|
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
1030
1042
|
|
|
1031
1043
|
const applyUpdate = (update: ChatModelRunResult) => {
|
|
1032
|
-
const
|
|
1033
|
-
Array.isArray(update.content) &&
|
|
1044
|
+
const hasStreamContent =
|
|
1045
|
+
Array.isArray(update.content) &&
|
|
1046
|
+
update.content.some((part) => part.type !== "data");
|
|
1034
1047
|
const resolved = this.updateAssistantMessage(
|
|
1035
|
-
ensureAssistant(
|
|
1048
|
+
ensureAssistant(hasStreamContent),
|
|
1036
1049
|
update,
|
|
1037
1050
|
);
|
|
1038
1051
|
if (resolved !== assistantMessageId) {
|
|
@@ -1040,19 +1053,51 @@ export class AgUiThreadRuntimeCore {
|
|
|
1040
1053
|
}
|
|
1041
1054
|
};
|
|
1042
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
|
+
|
|
1043
1093
|
const aggregator = new RunAggregator({
|
|
1044
1094
|
showThinking: this.showThinking,
|
|
1045
1095
|
logger: this.logger,
|
|
1046
1096
|
emit: applyUpdate,
|
|
1047
1097
|
onServerMessageId: (serverId) => {
|
|
1048
|
-
|
|
1049
|
-
if (placeholder === serverId) return;
|
|
1050
|
-
if (this.reassignAssistantId(placeholder, serverId)) {
|
|
1051
|
-
assistantMessageId = serverId;
|
|
1052
|
-
} else {
|
|
1053
|
-
assistantCollided = true;
|
|
1054
|
-
}
|
|
1098
|
+
adoptServerMessageId(serverId, false);
|
|
1055
1099
|
},
|
|
1100
|
+
onTextMessageStart: (serverId) => adoptServerMessageId(serverId, true),
|
|
1056
1101
|
});
|
|
1057
1102
|
const dispatch = (event: AgUiEvent) =>
|
|
1058
1103
|
this.handleEvent(aggregator, event, assistantMessageId);
|
|
@@ -1134,6 +1179,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1134
1179
|
|
|
1135
1180
|
if (this.pendingError) {
|
|
1136
1181
|
const err = this.pendingError;
|
|
1182
|
+
this.reportedErrors.add(err);
|
|
1137
1183
|
this.pendingError = null;
|
|
1138
1184
|
this.pendingResumeMessageId = null;
|
|
1139
1185
|
this.pendingA2uiResume = false;
|
|
@@ -1164,7 +1210,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1164
1210
|
return;
|
|
1165
1211
|
}
|
|
1166
1212
|
this.maybeAutoCancelPendingToolCalls();
|
|
1167
|
-
const parentId = this.
|
|
1213
|
+
const parentId = this.session.headId;
|
|
1168
1214
|
if (parentId !== null) {
|
|
1169
1215
|
this.startResumeRun(parentId);
|
|
1170
1216
|
} else {
|
|
@@ -1206,7 +1252,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1206
1252
|
unstable_threadId: ctx.threadId,
|
|
1207
1253
|
unstable_parentId: ctx.parentId,
|
|
1208
1254
|
unstable_getMessage: () => {
|
|
1209
|
-
const message = this.tryGetMessage(currentId())?.message;
|
|
1255
|
+
const message = this.session.tryGetMessage(currentId())?.message;
|
|
1210
1256
|
if (!message) {
|
|
1211
1257
|
throw new Error(
|
|
1212
1258
|
"[agui] resume stream requested the assistant message before it existed",
|
|
@@ -1233,7 +1279,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1233
1279
|
}
|
|
1234
1280
|
|
|
1235
1281
|
if (ctx.abortSignal.aborted) return;
|
|
1236
|
-
const current = this.tryGetMessage(currentId())?.message;
|
|
1282
|
+
const current = this.session.tryGetMessage(currentId())?.message;
|
|
1237
1283
|
if (!current || current.status?.type === "running") {
|
|
1238
1284
|
ctx.applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
1239
1285
|
}
|
|
@@ -1247,7 +1293,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1247
1293
|
) {
|
|
1248
1294
|
const threadId = this.agent.threadId || "main";
|
|
1249
1295
|
const messages = toAgUiMessages(
|
|
1250
|
-
historyMessages ?? this.
|
|
1296
|
+
historyMessages ?? this.session.getMessages(),
|
|
1251
1297
|
);
|
|
1252
1298
|
const context = this.runtime?.thread.getModelContext();
|
|
1253
1299
|
const input = {
|
|
@@ -1326,34 +1372,34 @@ export class AgUiThreadRuntimeCore {
|
|
|
1326
1372
|
custom: {},
|
|
1327
1373
|
},
|
|
1328
1374
|
};
|
|
1329
|
-
this.addOrUpdateMessage(parentId ?? this.
|
|
1330
|
-
this.switchToBranch(id);
|
|
1375
|
+
this.session.addOrUpdateMessage(parentId ?? this.session.headId, assistant);
|
|
1376
|
+
this.session.switchToBranch(id);
|
|
1331
1377
|
this.notifyUpdate();
|
|
1332
1378
|
return id;
|
|
1333
1379
|
}
|
|
1334
1380
|
|
|
1335
1381
|
private reassignAssistantId(oldId: string, newId: string): boolean {
|
|
1336
1382
|
if (oldId === newId) return true;
|
|
1337
|
-
const oldItem = this.tryGetMessage(oldId);
|
|
1383
|
+
const oldItem = this.session.tryGetMessage(oldId);
|
|
1338
1384
|
if (!oldItem) return false;
|
|
1339
1385
|
|
|
1340
|
-
const collidesWithExisting = this.hasMessage(newId);
|
|
1386
|
+
const collidesWithExisting = this.session.hasMessage(newId);
|
|
1341
1387
|
|
|
1342
1388
|
if (collidesWithExisting) {
|
|
1343
1389
|
this.logger.debug?.(
|
|
1344
1390
|
"[agui] reassignAssistantId: server id already present in messages or repository, dropping placeholder",
|
|
1345
1391
|
{ oldId, newId },
|
|
1346
1392
|
);
|
|
1347
|
-
this.deleteMessage(oldId, oldItem.parentId ?? null);
|
|
1393
|
+
this.session.deleteMessage(oldId, oldItem.parentId ?? null);
|
|
1348
1394
|
} else {
|
|
1349
1395
|
const { isOptimistic: _, ...metadata } = oldItem.message.metadata;
|
|
1350
|
-
this.addOrUpdateMessage(oldItem.parentId, {
|
|
1396
|
+
this.session.addOrUpdateMessage(oldItem.parentId, {
|
|
1351
1397
|
...oldItem.message,
|
|
1352
1398
|
id: newId,
|
|
1353
1399
|
metadata,
|
|
1354
1400
|
} as ThreadMessage);
|
|
1355
|
-
this.switchToBranch(newId);
|
|
1356
|
-
this.tryDeleteMessage(oldId);
|
|
1401
|
+
this.session.switchToBranch(newId);
|
|
1402
|
+
this.session.tryDeleteMessage(oldId);
|
|
1357
1403
|
}
|
|
1358
1404
|
|
|
1359
1405
|
const pendingParent = this.assistantHistoryParents.get(oldId);
|
|
@@ -1364,10 +1410,33 @@ export class AgUiThreadRuntimeCore {
|
|
|
1364
1410
|
}
|
|
1365
1411
|
}
|
|
1366
1412
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1413
|
+
for (const ids of [this.snapshotHistoryIds, this.persistedHistoryIds]) {
|
|
1414
|
+
if (ids.has(oldId)) {
|
|
1415
|
+
ids.delete(oldId);
|
|
1416
|
+
if (!collidesWithExisting) {
|
|
1417
|
+
ids.add(newId);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// An in-flight write completes under the id it was started with, so the
|
|
1423
|
+
// rename moves the chain entry and transfers the completion mark; without
|
|
1424
|
+
// this the resolve records the dead id and the live one appends again.
|
|
1425
|
+
const pendingWrite = this.historyWrites.get(oldId);
|
|
1426
|
+
if (pendingWrite) {
|
|
1427
|
+
this.historyWrites.delete(oldId);
|
|
1369
1428
|
if (!collidesWithExisting) {
|
|
1370
|
-
this.
|
|
1429
|
+
this.historyWrites.set(newId, pendingWrite);
|
|
1430
|
+
const settle = () => {
|
|
1431
|
+
if (this.historyWrites.get(newId) === pendingWrite) {
|
|
1432
|
+
this.historyWrites.delete(newId);
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
void pendingWrite.then(() => {
|
|
1436
|
+
this.persistedHistoryIds.delete(oldId);
|
|
1437
|
+
this.persistedHistoryIds.add(newId);
|
|
1438
|
+
settle();
|
|
1439
|
+
}, settle);
|
|
1371
1440
|
}
|
|
1372
1441
|
}
|
|
1373
1442
|
|
|
@@ -1380,7 +1449,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1380
1449
|
update: ChatModelRunResult,
|
|
1381
1450
|
): string {
|
|
1382
1451
|
let latestStatus: MessageStatus | undefined;
|
|
1383
|
-
const touched = this.updateMessage(messageId, (message) => {
|
|
1452
|
+
const touched = this.session.updateMessage(messageId, (message) => {
|
|
1384
1453
|
if (message.role !== "assistant") return message;
|
|
1385
1454
|
const assistant = message as ThreadAssistantMessage;
|
|
1386
1455
|
const metadata = update.metadata
|
|
@@ -1549,7 +1618,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1549
1618
|
messageId: string,
|
|
1550
1619
|
event: Extract<AgUiEvent, { type: "TOOL_CALL_RESULT" }>,
|
|
1551
1620
|
): void {
|
|
1552
|
-
const updated = this.updateMessage(messageId, (message) => {
|
|
1621
|
+
const updated = this.session.updateMessage(messageId, (message) => {
|
|
1553
1622
|
if (message.role !== "assistant") return message;
|
|
1554
1623
|
const assistant = message as ThreadAssistantMessage;
|
|
1555
1624
|
const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
|
|
@@ -1636,7 +1705,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1636
1705
|
? serverHash
|
|
1637
1706
|
: undefined;
|
|
1638
1707
|
const result = event.content["result"];
|
|
1639
|
-
const updated = this.updateMessage(messageId, (message) => {
|
|
1708
|
+
const updated = this.session.updateMessage(messageId, (message) => {
|
|
1640
1709
|
if (message.role !== "assistant") return message;
|
|
1641
1710
|
const assistant = message as ThreadAssistantMessage;
|
|
1642
1711
|
let matchedToolCall = false;
|
|
@@ -1688,7 +1757,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1688
1757
|
) {
|
|
1689
1758
|
try {
|
|
1690
1759
|
const activeMessage = activeAssistantId
|
|
1691
|
-
? this.tryGetMessage(activeAssistantId)?.message
|
|
1760
|
+
? this.session.tryGetMessage(activeAssistantId)?.message
|
|
1692
1761
|
: undefined;
|
|
1693
1762
|
const activeAssistant =
|
|
1694
1763
|
activeMessage?.role === "assistant" ? activeMessage : undefined;
|
|
@@ -1708,7 +1777,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
1708
1777
|
);
|
|
1709
1778
|
}
|
|
1710
1779
|
}
|
|
1711
|
-
const snapshotHeadId = converted.at(-1)?.id ?? null;
|
|
1712
1780
|
const snapshotContainsActiveAssistant = converted.some(
|
|
1713
1781
|
(message) => message.id === activeAssistant?.id,
|
|
1714
1782
|
);
|
|
@@ -1721,9 +1789,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1721
1789
|
converted.push(activeAssistant);
|
|
1722
1790
|
}
|
|
1723
1791
|
this.applyExternalMessages(converted);
|
|
1724
|
-
if (
|
|
1725
|
-
this.
|
|
1726
|
-
|
|
1792
|
+
if (activeAssistant !== undefined) {
|
|
1793
|
+
const activeItem = this.session.tryGetMessage(activeAssistant.id);
|
|
1794
|
+
if (activeItem) {
|
|
1795
|
+
if (preservesActiveAssistant) {
|
|
1796
|
+
this.snapshotHistoryIds.delete(activeAssistant.id);
|
|
1797
|
+
}
|
|
1798
|
+
this.markPendingAssistantHistory(
|
|
1799
|
+
activeAssistant.id,
|
|
1800
|
+
activeItem.parentId,
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1727
1803
|
}
|
|
1728
1804
|
} catch (error) {
|
|
1729
1805
|
this.logger.error?.("[agui] failed to import messages snapshot", error);
|
|
@@ -1748,7 +1824,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1748
1824
|
}
|
|
1749
1825
|
|
|
1750
1826
|
private recordHistoryEntry(parentId: string | null, message: ThreadMessage) {
|
|
1751
|
-
this.appendHistoryItem(parentId, message)
|
|
1827
|
+
void this.appendHistoryItem(parentId, message)?.catch((error) => {
|
|
1828
|
+
this.logger.error?.("[agui] failed to append history entry", error);
|
|
1829
|
+
});
|
|
1752
1830
|
}
|
|
1753
1831
|
|
|
1754
1832
|
private markPendingAssistantHistory(
|
|
@@ -1760,22 +1838,108 @@ export class AgUiThreadRuntimeCore {
|
|
|
1760
1838
|
}
|
|
1761
1839
|
|
|
1762
1840
|
private persistAssistantHistory(messageId: string) {
|
|
1763
|
-
|
|
1841
|
+
const history = this.history;
|
|
1842
|
+
if (!history) return;
|
|
1764
1843
|
const parentId = this.assistantHistoryParents.get(messageId);
|
|
1765
1844
|
if (parentId === undefined) return;
|
|
1766
|
-
const message = this.tryGetMessage(messageId)?.message;
|
|
1845
|
+
const message = this.session.tryGetMessage(messageId)?.message;
|
|
1767
1846
|
if (!message || message.role !== "assistant") return;
|
|
1768
1847
|
if (!this.isPersistableStatus(message.status)) return;
|
|
1848
|
+
const wasPersisted = this.persistedHistoryIds.has(messageId);
|
|
1849
|
+
const update = history.update;
|
|
1850
|
+
const shouldUpdate =
|
|
1851
|
+
update !== undefined &&
|
|
1852
|
+
(wasPersisted || this.snapshotHistoryIds.has(messageId));
|
|
1853
|
+
|
|
1854
|
+
if (shouldUpdate) {
|
|
1855
|
+
const write = this.chainHistoryWrite(messageId, () =>
|
|
1856
|
+
update.call(history, { parentId, message }),
|
|
1857
|
+
);
|
|
1858
|
+
this.assistantHistoryParents.delete(messageId);
|
|
1859
|
+
void write.then(
|
|
1860
|
+
() => {
|
|
1861
|
+
this.persistedHistoryIds.add(messageId);
|
|
1862
|
+
},
|
|
1863
|
+
(error) => {
|
|
1864
|
+
const pending = this.historyWrites.get(messageId);
|
|
1865
|
+
if (pending === undefined || pending === write) {
|
|
1866
|
+
this.assistantHistoryParents.set(messageId, parentId);
|
|
1867
|
+
}
|
|
1868
|
+
this.logger.error?.("[agui] failed to update history entry", error);
|
|
1869
|
+
},
|
|
1870
|
+
);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (wasPersisted) {
|
|
1875
|
+
this.assistantHistoryParents.delete(messageId);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
const write = this.appendHistoryItem(parentId, message);
|
|
1880
|
+
if (!write) return;
|
|
1769
1881
|
this.assistantHistoryParents.delete(messageId);
|
|
1770
|
-
|
|
1882
|
+
void write.then(
|
|
1883
|
+
() => {},
|
|
1884
|
+
(error) => {
|
|
1885
|
+
const pending = this.historyWrites.get(messageId);
|
|
1886
|
+
if (pending === undefined || pending === write) {
|
|
1887
|
+
this.assistantHistoryParents.set(messageId, parentId);
|
|
1888
|
+
}
|
|
1889
|
+
this.logger.error?.("[agui] failed to append history entry", error);
|
|
1890
|
+
},
|
|
1891
|
+
);
|
|
1771
1892
|
}
|
|
1772
1893
|
|
|
1773
|
-
private appendHistoryItem(
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1894
|
+
private appendHistoryItem(
|
|
1895
|
+
parentId: string | null,
|
|
1896
|
+
message: ThreadMessage,
|
|
1897
|
+
): Promise<void> | undefined {
|
|
1898
|
+
if (!this.history || this.persistedHistoryIds.has(message.id)) return;
|
|
1899
|
+
const pending = this.historyWrites.get(message.id);
|
|
1900
|
+
if (pending) return pending;
|
|
1901
|
+
|
|
1902
|
+
const append = this.history.append.bind(this.history);
|
|
1903
|
+
const write = this.chainHistoryWrite(message.id, () =>
|
|
1904
|
+
append({ parentId, message }),
|
|
1905
|
+
);
|
|
1906
|
+
void write.then(
|
|
1907
|
+
() => {
|
|
1908
|
+
this.persistedHistoryIds.add(message.id);
|
|
1909
|
+
},
|
|
1910
|
+
() => {},
|
|
1911
|
+
);
|
|
1912
|
+
return write;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
private chainHistoryWrite(
|
|
1916
|
+
id: string,
|
|
1917
|
+
write: () => Promise<void>,
|
|
1918
|
+
): Promise<void> {
|
|
1919
|
+
const pending = this.historyWrites.get(id);
|
|
1920
|
+
let next: Promise<void>;
|
|
1921
|
+
if (pending) {
|
|
1922
|
+
next = pending.then(write, write);
|
|
1923
|
+
} else {
|
|
1924
|
+
try {
|
|
1925
|
+
next = Promise.resolve(write());
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
next = Promise.reject(error);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
this.historyWrites.set(id, next);
|
|
1931
|
+
void next.then(
|
|
1932
|
+
() => {
|
|
1933
|
+
if (this.historyWrites.get(id) === next) {
|
|
1934
|
+
this.historyWrites.delete(id);
|
|
1935
|
+
}
|
|
1936
|
+
},
|
|
1937
|
+
() => {
|
|
1938
|
+
if (this.historyWrites.get(id) === next) {
|
|
1939
|
+
this.historyWrites.delete(id);
|
|
1940
|
+
}
|
|
1941
|
+
},
|
|
1942
|
+
);
|
|
1943
|
+
return next;
|
|
1780
1944
|
}
|
|
1781
1945
|
}
|