@agent-native/agentkit 0.2.4 → 0.2.5-nightly-20260917210345

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.
@@ -1,5 +1,5 @@
1
1
  import { AgentProtocolValidationError, AgentKitProtocolError, createRequestAbortedError, createAgentKitProtocolVersionOffer, parseAgentEvent, projectAgentCapabilities, resumeEntryFromApproval, } from "../protocol/index.js";
2
- import { classifyAgentEvent, createAgentThreadState, reduceAgentEvent, } from "./state.js";
2
+ import { classifyAgentEvent, createAgentThreadState, reduceAgentEvent, settleRunProjection, } from "./state.js";
3
3
  /**
4
4
  * Creates the deterministic AgentKit controller used by every UI binding.
5
5
  * Prefer this factory at application boundaries; the class remains public for
@@ -8,6 +8,72 @@ import { classifyAgentEvent, createAgentThreadState, reduceAgentEvent, } from ".
8
8
  export function createAgentKitClient(options) {
9
9
  return new AgentKitClient(options);
10
10
  }
11
+ function isTerminalRunEvent(event) {
12
+ return (event.type === "run.completed" ||
13
+ event.type === "run.failed" ||
14
+ event.type === "run.cancelled" ||
15
+ (event.type === "run.status" &&
16
+ (event.status === "completed" ||
17
+ event.status === "failed" ||
18
+ event.status === "cancelled")));
19
+ }
20
+ function sameQueuedMessageIds(first, second) {
21
+ return (first.length === second.length &&
22
+ first.every((message, index) => message.id === second[index]?.id));
23
+ }
24
+ function mergeQueuedMessageOverride(override, durable) {
25
+ const expectedIds = new Set(override.messages.map((message) => message.id));
26
+ const durableVisible = durable.filter((message) => !override.removedIds.has(message.id));
27
+ const merged = [...override.messages];
28
+ for (let index = 0; index < durableVisible.length; index += 1) {
29
+ const message = durableVisible[index];
30
+ if (expectedIds.has(message.id))
31
+ continue;
32
+ let nextExpectedIndex = -1;
33
+ for (let nextIndex = index + 1; nextIndex < durableVisible.length; nextIndex += 1) {
34
+ const candidateIndex = merged.findIndex((current) => current.id === durableVisible[nextIndex]?.id);
35
+ if (candidateIndex >= 0) {
36
+ nextExpectedIndex = candidateIndex;
37
+ break;
38
+ }
39
+ }
40
+ if (nextExpectedIndex >= 0) {
41
+ merged.splice(nextExpectedIndex, 0, message);
42
+ continue;
43
+ }
44
+ let previousExpectedIndex = -1;
45
+ for (let previousIndex = index - 1; previousIndex >= 0; previousIndex -= 1) {
46
+ const candidateIndex = merged.findIndex((current) => current.id === durableVisible[previousIndex]?.id);
47
+ if (candidateIndex >= 0) {
48
+ previousExpectedIndex = candidateIndex;
49
+ break;
50
+ }
51
+ }
52
+ if (previousExpectedIndex >= 0) {
53
+ merged.splice(previousExpectedIndex + 1, 0, message);
54
+ }
55
+ else {
56
+ merged.push(message);
57
+ }
58
+ }
59
+ return merged;
60
+ }
61
+ function mergeRuntimeRecordMap(loaded, baseline, current) {
62
+ if (current === baseline)
63
+ return loaded;
64
+ const merged = { ...loaded };
65
+ for (const id of Object.keys(baseline)) {
66
+ if (!Object.prototype.hasOwnProperty.call(current, id))
67
+ delete merged[id];
68
+ }
69
+ for (const [id, record] of Object.entries(current)) {
70
+ if (!Object.prototype.hasOwnProperty.call(baseline, id) ||
71
+ baseline[id] !== record) {
72
+ merged[id] = record;
73
+ }
74
+ }
75
+ return merged;
76
+ }
11
77
  function defaultCreateId(prefix) {
12
78
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
13
79
  return `${prefix}-${crypto.randomUUID()}`;
@@ -152,6 +218,8 @@ export class AgentKitClient {
152
218
  consumerAbortControllers = new Map();
153
219
  threadLoads = new Map();
154
220
  threadLeaseCounts = new Map();
221
+ queuedMessageOverrides = new Map();
222
+ queueMutationChains = new Map();
155
223
  queuePromotions = new Set();
156
224
  requestAbortController = new AbortController();
157
225
  capabilitiesLoad;
@@ -273,7 +341,10 @@ export class AgentKitClient {
273
341
  if (existing) {
274
342
  return this.invokeRequest(requestContext, () => existing);
275
343
  }
276
- const load = this.loadThreadProjection(threadId, requestContext);
344
+ // Durable projections can lag an accepted stream. Preserve the local
345
+ // cursor and event boundaries so refresh cannot reorder the transcript or
346
+ // turn the next streamed event into an artificial sequence gap.
347
+ const load = this.loadThreadProjection(threadId, requestContext, true);
277
348
  this.threadLoads.set(threadId, load);
278
349
  try {
279
350
  return await load;
@@ -284,6 +355,17 @@ export class AgentKitClient {
284
355
  }
285
356
  }
286
357
  }
358
+ enqueueQueueMutation(threadId, mutation) {
359
+ const previous = this.queueMutationChains.get(threadId) ?? Promise.resolve();
360
+ const next = previous.then(mutation);
361
+ const settled = next.then(() => undefined, () => undefined);
362
+ this.queueMutationChains.set(threadId, settled);
363
+ return next.finally(() => {
364
+ if (this.queueMutationChains.get(threadId) === settled) {
365
+ this.queueMutationChains.delete(threadId);
366
+ }
367
+ });
368
+ }
287
369
  async loadThreadProjection(threadId, requestContext, preserveRuntimeProjection = false) {
288
370
  const baseline = this.snapshot.threads[threadId] ?? createAgentThreadState(threadId);
289
371
  if (!this.snapshot.threads[threadId])
@@ -301,6 +383,20 @@ export class AgentKitClient {
301
383
  : undefined;
302
384
  this.assertActive();
303
385
  const runSnapshots = new Map((snapshot?.runs ?? []).map((run) => [run.id, run]));
386
+ const durableQueuedMessages = snapshot?.queuedMessages;
387
+ const queuedOverride = this.queuedMessageOverrides.get(threadId);
388
+ const queuedOverrideMessages = queuedOverride
389
+ ? durableQueuedMessages
390
+ ? mergeQueuedMessageOverride(queuedOverride, durableQueuedMessages)
391
+ : queuedOverride.messages
392
+ : undefined;
393
+ if (queuedOverride &&
394
+ durableQueuedMessages &&
395
+ queuedOverrideMessages &&
396
+ sameQueuedMessageIds(queuedOverrideMessages, durableQueuedMessages) &&
397
+ queuedOverride.removedIds.size === 0) {
398
+ this.queuedMessageOverrides.delete(threadId);
399
+ }
304
400
  const missingRunIds = (snapshot?.activeRunIds ?? []).filter((runId) => !runSnapshots.has(runId));
305
401
  const getRun = this.transport.getRun;
306
402
  const activeRuns = missingRunIds.length
@@ -351,26 +447,21 @@ export class AgentKitClient {
351
447
  queuedMessages,
352
448
  };
353
449
  }
354
- if (preserveRuntimeProjection) {
355
- const reconciliation = this.reconcileMessages(baseline.messages, thread.messages);
356
- const runtimeProjection = this.remapThreadMessageReferences(baseline, reconciliation.idRemap);
357
- const authoritative = {
358
- thread: thread.thread,
359
- messages: reconciliation.messages,
450
+ if (preserveRuntimeProjection && snapshot) {
451
+ const current = this.getThread(threadId);
452
+ thread = this.mergeLoadedThread(baseline, current, thread);
453
+ if (queuedOverrideMessages) {
360
454
  // Queue mutations are already optimistic and independently durable.
361
455
  // A completed-run snapshot can lag the accepted steer/remove write,
362
456
  // so it must not resurrect work the client has already promoted.
363
- queuedMessages: baseline.queuedMessages,
364
- };
365
- thread = {
366
- ...this.mergeLoadedThread(createAgentThreadState(threadId), runtimeProjection, thread),
367
- ...authoritative,
368
- };
457
+ thread = { ...thread, queuedMessages: queuedOverrideMessages };
458
+ }
369
459
  }
370
460
  const current = this.getThread(threadId);
371
461
  if (current !== baseline) {
372
462
  thread = this.mergeLoadedThread(baseline, current, thread);
373
463
  }
464
+ thread = this.settleTerminalThread(thread, snapshot);
374
465
  this.setThread(threadId, thread);
375
466
  this.setConnection("connected");
376
467
  for (const runId of thread.activeRunIds) {
@@ -488,9 +579,17 @@ export class AgentKitClient {
488
579
  }
489
580
  resubscribeRun(threadId, runId) {
490
581
  this.assertActive();
491
- const status = this.getThread(threadId).runs[runId]?.status;
492
- if (status && this.isTerminalStatus(status))
582
+ const thread = this.getThread(threadId);
583
+ const status = thread.runs[runId]?.status;
584
+ // A failed stream can be explicitly reattached to recover from a
585
+ // transient disconnect. Completed and cancelled runs have no live work to
586
+ // resume.
587
+ if (status === "completed" ||
588
+ status === "cancelled" ||
589
+ (status === "failed" &&
590
+ thread.events.some((event) => event.runId === runId && isTerminalRunEvent(event)))) {
493
591
  return Promise.resolve();
592
+ }
494
593
  const key = this.runKey(threadId, runId);
495
594
  const existing = this.consumers.get(key);
496
595
  if (existing)
@@ -680,19 +779,29 @@ export class AgentKitClient {
680
779
  if (!queueMessage) {
681
780
  throw new AgentKitCapabilityError("messageQueue");
682
781
  }
683
- const result = await this.invokeRequest(requestContext, (context) => queueMessage({
684
- threadId: input.threadId,
685
- text: input.text,
686
- attachments: input.attachments,
687
- metadata: input.metadata,
688
- }, context));
689
- this.assertActive();
690
- const thread = this.getThread(input.threadId);
691
- this.setThread(input.threadId, {
692
- ...thread,
693
- queuedMessages: [...thread.queuedMessages, result.message],
782
+ return this.enqueueQueueMutation(input.threadId, async () => {
783
+ this.assertActive();
784
+ const result = await this.invokeRequest(requestContext, (context) => queueMessage({
785
+ threadId: input.threadId,
786
+ text: input.text,
787
+ attachments: input.attachments,
788
+ metadata: input.metadata,
789
+ }, context));
790
+ this.assertActive();
791
+ const thread = this.getThread(input.threadId);
792
+ this.setThread(input.threadId, {
793
+ ...thread,
794
+ queuedMessages: [...thread.queuedMessages, result.message],
795
+ });
796
+ const queueOverride = this.queuedMessageOverrides.get(input.threadId);
797
+ const removedIds = new Set(queueOverride?.removedIds);
798
+ removedIds.delete(result.message.id);
799
+ this.queuedMessageOverrides.set(input.threadId, {
800
+ messages: [...thread.queuedMessages, result.message],
801
+ removedIds,
802
+ });
803
+ return result.message;
694
804
  });
695
- return result.message;
696
805
  }
697
806
  async removeQueuedMessage(threadId, messageId, context) {
698
807
  this.assertActive();
@@ -702,30 +811,52 @@ export class AgentKitClient {
702
811
  if (!removeQueuedMessage) {
703
812
  throw new AgentKitCapabilityError("messageQueue");
704
813
  }
705
- const previous = this.getThread(threadId);
706
- const removedIndex = previous.queuedMessages.findIndex((message) => message.id === messageId);
707
- const removed = previous.queuedMessages[removedIndex];
708
- this.setThread(threadId, {
709
- ...previous,
710
- queuedMessages: previous.queuedMessages.filter((message) => message.id !== messageId),
711
- });
712
- try {
713
- await this.invokeRequest(requestContext, (context) => removeQueuedMessage({ threadId, messageId }, context));
814
+ return this.enqueueQueueMutation(threadId, async () => {
714
815
  this.assertActive();
715
- }
716
- catch (error) {
717
- if (this.disposed)
718
- throw error;
816
+ const previous = this.getThread(threadId);
817
+ const removedIndex = previous.queuedMessages.findIndex((message) => message.id === messageId);
818
+ const removed = previous.queuedMessages[removedIndex];
819
+ const previousOverride = this.queuedMessageOverrides.get(threadId);
719
820
  if (removed) {
720
- const current = this.getThread(threadId);
721
- if (!current.queuedMessages.some((message) => message.id === messageId)) {
722
- const queuedMessages = [...current.queuedMessages];
723
- queuedMessages.splice(Math.min(removedIndex, queuedMessages.length), 0, removed);
724
- this.setThread(threadId, { ...current, queuedMessages });
821
+ const queuedMessages = previous.queuedMessages.filter((message) => message.id !== messageId);
822
+ this.setThread(threadId, { ...previous, queuedMessages });
823
+ const removedIds = new Set(previousOverride?.removedIds);
824
+ removedIds.add(messageId);
825
+ this.queuedMessageOverrides.set(threadId, {
826
+ messages: queuedMessages,
827
+ removedIds,
828
+ });
829
+ }
830
+ try {
831
+ await this.invokeRequest(requestContext, (context) => removeQueuedMessage({ threadId, messageId }, context));
832
+ this.assertActive();
833
+ }
834
+ catch (error) {
835
+ if (this.disposed)
836
+ throw error;
837
+ if (removed) {
838
+ const current = this.getThread(threadId);
839
+ if (!current.queuedMessages.some((message) => message.id === messageId)) {
840
+ const queuedMessages = [...current.queuedMessages];
841
+ queuedMessages.splice(Math.min(removedIndex, queuedMessages.length), 0, removed);
842
+ this.setThread(threadId, { ...current, queuedMessages });
843
+ const currentOverride = this.queuedMessageOverrides.get(threadId);
844
+ const removedIds = new Set(currentOverride?.removedIds);
845
+ removedIds.delete(messageId);
846
+ if (currentOverride || removedIds.size > 0) {
847
+ this.queuedMessageOverrides.set(threadId, {
848
+ messages: queuedMessages,
849
+ removedIds,
850
+ });
851
+ }
852
+ else {
853
+ this.queuedMessageOverrides.delete(threadId);
854
+ }
855
+ }
725
856
  }
857
+ throw error;
726
858
  }
727
- throw error;
728
- }
859
+ });
729
860
  }
730
861
  async steerQueuedMessage(threadId, messageId, context) {
731
862
  this.assertActive();
@@ -735,68 +866,94 @@ export class AgentKitClient {
735
866
  if (!steerQueuedMessage) {
736
867
  throw new AgentKitCapabilityError("messageQueue");
737
868
  }
738
- const previous = this.getThread(threadId);
739
- const queued = previous.queuedMessages.find((message) => message.id === messageId);
740
- if (!queued)
741
- throw new Error(`Unknown queued message: ${messageId}`);
742
- const message = {
743
- id: queued.id,
744
- role: "user",
745
- createdAt: queued.createdAt,
746
- status: "complete",
747
- parts: [
748
- { type: "text", text: queued.text },
749
- ...(queued.attachments ?? []),
750
- ],
751
- metadata: queued.metadata,
752
- };
753
- const previousConnection = this.snapshot.connection;
754
- const previousError = this.snapshot.error;
755
- this.setThread(threadId, {
756
- ...previous,
757
- messages: [...previous.messages, message],
758
- queuedMessages: previous.queuedMessages.filter((candidate) => candidate.id !== messageId),
759
- suggestions: [],
760
- });
761
- this.setConnection("connecting");
762
- try {
763
- const result = await this.invokeRequest(requestContext, (context) => steerQueuedMessage({ threadId, messageId }, context));
869
+ return this.enqueueQueueMutation(threadId, async () => {
764
870
  this.assertActive();
765
- if (!result) {
766
- this.setConnection("connected");
767
- return;
768
- }
769
- if (result.capabilities) {
770
- this.patch({
771
- capabilities: result.capabilities,
772
- capabilitiesStatus: "ready",
773
- });
774
- }
775
- this.markRunStarted(threadId, result.runId);
776
- const completed = this.consume(threadId, result.runId);
777
- this.trackConsumer(threadId, result.runId, completed);
778
- return {
779
- runId: result.runId,
780
- completed,
781
- cancel: () => this.cancelRun(threadId, result.runId),
871
+ const previous = this.getThread(threadId);
872
+ const queued = previous.queuedMessages.find((message) => message.id === messageId);
873
+ if (!queued)
874
+ throw new Error(`Unknown queued message: ${messageId}`);
875
+ const previousOverride = this.queuedMessageOverrides.get(threadId);
876
+ const message = {
877
+ id: queued.id,
878
+ role: "user",
879
+ createdAt: queued.createdAt,
880
+ status: "complete",
881
+ parts: [
882
+ { type: "text", text: queued.text },
883
+ ...(queued.attachments ?? []),
884
+ ],
885
+ metadata: queued.metadata,
782
886
  };
783
- }
784
- catch (error) {
785
- if (this.disposed)
786
- throw error;
787
- const current = this.getThread(threadId);
788
- const queuedMessages = current.queuedMessages.some((candidate) => candidate.id === queued.id)
789
- ? current.queuedMessages
790
- : [queued, ...current.queuedMessages];
887
+ const previousConnection = this.snapshot.connection;
888
+ const previousError = this.snapshot.error;
791
889
  this.setThread(threadId, {
792
- ...current,
793
- messages: current.messages.filter((candidate) => candidate.id !== message.id),
794
- queuedMessages,
890
+ ...previous,
891
+ messages: [...previous.messages, message],
892
+ queuedMessages: previous.queuedMessages.filter((candidate) => candidate.id !== messageId),
893
+ suggestions: [],
795
894
  });
796
- this.patch({ connection: previousConnection, error: previousError });
797
- this.report(error, "queue_steer_failed");
798
- throw error;
799
- }
895
+ const removedIds = new Set(previousOverride?.removedIds);
896
+ removedIds.add(messageId);
897
+ this.queuedMessageOverrides.set(threadId, {
898
+ messages: previous.queuedMessages.filter((candidate) => candidate.id !== messageId),
899
+ removedIds,
900
+ });
901
+ this.setConnection("connecting");
902
+ try {
903
+ const result = await this.invokeRequest(requestContext, (context) => steerQueuedMessage({ threadId, messageId }, context));
904
+ this.assertActive();
905
+ if (!result) {
906
+ this.setConnection("connected");
907
+ return;
908
+ }
909
+ if (result.capabilities) {
910
+ this.patch({
911
+ capabilities: result.capabilities,
912
+ capabilitiesStatus: "ready",
913
+ });
914
+ }
915
+ this.markRunStarted(threadId, result.runId);
916
+ const completed = this.consume(threadId, result.runId);
917
+ this.trackConsumer(threadId, result.runId, completed);
918
+ return {
919
+ runId: result.runId,
920
+ completed,
921
+ cancel: () => this.cancelRun(threadId, result.runId),
922
+ };
923
+ }
924
+ catch (error) {
925
+ if (this.disposed)
926
+ throw error;
927
+ const current = this.getThread(threadId);
928
+ const queuedMessages = current.queuedMessages.some((candidate) => candidate.id === queued.id)
929
+ ? current.queuedMessages
930
+ : (() => {
931
+ const restored = [...current.queuedMessages];
932
+ restored.splice(Math.min(previous.queuedMessages.indexOf(queued), restored.length), 0, queued);
933
+ return restored;
934
+ })();
935
+ this.setThread(threadId, {
936
+ ...current,
937
+ messages: current.messages.filter((candidate) => candidate.id !== message.id),
938
+ queuedMessages,
939
+ });
940
+ const currentOverride = this.queuedMessageOverrides.get(threadId);
941
+ const restoredRemovedIds = new Set(currentOverride?.removedIds);
942
+ restoredRemovedIds.delete(messageId);
943
+ if (currentOverride || restoredRemovedIds.size > 0) {
944
+ this.queuedMessageOverrides.set(threadId, {
945
+ messages: queuedMessages,
946
+ removedIds: restoredRemovedIds,
947
+ });
948
+ }
949
+ else {
950
+ this.queuedMessageOverrides.delete(threadId);
951
+ }
952
+ this.patch({ connection: previousConnection, error: previousError });
953
+ this.report(error, "queue_steer_failed");
954
+ throw error;
955
+ }
956
+ });
800
957
  }
801
958
  async cancelRun(threadId, runId, context) {
802
959
  this.assertActive();
@@ -872,6 +1029,7 @@ export class AgentKitClient {
872
1029
  await this.invokeRequest(requestContext, (context) => deleteThread({ threadId }, context));
873
1030
  this.assertActive();
874
1031
  this.stopThreadConsumers(threadId, "deleted");
1032
+ this.queuedMessageOverrides.delete(threadId);
875
1033
  const threads = { ...this.snapshot.threads };
876
1034
  delete threads[threadId];
877
1035
  this.patch({ threads });
@@ -886,6 +1044,8 @@ export class AgentKitClient {
886
1044
  }
887
1045
  this.consumerAbortControllers.clear();
888
1046
  this.threadLeaseCounts.clear();
1047
+ this.queueMutationChains.clear();
1048
+ this.queuedMessageOverrides.clear();
889
1049
  this.patch({ connection: "offline" });
890
1050
  this.listeners.clear();
891
1051
  this.consumers.clear();
@@ -953,9 +1113,7 @@ export class AgentKitClient {
953
1113
  event.request.status === "failed")) {
954
1114
  interruptedForContinuation = false;
955
1115
  }
956
- if (event.type === "run.completed" ||
957
- event.type === "run.failed" ||
958
- event.type === "run.cancelled") {
1116
+ if (isTerminalRunEvent(event)) {
959
1117
  terminalEvent = event;
960
1118
  }
961
1119
  }
@@ -972,7 +1130,9 @@ export class AgentKitClient {
972
1130
  throw new Error(`AgentKit run ${runId} ended without a terminal event.`);
973
1131
  }
974
1132
  this.setConnection("connected");
975
- if (terminalEvent.type === "run.completed") {
1133
+ if (terminalEvent.type === "run.completed" ||
1134
+ (terminalEvent.type === "run.status" &&
1135
+ terminalEvent.status === "completed")) {
976
1136
  if (!this.threadLoads.has(threadId) &&
977
1137
  (this.transport.getThreadSnapshot || this.transport.getThread)) {
978
1138
  await this.loadThreadProjection(threadId, this.createRequestContext(), true);
@@ -1001,6 +1161,8 @@ export class AgentKitClient {
1001
1161
  if (abortController.signal.reason instanceof AgentKitConsumerStoppedError) {
1002
1162
  return;
1003
1163
  }
1164
+ const runError = toError(error, "run_stream_failed");
1165
+ this.markRunFailed(threadId, runId, runError);
1004
1166
  this.fail(error, "run_stream_failed");
1005
1167
  throw error;
1006
1168
  }
@@ -1082,6 +1244,38 @@ export class AgentKitClient {
1082
1244
  Number.isFinite(current) &&
1083
1245
  expiration <= current);
1084
1246
  }
1247
+ settleTerminalThread(thread, snapshot) {
1248
+ const runs = Object.values(thread.runs);
1249
+ const terminalRuns = runs.filter((run) => this.isTerminalStatus(run.status));
1250
+ const settled = terminalRuns.reduce((currentThread, run) => settleRunProjection(currentThread, run.id, run.status, run.completedAt ?? this.now(), run.activeMessageId), thread);
1251
+ if (thread.activeRunIds.length > 0 ||
1252
+ runs.some((run) => !this.isTerminalStatus(run.status))) {
1253
+ return settled;
1254
+ }
1255
+ const snapshotHasNoActiveRuns = snapshot?.activeRunIds?.length === 0 ||
1256
+ (snapshot?.activeRunIds === undefined &&
1257
+ snapshot?.runs !== undefined &&
1258
+ snapshot.runs.length > 0 &&
1259
+ snapshot.runs.every((run) => this.isTerminalStatus(run.status)));
1260
+ if (!snapshotHasNoActiveRuns)
1261
+ return settled;
1262
+ // A snapshot can outlive the event association for its assistant message.
1263
+ // If any known run failed or was cancelled, settle an unassociated message
1264
+ // as an error rather than presenting a partial response as successful.
1265
+ const settledMessageStatus = terminalRuns.length > 0 &&
1266
+ terminalRuns.every((run) => run.status === "completed")
1267
+ ? "complete"
1268
+ : "error";
1269
+ // Lifecycle events can age out independently of the message projection.
1270
+ // Once the snapshot proves that no run remains active, any assistant
1271
+ // message still marked streaming is stale rather than in-flight work.
1272
+ return {
1273
+ ...settled,
1274
+ messages: settled.messages.map((message) => message.role === "assistant" && message.status === "streaming"
1275
+ ? { ...message, status: settledMessageStatus }
1276
+ : message),
1277
+ };
1278
+ }
1085
1279
  hydrateThread(snapshot, runs, activeRunIds) {
1086
1280
  let hydrated = createAgentThreadState(snapshot.id);
1087
1281
  for (const event of snapshot.events ?? []) {
@@ -1107,7 +1301,7 @@ export class AgentKitClient {
1107
1301
  ]));
1108
1302
  const mergedRuns = this.mergeRuns(hydrated.runs, runs);
1109
1303
  const currentActiveRunIds = activeRunIds.filter((runId) => !this.isTerminalStatus(mergedRuns[runId]?.status ?? "running"));
1110
- return {
1304
+ const projected = {
1111
1305
  ...hydrated,
1112
1306
  thread: snapshot,
1113
1307
  // The snapshot projection is authoritative. The event log rebuilds all
@@ -1157,6 +1351,7 @@ export class AgentKitClient {
1157
1351
  artifacts: snapshot.artifacts ?? hydrated.artifacts,
1158
1352
  suggestions: snapshot.suggestions ?? hydrated.suggestions,
1159
1353
  };
1354
+ return this.settleTerminalThread(projected, snapshot);
1160
1355
  }
1161
1356
  async requireCapability(capability, context) {
1162
1357
  const capabilities = await this.ensureCapabilities(context);
@@ -1180,81 +1375,86 @@ export class AgentKitClient {
1180
1375
  return new AgentKitCapabilityError(descriptor.id, unavailable ? "unavailable" : "unsupported", descriptor.error?.message ?? descriptor.description, descriptor.error?.retryable ?? unavailable, descriptor.error?.correlationId, descriptor.error?.details);
1181
1376
  }
1182
1377
  mergeLoadedThread(baseline, current, loaded) {
1183
- const runs = current.runs === baseline.runs
1184
- ? loaded.runs
1185
- : this.mergeRuns(loaded.runs, current.runs);
1186
- const activeRunIds = Array.from(new Set([...loaded.activeRunIds, ...current.activeRunIds])).filter((runId) => !this.isTerminalStatus(runs[runId]?.status ?? "running"));
1378
+ const messagesChanged = current.messages !== baseline.messages;
1379
+ const reconciliation = loaded.messages.length > 0
1380
+ ? this.reconcileMessages(current.messages, loaded.messages)
1381
+ : undefined;
1382
+ const hasMessageIdRemap = (reconciliation?.idRemap.size ?? 0) > 0;
1383
+ // Keep the live projection as the winner only for identities accepted by
1384
+ // a concurrent stream. A content match still needs the durable identity
1385
+ // when the stream finished before this refresh established its baseline.
1386
+ const live = reconciliation
1387
+ ? this.remapThreadMessageReferences(current, reconciliation.idRemap)
1388
+ : current;
1389
+ const loadedSnapshot = loaded.thread;
1390
+ const snapshotIncludes = (key) => loadedSnapshot !== undefined &&
1391
+ Object.prototype.hasOwnProperty.call(loadedSnapshot, key);
1392
+ const snapshotIncludesRuntimeProjection = (key) => snapshotIncludes(key) || snapshotIncludes("events");
1393
+ const mergeRecordProjection = (loadedProjection, baselineProjection, currentProjection, snapshotKey) => currentProjection === baselineProjection &&
1394
+ !snapshotIncludesRuntimeProjection(snapshotKey)
1395
+ ? currentProjection
1396
+ : mergeRuntimeRecordMap(loadedProjection, baselineProjection, currentProjection);
1397
+ const mergeListProjection = (loadedProjection, baselineProjection, currentProjection, snapshotKey) => {
1398
+ if (currentProjection === baselineProjection &&
1399
+ !snapshotIncludesRuntimeProjection(snapshotKey)) {
1400
+ return currentProjection;
1401
+ }
1402
+ if (currentProjection === baselineProjection)
1403
+ return loadedProjection;
1404
+ const baselineById = new Map(baselineProjection.map((item) => [item.id, item]));
1405
+ const currentById = new Map(currentProjection.map((item) => [item.id, item]));
1406
+ const removedIds = new Set([...baselineById.keys()].filter((id) => !currentById.has(id)));
1407
+ const changedItems = currentProjection.filter((item) => {
1408
+ const baselineItem = baselineById.get(item.id);
1409
+ return baselineItem === undefined || baselineItem !== item;
1410
+ });
1411
+ return this.mergeItemsById(loadedProjection.filter((item) => !removedIds.has(item.id)), changedItems);
1412
+ };
1413
+ const runs = this.mergeRuns(loaded.runs, live.runs);
1414
+ const activeRunIds = Array.from(new Set([...loaded.activeRunIds, ...live.activeRunIds])).filter((runId) => !this.isTerminalStatus(runs[runId]?.status ?? "running"));
1187
1415
  return {
1188
1416
  ...loaded,
1189
1417
  thread: current.thread === baseline.thread ? loaded.thread : current.thread,
1190
- messages: current.messages === baseline.messages
1191
- ? loaded.messages
1192
- : this.mergeItemsById(loaded.messages, current.messages),
1193
- queuedMessages: current.queuedMessages === baseline.queuedMessages
1194
- ? loaded.queuedMessages
1195
- : this.mergeQueuedMessages(baseline.queuedMessages, loaded.queuedMessages, current.queuedMessages),
1418
+ messages: messagesChanged || hasMessageIdRemap
1419
+ ? reconciliation.messages
1420
+ : loaded.messages.length > 0 || current.messages.length === 0
1421
+ ? loaded.messages
1422
+ : current.messages,
1423
+ queuedMessages: current.queuedMessages === baseline.queuedMessages &&
1424
+ !snapshotIncludes("queuedMessages")
1425
+ ? current.queuedMessages
1426
+ : this.mergeQueuedMessages(baseline.queuedMessages, loaded.queuedMessages, live.queuedMessages),
1196
1427
  runs,
1197
1428
  activeRunIds,
1198
- events: current.events === baseline.events
1199
- ? loaded.events
1200
- : this.mergeEvents(loaded.events, current.events),
1201
- agents: current.agents === baseline.agents
1202
- ? loaded.agents
1203
- : { ...loaded.agents, ...current.agents },
1204
- agentInteractions: current.agentInteractions === baseline.agentInteractions
1205
- ? loaded.agentInteractions
1206
- : this.mergeItemsById(loaded.agentInteractions, current.agentInteractions),
1207
- activities: current.activities === baseline.activities
1208
- ? loaded.activities
1209
- : { ...loaded.activities, ...current.activities },
1210
- tasks: current.tasks === baseline.tasks
1211
- ? loaded.tasks
1212
- : { ...loaded.tasks, ...current.tasks },
1213
- tools: current.tools === baseline.tools
1214
- ? loaded.tools
1215
- : { ...loaded.tools, ...current.tools },
1216
- approvals: current.approvals === baseline.approvals
1217
- ? loaded.approvals
1218
- : current.approvals,
1219
- approvalRunIds: current.approvalRunIds === baseline.approvalRunIds
1220
- ? loaded.approvalRunIds
1221
- : current.approvalRunIds,
1222
- connectionRequests: current.connectionRequests === baseline.connectionRequests
1223
- ? loaded.connectionRequests
1224
- : { ...loaded.connectionRequests, ...current.connectionRequests },
1225
- connectionRequestRunIds: current.connectionRequestRunIds === baseline.connectionRequestRunIds
1226
- ? loaded.connectionRequestRunIds
1227
- : {
1228
- ...loaded.connectionRequestRunIds,
1229
- ...current.connectionRequestRunIds,
1230
- },
1231
- artifacts: current.artifacts === baseline.artifacts
1232
- ? loaded.artifacts
1233
- : this.mergeItemsById(loaded.artifacts, current.artifacts),
1234
- widgets: current.widgets === baseline.widgets
1235
- ? loaded.widgets
1236
- : { ...loaded.widgets, ...current.widgets },
1237
- widgetMessageIds: current.widgetMessageIds === baseline.widgetMessageIds
1238
- ? loaded.widgetMessageIds
1239
- : { ...loaded.widgetMessageIds, ...current.widgetMessageIds },
1240
- annotations: current.annotations === baseline.annotations
1241
- ? loaded.annotations
1242
- : { ...loaded.annotations, ...current.annotations },
1243
- annotationMessageIds: current.annotationMessageIds === baseline.annotationMessageIds
1244
- ? loaded.annotationMessageIds
1245
- : {
1246
- ...loaded.annotationMessageIds,
1247
- ...current.annotationMessageIds,
1248
- },
1249
- suggestions: current.suggestions === baseline.suggestions
1250
- ? loaded.suggestions
1429
+ events: reconciliation
1430
+ ? this.mergeEvents(loaded.events, live.events)
1431
+ : current.events === baseline.events && !snapshotIncludes("events")
1432
+ ? live.events
1433
+ : current.events === baseline.events
1434
+ ? loaded.events
1435
+ : this.mergeEvents(loaded.events, live.events),
1436
+ agents: mergeRecordProjection(loaded.agents, baseline.agents, live.agents, "agents"),
1437
+ agentInteractions: mergeListProjection(loaded.agentInteractions, baseline.agentInteractions, live.agentInteractions, "interactions"),
1438
+ activities: mergeRecordProjection(loaded.activities, baseline.activities, live.activities, "activities"),
1439
+ tasks: mergeRecordProjection(loaded.tasks, baseline.tasks, live.tasks, "tasks"),
1440
+ taskGroups: mergeRecordProjection(loaded.taskGroups, baseline.taskGroups, live.taskGroups, "taskGroups"),
1441
+ tools: mergeRecordProjection(loaded.tools, baseline.tools, live.tools, "toolCalls"),
1442
+ approvals: mergeRecordProjection(loaded.approvals, baseline.approvals, live.approvals, "approvals"),
1443
+ approvalRunIds: mergeRecordProjection(loaded.approvalRunIds, baseline.approvalRunIds, live.approvalRunIds, "approvals"),
1444
+ connectionRequests: mergeRecordProjection(loaded.connectionRequests, baseline.connectionRequests, live.connectionRequests, "connectionRequests"),
1445
+ connectionRequestRunIds: mergeRecordProjection(loaded.connectionRequestRunIds, baseline.connectionRequestRunIds, live.connectionRequestRunIds, "connectionRequests"),
1446
+ artifacts: mergeListProjection(loaded.artifacts, baseline.artifacts, live.artifacts, "artifacts"),
1447
+ widgets: mergeRecordProjection(loaded.widgets, baseline.widgets, live.widgets, "widgets"),
1448
+ widgetMessageIds: mergeRecordProjection(loaded.widgetMessageIds, baseline.widgetMessageIds, live.widgetMessageIds, "widgets"),
1449
+ annotations: mergeRecordProjection(loaded.annotations, baseline.annotations, live.annotations, "annotations"),
1450
+ annotationMessageIds: mergeRecordProjection(loaded.annotationMessageIds, baseline.annotationMessageIds, live.annotationMessageIds, "annotations"),
1451
+ suggestions: snapshotIncludesRuntimeProjection("suggestions")
1452
+ ? current.suggestions === baseline.suggestions
1453
+ ? loaded.suggestions
1454
+ : live.suggestions
1251
1455
  : current.suggestions,
1252
- actions: current.actions === baseline.actions
1253
- ? loaded.actions
1254
- : { ...loaded.actions, ...current.actions },
1255
- uploads: current.uploads === baseline.uploads
1256
- ? loaded.uploads
1257
- : { ...loaded.uploads, ...current.uploads },
1456
+ actions: mergeRecordProjection(loaded.actions, baseline.actions, live.actions, "events"),
1457
+ uploads: mergeRecordProjection(loaded.uploads, baseline.uploads, live.uploads, "events"),
1258
1458
  };
1259
1459
  }
1260
1460
  mergeRuns(first, second) {
@@ -1290,9 +1490,8 @@ export class AgentKitClient {
1290
1490
  return merged;
1291
1491
  }
1292
1492
  reconcileMessages(current, durable) {
1293
- if (durable.length === 0) {
1493
+ if (durable.length === 0)
1294
1494
  return { messages: current, idRemap: new Map() };
1295
- }
1296
1495
  const durableIds = new Set(durable.map((message) => message.id));
1297
1496
  const currentIds = new Set(current.map((message) => message.id));
1298
1497
  const unmatchedContent = new Map();
@@ -1307,22 +1506,68 @@ export class AgentKitClient {
1307
1506
  message,
1308
1507
  ]);
1309
1508
  }
1310
- const optimistic = [];
1311
1509
  const idRemap = new Map();
1312
- for (const message of current) {
1313
- if (durableIds.has(message.id))
1314
- continue;
1510
+ let hasMatchedMessage = false;
1511
+ const liveMessages = current.map((message) => {
1512
+ if (durableIds.has(message.id)) {
1513
+ hasMatchedMessage = true;
1514
+ return message;
1515
+ }
1315
1516
  const key = this.messageContentKey(message);
1316
1517
  const matches = unmatchedContent.get(key);
1317
1518
  const durableMatch = matches?.shift();
1318
1519
  if (durableMatch) {
1520
+ hasMatchedMessage = true;
1319
1521
  idRemap.set(message.id, durableMatch.id);
1522
+ return durableMatch;
1523
+ }
1524
+ return message;
1525
+ });
1526
+ if (!hasMatchedMessage) {
1527
+ return { messages: [...durable, ...current], idRemap };
1528
+ }
1529
+ const presentIds = new Set(liveMessages.map((message) => message.id));
1530
+ const messages = [...liveMessages];
1531
+ for (let durableIndex = 0; durableIndex < durable.length; durableIndex++) {
1532
+ const message = durable[durableIndex];
1533
+ if (presentIds.has(message.id))
1534
+ continue;
1535
+ let insertBefore = -1;
1536
+ for (let nextIndex = durableIndex + 1; nextIndex < durable.length; nextIndex++) {
1537
+ const nextId = durable[nextIndex]?.id;
1538
+ if (!nextId)
1539
+ continue;
1540
+ const candidateIndex = messages.findIndex((currentMessage) => currentMessage.id === nextId);
1541
+ if (candidateIndex >= 0) {
1542
+ insertBefore = candidateIndex;
1543
+ break;
1544
+ }
1545
+ }
1546
+ if (insertBefore >= 0) {
1547
+ messages.splice(insertBefore, 0, message);
1320
1548
  }
1321
1549
  else {
1322
- optimistic.push(message);
1550
+ let previousDurableIndex = -1;
1551
+ for (let previousIndex = durableIndex - 1; previousIndex >= 0; previousIndex -= 1) {
1552
+ const previousId = durable[previousIndex]?.id;
1553
+ if (!previousId)
1554
+ continue;
1555
+ const candidateIndex = messages.findIndex((currentMessage) => currentMessage.id === previousId);
1556
+ if (candidateIndex >= 0) {
1557
+ previousDurableIndex = candidateIndex;
1558
+ break;
1559
+ }
1560
+ }
1561
+ if (previousDurableIndex >= 0) {
1562
+ messages.splice(previousDurableIndex + 1, 0, message);
1563
+ }
1564
+ else {
1565
+ messages.unshift(message);
1566
+ }
1323
1567
  }
1568
+ presentIds.add(message.id);
1324
1569
  }
1325
- return { messages: [...durable, ...optimistic], idRemap };
1570
+ return { messages, idRemap };
1326
1571
  }
1327
1572
  remapThreadMessageReferences(thread, idRemap) {
1328
1573
  if (idRemap.size === 0)
@@ -1412,13 +1657,38 @@ export class AgentKitClient {
1412
1657
  }
1413
1658
  mergeEvents(first, second) {
1414
1659
  const merged = [...first];
1415
- const keys = new Set(first.map((event) => `${event.threadId}\u0000${event.runId}\u0000${event.sequence}`));
1660
+ const indexByKey = new Map(first.map((event, index) => [
1661
+ `${event.threadId}\u0000${event.runId}\u0000${event.sequence}`,
1662
+ index,
1663
+ ]));
1416
1664
  for (const event of second) {
1417
1665
  const key = `${event.threadId}\u0000${event.runId}\u0000${event.sequence}`;
1418
- if (!keys.has(key)) {
1419
- keys.add(key);
1666
+ const existingIndex = indexByKey.get(key);
1667
+ if (existingIndex === undefined) {
1668
+ indexByKey.set(key, merged.length);
1420
1669
  merged.push(event);
1421
1670
  }
1671
+ else {
1672
+ // The second projection is the live stream. It may contain richer
1673
+ // payloads than a snapshot captured before that event was persisted.
1674
+ merged[existingIndex] = event;
1675
+ }
1676
+ }
1677
+ // Sequences are scoped to a run, so there is no valid global numeric sort.
1678
+ // Keep cross-run interleaving stable while repairing each run's local
1679
+ // order in the slots it already occupies.
1680
+ const positionsByRun = new Map();
1681
+ merged.forEach((event, index) => {
1682
+ const positions = positionsByRun.get(event.runId) ?? [];
1683
+ positions.push(index);
1684
+ positionsByRun.set(event.runId, positions);
1685
+ });
1686
+ for (const positions of positionsByRun.values()) {
1687
+ const events = positions.map((index) => merged[index]);
1688
+ events.sort((left, right) => left.sequence - right.sequence);
1689
+ positions.forEach((index, position) => {
1690
+ merged[index] = events[position];
1691
+ });
1422
1692
  }
1423
1693
  return merged;
1424
1694
  }
@@ -1449,18 +1719,37 @@ export class AgentKitClient {
1449
1719
  const thread = this.getThread(threadId);
1450
1720
  const run = thread.runs[runId] ?? this.runState(runId);
1451
1721
  const activeRunIds = thread.activeRunIds.filter((id) => id !== runId);
1452
- this.setThread(threadId, {
1722
+ const completedAt = this.now();
1723
+ this.setThread(threadId, settleRunProjection({
1453
1724
  ...thread,
1454
1725
  runs: {
1455
1726
  ...thread.runs,
1456
1727
  [runId]: {
1457
1728
  ...run,
1458
1729
  status: "cancelled",
1459
- completedAt: this.now(),
1730
+ completedAt,
1460
1731
  },
1461
1732
  },
1462
1733
  activeRunIds,
1463
- });
1734
+ }, runId, "cancelled", completedAt, run.activeMessageId));
1735
+ }
1736
+ markRunFailed(threadId, runId, error) {
1737
+ const thread = this.getThread(threadId);
1738
+ const run = thread.runs[runId] ?? this.runState(runId);
1739
+ const completedAt = this.now();
1740
+ this.setThread(threadId, settleRunProjection({
1741
+ ...thread,
1742
+ runs: {
1743
+ ...thread.runs,
1744
+ [runId]: {
1745
+ ...run,
1746
+ status: "failed",
1747
+ completedAt,
1748
+ error,
1749
+ },
1750
+ },
1751
+ activeRunIds: thread.activeRunIds.filter((id) => id !== runId),
1752
+ }, runId, "failed", completedAt, run.activeMessageId));
1464
1753
  }
1465
1754
  markRunStarted(threadId, runId) {
1466
1755
  const thread = this.getThread(threadId);
@@ -1494,6 +1783,7 @@ export class AgentKitClient {
1494
1783
  completedAt: run?.completedAt,
1495
1784
  usage: run?.usage,
1496
1785
  error: run?.error,
1786
+ activeMessageId: run?.activeMessageId,
1497
1787
  };
1498
1788
  }
1499
1789
  runSnapshot(threadId, runId) {
@@ -1565,7 +1855,13 @@ export class AgentKitClient {
1565
1855
  receivedSequence: admission.receivedSequence,
1566
1856
  });
1567
1857
  }
1568
- this.setThread(event.threadId, reduceAgentEvent(thread, event));
1858
+ const next = reduceAgentEvent(thread, event);
1859
+ if (event.type === "queue.updated") {
1860
+ // A replayed queue snapshot is the ordered server view. It supersedes
1861
+ // the temporary protection used while a queue mutation catches up.
1862
+ this.queuedMessageOverrides.delete(event.threadId);
1863
+ }
1864
+ this.setThread(event.threadId, next);
1569
1865
  }
1570
1866
  setThread(threadId, thread) {
1571
1867
  this.patch({