@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,511 @@
1
+ import { conversationNativeIdentity } from "./runtimeStateUpgrade.js";
2
+ import { createConversationTools } from "./tools.js";
3
+ import { conversationAttachmentIds } from "./attachments.js";
4
+ import { conversationHistoryVersions, conversationRequestText } from "./continuity.js";
5
+ import { failure, stateErrorFields } from "./conversationRefusals.js";
6
+ import { submittedMessage } from "./conversationReads.js";
7
+
8
+ /** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
9
+ /** @typedef {import("./conversationSending.js").ConversationRequest} ConversationRequest */
10
+
11
+ // API history turns within this many newest messages send their files again.
12
+ const HISTORY_FILE_MESSAGES = 20;
13
+
14
+ /**
15
+ * The turn an open conversation is running: the requests it admitted, the binding it started
16
+ * under, and its controls. conversationTurns' start creates it.
17
+ * @typedef {object} ActiveTurn
18
+ * @property {ConversationRequest} request The request the turn answers now; steering admits its successors.
19
+ * @property {Map<string, ConversationRequest>} requests Every request of the turn, by message id.
20
+ * @property {AbortController} controller
21
+ * @property {import("./provider.js").ConversationDriver} driver The entry's driver when the turn started.
22
+ * @property {string} engine
23
+ * @property {string | null} segmentId
24
+ * @property {object} bindingOwner The entry's binding owner when the turn started.
25
+ * @property {ConversationRequest | null} goalOwner The request that owns the turn's native goal.
26
+ * @property {Map<string, () => Promise<void>>} rejectionSaves A definite rejection's failed saves, by message id.
27
+ * @property {(request: ConversationRequest) => Promise<void>} submit Delivers a steering request into the turn.
28
+ * @property {(input: object) => unknown} nativeCheckpoint Saves the admitted work of a native turn its owner completed.
29
+ * @property {Promise<void>} done Settles once the turn saved its progress and ended.
30
+ * @property {ReturnType<typeof setTimeout>} [timer]
31
+ * @property {boolean} [timedOut]
32
+ * @property {Promise<void> | null} [steering] Settles once a steering request is reserved.
33
+ * @property {boolean} [finishing]
34
+ * @property {string} [nativeTurnId]
35
+ * @property {string} [steerError]
36
+ */
37
+
38
+ /** The checkpoint of a turn whose native owner keeps none: a bound or non-canonical one. */
39
+ function recordNoNativeCheckpoint() {}
40
+
41
+ /**
42
+ * Saves the application tool receipts of an admitted request on its turn.
43
+ * @param {ConversationEntry} entry
44
+ * @param {object} transaction
45
+ * @param {{ turnId: string, input: { messageId: string } }} request
46
+ * @param {object[]} calls
47
+ */
48
+ async function saveTools(entry, transaction, request, calls) {
49
+ if (entry.conversation) {
50
+ const saved = await transaction.readTurn(request.turnId);
51
+ if (submittedMessage(saved || {})?.messageId !== request.input.messageId) {
52
+ throw new Error("The application tool receipt does not belong to this authored request.");
53
+ }
54
+ }
55
+ await transaction.updateTurnMetadata(request.turnId, { applicationTools: calls });
56
+ }
57
+
58
+ /**
59
+ * Running one admitted turn: its provider run or native delivery, steering, application
60
+ * tools, native checkpoints, and the progress it saves when it settles.
61
+ * @param {object} dependencies
62
+ * @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
63
+ * @param {import("../types.js").ConversationStreams} dependencies.streams
64
+ * @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
65
+ * @param {import("./conversationEntries.js").ConversationEntries} dependencies.entries
66
+ * @param {import("./conversationReads.js").ConversationReads} dependencies.reads
67
+ * @param {import("./conversationDelivery.js").ConversationDelivery} dependencies.delivery
68
+ * @param {import("./conversationTurnDelivery.js").ConversationTurnDelivery} dependencies.turnDelivery
69
+ * @param {import("../types.js").ServiceToolCatalog | null} dependencies.catalog
70
+ * @param {import("../types.js").ConversationApiHistory | undefined} dependencies.apiHistory
71
+ * @param {() => boolean} dependencies.isClosed Whether the runtime has closed.
72
+ * @param {boolean} dependencies.completedEnvelope
73
+ * @param {number} dependencies.maximumToolCalls
74
+ * @param {number} dependencies.maximumFinalReply
75
+ * @param {number} dependencies.maximumCompletedOutput
76
+ * @param {number | undefined} dependencies.timeoutMs The time limit of one request, when the application sets one.
77
+ */
78
+ function createConversationTurns({ storage, streams, events, entries, reads, delivery, turnDelivery, catalog, apiHistory, isClosed,
79
+ completedEnvelope, maximumToolCalls, maximumFinalReply, maximumCompletedOutput, timeoutMs }) {
80
+ /**
81
+ * Starts the turn of a request no turn was running for.
82
+ * @param {ConversationEntry} entry
83
+ * @param {ConversationRequest} request
84
+ * @param {object} configuration
85
+ */
86
+ function start(entry, request, configuration) {
87
+ /** @type {ActiveTurn} */
88
+ const active = { request, requests: new Map([[request.input.messageId, request]]), controller: new AbortController(),
89
+ driver: entry.driver, engine: entry.engine, segmentId: entry.segmentId, bindingOwner: entry.bindingOwner,
90
+ goalOwner: request.input.goal ? request : null, rejectionSaves: new Map(), nativeCheckpoint: recordNoNativeCheckpoint };
91
+ entry.active = active;
92
+ entry.error = "";
93
+ if (timeoutMs !== undefined) active.timer = setTimeout(() => {
94
+ active.timedOut = true;
95
+ active.controller.abort(new Error("The conversation request exceeded its time limit."));
96
+ }, timeoutMs);
97
+ active.done = perform(entry, active, configuration);
98
+ }
99
+
100
+ /**
101
+ * @param {ConversationEntry} entry
102
+ * @param {ActiveTurn} active
103
+ * @param {object} configuration
104
+ */
105
+ async function perform(entry, active, configuration) {
106
+ const { driver, engine, segmentId, bindingOwner } = active;
107
+ const initial = active.request;
108
+ let problem;
109
+ const messageOwners = new Map();
110
+ const toolOwners = new Map();
111
+ let toolWork = Promise.resolve();
112
+ let toolFailure;
113
+ const applicationTools = entry.applicationTools;
114
+ const toolStorage = applicationTools?.storage || storage;
115
+
116
+ /** @param {ConversationRequest} request */
117
+ function prepareTools(request) {
118
+ if (request.completedEnvelope || !catalog || entry.conversation && !applicationTools) return;
119
+ request.tools = createConversationTools({ catalog, context: request.context, signal: active.controller.signal,
120
+ ...(applicationTools ? { prepareContext() {
121
+ if (!request.accepted) throw new Error("The application tool does not belong to an admitted message.");
122
+ return applicationTools.prepareContext(request.context, Object.freeze({ conversationId: entry.id,
123
+ turnId: request.turnId, messageId: request.input.messageId, nativeTurnId: request.nativeTurnId,
124
+ nativeThreadId: request.nativeThreadId, origin: request.input.origin,
125
+ assertCurrent() {
126
+ active.controller.signal.throwIfAborted();
127
+ if (entry.active !== active || active.request !== request || !request.accepted || request.finished) {
128
+ throw failure("This application tool's admitted message is no longer current.", "conversation_application_tool_message_stale", 409, { messageCode: "assistant.conversation.applicationToolMessageStale" });
129
+ }
130
+ },
131
+ ...(entry.conversation ? { assertCompletionCurrent() {
132
+ active.controller.signal.throwIfAborted();
133
+ if (isClosed() || entry.disposed || entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId ||
134
+ !active.finishing || active.timedOut || problem || toolFailure || entry.executionFailure || entry.storageFailure ||
135
+ active.request !== request || !request.accepted || entry.lastAcceptedRequest !== request ||
136
+ entry.active && entry.active !== active) {
137
+ throw failure("This native completion's admitted message is no longer current.", "conversation_native_completion_message_stale", 409, { messageCode: "assistant.conversation.nativeCompletionMessageStale" });
138
+ }
139
+ } } : {}) }));
140
+ } } : {}),
141
+ maximumCalls: maximumToolCalls, discoveryOnly: driver.toolDiscovery === true,
142
+ authorize: () => events.access(request.context, entry.id, "tool", entry),
143
+ save: calls => {
144
+ if (!request.accepted) throw new Error("The engine requested an application tool before admitting the message.");
145
+ return toolStorage.write(entry.id, transaction => saveTools(entry, transaction, request, calls));
146
+ },
147
+ emit: event => events.emit(entry, { ...event, turnId: request.turnId })
148
+ });
149
+ }
150
+
151
+ async function onNativeTurn({ turnId, nativeOwner = false }) {
152
+ if (active.nativeTurnId === turnId) return;
153
+ if (!nativeOwner || !driver.canonicalTranscript) throw new Error("The native continuation has no original conversation owner.");
154
+ await events.access(active.request.context, entry.id, active.request.operation, entry);
155
+ active.controller.signal.throwIfAborted();
156
+ // A native successor turn keeps the admitted request, actor and budget.
157
+ active.nativeTurnId = turnId;
158
+ if (!active.request.nativeTurnId && !entry.conversation) {
159
+ active.request.nativeTurnId = turnId;
160
+ await storage.write(entry.id, async transaction => {
161
+ const turn = await transaction.readTurn(active.request.turnId);
162
+ await transaction.updateTurnMetadata(active.request.turnId, { runtime: { ...turn.metadata.runtime, nativeTurnId: turnId } });
163
+ });
164
+ }
165
+ }
166
+
167
+ async function completeNativeTurn({ turnId, status = "completed", nativeOwner = false }) {
168
+ if (!nativeOwner && active.nativeTurnId !== turnId) throw new Error("The completed native goal turn has a different identity.");
169
+ // The native owner checkpoints from its notification queue. Steering's
170
+ // exact receipt can still be behind that task, so only the outside run
171
+ // continuation joins active.steering; this checkpoint saves admitted work.
172
+ if (!nativeOwner) await active.steering;
173
+ await toolWork;
174
+ if (toolFailure) throw toolFailure;
175
+ const completedStatus = nativeOwner && active.timedOut ? "failed"
176
+ : nativeOwner && active.controller.signal.aborted ? "cancelled" : status === "completed" ? "complete" : status;
177
+ const completedError = nativeOwner && active.timedOut ? "The conversation request exceeded its time limit."
178
+ : nativeOwner && active.controller.signal.aborted ? "Work stopped." : "";
179
+ await storage.write(entry.id, async transaction => {
180
+ const metadata = await transaction.readMetadata();
181
+ const written = [];
182
+ for (const request of active.requests.values()) {
183
+ if (request.turnId && request.accepted && !request.finished &&
184
+ (nativeOwner && driver.canonicalTranscript || request.nativeTurnId === turnId)) {
185
+ const versions = await turnDelivery.writeProgress(active, transaction, request, completedStatus, completedError);
186
+ if (engine === "api") Object.assign(metadata.runtime.seen, versions);
187
+ else written.push(await transaction.readTurn(request.turnId));
188
+ }
189
+ }
190
+ if (nativeOwner) for (const id of await transaction.listTurnIds()) {
191
+ const turn = await transaction.readTurn(id);
192
+ const saved = turn.metadata?.runtime;
193
+ if (saved?.segmentId !== segmentId || saved.nativeTurnId !== turnId || written.some(row => row.turnId === id)) continue;
194
+ if (["cancelled", "failed"].includes(saved.status)) continue;
195
+ await transaction.updateTurnMetadata(id, { runtime: { ...saved, status: completedStatus } });
196
+ written.push(await transaction.readTurn(id));
197
+ }
198
+ await transaction.writeMetadata(metadata);
199
+ if (written.length) await delivery.rememberNative(entry, transaction, written);
200
+ });
201
+ for (const request of active.requests.values()) {
202
+ if (request.accepted && (nativeOwner && driver.canonicalTranscript || request.nativeTurnId === turnId)) {
203
+ request.finished = request.sealed = true;
204
+ }
205
+ }
206
+ streams.clear(entry.id);
207
+ }
208
+ if (driver.canonicalTranscript && !entry.conversation) active.nativeCheckpoint = input => completeNativeTurn({ ...input, nativeOwner: true });
209
+
210
+ async function onMessage(message) {
211
+ const request = messageOwners.get(message.id) || active.request;
212
+ if (!request.accepted) throw new Error("The engine produced output before admitting the message.");
213
+ messageOwners.set(message.id, request);
214
+ // An already visible native item keeps its original request identity. Late
215
+ // snapshots cannot replay that answer after a newer instruction is admitted.
216
+ if (request.sealed) return;
217
+ await events.setPhase(entry, "working", request.turnId);
218
+ if (request.sealed) return;
219
+ const messageId = `${request.turnId}:${message.id}`;
220
+ const finalReplyLimit = completedEnvelope && request.completedEnvelope && request.input.origin === "application" && !request.input.goal
221
+ ? maximumCompletedOutput : maximumFinalReply;
222
+ if (message.role === "assistant" && message.complete && message.text.length > finalReplyLimit) {
223
+ if (request.answer?.messageId === messageId) request.answer = null;
224
+ throw new Error("The assistant exceeded the configured final reply limit.");
225
+ }
226
+ const previous = request.answer?.messageId === messageId ? request.answer : request.progress.get(messageId);
227
+ const outputOwner = message.outputId && messageOwners.get(message.outputId) || request;
228
+ const outputId = previous?.outputId || (message.outputId ? `${outputOwner.turnId}:${message.outputId}` : "");
229
+ const value = { messageId, ...(outputId ? { outputId } : {}),
230
+ role: message.role, text: message.text, at: previous?.at || new Date().toISOString() };
231
+ const authorship = request.completedEnvelope ? { authorship: { messageId: request.input.messageId,
232
+ turnId: request.turnId, origin: request.input.origin, completedEnvelope: true } } : {};
233
+ // Some native streams have temporary block identities. Only their final
234
+ // history snapshots belong in the transcript.
235
+ if (message.transient) {
236
+ streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
237
+ } else if (message.role === "assistant") {
238
+ if (request.answer && request.answer.messageId !== messageId) {
239
+ request.progress.set(request.answer.messageId, { ...request.answer, role: "commentary" });
240
+ streams.complete(entry.id, request.answer.messageId);
241
+ }
242
+ request.progress.delete(messageId);
243
+ request.answer = value;
244
+ streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
245
+ } else {
246
+ if (request.answer?.messageId === messageId) request.answer = null;
247
+ if (message.complete) request.progress.set(messageId, value);
248
+ if (message.complete) streams.complete(entry.id, messageId);
249
+ else streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
250
+ }
251
+ await events.emit(entry, { type: "message", turnId: request.turnId, messageId, origin: request.input.origin, role: message.role,
252
+ ...(outputId ? { outputId } : {}),
253
+ text: message.text, status: message.complete ? "complete" : "inProgress", streaming: streams.read(entry.id) });
254
+ }
255
+
256
+ active.submit = async request => {
257
+ try {
258
+ prepareTools(request);
259
+ await turnDelivery.deliverNative(entry, active, request, entry.provider.steer);
260
+ } catch (error) {
261
+ let reported = error;
262
+ if (!request.admissionSettled) {
263
+ try {
264
+ if (!entry.conversation) await storage.write(entry.id, async transaction => {
265
+ const metadata = await transaction.readMetadata();
266
+ if (metadata.runtime.request?.messageId === request.input.messageId) {
267
+ metadata.runtime.request.error = error.message;
268
+ await transaction.writeMetadata(metadata);
269
+ }
270
+ });
271
+ } catch (saveError) { reported = saveError; }
272
+ request.admission.reject(reported);
273
+ if (!request.accepted) active.requests.delete(request.input.messageId);
274
+ }
275
+ if (error.delivery === "uncertain") active.steerError = error.message;
276
+ }
277
+ };
278
+
279
+ try {
280
+ await events.setPhase(entry, "preparing");
281
+ const metadata = { runtime: await entries.readState(entry) };
282
+ const continuity = metadata.runtime.continuity;
283
+ // Existing application transcripts precede the first runtime segment.
284
+ // After replacement, their context is already carried by the briefing.
285
+ const inSegment = turn => turn.metadata?.runtime?.segmentId === segmentId ||
286
+ !turn.metadata?.runtime?.segmentId && !continuity;
287
+ // Only the API engine sends the application transcript as history; after
288
+ // a compaction, only the turns its summary doesn't cover.
289
+ const summary = engine === "api" ? await reads.readSummary(entry, inSegment) : null;
290
+ const recordedHistory = engine === "api" ? await reads.readTranscript(entry, summary?.throughTurnId) : [];
291
+ const activeHistory = recordedHistory.filter(turn => !turn.metadata?.runtime?.supersededBy);
292
+ const history = activeHistory.filter(inSegment);
293
+ prepareTools(initial);
294
+ const tools = initial.tools && { descriptors: initial.tools.descriptors, schemas: initial.tools.schemas, maximumArgumentBytes: initial.tools.maximumArgumentBytes,
295
+ execute(input, options) {
296
+ const request = toolOwners.get(input.id) || (options?.messageId ? active.requests.get(options.messageId) : active.request);
297
+ if (!request?.accepted) throw new Error("The application tool does not belong to an admitted message.");
298
+ toolOwners.set(input.id, request);
299
+ // Calls already in flight retain their actor and turn. Keep application
300
+ // mutations ordered across steering as well as within each request.
301
+ const work = toolWork.then(() => request.tools.execute(input, options));
302
+ toolWork = work.catch(error => {
303
+ // Claude accepts a separate successor query; its predecessor's
304
+ // effect cannot fail that query. Other engines keep one active-turn
305
+ // failure guard across steering.
306
+ if (engine !== "claude" || active.request === request) toolFailure ||= error;
307
+ });
308
+ return work;
309
+ }
310
+ };
311
+ const completedToolSet = engine === "claude" && initial.completedEnvelope && typeof initial.nativeToolAttempt === "boolean"
312
+ ? catalog.resolveToolSet(initial.context) : null;
313
+ const providerInput = { configuration, context: initial.context, tools,
314
+ ...(completedToolSet ? {
315
+ async onNativeToolUse({ messageId, threadId, name }) {
316
+ await initial.deliveryCommitted;
317
+ active.controller.signal.throwIfAborted();
318
+ if (entry.disposed || entry.active !== active || active.request !== initial || !initial.accepted || initial.finished || initial.sealed ||
319
+ entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId ||
320
+ messageId !== initial.input.messageId || threadId !== initial.nativeThreadId) {
321
+ throw failure("The native tool observation belongs to a retired response.", "conversation_tool_observation_retired", 409, { messageCode: "assistant.conversation.toolObservationRetired" });
322
+ }
323
+ if (!completedToolSet.byName.has(name)) return;
324
+ await storage.write(entry.id, async transaction => {
325
+ const metadata = await transaction.readMetadata();
326
+ const turn = await transaction.readTurn(initial.turnId);
327
+ const saved = turn?.metadata?.runtime;
328
+ if (metadata.runtime.engine !== engine || metadata.runtime.segmentId !== segmentId ||
329
+ conversationNativeIdentity(metadata.runtime) !== threadId || submittedMessage(turn || {})?.messageId !== messageId ||
330
+ saved?.engine !== engine || saved.segmentId !== segmentId || saved.completedEnvelope !== true ||
331
+ typeof saved.nativeToolAttempt !== "boolean" || saved.status !== "running" || saved.supersededBy ||
332
+ entry.active !== active || active.request !== initial || initial.finished || initial.sealed || active.controller.signal.aborted) {
333
+ throw failure("The native tool observation lost its exact admitted receipt.", "conversation_tool_observation_receipt_lost", 409, { messageCode: "assistant.conversation.toolObservationReceiptLost" });
334
+ }
335
+ await transaction.updateTurnMetadata(initial.turnId, { runtime: { ...saved, nativeToolAttempt: true } });
336
+ });
337
+ initial.nativeToolAttempt = true;
338
+ }
339
+ } : {}),
340
+ ...(engine === "opencode" || entry.conversation && ["codex", "claude"].includes(engine) ? {
341
+ async finishIfCurrent(isCurrent) {
342
+ // A reserved Send may still be preparing when the native turn ends.
343
+ // Join that same admission outside the native notification queue.
344
+ for (;;) {
345
+ const steering = active.steering;
346
+ await steering;
347
+ if (active.steering && active.steering !== steering) continue;
348
+ if (!isCurrent()) return false;
349
+ active.finishing = true;
350
+ return true;
351
+ }
352
+ }
353
+ } : {}),
354
+ onMessage, onNativeTurn,
355
+ onEvent: async event => {
356
+ if (event.type === "message-complete") {
357
+ const request = messageOwners.get(event.messageId);
358
+ if (!request) return;
359
+ const messageId = `${request.turnId}:${event.messageId}`;
360
+ streams.complete(entry.id, messageId);
361
+ return events.emit(entry, { ...event, turnId: request.turnId, messageId, streaming: streams.read(entry.id) });
362
+ }
363
+ if (event.type === "goal") {
364
+ if (!driver.canonicalTranscript && event.goal?.status === "active" && !active.goalOwner) {
365
+ await events.access(active.request.context, entry.id, "goal", entry);
366
+ await storage.write(entry.id, async transaction => {
367
+ const turn = await transaction.readTurn(active.request.turnId);
368
+ await transaction.updateTurnMetadata(active.request.turnId, { runtime: { ...turn.metadata.runtime, goalMessageId: active.request.input.messageId } });
369
+ });
370
+ active.goalOwner = active.request;
371
+ active.request.goalMessageId = active.request.input.messageId;
372
+ }
373
+ entry.goal = event.goal;
374
+ }
375
+ return event.type === "phase" ? events.setPhase(entry, event.phase, active.request.turnId) : events.emit(entry, { ...event, turnId: active.request.turnId });
376
+ }
377
+ };
378
+ if (engine === "api") {
379
+ const readAttachments = turnDelivery.attachmentReader(entry, active, initial);
380
+ const files = await readAttachments(initial.input.attachments);
381
+ initial.input.attachments = files.attachments;
382
+ const selectedHistory = apiHistory ? structuredClone(await apiHistory({ context: initial.context })) : undefined;
383
+ let requestHistory;
384
+ if (selectedHistory !== undefined) {
385
+ if (!Array.isArray(selectedHistory)) throw new TypeError("API request history must be an array of authored user/assistant context rows.");
386
+ requestHistory = [];
387
+ for (const message of selectedHistory) {
388
+ if (!["user", "assistant"].includes(message?.role) || typeof message.content !== "string") {
389
+ throw new TypeError("API request history requires user/assistant roles and text content.");
390
+ }
391
+ const attachmentIds = conversationAttachmentIds(message.attachmentIds);
392
+ if (message.role !== "user" && attachmentIds.length) throw new TypeError("Only authored user context may reference attachments.");
393
+ const previousFiles = await readAttachments(attachmentIds.map(attachmentId => ({ attachmentId })));
394
+ requestHistory.push({ role: message.role, content: previousFiles.content.length
395
+ ? [{ type: "text", text: message.content }, ...previousFiles.content] : message.content });
396
+ }
397
+ } else {
398
+ // Files go to the model again only within the newest messages; older
399
+ // turns name theirs, so a long chat's files never outgrow one request.
400
+ let newer = 0;
401
+ for (const turn of history.toReversed()) {
402
+ newer += turn.messages.filter(message => ["user", "system", "assistant"].includes(message.role)).length;
403
+ const message = submittedMessage(turn);
404
+ if (!message?.attachments?.length) continue;
405
+ message.content = newer <= HISTORY_FILE_MESSAGES ? (await readAttachments(message.attachments)).content
406
+ : [{ type: "text", text: `(Files attached to this message, no longer shown: ${message.attachments.map(file => file.fileName).join(", ")}.)` }];
407
+ }
408
+ }
409
+ // The versions saved before a summary still stand for the turns it covers.
410
+ const seen = summary ? { ...metadata.runtime.seen, ...conversationHistoryVersions(activeHistory) }
411
+ : conversationHistoryVersions(activeHistory);
412
+ const priorFiles = await readAttachments(metadata.runtime.continuityAttachments || []);
413
+ const input = { ...initial.input, text: conversationRequestText(initial.input), contextText: "",
414
+ content: [...priorFiles.content, ...files.content] };
415
+ await storage.write(entry.id, async transaction => {
416
+ const metadata = await transaction.readMetadata();
417
+ metadata.runtime.request.seen = seen;
418
+ metadata.runtime.request.attachments = files.attachments;
419
+ await transaction.writeMetadata(metadata);
420
+ });
421
+ const completion = await entry.provider.run({ ...providerInput, input, signal: active.controller.signal, continuity, history, requestHistory,
422
+ beforeDispatch: () => turnDelivery.beforeDispatch(entry, active, initial), accept: () => turnDelivery.admit(entry, active, initial, seen),
423
+ ...(entry.conversation ? {} : { summary: summary?.text || "",
424
+ saveSummary: saved => storage.write(entry.id, transaction => transaction.writeSummary(saved)) }) });
425
+ if (completion?.metadata && Object.keys(completion.metadata).length) {
426
+ await storage.write(entry.id, transaction => transaction.updateTurnMetadata(initial.turnId, { completion: completion.metadata }));
427
+ }
428
+ } else {
429
+ await turnDelivery.deliverNative(entry, active, initial, input => entry.provider.run({ ...providerInput, ...input }));
430
+ }
431
+ active.controller.signal.throwIfAborted();
432
+ } catch (error) { problem = error; }
433
+ active.finishing = true;
434
+ await active.steering;
435
+ await toolWork;
436
+ problem ||= toolFailure;
437
+ clearTimeout(active.timer);
438
+ if (problem?.cleanupFailed) {
439
+ entry.executionFailure = problem;
440
+ entry.cleanupTurnId = active.request.turnId;
441
+ }
442
+ const status = entry.executionFailure ? "interrupted" : active.timedOut ? "failed" : active.controller.signal.aborted ? "cancelled"
443
+ : problem?.status === "interrupted" ? "interrupted" : problem ? "failed" : initial.recovered?.status || "complete";
444
+ const error = entry.executionFailure?.message || (active.timedOut ? "The conversation request exceeded its time limit."
445
+ : status === "cancelled" ? "Work stopped." : problem?.message || active.steerError || initial.recovered?.error || "");
446
+ entry.saveProgress = async () => {
447
+ if (entry.conversation) {
448
+ if (applicationTools) await toolStorage.write(entry.id, async transaction => {
449
+ for (const request of active.requests.values()) {
450
+ if (request.accepted && request.tools?.records().length) await saveTools(entry, transaction, request, request.tools.records());
451
+ }
452
+ });
453
+ // Only opted-in application receipts are saved above. Native output,
454
+ // checkpoints and status remain with the native run owner and sender.
455
+ entry.error = error;
456
+ entry.storageFailure = null;
457
+ entry.saveProgress = null;
458
+ return { type: "settled", turnId: active.request.turnId, status, error, ...stateErrorFields(error) };
459
+ }
460
+ // A storage retry repeats each failed rejection save as it was; it never
461
+ // deletes or rebuilds that save's frozen draft.
462
+ for (const [messageId, save] of active.rejectionSaves) {
463
+ const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
464
+ if (runtime.request?.messageId === messageId) await save();
465
+ active.rejectionSaves.delete(messageId);
466
+ }
467
+ await storage.write(entry.id, async transaction => {
468
+ const metadata = await transaction.readMetadata();
469
+ const written = [];
470
+ for (const request of active.requests.values()) {
471
+ if (request.accepted && !request.finished) {
472
+ const versions = await turnDelivery.writeProgress(active, transaction, request, status, error);
473
+ if (engine === "api") Object.assign(metadata.runtime.seen, versions);
474
+ else written.push(await transaction.readTurn(request.turnId));
475
+ }
476
+ }
477
+ if (!initial.accepted && metadata.runtime.request?.messageId === initial.input.messageId) {
478
+ if (engine !== "api" || problem?.delivery === "uncertain") metadata.runtime.request.error = error;
479
+ else delete metadata.runtime.request;
480
+ }
481
+ await transaction.writeMetadata(metadata);
482
+ if (written.length) await delivery.rememberNative(entry, transaction, written);
483
+ });
484
+ streams.clear(entry.id);
485
+ entry.error = error;
486
+ entry.storageFailure = null;
487
+ entry.saveProgress = null;
488
+ return { type: "settled", turnId: active.request.turnId, status, error: entry.error, ...stateErrorFields(entry.error) };
489
+ };
490
+ try { await entry.saveProgress(); }
491
+ catch (saveError) {
492
+ entry.error = "Conversation progress could not be saved. Restore access to its storage before continuing.";
493
+ entry.storageFailure = failure(entry.error, "conversation_storage_unavailable", 503);
494
+ entry.storageFailure.cause = saveError;
495
+ problem ||= saveError;
496
+ }
497
+ if (!initial.admissionSettled) initial.admission.reject(problem || new Error("The engine did not acknowledge the message."));
498
+ entry.active = null;
499
+ await events.setPhase(entry, "", active.request.turnId);
500
+ await events.emit(entry, { type: "settled", turnId: active.request.turnId, status: entry.storageFailure || entry.executionFailure ? "failed" : status,
501
+ error: entry.error, ...stateErrorFields(entry.error) });
502
+ }
503
+
504
+ return Object.freeze({
505
+ start
506
+ });
507
+ }
508
+
509
+ /** @typedef {ReturnType<typeof createConversationTurns>} ConversationTurns */
510
+
511
+ export { createConversationTurns, saveTools };
@@ -9,6 +9,14 @@ const stages = new Set([
9
9
  ]);
