@jskit-ai/assistant-core 0.2.6 → 0.2.8

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 (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +18 -12
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -0,0 +1,121 @@
1
+ import { serial } from "./conversationEvents.js";
2
+
3
+ /** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
4
+
5
+ // The newest messages cancelled in one open conversation, refused if sent again.
6
+ const MAX_CANCELLED_MESSAGES = 100;
7
+
8
+ /**
9
+ * Stopping work in an open conversation: stopping its turn, cancelling one message, retrying
10
+ * a failed save, and disposing of its entry.
11
+ * @param {object} dependencies
12
+ * @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
13
+ * @param {import("./conversationEntries.js").ConversationEntries} dependencies.entries
14
+ * @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
15
+ * @param {import("./conversationDelivery.js").ConversationDelivery} dependencies.delivery
16
+ */
17
+ function createConversationStopping({ storage, entries, events, delivery }) {
18
+ /**
19
+ * @param {ConversationEntry} entry
20
+ * @param {object} [context]
21
+ * @param {object} [input]
22
+ * @param {string} [representation]
23
+ */
24
+ async function stop(entry, context, input, representation) {
25
+ if (representation === "native") return entry.provider.cancel({ context, input, representation });
26
+ const active = entry.active;
27
+ if (!active && entry.executionFailure) {
28
+ await entry.provider.cancel();
29
+ await retrySave(entry);
30
+ if (entry.cleanupTurnId && !entry.conversation) await storage.write(entry.id, async transaction => {
31
+ const turn = await transaction.readTurn(entry.cleanupTurnId);
32
+ await transaction.updateTurnMetadata(entry.cleanupTurnId,
33
+ { runtime: { ...turn.metadata.runtime, status: "cancelled", error: "Work stopped." } });
34
+ });
35
+ entry.executionFailure = null;
36
+ entry.cleanupTurnId = null;
37
+ entry.error = "Work stopped.";
38
+ return { stopped: true };
39
+ }
40
+ if (!active) {
41
+ if (!entry.conversation && !entry.driver.canonicalTranscript) return { stopped: false };
42
+ const metadata = { runtime: await entries.readState(entry) };
43
+ const result = await entry.provider.cancel({ configuration: metadata.runtime.configuration, context });
44
+ return { stopped: result?.interrupted === true || result?.operationOutcome === "interrupted" };
45
+ }
46
+ active.controller.abort(new Error("Work stopped."));
47
+ await active.done;
48
+ if (entry.executionFailure) throw entry.executionFailure;
49
+ return { stopped: true };
50
+ }
51
+
52
+ /**
53
+ * Cancel of one message in a runtime-owned conversation never waits for the conversation queue.
54
+ * The message is recorded as cancelled at once, so a send of it still queued, or arriving later,
55
+ * is refused as not sent; its own submission stops, ending any turn it started. Its unconfirmed
56
+ * request or pending native attempt is discarded afterwards, in queue order. A message already
57
+ * accepted stays history.
58
+ * @param {ConversationEntry} entry
59
+ * @param {object} context
60
+ * @param {string} messageId
61
+ * @param {string} representation
62
+ */
63
+ async function cancelDelivery(entry, context, messageId, representation) {
64
+ entry.cancelledMessages.delete(messageId);
65
+ entry.cancelledMessages.add(messageId);
66
+ if (entry.cancelledMessages.size > MAX_CANCELLED_MESSAGES) entry.cancelledMessages.delete(entry.cancelledMessages.values().next().value);
67
+ const interrupted = Boolean(entry.active?.requests.has(messageId));
68
+ if (interrupted) await stop(entry, context, undefined, representation);
69
+ void serial(entry, async () => {
70
+ await events.access(context, entry.id, "cancel", entry);
71
+ if (entry.conversation) {
72
+ await delivery.nativeChangeover(entry, context, null, () => { throw new Error("Cancel never sends a message."); }).discard({ messageId });
73
+ return;
74
+ }
75
+ await storage.write(entry.id, async transaction => {
76
+ const metadata = await transaction.readMetadata();
77
+ if (metadata.runtime.request?.messageId !== messageId) return;
78
+ delete metadata.runtime.request;
79
+ await transaction.writeMetadata(metadata);
80
+ });
81
+ }).catch(() => {});
82
+ return { ok: true, cancelled: true, messageId, interrupted };
83
+ }
84
+
85
+ /** @param {ConversationEntry} entry */
86
+ async function retrySave(entry) {
87
+ if (!entry.storageFailure || !entry.saveProgress) return { saved: false };
88
+ const event = await entry.saveProgress();
89
+ await events.emit(entry, event);
90
+ return { saved: true };
91
+ }
92
+
93
+ /**
94
+ * @param {ConversationEntry} entry
95
+ * @param {object} [context]
96
+ * @param {object} [options]
97
+ * @param {string} [representation]
98
+ */
99
+ async function disposeEntry(entry, context, options, representation) {
100
+ if (entry.conversation?.commands) { entries.retire(entry); return; }
101
+ if (!entry.conversation) await stop(entry, context);
102
+ await retrySave(entry);
103
+ const result = await entry.provider.dispose(entry.conversation?.native?.scoped && representation === "native"
104
+ ? { input: options, context } : representation === "native" ? options : undefined);
105
+ if (result?.ok === false) return result;
106
+ if (entry.conversation) await entry.active?.done;
107
+ entries.retire(entry);
108
+ return result;
109
+ }
110
+
111
+ return Object.freeze({
112
+ stop,
113
+ cancelDelivery,
114
+ retrySave,
115
+ disposeEntry
116
+ });
117
+ }
118
+
119
+ /** @typedef {ReturnType<typeof createConversationStopping>} ConversationStopping */
120
+
121
+ export { createConversationStopping };
@@ -0,0 +1,413 @@
1
+ import { conversationAttachmentManifest } from "../../shared/conversation/attachments.js";
2
+ import { createConversationAttachmentReader } from "./attachments.js";
3
+ import { conversationHistoryVersions, conversationNativeMessages, conversationRequestText } from "./continuity.js";
4
+ import { ignorePromptRejection, ignorePromptSending } from "./nativeCommandCallbacks.js";
5
+ import { deliveryFailure, failure } from "./conversationRefusals.js";
6
+ import { submittedMessage } from "./conversationReads.js";
7
+
8
+ /** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
9
+ /** @typedef {import("./conversationTurns.js").ActiveTurn} ActiveTurn */
10
+ /** @typedef {import("./conversationSending.js").ConversationRequest} ConversationRequest */
11
+
12
+ /**
13
+ * How a running turn admits and delivers its requests: their admission and its publication, the
14
+ * attribution and progress every saved turn records, attachment reading and native dispatch.
15
+ * @param {object} dependencies
16
+ * @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
17
+ * @param {import("../types.js").ConversationStreams} dependencies.streams
18
+ * @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
19
+ * @param {import("./conversationReads.js").ConversationReads} dependencies.reads
20
+ * @param {import("./conversationDelivery.js").ConversationDelivery} dependencies.delivery
21
+ * @param {import("../types.js").ConversationAttachments} dependencies.attachments
22
+ * @param {boolean} dependencies.persistCommentary
23
+ * @param {number} dependencies.maximumContinuity
24
+ * @param {number} dependencies.maximumAttachmentBytes
25
+ */
26
+ function createConversationTurnDelivery({ storage, streams, events, reads, delivery, attachments, persistCommentary, maximumContinuity,
27
+ maximumAttachmentBytes }) {
28
+ /**
29
+ * Every saved turn and pending request records its attribution identically.
30
+ * @param {ActiveTurn} active
31
+ * @param {ConversationRequest} request
32
+ * @param {object} [extra]
33
+ */
34
+ const turnAttribution = (active, request, extra = {}) => ({ engine: active.engine, segmentId: active.segmentId, origin: request.input.origin,
35
+ ...(request.completedEnvelope ? { completedEnvelope: true } : {}),
36
+ ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}), ...extra });
37
+ /** @param {ConversationRequest} request */
38
+ const authoredRequestRecord = request => ({ ...request.input, at: request.at,
39
+ ...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}),
40
+ ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}) });
41
+
42
+ /**
43
+ * Saves a request's progress on its turn, and returns the history versions it wrote.
44
+ * @param {ActiveTurn} active
45
+ * @param {object} transaction
46
+ * @param {ConversationRequest} request
47
+ * @param {string} status
48
+ * @param {string} [error]
49
+ * @param {object} [extra]
50
+ */
51
+ async function writeProgress(active, transaction, request, status, error = "", extra = {}) {
52
+ if (request.tools) await transaction.updateTurnMetadata(request.turnId, { applicationTools: request.tools.records() });
53
+ if (!active.driver.canonicalTranscript) {
54
+ for (const message of request.progress.values()) {
55
+ if (!persistCommentary && message.role === "commentary") continue;
56
+ if (!await transaction.hasMessage(message.messageId)) await transaction.appendMessage(request.turnId, message);
57
+ }
58
+ if (request.answer?.text.trim()) await transaction.replaceAssistant(request.turnId, request.answer);
59
+ }
60
+ const saved = active.driver.canonicalTranscript ? await transaction.readTurn(request.turnId) : null;
61
+ await transaction.updateTurnMetadata(request.turnId, { runtime: { ...saved?.metadata?.runtime, status, ...turnAttribution(active, request, {
62
+ ...(request.nativeTurnId ? { nativeTurnId: request.nativeTurnId } : {}),
63
+ ...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
64
+ ...(request.continuedBy ? { continuedBy: request.continuedBy } : {}) }), ...extra, ...(error ? { error } : {}) } });
65
+ return conversationHistoryVersions([{ turnId: request.turnId,
66
+ messages: [...request.progress.values()].filter(message => !(message.turnId === request.turnId && message.status === "complete"))
67
+ .concat(request.answer?.text.trim() ? [request.answer] : []) }]);
68
+ }
69
+
70
+ /**
71
+ * Admits a request into the running turn: its receipt is saved on its turn (or located in a bound
72
+ * conversation's transcript), and the request becomes the one the turn answers.
73
+ * @param {ConversationEntry} entry
74
+ * @param {ActiveTurn} active
75
+ * @param {ConversationRequest} request
76
+ * @param {Record<string, string> | undefined} seen The history versions an API request sends.
77
+ * @param {{ nativeDelivery?: boolean, conversationTurn?: { turnId: string } | null }} [options]
78
+ */
79
+ async function admit(entry, active, request, seen, { nativeDelivery = false, conversationTurn = null } = {}) {
80
+ await events.access(request.context, entry.id, request.operation, entry);
81
+ request.deliveryTiming("admission-authorized");
82
+ if (!conversationTurn) active.controller.signal.throwIfAborted();
83
+ if (request.input.goal) request.goalMessageId = request.input.messageId;
84
+ else if (active.goalOwner) request.goalMessageId = active.goalOwner.input.messageId;
85
+ const nativeTurnId = active.nativeTurnId;
86
+ const previous = active.request;
87
+ if (entry.conversation) {
88
+ const saved = conversationTurn?.turnId ? await reads.readBoundTurn(entry, conversationTurn.turnId) : null;
89
+ request.deliveryTiming("turn-located");
90
+ if (submittedMessage(saved || {})?.messageId !== request.input.messageId) {
91
+ throw new Error("The native receipt does not belong to this authored request.");
92
+ }
93
+ request.turnId = saved.turnId;
94
+ } else request.turnId = await storage.write(entry.id, async transaction => {
95
+ const metadata = await transaction.readMetadata();
96
+ if (metadata.runtime.request?.messageId !== request.input.messageId) throw new Error("The pending conversation message changed before admission.");
97
+ if (previous !== request && !previous.finished) {
98
+ const versions = await writeProgress(active, transaction, previous, "running", "", { continuedBy: request.input.messageId });
99
+ if (active.engine === "api") Object.assign(metadata.runtime.seen, versions);
100
+ }
101
+ let id;
102
+ if (conversationTurn) {
103
+ const saved = await transaction.readTurn(conversationTurn.turnId);
104
+ if (submittedMessage(saved || {})?.messageId !== request.input.messageId || saved.metadata?.runtime?.segmentId !== active.segmentId) {
105
+ throw new Error("The native receipt does not belong to this authored request.");
106
+ }
107
+ if (request.completedEnvelope && saved.metadata.runtime.completedEnvelope !== true) {
108
+ throw new Error("The native receipt lost its completed-response marker.");
109
+ }
110
+ id = saved.turnId;
111
+ await transaction.updateTurnMetadata(id, { runtime: { ...saved.metadata.runtime,
112
+ ...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
113
+ ...(nativeTurnId ? { nativeTurnId } : {}) } });
114
+ } else {
115
+ id = await transaction.nextTurnId();
116
+ await transaction.appendMessage(id, { role: request.input.origin === "application" ? "system" : "user", ...request.input, at: new Date().toISOString(),
117
+ turnMetadata: { runtime: { status: "running", ...turnAttribution(active, request, {
118
+ ...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
119
+ ...(nativeTurnId ? { nativeTurnId } : {}) }) } } });
120
+ }
121
+ if (active.engine === "api") {
122
+ metadata.runtime.seen = { ...(seen || metadata.runtime.seen), ...conversationHistoryVersions([await transaction.readTurn(id)]) };
123
+ metadata.runtime.lastEngine = active.engine;
124
+ delete metadata.runtime.request;
125
+ }
126
+ await transaction.writeMetadata(metadata);
127
+ if (active.engine !== "api" && !nativeDelivery) await delivery.rememberNative(entry, transaction, [await transaction.readTurn(id)]);
128
+ return id;
129
+ });
130
+ if (previous !== request) {
131
+ previous.sealed = true;
132
+ previous.continuedBy = request.input.messageId;
133
+ if (!active.driver.canonicalTranscript) streams.clear(entry.id);
134
+ }
135
+ request.accepted = true;
136
+ if (!entry.conversation) request.deliveryTiming("receipt-persisted");
137
+ request.nativeTurnId = nativeTurnId;
138
+ if (request.input.goal) active.goalOwner = request;
139
+ active.request = request;
140
+ if (entry.conversation) entry.lastAcceptedRequest = request;
141
+ if (!nativeDelivery) await publishAdmission(entry, active, request);
142
+ }
143
+
144
+ /**
145
+ * Resolves a request's admission and tells subscribers, with the native items it already owns.
146
+ * @param {ConversationEntry} entry
147
+ * @param {ActiveTurn} active
148
+ * @param {ConversationRequest} request
149
+ * @param {unknown} [nativeResult]
150
+ */
151
+ async function publishAdmission(entry, active, request, nativeResult) {
152
+ request.deliveryTiming("accepted");
153
+ const receipt = { status: "accepted", messageId: request.input.messageId, turnId: request.turnId, origin: request.input.origin };
154
+ request.admission.resolve(entry.conversation ? { receipt, nativeResult } : receipt);
155
+ request.admissionSettled = true;
156
+ await events.emit(entry, { type: "accepted", ...receipt });
157
+ if (active.driver.canonicalTranscript && !entry.conversation) {
158
+ // Native items can complete before common admission finishes. Their
159
+ // exact request already owns them; publish before tools join the
160
+ // existing delivery commit, without blocking the native event queue.
161
+ for (const [id, message] of request.progress) {
162
+ if (message.turnId !== request.turnId || message.status !== "complete") continue;
163
+ request.progress.delete(id);
164
+ if (request.sealed || entry.disposed || entry.active !== active ||
165
+ entry.bindingOwner !== active.bindingOwner || entry.segmentId !== active.segmentId) continue;
166
+ await events.emit(entry, { type: "message", ...message, streaming: streams.read(entry.id) },
167
+ () => !entry.disposed && !request.sealed && entry.active === active && entry.bindingOwner === active.bindingOwner &&
168
+ entry.segmentId === active.segmentId);
169
+ }
170
+ }
171
+ request.admissionPublished = true;
172
+ await events.setPhase(entry, "working", request.turnId);
173
+ if ((entry.conversation || active.driver.canonicalTranscript) && entry.nativePhase && entry.nativePhase !== "working") await events.setPhase(entry, entry.nativePhase, request.turnId);
174
+ }
175
+
176
+ /**
177
+ * @param {ConversationEntry} entry
178
+ * @param {ActiveTurn} active
179
+ * @param {ConversationRequest} request
180
+ */
181
+ function attachmentReader(entry, active, request) {
182
+ return createConversationAttachmentReader({ attachments, context: request.context, conversationId: entry.id,
183
+ signal: active.controller.signal, authorize: () => events.access(request.context, entry.id, request.operation, entry),
184
+ types: active.driver.attachmentTypes, maximumBytes: maximumAttachmentBytes });
185
+ }
186
+
187
+ /**
188
+ * The last checks before a request is dispatched to its engine.
189
+ * @param {ConversationEntry} entry
190
+ * @param {ActiveTurn} active
191
+ * @param {ConversationRequest} request
192
+ */
193
+ async function beforeDispatch(entry, active, request) {
194
+ await events.access(request.context, entry.id, request.operation, entry);
195
+ active.controller.signal.throwIfAborted();
196
+ if (request.input.goal && request.context?.signal?.aborted) {
197
+ throw Object.assign(failure("The goal command was stopped before delivery.", "conversation_goal_stopped"), { delivery: "not-sent" });
198
+ }
199
+ if (active.finishing) throw Object.assign(failure("The native turn finished before this instruction was sent. Send it as a new message.", "conversation_steer_turn_finished", 409, { messageCode: "assistant.conversation.turnFinishedBeforeSend" }), { delivery: "not-sent" });
200
+ }
201
+
202
+ /**
203
+ * Delivers a request to its native engine through the changeover that catches the engine up,
204
+ * then admits it when the engine acknowledges it.
205
+ * @param {ConversationEntry} entry
206
+ * @param {ActiveTurn} active
207
+ * @param {ConversationRequest} request
208
+ * @param {(input: object) => Promise<unknown>} dispatch Runs the engine with the prepared input.
209
+ */
210
+ async function deliverNative(entry, active, request, dispatch) {
211
+ // A bound conversation supplies its own native history below.
212
+ const history = entry.conversation ? [] : (await reads.readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy);
213
+ let completion;
214
+ let committed;
215
+ let rendered;
216
+ let threadId;
217
+ let attachmentManifest;
218
+ let nativeResult;
219
+ const dispatchPrepared = async input => {
220
+ rendered = input;
221
+ active.rejectionSaves.delete(request.input.messageId);
222
+ if (request.operation === "steer" && !request.steering) throw Object.assign(failure("There is no active native turn to steer. Send a new message.", "conversation_steer_turn_finished", 409, { messageCode: "assistant.conversation.noActiveTurnToSteer" }), { delivery: "not-sent" });
223
+ if ((input.contextText || "").length > maximumContinuity) throw new Error("Conversation updates exceed the configured history budget. Renew the context with a continuity briefing before continuing.");
224
+ if (request.input.goal && input.contextText) {
225
+ throw failure("Send a chat message to deliver the saved continuity briefing before starting a native terminal or goal.", "conversation_briefing_before_terminal", 409, { messageCode: "assistant.conversation.briefingBeforeTerminal" });
226
+ }
227
+ const readAttachments = attachmentReader(entry, active, request);
228
+ // The bound host has already resolved its original attachment records;
229
+ // the production preparation owner keeps their manifest/native mapping.
230
+ const boundInput = entry.conversation
231
+ ? await entry.conversation.prepareInput(input, request.context, { deliveryTiming: request.deliveryTiming }) : null;
232
+ const files = boundInput ? { attachments: boundInput.displayAttachments || [], localFiles: [], content: [] }
233
+ : await readAttachments((input.attachmentIds || []).map(attachmentId => ({ attachmentId })));
234
+ const contextFiles = boundInput ? { localFiles: [], content: [] } : await readAttachments(input.contextAttachments || []);
235
+ request.deliveryTiming("input-prepared");
236
+ const localFiles = [...contextFiles.localFiles, ...files.localFiles];
237
+ if (request.input.goal && localFiles.length) {
238
+ throw failure("Native goals require extracted text or supported image bytes for attachments.", "conversation_goal_attachment_content_unsupported", 400, { messageCode: "assistant.conversation.goalAttachmentContentUnsupported" });
239
+ }
240
+ attachmentManifest = conversationAttachmentManifest(localFiles);
241
+ request.input.attachments = files.attachments;
242
+ if (!entry.conversation) await storage.write(entry.id, async transaction => {
243
+ const metadata = await transaction.readMetadata();
244
+ const pending = metadata.runtime.request;
245
+ if (pending && pending.messageId !== request.input.messageId && (pending.attempted || pending.inspectionOnly)) throw new Error("The pending conversation instruction changed.");
246
+ if (pending?.messageId === request.input.messageId && pending.attachmentManifest !== undefined &&
247
+ pending.attachmentManifest !== attachmentManifest) {
248
+ throw failure("Authorized attachment paths changed after this instruction was prepared. Restore the original files or send a new message.", "conversation_attachment_manifest_changed");
249
+ }
250
+ metadata.runtime.request = { ...(pending?.messageId === request.input.messageId ? pending
251
+ : { ...authoredRequestRecord(request), attempted: false }), attachmentManifest };
252
+ await transaction.writeMetadata(metadata);
253
+ });
254
+ const admitted = Promise.withResolvers();
255
+ committed = Promise.withResolvers();
256
+ committed.promise.catch(() => {});
257
+ request.deliveryCommitted = committed.promise;
258
+ request.deliveryTiming("provider-started");
259
+ completion = Promise.resolve().then(() => dispatch({
260
+ context: request.context,
261
+ input: { ...request.input, authoredInput: { ...request.input, at: request.at },
262
+ ...(request.input.goal ? { signal: request.context?.signal } : {}),
263
+ ...(boundInput ? { nativeMessage: boundInput } : {}),
264
+ text: input.message, contextText: input.contextText || "",
265
+ content: [...contextFiles.content, ...files.content], localFiles, attachmentManifest },
266
+ signal: active.controller.signal,
267
+ deliveryTiming: request.deliveryTiming,
268
+ onNativeReceipt: () => request.deliveryTiming("native-acknowledged"),
269
+ onReceiptPersisted: () => request.deliveryTiming("receipt-persisted"),
270
+ ...(active.engine === "opencode" ? { deliveryCommitted: committed.promise } : {}),
271
+ async beforeDispatch(native) {
272
+ request.deliveryTiming("runtime-ready");
273
+ await beforeDispatch(entry, active, request);
274
+ threadId = native?.threadId;
275
+ if (!threadId) throw new Error("Native admission requires the saved conversation identity.");
276
+ await input.onPromptSending(entry.conversation ? native : { threadId, displayAttachments: files.attachments,
277
+ turnMetadata: { runtime: turnAttribution(active, request) } });
278
+ if (request.input.goal && request.context?.signal?.aborted) {
279
+ throw Object.assign(failure("The goal command was stopped before delivery.", "conversation_goal_stopped"), { delivery: "not-sent" });
280
+ }
281
+ request.deliveryTiming("dispatch-started");
282
+ },
283
+ async accept(native) {
284
+ if (entry.conversation) nativeResult = native.nativeResult;
285
+ if (native?.nativeTurnId) active.nativeTurnId = native.nativeTurnId;
286
+ request.nativeThreadId = threadId;
287
+ await admit(entry, active, request, undefined, { nativeDelivery: true, conversationTurn: native?.conversationTurn });
288
+ admitted.resolve(entry.conversation ? nativeResult.value
289
+ : { ok: true, delivered: true, messageId: request.input.messageId, threadId });
290
+ // Drivers may stream or continue a goal as soon as accept returns.
291
+ // The original delivery marker must be durable before that happens.
292
+ await committed.promise;
293
+ }
294
+ })).then(result => {
295
+ if (entry.conversation && !request.accepted && result) {
296
+ nativeResult = result;
297
+ admitted.resolve(result.value);
298
+ return;
299
+ }
300
+ if (!request.accepted) throw new Error("The engine did not acknowledge the message.");
301
+ }).catch(async error => {
302
+ if (!request.accepted && (error.delivery === "not-sent" || !entry.conversation && error.delivery !== "uncertain")) {
303
+ try { await input.onPromptRejected(); }
304
+ catch (saveError) {
305
+ active.rejectionSaves.set(request.input.messageId, input.onPromptRejected);
306
+ error = saveError;
307
+ }
308
+ }
309
+ admitted.reject(error);
310
+ throw error;
311
+ });
312
+ completion.catch(() => {});
313
+ return admitted.promise;
314
+ };
315
+ // A native caller's own input may carry its prompt callbacks.
316
+ const { onPromptSending: originalPromptSending = ignorePromptSending,
317
+ onPromptRejected: originalPromptRejected = ignorePromptRejection } = request.originalInput || {};
318
+ try {
319
+ const input = {
320
+ ...request.originalInput,
321
+ messageId: request.input.messageId,
322
+ message: request.originalInput ? request.originalInput.message ?? request.originalInput.text ?? "" : conversationRequestText(request.input),
323
+ displayMessage: request.input.text,
324
+ displayAttachments: request.originalInput?.displayAttachments || request.input.attachments,
325
+ attachmentIds: request.input.attachments.map(file => file.attachmentId),
326
+ async onPromptSending({ threadId, displayAttachments, turnMetadata }) {
327
+ if (entry.conversation) return originalPromptSending({ threadId, displayAttachments, turnMetadata });
328
+ await storage.write(entry.id, async transaction => {
329
+ const metadata = await transaction.readMetadata();
330
+ const pending = metadata.runtime.request;
331
+ if (pending && pending.messageId !== request.input.messageId && (pending.attempted || pending.inspectionOnly)) throw new Error("The pending conversation instruction changed.");
332
+ metadata.runtime.request = { ...authoredRequestRecord(request),
333
+ message: rendered.message, displayMessage: rendered.displayMessage,
334
+ displayAttachments, attachmentIds: rendered.attachmentIds,
335
+ attachmentManifest,
336
+ ...(rendered.contextText !== undefined ? { contextText: rendered.contextText, contextAttachments: rendered.contextAttachments } : {}),
337
+ seen: pending?.seen || conversationHistoryVersions(history), attempted: true, threadId, turnMetadata,
338
+ ...(request.operation === "steer" ? { steering: true } : {}) };
339
+ await transaction.writeMetadata(metadata);
340
+ });
341
+ },
342
+ async onPromptRejected() {
343
+ if (entry.conversation) return originalPromptRejected();
344
+ await storage.write(entry.id, async transaction => {
345
+ const metadata = await transaction.readMetadata();
346
+ if (metadata.runtime.request?.messageId === request.input.messageId) {
347
+ metadata.runtime.request.attempted = false;
348
+ await transaction.writeMetadata(metadata);
349
+ }
350
+ });
351
+ }
352
+ };
353
+ // A native goal command calls the native sender directly. Chat
354
+ // context rendering and its delivery cursor must not change /goal input.
355
+ const delivered = entry.conversation && request.input.goal
356
+ ? await dispatchPrepared(input)
357
+ : await delivery.nativeChangeover(entry, request.context, request, dispatchPrepared, active.controller.signal).send({ engineId: active.engine,
358
+ messages: entry.conversation ? await entry.conversation.transcript.history() : conversationNativeMessages(history),
359
+ ...(entry.conversation ? { turnMetadata: { engineId: active.engine } } : {}),
360
+ ...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}), input });
361
+ if (!delivered?.delivered) {
362
+ const error = deliveryFailure(delivered);
363
+ if (entry.conversation) {
364
+ request.admission.resolve({ nativeResult: nativeResult || { value: delivered }, error });
365
+ request.admissionSettled = true;
366
+ }
367
+ throw error;
368
+ }
369
+ if (request.accepted) await publishAdmission(entry, active, request, nativeResult);
370
+ else {
371
+ const saved = await reads.findSubmittedTurn(entry, request.input.messageId);
372
+ if (!saved && entry.conversation) {
373
+ // A duplicate found in native history need not have an authored
374
+ // application row. Preserve its exact result without inventing a
375
+ // canonical receipt or changing native success into a failure.
376
+ request.admission.resolve({ nativeResult: nativeResult || { value: delivered },
377
+ error: failure("The native receipt has no stored authored message.", "conversation_receipt_unavailable") });
378
+ request.admissionSettled = request.finished = true;
379
+ committed?.resolve();
380
+ await completion;
381
+ return;
382
+ }
383
+ if (!saved) throw new Error("The native receipt has no stored authored message.");
384
+ request.accepted = request.finished = true;
385
+ if (entry.conversation) entry.lastAcceptedRequest = request;
386
+ request.turnId = saved.turnId;
387
+ request.recovered = saved.metadata?.runtime;
388
+ const receipt = { status: "accepted", messageId: request.input.messageId, turnId: saved.turnId,
389
+ origin: request.input.origin, ...(delivered.duplicate ? { duplicate: true } : { recovered: true }) };
390
+ request.admission.resolve(entry.conversation ? { receipt, nativeResult: { value: delivered } } : receipt);
391
+ request.admissionSettled = true;
392
+ }
393
+ committed?.resolve();
394
+ await completion;
395
+ } catch (error) {
396
+ committed?.reject(error);
397
+ await completion?.catch(() => {});
398
+ throw error;
399
+ }
400
+ }
401
+
402
+ return Object.freeze({
403
+ writeProgress,
404
+ admit,
405
+ attachmentReader,
406
+ beforeDispatch,
407
+ deliverNative
408
+ });
409
+ }
410
+
411
+ /** @typedef {ReturnType<typeof createConversationTurnDelivery>} ConversationTurnDelivery */
412
+
413
+ export { createConversationTurnDelivery };