@jskit-ai/assistant-core 0.2.7 → 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 +13 -10
  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,464 @@
1
+ import { codexAppServerFrozenTurnInterruptResponse } from "../codexTurn.js";
2
+ import { assertCodexApplicationToolSchemaIdentity, codexApplicationToolConfiguration } from "../codexProviderThreads.js";
3
+ import { codexLocalImageInput } from "../codexProviderTurns.js";
4
+ import { jsonHash as hash } from "../normalize.js";
5
+
6
+ /** The goal a conversation reports, from Codex's native goal. */
7
+ export const goalValue = goal => goal ? {
8
+ id: hash([goal.threadId, goal.createdAt, goal.objective]), objective: goal.objective, status: goal.status,
9
+ tokenBudget: goal.tokenBudget ?? null, tokensUsed: goal.tokensUsed, timeUsedSeconds: goal.timeUsedSeconds,
10
+ ...(Number.isFinite(goal.createdAt) ? { createdAt: new Date(goal.createdAt * 1000).toISOString() } : {}),
11
+ ...(Number.isFinite(goal.updatedAt) ? { updatedAt: new Date(goal.updatedAt * 1000).toISOString() } : {})
12
+ } : null;
13
+
14
+ const nativeInput = input => [
15
+ ...(input.text || input.attachmentManifest ? [{ type: "text", text: `${input.text || ""}${input.attachmentManifest || ""}` }] : []),
16
+ ...(input.content || []).map(part => part.type === "text" ? part :
17
+ { type: "image", url: `data:${part.mediaType};base64,${part.image.toString("base64")}` }),
18
+ ...codexLocalImageInput(input.localFiles)
19
+ ];
20
+
21
+ // A supplied native message without its own rejection step leaves rejection to this conversation.
22
+ async function rejectNoNativePrompt() {}
23
+
24
+ /**
25
+ * The turns of one driver conversation: message delivery, goal context, interruption and the run of a turn. It
26
+ * reads and updates the conversation's session record and uses its native connection.
27
+ * @param {object} dependencies
28
+ * @param {import("./codexDriver.js").CodexDriverSession} dependencies.session
29
+ * @param {object | undefined} dependencies.supplied
30
+ * @param {object} dependencies.conversation
31
+ * @param {string} dependencies.sessionId
32
+ * @param {string} dependencies.providerKey
33
+ * @param {import("../codexProviderOwner.js").CodexAppServerProviderOwner} dependencies.providerOwner
34
+ * @param {import("../codexRuntimeLifecycle.js").CodexRunOwner} dependencies.owner
35
+ * @param {object} dependencies.runtime
36
+ * @param {(update: (binding: object) => object) => Promise<void>} dependencies.updateBinding
37
+ * @param {() => Promise<void>} dependencies.drainNotifications
38
+ * @param {ReturnType<typeof import("./codexDriverConnection.js").createCodexDriverConnection>} dependencies.connection
39
+ */
40
+ export function createCodexDriverTurns({
41
+ session,
42
+ supplied,
43
+ conversation,
44
+ sessionId,
45
+ providerKey,
46
+ providerOwner,
47
+ owner,
48
+ runtime,
49
+ updateBinding,
50
+ drainNotifications,
51
+ connection
52
+ }) {
53
+ const { stop, selection, connect, prepareConfiguration, prepare } = connection;
54
+
55
+ function deliveryInput(input, beforeDispatch, rejected) {
56
+ if (supplied) {
57
+ const { onPromptRejected = rejectNoNativePrompt } = input.nativeMessage;
58
+ return { ...input.nativeMessage,
59
+ onPromptSending: beforeDispatch,
60
+ onPromptRejected: async () => { await onPromptRejected(); await rejected(); } };
61
+ }
62
+ return { messageId: input.messageId, message: input.text,
63
+ displayMessage: input.authoredInput?.text ?? input.text,
64
+ displayAttachments: input.attachments, attachments: input.localFiles,
65
+ onPromptSending: value => beforeDispatch({ threadId: value.threadId }), onPromptRejected: rejected };
66
+ }
67
+
68
+ async function deliveredRow(input, nativeIdentity) {
69
+ const written = await owner.writeDeliveredUserMessage(runtime, sessionId,
70
+ input.authoredInput?.text ?? input.text, input.messageId, null, input.attachments,
71
+ nativeIdentity);
72
+ if (written) return written;
73
+ const saved = (await runtime.store.readConversationLog(sessionId)).find(turn =>
74
+ (turn.user || turn.system)?.messageId === input.messageId);
75
+ if (!saved) throw new Error("The accepted native receipt has no authored conversation message.");
76
+ return saved;
77
+ }
78
+
79
+ async function withDelivery(command, operation) {
80
+ const input = command.input;
81
+ return owner.withMessageDelivery(sessionId, input.messageId, {
82
+ attachments: input.attachments, text: input.authoredInput?.text ?? input.text,
83
+ turnMetadata: input.nativeMessage?.turnMetadata || null, actorContext: input.nativeMessage?.actorContext || null,
84
+ authoredInput: input.authoredInput,
85
+ onNativeReceipt: command.onNativeReceipt,
86
+ onReceiptPersisted: command.onReceiptPersisted
87
+ }, operation);
88
+ }
89
+
90
+ function messagePreparation(configuration, context, signal, tools) {
91
+ if (supplied) return tools ? { ...supplied.messagePreparation, applicationTools: tools,
92
+ providerReady: session.current.providerReady } : supplied.messagePreparation;
93
+ return {
94
+ async readContext() {
95
+ const runtimeSession = await runtime.getSession(sessionId);
96
+ return { runtime, session: runtimeSession, selection: {
97
+ runtime, session: runtimeSession, threadId: () => session.binding.threadId,
98
+ async acquireProvider() {
99
+ const reused = Boolean(session.native?.isAvailable());
100
+ await connect(signal, await selection(configuration, context, signal));
101
+ return { provider: session.native, reused };
102
+ }
103
+ } };
104
+ },
105
+ threadPreparation() {
106
+ return {
107
+ get failureThreadId() { return session.binding.threadId; },
108
+ async provider() {
109
+ const { selected, params, toolSchemaIdentity } = await prepareConfiguration(configuration, context, signal, tools);
110
+ const retainedThreadId = session.binding.threadId;
111
+ return {
112
+ provider: session.native,
113
+ workdir: session.binding.workdir,
114
+ observerOptions: { providerKey },
115
+ requirePaginatedHistory: !retainedThreadId,
116
+ settings: { ...selected, resumeOptions: params },
117
+ preparation: {
118
+ settings: async () => ({ threadSettings: params, threadStartSettings: params }),
119
+ identity: { read: () => retainedThreadId,
120
+ async write({ threadId }) { await updateBinding({ threadId, toolSchemaIdentity }); } }
121
+ }
122
+ };
123
+ }
124
+ };
125
+ },
126
+ async prepareMessage(input, prepared, { starting }) {
127
+ if (!starting) return null;
128
+ const { settings } = prepared;
129
+ return { renderedPrompt: input.message,
130
+ get turnSettings() { return { cwd: session.binding.workdir,
131
+ ...(settings.model ? { model: settings.model } : {}), effort: configuration.effort || null,
132
+ ...(configuration.outputSchema ? { outputSchema: configuration.outputSchema } : {}) }; }
133
+ };
134
+ },
135
+ async finishMessage(input, prepared, outcome) {
136
+ if (Object.hasOwn(outcome, "error")) throw outcome.error;
137
+ }
138
+ };
139
+ }
140
+
141
+ async function acceptMessage(command, nativeIdentity, row, nativeResult) {
142
+ if (typeof nativeIdentity?.threadId !== "string" || !nativeIdentity.threadId.trim() ||
143
+ typeof nativeIdentity?.turnId !== "string" || !nativeIdentity.turnId.trim()) {
144
+ throw Object.assign(new Error("The native delivery has no exact turn identity. Inspect this message's receipt before continuing."), {
145
+ code: "conversation_delivery_uncertain", delivery: "uncertain"
146
+ });
147
+ }
148
+ const { turnId } = nativeIdentity;
149
+ const active = session.current;
150
+ if (!active) throw new Error("The native delivery lost its active conversation owner.");
151
+ active.inputId = command.input.messageId;
152
+ if (!active.turnId) active.turnId = turnId;
153
+ await command.accept({ nativeTurnId: turnId, conversationTurn: row || await deliveredRow(command.input, nativeIdentity),
154
+ ...(supplied ? { nativeResult } : {}) });
155
+ active.admitted = true;
156
+ active.admission.resolve();
157
+ if (active.pendingGoal !== undefined) {
158
+ const goal = active.pendingGoal;
159
+ delete active.pendingGoal;
160
+ await active.onEvent({ type: "goal", goal: goalValue(goal) });
161
+ }
162
+ if (active.pendingNativeTurn) {
163
+ const next = active.pendingNativeTurn;
164
+ delete active.pendingNativeTurn;
165
+ await active.onNativeTurn({ turnId: next, nativeOwner: true });
166
+ }
167
+ }
168
+
169
+ async function sendMessage(command, configuration, context, signal, tools) {
170
+ return withDelivery(command, async () => {
171
+ let attempted = false;
172
+ let rejected = false;
173
+ let accepted = false;
174
+ const input = deliveryInput(command.input, async value => {
175
+ await command.beforeDispatch(value);
176
+ signal.throwIfAborted();
177
+ attempted = true;
178
+ session.current.dispatched = true;
179
+ }, () => { rejected = true; });
180
+ try {
181
+ const delivered = await owner.dispatchMessage(sessionId, input, {
182
+ ...(supplied ? { ...context, actorContext: input.actorContext || null } : { preparedInput: nativeInput(command.input) })
183
+ }, messagePreparation(configuration, context, signal, tools));
184
+ const { value, nativeIdentity } = delivered;
185
+ if (supplied ? value.delivered === true : value.delivered || nativeIdentity) {
186
+ await acceptMessage(command, nativeIdentity || { threadId: value.threadId, turnId: value.turnId }, value.conversationTurn, delivered);
187
+ accepted = true;
188
+ if (value.ok === false) throw new Error(value.error);
189
+ return delivered;
190
+ }
191
+ // The host's main sender keeps its pending evidence when the
192
+ // command returns delivered:false, even if a native ID is known.
193
+ if (supplied) return delivered;
194
+ throw Object.assign(new Error(value?.error || "Codex is not ready to receive this instruction."), value);
195
+ } catch (error) {
196
+ if (attempted && !accepted && !rejected && error.delivery !== "not-sent" && !Number.isInteger(error.code)) error.delivery = "uncertain";
197
+ throw error;
198
+ }
199
+ });
200
+ }
201
+
202
+ async function goalContext(configuration, context, signal, { prepareThread = false, tools } = {}) {
203
+ if (supplied) return owner.prepareGoalContext(sessionId, context, supplied.goalPreparation, { prepareThread });
204
+ let selected;
205
+ if (prepareThread && !session.binding.threadId) selected = await prepare(configuration, context, signal, tools);
206
+ else if (prepareThread) {
207
+ const prepared = await prepareConfiguration(configuration, context, signal, tools);
208
+ selected = { ...prepared.selected, resumeOptions: prepared.params };
209
+ } else {
210
+ selected = await selection(configuration, context, signal);
211
+ await connect(signal, selected);
212
+ }
213
+ return { runtime, session: await runtime.getSession(sessionId), provider: session.native,
214
+ threadId: session.binding.threadId, providerKey, resumeOptions: selected.resumeOptions };
215
+ }
216
+
217
+ async function interrupt({ configuration, context, input = {}, representation } = {}) {
218
+ if (supplied) {
219
+ const admission = supplied.admission();
220
+ if (admission.ok === false) {
221
+ const value = codexAppServerFrozenTurnInterruptResponse({
222
+ threadId: input.threadId || input.codexSessionId, turnId: input.turnId || input.codexTurnId
223
+ });
224
+ return representation === "native" ? { value } : value;
225
+ }
226
+ try {
227
+ const prepared = await supplied.controlPreparation(context, input);
228
+ if (prepared.ok === false) {
229
+ if (representation === "native") return { value: prepared };
230
+ throw Object.assign(new Error(prepared.error), prepared, { cleanupFailed: true });
231
+ }
232
+ const result = await owner.interruptTurn(sessionId, input, owner.controlContext(sessionId, prepared));
233
+ if (representation === "native") return result;
234
+ if (result.value?.ok === false) throw Object.assign(new Error(result.value.error), result.value, { cleanupFailed: true });
235
+ return result.value;
236
+ } finally { admission.release(); }
237
+ }
238
+ if (session.observationError) {
239
+ await stop();
240
+ return { ok: true, interrupted: true };
241
+ }
242
+ const result = await owner.interruptTurn(sessionId, {}, {
243
+ runtime, admissionError: () => null, threadId: () => session.binding.threadId,
244
+ observationOwner: () => session.native,
245
+ async acquireProvider() {
246
+ if (!session.native && configuration) {
247
+ const signal = AbortSignal.timeout(30_000);
248
+ await connect(signal, await selection(configuration, context, signal));
249
+ }
250
+ if (!session.native) throw new Error("Codex's retained runtime owner is unavailable.");
251
+ await session.native.ensureAvailable();
252
+ return session.native;
253
+ }
254
+ }).catch(error => {
255
+ // A thrown control error also leaves this native owner unconfirmed.
256
+ if (error === session.current?.toolFailure && !session.current.signal.aborted && !error.cleanupFailed) {
257
+ session.current.toolFailureCleanup = error;
258
+ }
259
+ error.cleanupFailed = true;
260
+ throw error;
261
+ });
262
+ if (result.value?.ok === false) throw Object.assign(new Error(result.value.error || "Codex could not confirm that work stopped."), { cleanupFailed: true });
263
+ return result.value;
264
+ }
265
+
266
+ async function run({ configuration, context, input, tools, signal, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted,
267
+ onEvent, onNativeTurn, finishIfCurrent }) {
268
+ if (session.disposed || session.current) throw new Error("This Codex conversation is closed or already working.");
269
+ // input validation: a host's bound application tools need the conversation's schema identity reader.
270
+ if (supplied && tools && !conversation.identity?.readToolSchemaIdentity) {
271
+ throw new TypeError("Bound Codex application tools require the original schema identity reader and writer.");
272
+ }
273
+ const completion = Promise.withResolvers();
274
+ const admission = Promise.withResolvers();
275
+ completion.promise.catch(() => {});
276
+ admission.promise.catch(() => {});
277
+ if (input.goal) throw new Error("Codex goals are controls. Inspect any unresolved goal-message record before continuing.");
278
+ const active = session.current = { configuration, context, inputId: input.messageId,
279
+ onEvent, onNativeTurn, signal, completion, admission, inputReady: admission.promise, finishIfCurrent,
280
+ admitted: false, dispatched: false, turnId: "" };
281
+ let cancellation;
282
+ let toolFailureRecovery;
283
+ let problem;
284
+ let delivered;
285
+ let waitForTurn = false;
286
+ let executingProvider;
287
+ const toolCalls = new Set();
288
+ const toolRegistrations = new Map();
289
+ // This run still owns the original native provider's current turn.
290
+ const ownsTurn = (provider, threadId, turnId) => session.current === active && !session.disposed && !signal.aborted &&
291
+ Boolean(provider) && provider === executingProvider && provider === session.native &&
292
+ threadId === session.binding.threadId && turnId === active.turnId;
293
+ function toolHandler(provider, threadId, isRegistered = () => true) {
294
+ return async ({ method, params }) => {
295
+ if (method !== "item/tool/call" || !tools || params?.threadId !== (threadId || session.binding.threadId) || params.namespace ||
296
+ supplied && (session.disposed || session.current !== active || active.finished || signal.aborted || owner.runtimeLifecycle?.closing === true || !isRegistered())) {
297
+ throw new Error("This conversation does not authorize that native tool request.");
298
+ }
299
+ let owned = !supplied;
300
+ const operation = (async () => {
301
+ await active.inputReady;
302
+ if (supplied && owner.runtimeLifecycle?.closing === true) {
303
+ throw new Error("This conversation does not authorize that native tool request.");
304
+ }
305
+ const ownership = Promise.withResolvers();
306
+ owner.notificationQueue.run({ sessionId, provider }, () => {
307
+ if (supplied && (params.turnId !== active.turnId || session.disposed || session.current !== active || active.finished ||
308
+ signal.aborted || !isRegistered())) {
309
+ ownership.resolve(false);
310
+ return;
311
+ }
312
+ owned = true;
313
+ const failure = provider.observationFailure;
314
+ // readable-check: allow no-dual-shape - data: a lost observation's failure wraps its cause or is the cause itself
315
+ if (failure) ownership.reject(failure.cause || failure);
316
+ else ownership.resolve(params.turnId === active.turnId);
317
+ });
318
+ if (!await ownership.promise || supplied && (session.disposed || session.current !== active || active.finished || signal.aborted || !isRegistered())) {
319
+ owned = false;
320
+ throw new Error("This application tool call belongs to another turn.");
321
+ }
322
+ let result;
323
+ try {
324
+ result = await tools.execute({ id: params.callId, name: params.tool, arguments: JSON.stringify(params.arguments) });
325
+ } catch (error) {
326
+ if (!supplied && owned && ownsTurn(provider, params.threadId, params.turnId)) active.toolFailure ||= error;
327
+ throw error;
328
+ }
329
+ return { success: result.ok, contentItems: [{ type: "inputText", text: JSON.stringify(result) }] };
330
+ })();
331
+ toolCalls.add(operation);
332
+ try { return await operation; }
333
+ catch (error) {
334
+ if (!supplied || owned && session.current === active && params.turnId === active.turnId && isRegistered()) {
335
+ if (supplied) active.toolFailure = error;
336
+ active.completion.reject(error);
337
+ }
338
+ throw error;
339
+ }
340
+ finally { toolCalls.delete(operation); }
341
+ };
342
+ }
343
+ active.providerReady = async ({ provider, threadId }) => {
344
+ signal.throwIfAborted();
345
+ if (!threadId) return;
346
+ if (await conversation.identity.read() !== threadId) throw new Error("The native application tool thread changed before dispatch.");
347
+ assertCodexApplicationToolSchemaIdentity(threadId, await conversation.identity.readToolSchemaIdentity(),
348
+ codexApplicationToolConfiguration(tools).toolSchemaIdentity);
349
+ signal.throwIfAborted();
350
+ if (session.disposed || session.current !== active) throw new Error("This conversation handle is closed.");
351
+ for (const [previous, owned] of toolRegistrations) {
352
+ if (previous === provider && owned.threadId === threadId && owned.client === provider.client && owned.registration.isCurrent()) return;
353
+ owned.registration.release();
354
+ toolRegistrations.delete(previous);
355
+ }
356
+ const client = provider.client;
357
+ let registration;
358
+ registration = provider.registerThreadRequestHandler(threadId, toolHandler(provider, threadId,
359
+ () => registration?.isCurrent() === true && provider.client === client));
360
+ toolRegistrations.set(provider, { threadId, client, registration });
361
+ };
362
+ function abort() {
363
+ if (cancellation) return;
364
+ cancellation = interrupt();
365
+ cancellation.then(() => active.completion.reject(signal.reason), error => active.completion.reject(error));
366
+ }
367
+ async function submit(command, operation) {
368
+ await admission.promise;
369
+ const ready = Promise.withResolvers();
370
+ const previous = active.inputReady;
371
+ active.inputReady = ready.promise;
372
+ try { return await operation(command, configuration, context, signal, tools); }
373
+ finally { ready.resolve(); active.inputReady = previous; }
374
+ }
375
+ active.steer = command => submit(command, sendMessage);
376
+ signal.addEventListener("abort", abort, { once: true });
377
+ try {
378
+ signal.throwIfAborted();
379
+ // Preparation retains the provider's authenticated account and the
380
+ // stable application-tool entry points. The run owner alone
381
+ // subscribes to notifications and handles native turn state/output.
382
+ if (!supplied) {
383
+ const selected = await selection(configuration, context, signal);
384
+ await connect(signal, selected);
385
+ executingProvider = session.native;
386
+ }
387
+ if (!supplied) session.native.setServerRequestHandler(toolHandler(executingProvider));
388
+ const command = { input, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted };
389
+ delivered = await sendMessage(command, configuration, context, signal, tools);
390
+ waitForTurn = !supplied || delivered.value?.delivered === true;
391
+ if (!finishIfCurrent && waitForTurn) {
392
+ await completion.promise;
393
+ await drainNotifications();
394
+ }
395
+ } catch (error) { problem = error; }
396
+ if (finishIfCurrent) {
397
+ try {
398
+ for (;;) {
399
+ const settling = active.completion;
400
+ if (waitForTurn) {
401
+ try { await settling.promise; }
402
+ catch (error) { problem = error; }
403
+ }
404
+ if (await finishIfCurrent(() => active.completion === settling)) break;
405
+ // The same dispatch established another native turn.
406
+ // Keep its completion and Stop owner until that turn settles.
407
+ problem = undefined;
408
+ waitForTurn = true;
409
+ }
410
+ if (!problem && waitForTurn) await drainNotifications();
411
+ } catch (error) { problem = error; }
412
+ }
413
+ signal.removeEventListener("abort", abort);
414
+ admission.reject(problem || signal.reason);
415
+ const failureThreadId = session.binding.threadId;
416
+ const failureTurnId = active.turnId;
417
+ try {
418
+ // Account invalidation already owns this stop attempt. Its failed
419
+ // proof stays available for explicit cleanup through the same owner.
420
+ if ((!supplied && problem || supplied && active.toolFailure || signal.aborted) && problem?.code !== "codex_runtime_invalidated") {
421
+ await (cancellation || interrupt());
422
+ }
423
+ } catch (error) {
424
+ problem = error;
425
+ // A direct Stop checkpoint can throw before its native completion
426
+ // reaches the queue. Keep the same admitted tool failure with the
427
+ // provider's observation-stop owner until its pending recovery joins.
428
+ if (!supplied && error === active.toolFailure && ownsTurn(executingProvider, failureThreadId, failureTurnId) &&
429
+ error.code !== "codex_runtime_invalidated") {
430
+ toolFailureRecovery = executingProvider.failObservation(error);
431
+ }
432
+ }
433
+ try {
434
+ await drainNotifications();
435
+ // The notification queue starts observation recovery without awaiting
436
+ // it. Join only this provider's pending attempt before settling.
437
+ if (!supplied) await providerOwner.pendingRecovery(providerKey, executingProvider);
438
+ if (toolFailureRecovery) {
439
+ await toolFailureRecovery;
440
+ if (problem === active.toolFailureCleanup && ownsTurn(executingProvider, failureThreadId, failureTurnId) &&
441
+ !session.observationError && session.binding.observationLoss?.stopped === true) delete problem.cleanupFailed;
442
+ }
443
+ } catch (error) { problem = error; }
444
+ finally {
445
+ for (const owned of toolRegistrations.values()) owned.registration.release();
446
+ toolRegistrations.clear();
447
+ await Promise.allSettled([...toolCalls]);
448
+ if (!supplied) {
449
+ session.native?.setServerRequestHandler(null);
450
+ owner.clearSessionRecoveryTimers(sessionId);
451
+ }
452
+ session.current = null;
453
+ }
454
+ if (problem) throw problem;
455
+ return delivered;
456
+ }
457
+
458
+ return Object.freeze({
459
+ sendMessage,
460
+ goalContext,
461
+ interrupt,
462
+ run
463
+ });
464
+ }
@@ -1,6 +1,16 @@
1
- /** OpenCode's native system lane is rebuilt for each inference, without a chat message. */
2
- export function createOpenCodeConversationAdapter({ resolveInstructions } = {}) {
3
- if (typeof resolveInstructions !== "function") throw new TypeError("OpenCode requires an instruction resolver.");
1
+ /**
2
+ * A native conversation's instruction source: its text, an optional identity that keeps it cached, and where it goes.
3
+ * @typedef {object} OpenCodeInstructionSource
4
+ * @property {() => string | Promise<string>} read
5
+ * @property {string} [identity]
6
+ * @property {"append" | "replace"} [placement]
7
+ */
8
+
9
+ /**
10
+ * OpenCode's native system lane is rebuilt for each inference, without a chat message.
11
+ * @param {{ resolveInstructions: (id: string) => Promise<OpenCodeInstructionSource | null | undefined> }} dependencies
12
+ */
13
+ export function createOpenCodeConversationAdapter({ resolveInstructions }) {
4
14
  const contexts = new Map();
5
15
  const contributions = new WeakMap();
6
16
 
@@ -9,6 +19,7 @@ export function createOpenCodeConversationAdapter({ resolveInstructions } = {})
9
19
  if (!id) return;
10
20
  const source = await resolveInstructions(id);
11
21
  if (!source) return;
22
+ // readable-check: allow method-probe - input validation: the instruction source a host resolves for a native conversation
12
23
  if ((source.identity !== undefined && (typeof source.identity !== "string" || !source.identity)) || typeof source.read !== "function" ||
13
24
  ![undefined, "append", "replace"].includes(source.placement)) {
14
25
  throw new TypeError("Conversation instructions require read() and an optional non-empty identity.");
@@ -45,5 +56,20 @@ export function createOpenCodeConversationAdapter({ resolveInstructions } = {})
45
56
  if (id) contexts.delete(id);
46
57
  }
47
58
 
48
- return Object.freeze({ transformSystem, event, invalidate: (id) => contexts.delete(id), close: () => contexts.clear() });
59
+ /** Forgets one conversation's cached instructions. */
60
+ function invalidate(id) {
61
+ return contexts.delete(id);
62
+ }
63
+
64
+ /** Forgets every cached instruction. */
65
+ function close() {
66
+ return contexts.clear();
67
+ }
68
+
69
+ return Object.freeze({
70
+ transformSystem,
71
+ event,
72
+ invalidate,
73
+ close
74
+ });
49
75
  }