10
10
  const hostStagePattern = /^[a-z][a-z-]{0,63}$/u;
11
11
 
12
+ /** The stages of a host that declares none of its own. */
13
+ const NO_HOST_STAGES = Object.freeze([]);
14
+
15
+ /** The monotonic time a delivery's stages are measured against. */
16
+ function monotonicNow() {
17
+ return performance.now();
18
+ }
19
+
12
20
  export function normalizeConversationDeliveryHostStages(hostStages = []) {
13
21
  if (!Array.isArray(hostStages) || hostStages.some(stage => typeof stage !== "string" || !hostStagePattern.test(stage))) {
14
22
  throw new TypeError("Host delivery stages must be lowercase hyphenated names.");
@@ -16,9 +24,20 @@ export function normalizeConversationDeliveryHostStages(hostStages = []) {
16
24
  return Object.freeze([...new Set(hostStages)]);
17
25
  }
18
26
 
19
- // Observer results are never awaited and failures cannot change delivery.
20
- export function createConversationDeliveryTiming({ engine, conversationId, messageId, report, hostStages = [],
21
- clock = () => performance.now() }) {
27
+ /**
28
+ * Records how long one message's delivery spends in each stage. Observer results are never
29
+ * awaited and failures cannot change delivery.
30
+ * @param {object} options
31
+ * @param {string} options.engine
32
+ * @param {string} options.conversationId
33
+ * @param {string | undefined} options.messageId
34
+ * @param {import("../types.js").ConversationDeliveryTimingObserver | undefined} options.report Receives each stage; without one nothing is recorded.
35
+ * @param {readonly string[]} [options.hostStages] The host's own preparation stages.
36
+ * @param {() => number} [options.clock]
37
+ * @returns {(stage: string) => void}
38
+ */
39
+ export function createConversationDeliveryTiming({ engine, conversationId, messageId, report,
40
+ hostStages = NO_HOST_STAGES, clock = monotonicNow }) {
22
41
  const startedAt = clock();
23
42
  let previousAt = startedAt;
24
43
  let previousStage = "";
@@ -35,3 +54,6 @@ export function createConversationDeliveryTiming({ engine, conversationId, messa
35
54
  catch { /* Instrumentation never changes admission or provider work. */ }
36
55
  };
37
56
  }
57
+
58
+ /** The delivery timing of a request that records none, such as a goal's message command. */
59
+ export function recordNoDeliveryTiming() {}
@@ -3,10 +3,12 @@ import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promis
3
3
  import { join, resolve } from "node:path";
4
4
  import { createConversationStorage } from "./storage.js";
5
5
 
6
- /** Persistent single-process storage. The application supplies a private directory
6
+ /**
7
+ * Persistent single-process storage. The application supplies a private directory
7
8
  * and shares one instance; multiple writers need a transactional application store.
9
+ * @param {{ directory: string }} options
8
10
  */
9
- export function createFileConversationStorage({ directory } = {}) {
11
+ export function createFileConversationStorage({ directory }) {
10
12
  if (typeof directory !== "string" || !directory.trim()) {
11
13
  throw new TypeError("File conversation storage requires a directory.");
12
14
  }
@@ -26,10 +28,13 @@ export function createFileConversationStorage({ directory } = {}) {
26
28
  typeof entry[0] !== "string" || !entry[0] || !Array.isArray(entry[1]?.messages) ||
27
29
  entry[1].messages.some((message) => !message || typeof message.role !== "string" ||
28
30
  typeof message.text !== "string" || typeof message.at !== "string")) ||
29
- new Set(document.turns.map(([turnId]) => turnId)).size !== document.turns.length) {
31
+ new Set(document.turns.map(([turnId]) => turnId)).size !== document.turns.length ||
32
+ (document.summary !== undefined && (typeof document.summary?.text !== "string" ||
33
+ !document.turns.some(([turnId]) => turnId === document.summary.throughTurnId)))) {
30
34
  throw new Error("Conversation storage has an unsupported or damaged format. Restore it from a backup before writing.");
31
35
  }
32
- return { turns: new Map(document.turns), metadata: document.metadata || {} };
36
+ return { turns: new Map(document.turns), metadata: document.metadata || {},
37
+ ...(document.summary ? { summary: document.summary } : {}) };
33
38
  }
34
39
 
35
40
  return createConversationStorage({
@@ -45,10 +50,10 @@ export function createFileConversationStorage({ directory } = {}) {
45
50
  throw error;
46
51
  }
47
52
  },
48
- async writeRecord(id, { turns, metadata }) {
53
+ async writeRecord(id, { turns, metadata, summary }) {
49
54
  // Serialize before touching the filesystem: failed transactions cannot
50
55
  // replace the last saved conversation or leave a partial document.
51
- const body = JSON.stringify({ version: 1, scope: id, metadata, turns: [...turns] });
56
+ const body = JSON.stringify({ version: 1, scope: id, metadata, turns: [...turns], ...(summary ? { summary } : {}) });
52
57
  await mkdir(root, { recursive: true, mode: 0o700 });
53
58
  const path = filename(id);
54
59
  const temporary = `${path}.${randomUUID()}.tmp`;
@@ -1,5 +1,17 @@
1
- /** A generic native-hook hand-off; neither the source format nor the host is known here. */
2
- export function createConversationHookBridge({ resolveConversation, readInstructions } = {}) {
1
+ /** The instruction resolver of a host that delivers no instructions through hooks. */
2
+ function refuseHookInstructions() {
3
+ throw new TypeError("Hook delivery requires an instruction resolver.");
4
+ }
5
+
6
+ /**
7
+ * A generic native-hook hand-off; neither the source format nor the host is known here.
8
+ * @param {object} options
9
+ * @param {(request: { scope: string }) => Promise<{ delivery: "native" | "hook" } | null>} options.resolveConversation
10
+ * The conversation a native hook request belongs to, or null when it is not one of the host's.
11
+ * @param {(binding: object, request: object) => Promise<string>} [options.readInstructions] Required for hook delivery.
12
+ */
13
+ export function createConversationHookBridge({ resolveConversation, readInstructions = refuseHookInstructions }) {
14
+ // readable-check: allow method-probe - input validation: the host passes the resolver of its conversation bindings
3
15
  if (typeof resolveConversation !== "function") {
4
16
  throw new TypeError("Conversation hook delivery requires a binding resolver.");
5
17
  }
@@ -11,7 +23,6 @@ export function createConversationHookBridge({ resolveConversation, readInstruct
11
23
  // the delivery path. Suppress only this positively identified conversation.
12
24
  if (binding.delivery === "native") return { kind: "delivery", text: "" };
13
25
  if (binding.delivery !== "hook") throw new TypeError("Unknown conversation instruction delivery mode.");
14
- if (typeof readInstructions !== "function") throw new TypeError("Hook delivery requires an instruction resolver.");
15
26
  const text = await readInstructions(binding, request);
16
27
  if (typeof text !== "string" || !text.trim()) throw new TypeError("Managed conversation instructions are unavailable.");
17
28
  return { kind: "delivery", text };
@@ -12,6 +12,6 @@ export { normalizeConversationConfiguration } from "./configuration.js";
12
12
  export { upgradeConversationRuntimeState, prepareConversationRuntimeCopy } from "./runtimeStateUpgrade.js";
13
13
  export { createConversationHookBridge } from "./hookBridge.js";
14
14
  export { nativeConversationStoragePolicy } from "./nativeStorage.js";
15
- export { createConversationProcessIdentity } from "./localExecution.js";
15
+ export { createConversationProcessIdentity } from "./processIdentity.js";
16
16
  export { validateConversationOutputSchema } from "./structuredOutput.js";
17
17
  export { conversationAttachmentIds } from "./attachments.js";