@assistant-ui/core 0.3.4 → 0.3.6

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.
Files changed (147) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.js.map +1 -1
  3. package/dist/internal.js +1 -1
  4. package/dist/react/AssistantProvider.d.ts +3 -2
  5. package/dist/react/AssistantProvider.d.ts.map +1 -1
  6. package/dist/react/AssistantProvider.js +19 -26
  7. package/dist/react/AssistantProvider.js.map +1 -1
  8. package/dist/react/AssistantRuntimeProvider.d.ts +7 -2
  9. package/dist/react/AssistantRuntimeProvider.d.ts.map +1 -1
  10. package/dist/react/AssistantRuntimeProvider.js +9 -7
  11. package/dist/react/AssistantRuntimeProvider.js.map +1 -1
  12. package/dist/react/providers/AttachmentByIndexProvider.d.ts.map +1 -1
  13. package/dist/react/providers/AttachmentByIndexProvider.js +33 -39
  14. package/dist/react/providers/AttachmentByIndexProvider.js.map +1 -1
  15. package/dist/react/providers/ChainOfThoughtByIndicesProvider.d.ts.map +1 -1
  16. package/dist/react/providers/ChainOfThoughtByIndicesProvider.js +15 -20
  17. package/dist/react/providers/ChainOfThoughtByIndicesProvider.js.map +1 -1
  18. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +17 -20
  19. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
  20. package/dist/react/providers/MessageByIdProvider.js +28 -38
  21. package/dist/react/providers/MessageByIdProvider.js.map +1 -1
  22. package/dist/react/providers/MessageByIndexProvider.js +28 -38
  23. package/dist/react/providers/MessageByIndexProvider.js.map +1 -1
  24. package/dist/react/providers/PartByIndexProvider.js +17 -20
  25. package/dist/react/providers/PartByIndexProvider.js.map +1 -1
  26. package/dist/react/providers/QueueItemByIndexProvider.js +21 -21
  27. package/dist/react/providers/QueueItemByIndexProvider.js.map +1 -1
  28. package/dist/react/providers/ReadonlyThreadProvider.js +24 -32
  29. package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
  30. package/dist/react/providers/SuggestionByIndexProvider.js +17 -20
  31. package/dist/react/providers/SuggestionByIndexProvider.js.map +1 -1
  32. package/dist/react/providers/TextMessagePartProvider.d.ts.map +1 -1
  33. package/dist/react/providers/TextMessagePartProvider.js +16 -19
  34. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  35. package/dist/react/providers/ThreadListItemByIndexProvider.js +17 -20
  36. package/dist/react/providers/ThreadListItemByIndexProvider.js.map +1 -1
  37. package/dist/react/providers/ThreadListItemRuntimeProvider.js +15 -18
  38. package/dist/react/providers/ThreadListItemRuntimeProvider.js.map +1 -1
  39. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +7 -3
  40. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  41. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -2
  42. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  43. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +4 -3
  44. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  45. package/dist/react/runtimes/useLocalRuntime.d.ts +3 -1
  46. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  47. package/dist/react/runtimes/useLocalRuntime.js +4 -2
  48. package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
  49. package/dist/runtime/api/composer-runtime.d.ts +5 -1
  50. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  51. package/dist/runtime/api/composer-runtime.js +8 -1
  52. package/dist/runtime/api/composer-runtime.js.map +1 -1
  53. package/dist/runtime/api/message-runtime.js +1 -1
  54. package/dist/runtime/api/message-runtime.js.map +1 -1
  55. package/dist/runtime/api/thread-runtime.d.ts +4 -2
  56. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  57. package/dist/runtime/base/base-composer-runtime-core.d.ts +2 -1
  58. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  59. package/dist/runtime/base/base-composer-runtime-core.js +1 -1
  60. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  61. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +3 -1
  62. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  63. package/dist/runtime/base/default-thread-composer-runtime-core.js +14 -3
  64. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  65. package/dist/runtime/interfaces/composer-runtime-core.d.ts +2 -1
  66. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  67. package/dist/runtime/interfaces/thread-runtime-core.d.ts +3 -1
  68. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  69. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +19 -6
  70. package/dist/runtime/queue/external-thread-queue-adapter.d.ts.map +1 -1
  71. package/dist/runtime/queue/message-queue.d.ts +10 -1
  72. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  73. package/dist/runtime/queue/message-queue.js +138 -36
  74. package/dist/runtime/queue/message-queue.js.map +1 -1
  75. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  76. package/dist/runtime/utils/thread-message-like.js +2 -1
  77. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  78. package/dist/runtimes/external-store/external-store-adapter.d.ts +1 -1
  79. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +3 -1
  80. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  81. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +7 -6
  82. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  83. package/dist/runtimes/local/local-runtime-options.d.ts +13 -0
  84. package/dist/runtimes/local/local-runtime-options.d.ts.map +1 -1
  85. package/dist/runtimes/local/local-thread-runtime-core.d.ts +5 -1
  86. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  87. package/dist/runtimes/local/local-thread-runtime-core.js +30 -14
  88. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  89. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -1
  90. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -1
  91. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  92. package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -1
  93. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  94. package/dist/runtimes/remote-thread-list/types.d.ts +6 -3
  95. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  96. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  97. package/dist/store/runtime-clients/composer-runtime-client.js +11 -4
  98. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  99. package/dist/store/scopes/composer.d.ts +7 -3
  100. package/dist/store/scopes/composer.d.ts.map +1 -1
  101. package/dist/store/scopes/queue-item.d.ts +10 -0
  102. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  103. package/dist/store/scopes/queue-item.js.map +1 -1
  104. package/package.json +6 -6
  105. package/src/index.ts +5 -1
  106. package/src/react/AssistantProvider.tsx +20 -18
  107. package/src/react/AssistantRuntimeProvider.test.tsx +55 -1
  108. package/src/react/AssistantRuntimeProvider.tsx +12 -2
  109. package/src/react/providers/AttachmentByIndexProvider.tsx +15 -7
  110. package/src/react/providers/ChainOfThoughtByIndicesProvider.tsx +12 -4
  111. package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +8 -4
  112. package/src/react/providers/MessageByIdProvider.tsx +8 -4
  113. package/src/react/providers/MessageByIndexProvider.tsx +8 -4
  114. package/src/react/providers/PartByIndexProvider.tsx +8 -4
  115. package/src/react/providers/QueueItemByIndexProvider.tsx +9 -5
  116. package/src/react/providers/ReadonlyThreadProvider.tsx +9 -4
  117. package/src/react/providers/SuggestionByIndexProvider.tsx +8 -4
  118. package/src/react/providers/TextMessagePartProvider.tsx +13 -4
  119. package/src/react/providers/ThreadListItemByIndexProvider.tsx +8 -4
  120. package/src/react/providers/ThreadListItemRuntimeProvider.tsx +8 -4
  121. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +23 -5
  122. package/src/react/runtimes/useLocalRuntime.ts +4 -0
  123. package/src/runtime/api/composer-runtime.ts +11 -2
  124. package/src/runtime/api/message-runtime.test.ts +25 -0
  125. package/src/runtime/api/message-runtime.ts +1 -1
  126. package/src/runtime/base/base-composer-runtime-core.ts +5 -1
  127. package/src/runtime/base/default-thread-composer-runtime-core.ts +27 -3
  128. package/src/runtime/interfaces/composer-runtime-core.ts +2 -1
  129. package/src/runtime/interfaces/thread-runtime-core.ts +3 -1
  130. package/src/runtime/queue/external-thread-queue-adapter.ts +19 -3
  131. package/src/runtime/queue/message-queue.ts +200 -38
  132. package/src/runtime/utils/thread-message-like.ts +2 -4
  133. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +10 -8
  134. package/src/runtimes/local/local-runtime-options.ts +15 -0
  135. package/src/runtimes/local/local-thread-runtime-core.test.ts +43 -0
  136. package/src/runtimes/local/local-thread-runtime-core.ts +48 -13
  137. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -1
  138. package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -1
  139. package/src/runtimes/remote-thread-list/types.ts +9 -2
  140. package/src/store/runtime-clients/composer-runtime-client.ts +13 -5
  141. package/src/store/scopes/composer.ts +6 -4
  142. package/src/store/scopes/queue-item.ts +8 -1
  143. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-order.test.ts +31 -0
  144. package/src/tests/external-store-thread-runtime-core.test.ts +56 -13
  145. package/src/tests/framework-neutral-boundary.test.ts +221 -0
  146. package/src/tests/message-queue.test.ts +494 -34
  147. package/src/tests/thread-message-like.test.ts +66 -0
