@assistant-ui/react-ag-ui 0.0.53 → 0.0.56
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 +23 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +183 -39
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +19 -3
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +197 -36
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +15 -2
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +62 -10
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +6 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +10 -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 +15 -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/runtime/types.d.ts +5 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +4 -1
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +8 -2
- package/src/runtime/AgUiThreadRuntimeCore.ts +335 -42
- package/src/runtime/adapter/conversions.content.test.ts +278 -0
- package/src/runtime/adapter/conversions.ts +298 -50
- package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
- package/src/runtime/adapter/run-aggregator.ts +96 -5
- package/src/runtime/adapter/subscriber.ts +17 -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 +23 -1
- package/src/runtime/interrupt-internals.ts +35 -0
- package/src/runtime/types.ts +6 -0
- package/src/useAgUiRuntime.approval.test.tsx +885 -0
- package/src/useAgUiRuntime.queue.test.tsx +68 -1
- package/src/useAgUiRuntime.ts +5 -1
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ChatModelRunResult,
|
|
15
15
|
ExportedMessageRepository,
|
|
16
16
|
MessageStatus,
|
|
17
|
+
RespondToToolApprovalOptions,
|
|
17
18
|
ThreadAssistantMessage,
|
|
18
19
|
ThreadHistoryAdapter,
|
|
19
20
|
ThreadMessage,
|
|
@@ -45,6 +46,12 @@ import {
|
|
|
45
46
|
toAgUiTools,
|
|
46
47
|
} from "./adapter/conversions";
|
|
47
48
|
import { createAgUiSubscriber } from "./adapter/subscriber";
|
|
49
|
+
import {
|
|
50
|
+
buildToolApprovalResume,
|
|
51
|
+
projectAgUiToolApprovals,
|
|
52
|
+
withSettledToolApprovals,
|
|
53
|
+
withToolApprovalDecision,
|
|
54
|
+
} from "./adapter/tool-approval";
|
|
48
55
|
|
|
49
56
|
// AbstractAgent.runAgent declares two parameters. HttpAgent ignores a third and
|
|
50
57
|
// is cancelled through agent.abortRun(); the run options stay for subclasses
|
|
@@ -120,6 +127,26 @@ const invokeRuntimeCallback = <TArgs extends unknown[]>(
|
|
|
120
127
|
}
|
|
121
128
|
};
|
|
122
129
|
|
|
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.
|
|
132
|
+
const mergeAgUiNamespace = (
|
|
133
|
+
current: Record<string, unknown> | undefined,
|
|
134
|
+
incoming: Record<string, unknown>,
|
|
135
|
+
) => {
|
|
136
|
+
const merged = { ...current, ...incoming };
|
|
137
|
+
const currentNs = current?.[AG_UI_METADATA_NAMESPACE];
|
|
138
|
+
const incomingNs = incoming[AG_UI_METADATA_NAMESPACE];
|
|
139
|
+
if (
|
|
140
|
+
typeof currentNs === "object" &&
|
|
141
|
+
currentNs !== null &&
|
|
142
|
+
typeof incomingNs === "object" &&
|
|
143
|
+
incomingNs !== null
|
|
144
|
+
) {
|
|
145
|
+
merged[AG_UI_METADATA_NAMESPACE] = { ...currentNs, ...incomingNs };
|
|
146
|
+
}
|
|
147
|
+
return merged;
|
|
148
|
+
};
|
|
149
|
+
|
|
123
150
|
export class AgUiThreadRuntimeCore {
|
|
124
151
|
private agent: AbstractAgent;
|
|
125
152
|
private logger: Logger;
|
|
@@ -128,6 +155,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
128
155
|
private onError: ((error: Error) => void) | undefined;
|
|
129
156
|
private onCancel: (() => void) | undefined;
|
|
130
157
|
private readonly notifyUpdate: () => void;
|
|
158
|
+
private readonly reportedErrors = new WeakSet<object>();
|
|
131
159
|
|
|
132
160
|
private runtime: AssistantRuntime | undefined;
|
|
133
161
|
private readonly repository = new MessageRepository();
|
|
@@ -142,7 +170,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
142
170
|
private history: ThreadHistoryAdapter | undefined;
|
|
143
171
|
private lastRunConfig: RunConfig | undefined;
|
|
144
172
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
145
|
-
private readonly
|
|
173
|
+
private readonly snapshotHistoryIds = new Set<string>();
|
|
174
|
+
private readonly persistedHistoryIds = new Set<string>();
|
|
175
|
+
private readonly historyWrites = new Map<string, Promise<void>>();
|
|
146
176
|
private _isLoading = false;
|
|
147
177
|
private _loadPromise: Promise<void> | undefined;
|
|
148
178
|
private pendingResumeMessageId: string | null = null;
|
|
@@ -178,6 +208,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
178
208
|
|
|
179
209
|
detachRuntime() {
|
|
180
210
|
this.runtime = undefined;
|
|
211
|
+
void this.cancel().catch((error) => {
|
|
212
|
+
this.logger.error("[agui] failed to cancel run during teardown", error);
|
|
213
|
+
});
|
|
181
214
|
}
|
|
182
215
|
|
|
183
216
|
getMessages(): readonly ThreadMessage[] {
|
|
@@ -506,34 +539,142 @@ export class AgUiThreadRuntimeCore {
|
|
|
506
539
|
);
|
|
507
540
|
}
|
|
508
541
|
|
|
542
|
+
this.assertInterruptsAnswerable(
|
|
543
|
+
"submitInterruptResponses",
|
|
544
|
+
pending.interrupts,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
await this.resumeWithResponses(
|
|
548
|
+
pending.messageId,
|
|
549
|
+
openIds.map((id) => responsesById.get(id)!),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Resumes with an already validated resume array. Answerability is checked
|
|
555
|
+
* once per submission: re-checking here would let a clock crossing reject a
|
|
556
|
+
* decision this runtime has already recorded, stranding the gate decided and
|
|
557
|
+
* unretryable.
|
|
558
|
+
*/
|
|
559
|
+
private async resumeWithResponses(
|
|
560
|
+
messageId: string,
|
|
561
|
+
resume: AgUiResumeEntry[],
|
|
562
|
+
): Promise<void> {
|
|
563
|
+
this.clearPendingInterrupts(messageId, resume);
|
|
564
|
+
await this.startRun(messageId, this.lastRunConfig, resume);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The core seams that reach `respondToToolApproval` discard the promise they
|
|
569
|
+
* receive, so a rejected decision would otherwise surface only as an
|
|
570
|
+
* unhandled rejection. A failure raised by the resumed run itself was already
|
|
571
|
+
* reported by `startRun` before it rethrew, so reporting it here again would
|
|
572
|
+
* give the consumer two notifications for one failure.
|
|
573
|
+
*/
|
|
574
|
+
reportError(error: unknown): void {
|
|
575
|
+
if (this.reportedErrors.has(error as object)) {
|
|
576
|
+
this.reportedErrors.delete(error as object);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
invokeRuntimeCallback(
|
|
580
|
+
"onError",
|
|
581
|
+
this.onError,
|
|
582
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private assertInterruptsAnswerable(
|
|
587
|
+
method: "submitInterruptResponses" | "respondToToolApproval",
|
|
588
|
+
interrupts: readonly AgUiInterrupt[],
|
|
589
|
+
): void {
|
|
509
590
|
const now = Date.now();
|
|
510
|
-
for (const interrupt of
|
|
591
|
+
for (const interrupt of interrupts) {
|
|
511
592
|
if (!interrupt.expiresAt) continue;
|
|
512
593
|
const expiry = new Date(interrupt.expiresAt).getTime();
|
|
513
594
|
if (Number.isNaN(expiry)) {
|
|
514
595
|
throw new Error(
|
|
515
|
-
`[agui]
|
|
596
|
+
`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`,
|
|
516
597
|
);
|
|
517
598
|
}
|
|
518
599
|
if (expiry <= now) {
|
|
519
600
|
throw new Error(
|
|
520
|
-
`[agui]
|
|
601
|
+
`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`,
|
|
521
602
|
);
|
|
522
603
|
}
|
|
523
604
|
}
|
|
605
|
+
if (this.isRunningFlag) {
|
|
606
|
+
throw new Error(`[agui] ${method}: a run is already in progress`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async respondToToolApproval(
|
|
611
|
+
options: RespondToToolApprovalOptions,
|
|
612
|
+
): Promise<void> {
|
|
613
|
+
const pending = this.getPendingInterrupts();
|
|
614
|
+
if (!pending) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
"[agui] respondToToolApproval: no pending interrupts on this thread",
|
|
617
|
+
);
|
|
618
|
+
}
|
|
524
619
|
|
|
525
|
-
|
|
526
|
-
|
|
620
|
+
// Bound against the message the gates landed on, so this check claims a
|
|
621
|
+
// batch only where the projection did.
|
|
622
|
+
const gatedMessage = this.tryGetMessage(pending.messageId)?.message as
|
|
623
|
+
| ThreadAssistantMessage
|
|
624
|
+
| undefined;
|
|
625
|
+
const gated = projectAgUiToolApprovals(
|
|
626
|
+
pending.interrupts,
|
|
627
|
+
new Set(
|
|
628
|
+
(gatedMessage?.content ?? [])
|
|
629
|
+
.filter((part) => part.type === "tool-call")
|
|
630
|
+
.map((part) => part.toolCallId),
|
|
631
|
+
),
|
|
632
|
+
);
|
|
633
|
+
const isGated = [...gated.values()].some(
|
|
634
|
+
(approval) => approval.id === options.approvalId,
|
|
527
635
|
);
|
|
636
|
+
if (!isGated) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`[agui] respondToToolApproval: no pending tool-call interrupt for approval id "${options.approvalId}"`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
528
641
|
|
|
529
|
-
|
|
642
|
+
// The decision is recorded only once the batch is known to be answerable:
|
|
643
|
+
// a rejected submission would otherwise leave the gate decided and
|
|
644
|
+
// unretryable, because a second click reports it as already decided.
|
|
645
|
+
this.assertInterruptsAnswerable(
|
|
646
|
+
"respondToToolApproval",
|
|
647
|
+
pending.interrupts,
|
|
648
|
+
);
|
|
649
|
+
|
|
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
|
+
});
|
|
657
|
+
if (!recorded) {
|
|
530
658
|
throw new Error(
|
|
531
|
-
|
|
659
|
+
`[agui] respondToToolApproval: approval "${options.approvalId}" is already decided`,
|
|
532
660
|
);
|
|
533
661
|
}
|
|
662
|
+
this.notifyUpdate();
|
|
663
|
+
|
|
664
|
+
const assistant = this.tryGetMessage(pending.messageId)?.message as
|
|
665
|
+
| ThreadAssistantMessage
|
|
666
|
+
| undefined;
|
|
667
|
+
if (!assistant) return;
|
|
668
|
+
|
|
669
|
+
// AG-UI resumes a run with one response per open interrupt, so the run
|
|
670
|
+
// stays paused until every gate in the batch has been answered.
|
|
671
|
+
const resume = buildToolApprovalResume(
|
|
672
|
+
assistant.content,
|
|
673
|
+
pending.interrupts,
|
|
674
|
+
);
|
|
675
|
+
if (!resume) return;
|
|
534
676
|
|
|
535
|
-
this.
|
|
536
|
-
await this.startRun(pending.messageId, this.lastRunConfig, resume);
|
|
677
|
+
await this.resumeWithResponses(pending.messageId, resume);
|
|
537
678
|
}
|
|
538
679
|
|
|
539
680
|
async steerAway(
|
|
@@ -575,7 +716,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
575
716
|
}
|
|
576
717
|
|
|
577
718
|
const normalized = this.toAppendMessage(message);
|
|
578
|
-
this.clearPendingInterrupts(pending.messageId);
|
|
719
|
+
this.clearPendingInterrupts(pending.messageId, resume);
|
|
579
720
|
const threadMessageId = this.appendEntry(normalized);
|
|
580
721
|
await this.startRun(threadMessageId, normalized.runConfig, resume);
|
|
581
722
|
}
|
|
@@ -657,7 +798,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
657
798
|
} as AppendMessage;
|
|
658
799
|
}
|
|
659
800
|
|
|
660
|
-
private clearPendingInterrupts(
|
|
801
|
+
private clearPendingInterrupts(
|
|
802
|
+
messageId: string,
|
|
803
|
+
resume: readonly AgUiResumeEntry[],
|
|
804
|
+
): void {
|
|
661
805
|
const touched = this.updateMessage(messageId, (message) => {
|
|
662
806
|
if (message.role !== "assistant") return message;
|
|
663
807
|
const assistant = message as ThreadAssistantMessage;
|
|
@@ -679,6 +823,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
679
823
|
}
|
|
680
824
|
return {
|
|
681
825
|
...assistant,
|
|
826
|
+
content: withSettledToolApprovals(assistant.content, resume),
|
|
682
827
|
status: { type: "complete" as const, reason: "unknown" as const },
|
|
683
828
|
metadata: { ...assistant.metadata, custom: newCustom },
|
|
684
829
|
};
|
|
@@ -870,9 +1015,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
870
1015
|
this.resetRepositoryHead(lastAppliedId);
|
|
871
1016
|
}
|
|
872
1017
|
|
|
873
|
-
this.
|
|
1018
|
+
this.snapshotHistoryIds.clear();
|
|
874
1019
|
for (const { message } of this.getMessageRepository().messages) {
|
|
875
|
-
this.
|
|
1020
|
+
this.snapshotHistoryIds.add(message.id);
|
|
876
1021
|
}
|
|
877
1022
|
this.notifyUpdate();
|
|
878
1023
|
}
|
|
@@ -934,9 +1079,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
934
1079
|
}
|
|
935
1080
|
|
|
936
1081
|
this.assistantHistoryParents.clear();
|
|
937
|
-
this.
|
|
1082
|
+
this.snapshotHistoryIds.clear();
|
|
1083
|
+
this.persistedHistoryIds.clear();
|
|
938
1084
|
for (const { message } of loaded.messages) {
|
|
939
|
-
this.
|
|
1085
|
+
this.persistedHistoryIds.add(message.id);
|
|
940
1086
|
}
|
|
941
1087
|
this.notifyUpdate();
|
|
942
1088
|
}
|
|
@@ -979,14 +1125,20 @@ export class AgUiThreadRuntimeCore {
|
|
|
979
1125
|
? (this.tryGetMessages(parentId) ?? this.repository.getMessages())
|
|
980
1126
|
: this.repository.getMessages()),
|
|
981
1127
|
];
|
|
1128
|
+
const runStartMessageIds = new Set(
|
|
1129
|
+
this.getMessageRepository().messages.map(({ message }) => message.id),
|
|
1130
|
+
);
|
|
982
1131
|
|
|
983
1132
|
this.pendingError = null;
|
|
984
1133
|
const assistantParentId = parent ? parentId : this.repository.headId;
|
|
985
1134
|
let assistantMessageId: string | undefined;
|
|
986
1135
|
// A snapshot the preserve gate declines still evicts the in-flight
|
|
987
1136
|
// assistant; recreating under the cached id on the next content-bearing
|
|
988
|
-
// emit keeps both the stream and the message identity. Status-only emits
|
|
989
|
-
// and server-id collisions must not
|
|
1137
|
+
// emit keeps both the stream and the message identity. Status-only emits,
|
|
1138
|
+
// data-only content, and off-branch server-id collisions must not
|
|
1139
|
+
// recreate: the snapshot already carries this turn's assistant, so
|
|
1140
|
+
// resurrecting for custom-event data would leave a trailing data-only
|
|
1141
|
+
// duplicate.
|
|
990
1142
|
let assistantCollided = false;
|
|
991
1143
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
992
1144
|
const cached = assistantMessageId;
|
|
@@ -994,12 +1146,20 @@ export class AgUiThreadRuntimeCore {
|
|
|
994
1146
|
if (cached !== undefined && (assistantCollided || !allowRecreate)) {
|
|
995
1147
|
return cached;
|
|
996
1148
|
}
|
|
1149
|
+
const repositoryHeadId = this.repository.headId;
|
|
1150
|
+
const shouldUseSelectedParent =
|
|
1151
|
+
cached === undefined ||
|
|
1152
|
+
(shouldEagerlyInsertAssistant &&
|
|
1153
|
+
(repositoryHeadId === null ||
|
|
1154
|
+
runStartMessageIds.has(repositoryHeadId)));
|
|
1155
|
+
// Branch runs keep their selected parent unless the snapshot advanced to
|
|
1156
|
+
// a message introduced during this run.
|
|
997
1157
|
const parentId =
|
|
998
|
-
|
|
1158
|
+
shouldUseSelectedParent &&
|
|
999
1159
|
assistantParentId &&
|
|
1000
1160
|
this.hasMessage(assistantParentId)
|
|
1001
1161
|
? assistantParentId
|
|
1002
|
-
:
|
|
1162
|
+
: repositoryHeadId;
|
|
1003
1163
|
const created = this.insertAssistantPlaceholder(parentId, cached);
|
|
1004
1164
|
assistantMessageId = created;
|
|
1005
1165
|
this.markPendingAssistantHistory(created, parentId);
|
|
@@ -1009,10 +1169,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
1009
1169
|
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
1010
1170
|
|
|
1011
1171
|
const applyUpdate = (update: ChatModelRunResult) => {
|
|
1012
|
-
const
|
|
1013
|
-
Array.isArray(update.content) &&
|
|
1172
|
+
const hasStreamContent =
|
|
1173
|
+
Array.isArray(update.content) &&
|
|
1174
|
+
update.content.some((part) => part.type !== "data");
|
|
1014
1175
|
const resolved = this.updateAssistantMessage(
|
|
1015
|
-
ensureAssistant(
|
|
1176
|
+
ensureAssistant(hasStreamContent),
|
|
1016
1177
|
update,
|
|
1017
1178
|
);
|
|
1018
1179
|
if (resolved !== assistantMessageId) {
|
|
@@ -1027,8 +1188,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
1027
1188
|
onServerMessageId: (serverId) => {
|
|
1028
1189
|
const placeholder = ensureAssistant(true);
|
|
1029
1190
|
if (placeholder === serverId) return;
|
|
1030
|
-
|
|
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) {
|
|
1031
1201
|
assistantMessageId = serverId;
|
|
1202
|
+
if (adoptsVisibleCollision) {
|
|
1203
|
+
const parentId = this.tryGetMessage(serverId)?.parentId ?? null;
|
|
1204
|
+
this.markPendingAssistantHistory(serverId, parentId);
|
|
1205
|
+
}
|
|
1032
1206
|
} else {
|
|
1033
1207
|
assistantCollided = true;
|
|
1034
1208
|
}
|
|
@@ -1114,6 +1288,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1114
1288
|
|
|
1115
1289
|
if (this.pendingError) {
|
|
1116
1290
|
const err = this.pendingError;
|
|
1291
|
+
this.reportedErrors.add(err);
|
|
1117
1292
|
this.pendingError = null;
|
|
1118
1293
|
this.pendingResumeMessageId = null;
|
|
1119
1294
|
this.pendingA2uiResume = false;
|
|
@@ -1344,10 +1519,33 @@ export class AgUiThreadRuntimeCore {
|
|
|
1344
1519
|
}
|
|
1345
1520
|
}
|
|
1346
1521
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1522
|
+
for (const ids of [this.snapshotHistoryIds, this.persistedHistoryIds]) {
|
|
1523
|
+
if (ids.has(oldId)) {
|
|
1524
|
+
ids.delete(oldId);
|
|
1525
|
+
if (!collidesWithExisting) {
|
|
1526
|
+
ids.add(newId);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// An in-flight write completes under the id it was started with, so the
|
|
1532
|
+
// rename moves the chain entry and transfers the completion mark; without
|
|
1533
|
+
// this the resolve records the dead id and the live one appends again.
|
|
1534
|
+
const pendingWrite = this.historyWrites.get(oldId);
|
|
1535
|
+
if (pendingWrite) {
|
|
1536
|
+
this.historyWrites.delete(oldId);
|
|
1349
1537
|
if (!collidesWithExisting) {
|
|
1350
|
-
this.
|
|
1538
|
+
this.historyWrites.set(newId, pendingWrite);
|
|
1539
|
+
const settle = () => {
|
|
1540
|
+
if (this.historyWrites.get(newId) === pendingWrite) {
|
|
1541
|
+
this.historyWrites.delete(newId);
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
void pendingWrite.then(() => {
|
|
1545
|
+
this.persistedHistoryIds.delete(oldId);
|
|
1546
|
+
this.persistedHistoryIds.add(newId);
|
|
1547
|
+
settle();
|
|
1548
|
+
}, settle);
|
|
1351
1549
|
}
|
|
1352
1550
|
}
|
|
1353
1551
|
|
|
@@ -1456,7 +1654,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1456
1654
|
...(current.isOptimistic ? { isOptimistic: true } : {}),
|
|
1457
1655
|
...(incoming.timing ? { timing: incoming.timing } : {}),
|
|
1458
1656
|
custom: incoming.custom
|
|
1459
|
-
?
|
|
1657
|
+
? mergeAgUiNamespace(current.custom, incoming.custom)
|
|
1460
1658
|
: current.custom,
|
|
1461
1659
|
};
|
|
1462
1660
|
}
|
|
@@ -1688,7 +1886,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
1688
1886
|
);
|
|
1689
1887
|
}
|
|
1690
1888
|
}
|
|
1691
|
-
const snapshotHeadId = converted.at(-1)?.id ?? null;
|
|
1692
1889
|
const snapshotContainsActiveAssistant = converted.some(
|
|
1693
1890
|
(message) => message.id === activeAssistant?.id,
|
|
1694
1891
|
);
|
|
@@ -1701,9 +1898,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1701
1898
|
converted.push(activeAssistant);
|
|
1702
1899
|
}
|
|
1703
1900
|
this.applyExternalMessages(converted);
|
|
1704
|
-
if (
|
|
1705
|
-
this.
|
|
1706
|
-
|
|
1901
|
+
if (activeAssistant !== undefined) {
|
|
1902
|
+
const activeItem = this.tryGetMessage(activeAssistant.id);
|
|
1903
|
+
if (activeItem) {
|
|
1904
|
+
if (preservesActiveAssistant) {
|
|
1905
|
+
this.snapshotHistoryIds.delete(activeAssistant.id);
|
|
1906
|
+
}
|
|
1907
|
+
this.markPendingAssistantHistory(
|
|
1908
|
+
activeAssistant.id,
|
|
1909
|
+
activeItem.parentId,
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1707
1912
|
}
|
|
1708
1913
|
} catch (error) {
|
|
1709
1914
|
this.logger.error?.("[agui] failed to import messages snapshot", error);
|
|
@@ -1728,7 +1933,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1728
1933
|
}
|
|
1729
1934
|
|
|
1730
1935
|
private recordHistoryEntry(parentId: string | null, message: ThreadMessage) {
|
|
1731
|
-
this.appendHistoryItem(parentId, message)
|
|
1936
|
+
void this.appendHistoryItem(parentId, message)?.catch((error) => {
|
|
1937
|
+
this.logger.error?.("[agui] failed to append history entry", error);
|
|
1938
|
+
});
|
|
1732
1939
|
}
|
|
1733
1940
|
|
|
1734
1941
|
private markPendingAssistantHistory(
|
|
@@ -1740,22 +1947,108 @@ export class AgUiThreadRuntimeCore {
|
|
|
1740
1947
|
}
|
|
1741
1948
|
|
|
1742
1949
|
private persistAssistantHistory(messageId: string) {
|
|
1743
|
-
|
|
1950
|
+
const history = this.history;
|
|
1951
|
+
if (!history) return;
|
|
1744
1952
|
const parentId = this.assistantHistoryParents.get(messageId);
|
|
1745
1953
|
if (parentId === undefined) return;
|
|
1746
1954
|
const message = this.tryGetMessage(messageId)?.message;
|
|
1747
1955
|
if (!message || message.role !== "assistant") return;
|
|
1748
1956
|
if (!this.isPersistableStatus(message.status)) return;
|
|
1957
|
+
const wasPersisted = this.persistedHistoryIds.has(messageId);
|
|
1958
|
+
const update = history.update;
|
|
1959
|
+
const shouldUpdate =
|
|
1960
|
+
update !== undefined &&
|
|
1961
|
+
(wasPersisted || this.snapshotHistoryIds.has(messageId));
|
|
1962
|
+
|
|
1963
|
+
if (shouldUpdate) {
|
|
1964
|
+
const write = this.chainHistoryWrite(messageId, () =>
|
|
1965
|
+
update.call(history, { parentId, message }),
|
|
1966
|
+
);
|
|
1967
|
+
this.assistantHistoryParents.delete(messageId);
|
|
1968
|
+
void write.then(
|
|
1969
|
+
() => {
|
|
1970
|
+
this.persistedHistoryIds.add(messageId);
|
|
1971
|
+
},
|
|
1972
|
+
(error) => {
|
|
1973
|
+
const pending = this.historyWrites.get(messageId);
|
|
1974
|
+
if (pending === undefined || pending === write) {
|
|
1975
|
+
this.assistantHistoryParents.set(messageId, parentId);
|
|
1976
|
+
}
|
|
1977
|
+
this.logger.error?.("[agui] failed to update history entry", error);
|
|
1978
|
+
},
|
|
1979
|
+
);
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
if (wasPersisted) {
|
|
1984
|
+
this.assistantHistoryParents.delete(messageId);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const write = this.appendHistoryItem(parentId, message);
|
|
1989
|
+
if (!write) return;
|
|
1749
1990
|
this.assistantHistoryParents.delete(messageId);
|
|
1750
|
-
|
|
1991
|
+
void write.then(
|
|
1992
|
+
() => {},
|
|
1993
|
+
(error) => {
|
|
1994
|
+
const pending = this.historyWrites.get(messageId);
|
|
1995
|
+
if (pending === undefined || pending === write) {
|
|
1996
|
+
this.assistantHistoryParents.set(messageId, parentId);
|
|
1997
|
+
}
|
|
1998
|
+
this.logger.error?.("[agui] failed to append history entry", error);
|
|
1999
|
+
},
|
|
2000
|
+
);
|
|
1751
2001
|
}
|
|
1752
2002
|
|
|
1753
|
-
private appendHistoryItem(
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2003
|
+
private appendHistoryItem(
|
|
2004
|
+
parentId: string | null,
|
|
2005
|
+
message: ThreadMessage,
|
|
2006
|
+
): Promise<void> | undefined {
|
|
2007
|
+
if (!this.history || this.persistedHistoryIds.has(message.id)) return;
|
|
2008
|
+
const pending = this.historyWrites.get(message.id);
|
|
2009
|
+
if (pending) return pending;
|
|
2010
|
+
|
|
2011
|
+
const append = this.history.append.bind(this.history);
|
|
2012
|
+
const write = this.chainHistoryWrite(message.id, () =>
|
|
2013
|
+
append({ parentId, message }),
|
|
2014
|
+
);
|
|
2015
|
+
void write.then(
|
|
2016
|
+
() => {
|
|
2017
|
+
this.persistedHistoryIds.add(message.id);
|
|
2018
|
+
},
|
|
2019
|
+
() => {},
|
|
2020
|
+
);
|
|
2021
|
+
return write;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
private chainHistoryWrite(
|
|
2025
|
+
id: string,
|
|
2026
|
+
write: () => Promise<void>,
|
|
2027
|
+
): Promise<void> {
|
|
2028
|
+
const pending = this.historyWrites.get(id);
|
|
2029
|
+
let next: Promise<void>;
|
|
2030
|
+
if (pending) {
|
|
2031
|
+
next = pending.then(write, write);
|
|
2032
|
+
} else {
|
|
2033
|
+
try {
|
|
2034
|
+
next = Promise.resolve(write());
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
next = Promise.reject(error);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
this.historyWrites.set(id, next);
|
|
2040
|
+
void next.then(
|
|
2041
|
+
() => {
|
|
2042
|
+
if (this.historyWrites.get(id) === next) {
|
|
2043
|
+
this.historyWrites.delete(id);
|
|
2044
|
+
}
|
|
2045
|
+
},
|
|
2046
|
+
() => {
|
|
2047
|
+
if (this.historyWrites.get(id) === next) {
|
|
2048
|
+
this.historyWrites.delete(id);
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
);
|
|
2052
|
+
return next;
|
|
1760
2053
|
}
|
|
1761
2054
|
}
|