@@ -958,6 +958,49 @@ describe("LocalThreadRuntimeCore tool approval persistence", () => {
958
958
  });
959
959
  });
960
960
 
961
+ it("does not persist an existing falsy tool result again", async () => {
962
+ const { history, updated } = createHistory();
963
+ const twoHumanTools: ChatModelRunResult = {
964
+ content: [
965
+ {
966
+ ...toolCallPart("send_email"),
967
+ toolCallId: "call-1",
968
+ },
969
+ {
970
+ ...toolCallPart("send_email"),
971
+ toolCallId: "call-2",
972
+ },
973
+ ],
974
+ status: { type: "requires-action", reason: "tool-calls" },
975
+ };
976
+ const thread = createThread(
977
+ {
978
+ async run() {
979
+ return twoHumanTools;
980
+ },
981
+ },
982
+ { history },
983
+ );
984
+
985
+ await thread.append(userMessage("send two emails"));
986
+ await flush();
987
+
988
+ const options = {
989
+ messageId: thread.messages.at(-1)!.id,
990
+ toolName: "send_email",
991
+ toolCallId: "call-1",
992
+ result: false,
993
+ isError: false,
994
+ };
995
+ thread.addToolResult(options);
996
+ await flush();
997
+ expect(updated).toHaveLength(1);
998
+
999
+ thread.addToolResult(options);
1000
+ await flush();
1001
+ expect(updated).toHaveLength(1);
1002
+ });
1003
+
961
1004
  it("persists a multi-step run once instead of writing intermediate steps", async () => {
962
1005
  const { history, appended, updated } = createHistory();
963
1006
  const runs: ChatModelRunOptions[] = [];
@@ -28,6 +28,7 @@ import {
28
28
  createMessageQueue,
29
29
  type MessageQueueController,
30
30
  } from "../../runtime/queue/message-queue";
31
+ import type { QueuePlacement } from "../../runtime/queue/external-thread-queue-adapter";
31
32
  import {
32
33
  EMPTY_QUEUE_ITEMS,
33
34
  type QueueItemState,
@@ -68,6 +69,8 @@ export class LocalThreadRuntimeCore
68
69
 
69
70
  private _queue: MessageQueueController | null = null;
70
71
  private _queueRunInFlight = false;
72
+ private _activeRun: { cancelled: boolean } | null = null;
73
+ private _runGeneration = 0;
71
74
 
72
75
  private _historyWrites = new Map<string, Promise<void>>();
73
76
 
@@ -206,17 +209,23 @@ export class LocalThreadRuntimeCore
206
209
  // release the queue when the dispatch settles, even if it rejects
207
210
  // before reaching startRun's finally, so a failure can't deadlock it
208
211
  this._queueRunInFlight = true;
209
- void this._runAppend(message)
212
+ const generation = this._runGeneration;
213
+ // the tail may have moved since the message was enqueued
214
+ void this._runAppend({
215
+ ...message,
216
+ parentId: this.messages.at(-1)?.id ?? null,
217
+ })
210
218
  .finally(() => {
211
219
  this._queueRunInFlight = false;
212
- this._queue?.notifyIdle();
220
+ // a dispatch that failed before starting a run settles here;
221
+ // runs that did start release from _runLoop
222
+ if (this._runGeneration === generation) this._queue?.notifyIdle();
213
223
  })
214
224
  .catch(() => {});
215
225
  },
216
226
  });
217
227
  this._queue.subscribe(() => this._notifySubscribers());
218
228
  } else if (!canQueue && this._queue) {
219
- this._queue.adapter.clear("cancel-run");
220
229
  this._queue = null;
221
230
  }
222
231
  if (this.capabilities.queue !== canQueue) {
@@ -271,10 +280,17 @@ export class LocalThreadRuntimeCore
271
280
  const isTail = message.parentId === (this.messages.at(-1)?.id ?? null);
272
281
  const willRun = message.startRun ?? message.role === "user";
273
282
  if (this._queue && willRun && isTail) {
274
- this._queue.adapter.enqueue(message, { steer: message.steer ?? false });
283
+ if (message.steer ?? this._queueRunInFlight)
284
+ this._queue.adapter.steer(message);
285
+ else this._queue.adapter.enqueue(message);
275
286
  return;
276
287
  }
277
- if (this._queue && !isTail) this._queue.adapter.clear("edit");
288
+ if (
289
+ this._queue &&
290
+ !isTail &&
291
+ (this._options.unstable_queueClearOnRewind ?? true)
292
+ )
293
+ this._queue.clear();
278
294
  return this._runAppend(message);
279
295
  }
280
296
 
@@ -284,8 +300,12 @@ export class LocalThreadRuntimeCore
284
300
  return this._queue?.adapter.items ?? EMPTY_QUEUE_ITEMS;
285
301
  }
286
302
 
287
- public steerQueueItem(queueItemId: string): void {
288
- this._queue?.adapter.steer(queueItemId);
303
+ public getSteerQueueItems(): readonly QueueItemState[] {
304
+ return this._queue?.adapter.steerItems ?? EMPTY_QUEUE_ITEMS;
305
+ }
306
+
307
+ public moveQueueItem(queueItemId: string, placement: QueuePlacement): void {
308
+ this._queue?.adapter.move(queueItemId, placement);
289
309
  }
290
310
 
291
311
  public removeQueueItem(queueItemId: string): void {
@@ -397,6 +417,10 @@ export class LocalThreadRuntimeCore
397
417
  message.status?.type === "requires-action" &&
398
418
  this._options.adapters.history?.update !== undefined;
399
419
 
420
+ const run = { cancelled: false };
421
+ this._activeRun = run;
422
+ this._runGeneration++;
423
+
400
424
  try {
401
425
  // mark busy for runs not started through the queue (regenerate, resume)
402
426
  this._queue?.notifyBusy();
@@ -417,9 +441,12 @@ export class LocalThreadRuntimeCore
417
441
  } while (shouldContinue(message, this._options.unstable_humanToolNames));
418
442
  } finally {
419
443
  this._notifyEventSubscribers("runEnd", {});
420
- // queue-driven runs release from the driver settle handler; a direct
421
- // run (regenerate, resume) releases here
422
- if (!this._queueRunInFlight) {
444
+ // the settle belongs to this run only while it is still the active run
445
+ // or was cancelled (the engine expects a cancelled run's settle); a run
446
+ // superseded by a newer one stays silent
447
+ const active = this._activeRun === run;
448
+ if (active) this._activeRun = null;
449
+ if (active || run.cancelled) {
423
450
  queueMicrotask(() => this._queue?.notifyIdle());
424
451
  }
425
452
  }
@@ -644,7 +671,8 @@ export class LocalThreadRuntimeCore
644
671
  }
645
672
 
646
673
  public detach() {
647
- this._queue?.adapter.clear("cancel-run");
674
+ // drop the queue so pending items cannot dispatch on a detached thread
675
+ this._queue = null;
648
676
  const error = new AbortError(true);
649
677
  this.abortController?.abort(error);
650
678
  this.abortController = null;
@@ -653,7 +681,14 @@ export class LocalThreadRuntimeCore
653
681
  }
654
682
 
655
683
  public cancelRun() {
656
- this._queue?.adapter.clear("cancel-run");
684
+ if (this._queue) {
685
+ if (this._options.unstable_queueClearOnCancel ?? true) {
686
+ this._queue.clear();
687
+ } else {
688
+ this._queue.notifyCancelled();
689
+ if (this._activeRun) this._activeRun.cancelled = true;
690
+ }
691
+ }
657
692
  const error = new AbortError(false);
658
693
  this.abortController?.abort(error);
659
694
  this.abortController = null;
@@ -681,7 +716,7 @@ export class LocalThreadRuntimeCore
681
716
  if (c.type !== "tool-call") return c;
682
717
  if (c.toolCallId !== toolCallId) return c;
683
718
  found = true;
684
- if (!c.result) added = true;
719
+ if (c.result === undefined) added = true;
685
720
  return {
686
721
  ...c,
687
722
  result,
@@ -163,7 +163,7 @@ export class ReadonlyThreadRuntimeCore
163
163
  },
164
164
 
165
165
  queue: [] as never[],
166
- steerQueueItem() {},
166
+ moveQueueItem() {},
167
167
  removeQueueItem() {},
168
168
 
169
169
  dictation: undefined,
@@ -144,7 +144,7 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
144
144
  },
145
145
 
146
146
  queue: [] as never[],
147
- steerQueueItem() {},
147
+ moveQueueItem() {},
148
148
  removeQueueItem() {},
149
149
 
150
150
  dictation: undefined,
@@ -1,8 +1,15 @@
1
- import type { ComponentType, PropsWithChildren } from "react";
2
1
  import type { ThreadMessage } from "../../types/message";
3
2
  import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
4
3
  import type { AssistantStream } from "assistant-stream";
5
4
 
5
+ /* oxlint-disable typescript/no-explicit-any -- structural stand-in for ComponentType without depending on react types */
6
+ type RemoteThreadListProviderProps = { children?: any };
7
+
8
+ export type RemoteThreadListProviderComponent =
9
+ | ((props: RemoteThreadListProviderProps) => any)
10
+ | (new (props: RemoteThreadListProviderProps) => any);
11
+ /* oxlint-enable typescript/no-explicit-any */
12
+
6
13
  export type RemoteThreadInitializeResponse = {
7
14
  remoteId: string;
8
15
  externalId?: string | undefined;
@@ -54,7 +61,7 @@ export type RemoteThreadListAdapter = {
54
61
  * is unsupported and leaves thread context unavailable to downstream
55
62
  * consumers. Load data inside an always-mounted child instead.
56
63
  */
57
- unstable_Provider?: ComponentType<PropsWithChildren> | undefined;
64
+ unstable_Provider?: RemoteThreadListProviderComponent | undefined;
58
65
  };
59
66
 
60
67
  export type RemoteThreadListOptions = {
@@ -12,6 +12,7 @@ import type {
12
12
  } from "../../runtime/api/composer-runtime";
13
13
  import type { ComposerState } from "../scopes/composer";
14
14
  import type { QueueItemState } from "../scopes/queue-item";
15
+ import type { QueuePlacement } from "../../runtime/queue/external-thread-queue-adapter";
15
16
  import { AttachmentRuntimeClient } from "./attachment-runtime-client";
16
17
  import { useSubscribable } from "./useSubscribable";
17
18
 
@@ -40,16 +41,17 @@ const ComposerAttachmentClientByIndex = resource(
40
41
 
41
42
  const useQueueItemClient = ({
42
43
  item,
43
- onSteer,
44
+ onMove,
44
45
  onRemove,
45
46
  }: {
46
47
  item: QueueItemState;
47
- onSteer: () => void;
48
+ onMove: (placement: QueuePlacement) => void;
48
49
  onRemove: () => void;
49
50
  }): ClientOutput<"queueItem"> => {
50
51
  return {
51
52
  getState: () => item,
52
- steer: onSteer,
53
+ steer: () => onMove({ lane: "steer", insertAfter: null }),
54
+ move: onMove,
53
55
  remove: onRemove,
54
56
  };
55
57
  };
@@ -121,7 +123,7 @@ const useComposerClient = ({
121
123
  item.id,
122
124
  QueueItemClient({
123
125
  item,
124
- onSteer: () => runtime.steerQueueItem(item.id),
126
+ onMove: (placement) => runtime.moveQueueItem(item.id, placement),
125
127
  onRemove: () => runtime.removeQueueItem(item.id),
126
128
  }),
127
129
  ),
@@ -171,7 +173,13 @@ const useComposerClient = ({
171
173
  return attachments.get(selector);
172
174
  }
173
175
  },
174
- queueItem: (selector) => queueItems.get(selector),
176
+ queueItem: (selector) => {
177
+ if ("id" in selector) {
178
+ return queueItems.get({ key: selector.id });
179
+ } else {
180
+ return queueItems.get(selector);
181
+ }
182
+ },
175
183
  __internal_getRuntime: () => runtime,
176
184
  };
177
185
  };
@@ -13,8 +13,10 @@ import type { QueueItemState, QueueItemMethods } from "./queue-item";
13
13
 
14
14
  export type ComposerSendOptions = SendOptions & {
15
15
  /**
16
- * Whether to steer (interrupt the current run and process this message immediately).
17
- * When false (default), the message is queued and processed in order.
16
+ * Whether to steer (process this message next, interrupting the current run
17
+ * when the runtime supports it). Defaults to true while a queued run is in
18
+ * flight and false when idle. Pass false to queue behind the pending
19
+ * messages.
18
20
  */
19
21
  steer?: boolean;
20
22
  };
@@ -88,9 +90,9 @@ export type ComposerMethods = {
88
90
  setQuote(quote: QuoteInfo | undefined): void;
89
91
 
90
92
  /**
91
- * Access a queue item by index.
93
+ * Access a queue item by index or id.
92
94
  */
93
- queueItem(selector: { index: number }): QueueItemMethods;
95
+ queueItem(selector: { index: number } | { id: string }): QueueItemMethods;
94
96
 
95
97
  __internal_getRuntime?(): ComposerRuntime;
96
98
  };
@@ -1,19 +1,26 @@
1
+ import type { FileMessagePart, TextMessagePart } from "../../types/message";
2
+ import type { QueuePlacement } from "../../runtime/queue/external-thread-queue-adapter";
3
+
1
4
  export type QueueItemState = {
2
5
  readonly id: string;
6
+ /** @deprecated Derive from the text parts of `parts` instead. Removal after 2026-11-05. */
3
7
  readonly prompt: string;
8
+ readonly parts: readonly (FileMessagePart | TextMessagePart)[];
4
9
  };
5
10
 
6
11
  export const EMPTY_QUEUE_ITEMS: readonly QueueItemState[] = Object.freeze([]);
7
12
 
8
13
  export type QueueItemMethods = {
9
14
  getState(): QueueItemState;
15
+ /** @deprecated Use `move({ lane: "steer", insertAfter: null })` instead. Removal after 2026-11-05. */
10
16
  steer(): void;
17
+ move(placement: QueuePlacement): void;
11
18
  remove(): void;
12
19
  };
13
20
 
14
21
  export type QueueItemMeta = {
15
22
  source: "composer";
16
- query: { index: number };
23
+ query: { type: "index"; index: number } | { type: "id"; id: string };
17
24
  };
18
25
 
19
26
  export type QueueItemClientSchema = {
@@ -64,6 +64,37 @@ describe("RemoteThreadListThreadListRuntimeCore.switchToThread order", () => {
64
64
  expect(core.mainThreadId).toBe(core.getItemById("thread-c")?.id);
65
65
  });
66
66
 
67
+ it("handles a stopped initial runtime start", async () => {
68
+ const rejections: unknown[] = [];
69
+ const onUnhandledRejection = (reason: unknown) => {
70
+ rejections.push(reason);
71
+ };
72
+ const priorListeners = process.listeners("unhandledRejection");
73
+ process.removeAllListeners("unhandledRejection");
74
+ process.on("unhandledRejection", onUnhandledRejection);
75
+ try {
76
+ const core = createCore(makeAdapter());
77
+ const newThreadId = core.newThreadId;
78
+ if (!newThreadId) throw new Error("Expected an initial new thread");
79
+ const runtimeId = core.getItemById(newThreadId)?.id;
80
+ if (!runtimeId) throw new Error("Expected an initial runtime id");
81
+
82
+ (
83
+ core as unknown as {
84
+ _hookManager: { stopThreadRuntime: (id: string) => void };
85
+ }
86
+ )._hookManager.stopThreadRuntime(runtimeId);
87
+ await new Promise((resolve) => setTimeout(resolve, 20));
88
+ } finally {
89
+ process.removeListener("unhandledRejection", onUnhandledRejection);
90
+ for (const listener of priorListeners) {
91
+ process.on("unhandledRejection", listener);
92
+ }
93
+ }
94
+
95
+ expect(rejections).toEqual([]);
96
+ });
97
+
67
98
  it("invalidates an earlier switch while a new thread request is pending", async () => {
68
99
  const initializeThread = deferred<{
69
100
  remoteId: string;
@@ -832,10 +832,12 @@ describe("ExternalStoreThreadRuntimeCore - initialize event replay", () => {
832
832
  describe("ExternalStoreThreadRuntimeCore - message queue", () => {
833
833
  const makeQueue = () => ({
834
834
  items: [] as never[],
835
+ steerItems: [] as never[],
835
836
  enqueue: vi.fn(),
836
837
  steer: vi.fn(),
838
+ move: vi.fn(),
839
+ edit: vi.fn(),
837
840
  remove: vi.fn(),
838
- clear: vi.fn(),
839
841
  });
840
842
 
841
843
  const appendMessage = (
@@ -866,7 +868,7 @@ describe("ExternalStoreThreadRuntimeCore - message queue", () => {
866
868
  expect(withoutQueue.capabilities.queue).toBe(false);
867
869
  });
868
870
 
869
- it("routes a tail append through queue.enqueue instead of onNew", async () => {
871
+ it("routes a tail append through the queue adapter instead of onNew", async () => {
870
872
  const queue = makeQueue();
871
873
  const onNew = vi.fn();
872
874
  const runtime = new ExternalStoreThreadRuntimeCore(
@@ -877,8 +879,48 @@ describe("ExternalStoreThreadRuntimeCore - message queue", () => {
877
879
  await runtime.append(appendMessage({ steer: true }));
878
880
 
879
881
  expect(onNew).not.toHaveBeenCalled();
882
+ expect(queue.enqueue).not.toHaveBeenCalled();
883
+ expect(queue.steer).toHaveBeenCalledTimes(1);
884
+ });
885
+
886
+ it("defaults a mid-run send to steer and an idle send to enqueue", async () => {
887
+ const queue = makeQueue();
888
+ const running = new ExternalStoreThreadRuntimeCore(
889
+ mockContextProvider,
890
+ makeStore({ queue, onNew: vi.fn(), isRunning: true }),
891
+ );
892
+ await running.append(
893
+ appendMessage({ parentId: running.messages.at(-1)?.id ?? null }),
894
+ );
895
+ expect(queue.steer).toHaveBeenCalledTimes(1);
896
+ expect(queue.enqueue).not.toHaveBeenCalled();
897
+
898
+ const idleQueue = makeQueue();
899
+ const idle = new ExternalStoreThreadRuntimeCore(
900
+ mockContextProvider,
901
+ makeStore({ queue: idleQueue, onNew: vi.fn(), isRunning: false }),
902
+ );
903
+ await idle.append(
904
+ appendMessage({ parentId: idle.messages.at(-1)?.id ?? null }),
905
+ );
906
+ expect(idleQueue.enqueue).toHaveBeenCalledTimes(1);
907
+ expect(idleQueue.steer).not.toHaveBeenCalled();
908
+ });
909
+
910
+ it("queues behind pending items when steer is explicitly false mid-run", async () => {
911
+ const queue = makeQueue();
912
+ const runtime = new ExternalStoreThreadRuntimeCore(
913
+ mockContextProvider,
914
+ makeStore({ queue, onNew: vi.fn(), isRunning: true }),
915
+ );
916
+ await runtime.append(
917
+ appendMessage({
918
+ steer: false,
919
+ parentId: runtime.messages.at(-1)?.id ?? null,
920
+ }),
921
+ );
880
922
  expect(queue.enqueue).toHaveBeenCalledTimes(1);
881
- expect(queue.enqueue.mock.calls[0]![1]).toEqual({ steer: true });
923
+ expect(queue.steer).not.toHaveBeenCalled();
882
924
  });
883
925
 
884
926
  it("does not abort in-flight tools when buffering a queued send", async () => {
@@ -913,7 +955,7 @@ describe("ExternalStoreThreadRuntimeCore - message queue", () => {
913
955
  expect(abort).toHaveBeenCalledTimes(1);
914
956
  });
915
957
 
916
- it("clears the queue on cancel, reload, and edit", async () => {
958
+ it("keeps the queue on cancel, reload, and edit", async () => {
917
959
  const queue = makeQueue();
918
960
  const runtime = new ExternalStoreThreadRuntimeCore(
919
961
  mockContextProvider,
@@ -926,29 +968,30 @@ describe("ExternalStoreThreadRuntimeCore - message queue", () => {
926
968
  );
927
969
 
928
970
  runtime.cancelRun();
929
- expect(queue.clear).toHaveBeenCalledWith("cancel-run");
930
-
931
971
  await runtime.startRun({ parentId: null, sourceId: null, runConfig: {} });
932
- expect(queue.clear).toHaveBeenCalledWith("reload");
933
-
934
972
  // a non-tail parentId routes to the edit branch
935
973
  await runtime.append(appendMessage({ parentId: "not-the-tail" }));
936
- expect(queue.clear).toHaveBeenCalledWith("edit");
974
+
975
+ expect(queue.remove).not.toHaveBeenCalled();
976
+ expect(queue.move).not.toHaveBeenCalled();
937
977
  });
938
978
 
939
- it("delegates getQueueItems / steer / remove to the adapter", () => {
979
+ it("delegates queue reads / move / remove to the adapter", () => {
940
980
  const queue = makeQueue();
941
- const items = [{ id: "q1", prompt: "queued" }];
981
+ const items = [{ id: "q1", prompt: "queued", parts: [] }];
982
+ const steerItems = [{ id: "s1", prompt: "steered", parts: [] }];
942
983
  queue.items = items as never;
984
+ queue.steerItems = steerItems as never;
943
985
  const runtime = new ExternalStoreThreadRuntimeCore(
944
986
  mockContextProvider,
945
987
  makeStore({ queue }),
946
988
  );
947
989
 
948
990
  expect(runtime.getQueueItems()).toBe(items);
949
- runtime.steerQueueItem("q1");
991
+ expect(runtime.getSteerQueueItems()).toBe(steerItems);
992
+ runtime.moveQueueItem("q1", { lane: "steer" });
950
993
  runtime.removeQueueItem("q1");
951
- expect(queue.steer).toHaveBeenCalledWith("q1");
994
+ expect(queue.move).toHaveBeenCalledWith("q1", { lane: "steer" });
952
995
  expect(queue.remove).toHaveBeenCalledWith("q1");
953
996
  });
954
997
  });