@jskit-ai/assistant-core 0.2.7 → 0.2.9

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,394 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import {
3
+ OPENCODE_INTERRUPT_TIMEOUT_MS, openCodeLastAssistantResult, openCodeMessageRows, openCodeRowsForInput, readOpenCodeMessagesUntil,
4
+ openCodePersistentConversationMessages, openCodeStructuredOutput, waitForOpenCodeMessages, steerOpenCodeTurn, dispatchOpenCodeTurn,
5
+ observeOpenCodeTurnCompletion
6
+ } from "./openCodeTurn.js";
7
+ import { openCodeServerForDirectory } from "./openCodeNativeSession.js";
8
+ import { observePreparedEvents } from "./openCodeTurnMonitor.js";
9
+ import { normalizeNullableText } from "./normalize.js";
10
+
11
+ /** The target, with its server bound to a prepared conversation's own directory when it has one. */
12
+ function scopedTarget(target, prepared) {
13
+ const workdir = prepared.workdir;
14
+ return workdir ? { ...target, server: openCodeServerForDirectory(target.server, workdir), workdir } : target;
15
+ }
16
+
17
+ /**
18
+ * Temporary native conversations beside a session's main one: their creation,
19
+ * selection, turns, reads, waits, stops and deletion.
20
+ * @param {{
21
+ * processes: Map<string, object>,
22
+ * temporaryConversations: Map<string, object>,
23
+ * processOwner: import("./openCodeProcessOwner.js").OpenCodeProcessOwner
24
+ * }} options
25
+ */
26
+ export function createOpenCodeTemporaryConversations({ processes, temporaryConversations, processOwner }) {
27
+ async function createPreparedConversation(prepared) {
28
+ const target = scopedTarget(await processOwner.acquirePrepared(prepared.process), prepared);
29
+ const conversation = await createConversation(prepared.key, target, prepared.selection(), prepared.executionProfile);
30
+ return { conversationId: conversation.id, ephemeral: prepared.ephemeral, ok: true, status: "ready" };
31
+ }
32
+
33
+ async function selectPreparedConversation(prepared) {
34
+ const target = scopedTarget(await processOwner.acquirePrepared(prepared.process), prepared);
35
+ const selection = prepared.selection();
36
+ const conversationId = selection.conversationId;
37
+ const previous = conversationId ? temporaryConversations.get(`${prepared.key}\0${conversationId}`) : null;
38
+ if (selection.scoped && previous && JSON.stringify(previous.executionProfile || null) !== JSON.stringify(prepared.executionProfile)) {
39
+ throw selection.profileChanged();
40
+ }
41
+ if (!conversationId && !selection.createIfMissing) throw selection.identityRequired();
42
+ if (!previous?.active) await processOwner.currentDirectory(target.workdir);
43
+ const selected = await selectConversation(prepared.key, target, conversationId, selection.input, selection.registration);
44
+ return { ...selected, executionProfile: prepared.executionProfile, target };
45
+ }
46
+
47
+ async function existingPreparedConversation(prepared) {
48
+ return { conversationId: prepared.conversationId, ...await existingConversation(
49
+ prepared.key, prepared.conversationId, prepared.persistent,
50
+ async () => processOwner.acquirePrepared(await prepared.process())
51
+ ) };
52
+ }
53
+
54
+ async function runPreparedConversationTurn(prepared) {
55
+ const selected = await selectPreparedConversation(prepared);
56
+ const turn = prepared.turn(selected);
57
+ return runConversationTurn(selected.target, selected.conversationId, selected.tracked,
58
+ turn.input, turn.options).catch(prepared.failure);
59
+ }
60
+
61
+ async function readPreparedConversation(prepared, input) {
62
+ const { conversationId, target, tracked } = await existingPreparedConversation(prepared);
63
+ if (!target) throw prepared.readUnavailable();
64
+ if (!input.persistent) return prepared.projectResult(
65
+ await readConversation(target, conversationId, tracked, {}, prepared.readError), tracked?.executionProfile);
66
+ return readPersistentConversation(target, conversationId, tracked, prepared.messages).catch(prepared.failure);
67
+ }
68
+
69
+ async function waitPreparedConversationTurn(prepared, input) {
70
+ const { conversationId, target, tracked } = await existingPreparedConversation(prepared);
71
+ if (!target) throw prepared.readUnavailable();
72
+ return waitForConversationTurn(target, conversationId, tracked, input, prepared.readError);
73
+ }
74
+
75
+ async function stopPreparedConversation(prepared, input) {
76
+ const { conversationId, target, tracked } = await existingPreparedConversation(prepared);
77
+ return stopConversation(target, conversationId, tracked, { persistent: input.persistent }).catch(prepared.failure);
78
+ }
79
+
80
+ async function deletePreparedConversation(prepared, input) {
81
+ const { conversationId, key, target, tracked } = await existingPreparedConversation(prepared);
82
+ const result = await deleteConversation(key, target, conversationId, tracked, {
83
+ allowMissing: Boolean(input.persistent || prepared.scoped), onRemoved: prepared.onRemoved
84
+ });
85
+ if (!result.deleted) return result;
86
+ const cleanup = prepared.cleanupAfterDelete;
87
+ let providerExit = null;
88
+ if (cleanup) {
89
+ providerExit = await processOwner.closeSession(cleanup.sessionId, cleanup.options, cleanup.application);
90
+ // The result carries the deleted scope's exit proof. Only renewal releases
91
+ // a kept proof, and a scope is never renewed, so the kept one is dropped.
92
+ processOwner.releaseProcessExitProof(cleanup.sessionId);
93
+ }
94
+ return { conversationId, deleted: true, ok: providerExit?.ok !== false,
95
+ ...(providerExit ? { providerExit } : {}) };
96
+ }
97
+
98
+ async function createConversation(key, target, input, executionProfile = null) {
99
+ await target.server.client.prepareDirectory({ signal: target.abortController?.signal });
100
+ const conversation = await target.server.client.createSession({
101
+ agent: input.agent,
102
+ location: { directory: target.workdir },
103
+ model: input.model
104
+ });
105
+ temporaryConversations.set(`${key}\0${conversation.id}`, {
106
+ active: false,
107
+ executionProfile,
108
+ target
109
+ });
110
+ return conversation;
111
+ }
112
+
113
+ async function selectConversation(parentKey, target, conversationId, input, registration) {
114
+ await target.server.client.prepareDirectory({ signal: target.abortController?.signal });
115
+ if (!conversationId) {
116
+ const created = await target.server.client.createSession({
117
+ agent: input.agent,
118
+ location: { directory: target.workdir },
119
+ model: input.model
120
+ });
121
+ conversationId = created.id;
122
+ } else {
123
+ await target.server.client.switchModel(conversationId, input.model);
124
+ await target.server.client.switchAgent(conversationId, input.agent);
125
+ }
126
+ const key = `${parentKey}\0${conversationId}`;
127
+ const tracked = temporaryConversations.get(key) || { active: false, target };
128
+ tracked.target = target;
129
+ tracked.executionProfile = registration.executionProfile;
130
+ tracked.conversationId = conversationId;
131
+ tracked.promptContext = registration.promptContext;
132
+ temporaryConversations.set(key, tracked);
133
+ await registration.onSelected();
134
+ return { conversationId, key, tracked };
135
+ }
136
+
137
+ async function existingConversation(parentKey, conversationId, persistent, prepareTarget) {
138
+ const key = `${parentKey}\0${conversationId}`;
139
+ let tracked = temporaryConversations.get(key) || null;
140
+ let target = tracked?.target || processes.get(parentKey) || null;
141
+ if (persistent && (!target || target.abortController.signal.aborted)) {
142
+ temporaryConversations.delete(key);
143
+ tracked = null;
144
+ target = await prepareTarget();
145
+ }
146
+ return { key, tracked, target };
147
+ }
148
+
149
+ async function readConversation(target = {}, conversationId = "", tracked = null, options = {}, readError) {
150
+ const messages = await readOpenCodeMessagesUntil(target.server.client, conversationId,
151
+ rows => rows.some(row => row?.type === "assistant"), options);
152
+ const result = openCodeLastAssistantResult(messages, { readError });
153
+ return {
154
+ conversationId,
155
+ error: normalizeNullableText(tracked?.error?.message) || result.error,
156
+ ok: !tracked?.error && !result.error,
157
+ runId: tracked?.inputMessageId || "",
158
+ status: tracked?.active ? "inProgress"
159
+ : tracked?.interrupted ? "interrupted"
160
+ : tracked?.error || result.error ? "failed" : "completed",
161
+ text: result.text
162
+ };
163
+ }
164
+
165
+ async function readPersistentConversation(target, conversationId, tracked, projection) {
166
+ const { readError } = projection;
167
+ let status = await target.server.client.sessionStatus(conversationId);
168
+ if (!tracked && status.type !== "idle") {
169
+ await processOwner.stopUnobservedSession(target, conversationId);
170
+ status = { type: "idle" };
171
+ }
172
+ const rows = openCodeMessageRows(await target.server.client.messages(conversationId));
173
+ const result = openCodeLastAssistantResult(rows, { readError });
174
+ const messages = openCodePersistentConversationMessages(rows, projection.messageId);
175
+ let conversationStatus = "completed";
176
+ if (status.type !== "idle" || tracked?.active) conversationStatus = "inProgress";
177
+ else if (tracked?.interrupted) conversationStatus = "interrupted";
178
+ else if (tracked?.error || result.error) conversationStatus = "failed";
179
+ return {
180
+ conversationId,
181
+ ok: true,
182
+ messages,
183
+ text: result.text,
184
+ admitted: Boolean(projection.inputMessageId && rows.some((row) => row.id === projection.inputMessageId)),
185
+ error: normalizeNullableText(tracked?.error?.message) || result.error,
186
+ runId: tracked?.inputMessageId || [...rows].reverse().find((row) => row.type === "user")?.id || "",
187
+ status: conversationStatus
188
+ };
189
+ }
190
+
191
+ async function waitForConversationTurn(target, conversationId, tracked, input = {}, readError) {
192
+ if (tracked?.completion && (tracked.executionProfile || !input.timeoutMs)) {
193
+ return tracked.completion;
194
+ }
195
+ await waitForOpenCodeMessages(target.server.client, conversationId, tracked?.inputMessageId || "", {
196
+ signal: input.timeoutMs ? AbortSignal.timeout(Number(input.timeoutMs)) : undefined,
197
+ readError
198
+ });
199
+ return readConversation(target, conversationId, null, {}, readError);
200
+ }
201
+
202
+ async function runConversationTurn(target, conversationId, tracked, input, options) {
203
+ if (tracked.active && tracked.error) {
204
+ await processOwner.stopUnobservedSession(tracked.target, conversationId);
205
+ tracked.active = false;
206
+ }
207
+ if (tracked.active && input.steer === true) {
208
+ const messageId = input.steeringId;
209
+ await steerOpenCodeTurn(target.server.client, conversationId, tracked, {
210
+ agent: input.agent,
211
+ id: messageId,
212
+ model: input.model,
213
+ prompt: input.prompt,
214
+ attachments: input.attachments
215
+ }, { signal: AbortSignal.any([target.abortController.signal, tracked.abortController.signal]) });
216
+ return {
217
+ conversationId,
218
+ ok: true,
219
+ runId: tracked.inputMessageId,
220
+ status: "inProgress",
221
+ deliveryMode: "steer"
222
+ };
223
+ }
224
+ // A new native turn starts only on its directory's current credentials.
225
+ while (!tracked.active && !processOwner.directoryCurrent(target.workdir)) await processOwner.waitForDirectory(target.workdir);
226
+ if (tracked.active) {
227
+ throw new AppError(409, "This conversation is still working.", {
228
+ code: "assistant_opencode_conversation_busy", details: {}
229
+ });
230
+ }
231
+ tracked.active = true;
232
+ const releaseDirectory = processOwner.holdDirectory(target.workdir);
233
+ tracked.error = null;
234
+ tracked.interrupted = false;
235
+ const turnAbort = new AbortController();
236
+ tracked.abortController = turnAbort;
237
+ const inputMessageId = input.id;
238
+ const observedTarget = { ...target, upstreamSessionId: conversationId };
239
+ const signal = AbortSignal.any([target.abortController.signal, turnAbort.signal, ...(options.signal ? [options.signal] : [])]);
240
+ const events = options.observation
241
+ ? observePreparedEvents(observedTarget, null, turnAbort, signal, options.observation())
242
+ : options.observe(observedTarget, turnAbort, signal);
243
+ let admitted;
244
+ try {
245
+ admitted = await dispatchOpenCodeTurn(tracked, {
246
+ get agent() { return input.agent; },
247
+ id: inputMessageId,
248
+ get model() { return input.model; },
249
+ get prompt() { return input.prompt; },
250
+ get attachments() { return input.attachments; }
251
+ }, {
252
+ get client() { return target.server.client; },
253
+ get conversationId() { return conversationId; },
254
+ events, signal,
255
+ readiness: options.readiness,
256
+ beforeDispatch: options.beforeDispatch
257
+ });
258
+ } catch (error) {
259
+ try {
260
+ await events.close();
261
+ tracked.error = error;
262
+ if (tracked.promptAttempted) await processOwner.stopUnobservedSession(observedTarget, conversationId);
263
+ tracked.active = false;
264
+ } finally {
265
+ releaseDirectory();
266
+ }
267
+ throw error;
268
+ }
269
+ tracked.inputMessageId = normalizeNullableText(admitted.id);
270
+ tracked.completion = (async () => {
271
+ const completion = options.completion;
272
+ const onMessages = completion.onMessages;
273
+ const result = await observeOpenCodeTurnCompletion(processOwner, observedTarget, conversationId, tracked, {
274
+ ...completion, events, signal,
275
+ ...(onMessages ? {
276
+ onMessages: (messages, inputMessageId) => onMessages(
277
+ openCodePersistentConversationMessages(openCodeRowsForInput(messages, inputMessageId), completion.messageId),
278
+ inputMessageId
279
+ )
280
+ } : {}),
281
+ projectResult: async () => {
282
+ const result = completion.projectResult(
283
+ await readConversation(target, conversationId, null, { signal }, completion.readError)
284
+ );
285
+ return { ...result, text: input.outputSchema ? openCodeStructuredOutput(result.text) : result.text };
286
+ }
287
+ });
288
+ if (result.interrupted) {
289
+ return { conversationId, ok: true, runId: tracked.inputMessageId, status: "interrupted", text: "" };
290
+ }
291
+ return result;
292
+ })();
293
+ void tracked.completion.then(releaseDirectory, releaseDirectory);
294
+ // Keep a failed admitted turn observable to the client's conversation reads.
295
+ void tracked.completion.catch((error) => {
296
+ if (tracked.abortController === turnAbort) tracked.error = error;
297
+ });
298
+ options.onCompletion(tracked.completion);
299
+ return options.waitForCompletion ? tracked.completion : {
300
+ conversationId,
301
+ ok: true,
302
+ runId: tracked.inputMessageId,
303
+ status: "inProgress",
304
+ threadId: conversationId,
305
+ turnId: tracked.inputMessageId
306
+ };
307
+ }
308
+
309
+ async function stopConversation(target, conversationId, tracked, { persistent = false } = {}) {
310
+ if (!target) {
311
+ return { conversationId, ok: true, stopped: false };
312
+ }
313
+ try {
314
+ if (tracked?.active && tracked.error) {
315
+ await processOwner.stopUnobservedSession(target, conversationId);
316
+ } else {
317
+ const confirmed = await target.server.client.interrupt(conversationId, {
318
+ signal: AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)
319
+ });
320
+ const status = persistent ? await target.server.client.sessionStatus(conversationId, {
321
+ signal: AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)
322
+ }) : null;
323
+ if (confirmed !== true || status && status.type !== "idle") {
324
+ throw new AppError(502, "OpenCode did not confirm Stop. Try Stop again.", {
325
+ code: "assistant_opencode_interrupt_unconfirmed", details: {}
326
+ });
327
+ }
328
+ }
329
+ } catch (error) {
330
+ if (persistent && error.statusCode === 404) return { conversationId, ok: true, stopped: true };
331
+ if (error?.name === "TimeoutError") {
332
+ throw new AppError(504, "OpenCode did not confirm Stop within 5 seconds. Try Stop again.", {
333
+ code: "assistant_opencode_interrupt_timeout", details: {}
334
+ });
335
+ }
336
+ throw error;
337
+ }
338
+ if (tracked) {
339
+ tracked.interrupted = true;
340
+ tracked.abortController?.abort();
341
+ await tracked.completion?.catch(() => null);
342
+ tracked.active = false;
343
+ }
344
+ return { conversationId, ok: true, stopped: true };
345
+ }
346
+
347
+ async function deleteConversation(key, target, conversationId, tracked, { allowMissing, onRemoved }) {
348
+ if (!target) {
349
+ temporaryConversations.delete(key);
350
+ await onRemoved();
351
+ return { conversationId, deleted: false, ok: true };
352
+ }
353
+ try {
354
+ await target.server.client.deleteSession(conversationId);
355
+ } catch (error) {
356
+ if (!allowMissing || error.statusCode !== 404) throw error;
357
+ }
358
+ if (tracked) {
359
+ tracked.interrupted = true;
360
+ tracked.abortController?.abort();
361
+ await tracked.completion?.catch(() => null);
362
+ }
363
+ temporaryConversations.delete(key);
364
+ await onRemoved();
365
+ return { conversationId, deleted: true, ok: true };
366
+ }
367
+
368
+ function hasActiveTemporaryConversation(sessionId) {
369
+ return [...temporaryConversations.values()].some((entry) => (
370
+ !entry.reasoningSummary && entry.active === true && entry.target?.sessionId === sessionId
371
+ ));
372
+ }
373
+
374
+ return Object.freeze({
375
+ createPreparedConversation,
376
+ selectPreparedConversation,
377
+ existingPreparedConversation,
378
+ runPreparedConversationTurn,
379
+ readPreparedConversation,
380
+ waitPreparedConversationTurn,
381
+ stopPreparedConversation,
382
+ deletePreparedConversation,
383
+ createConversation,
384
+ selectConversation,
385
+ existingConversation,
386
+ runConversationTurn,
387
+ readConversation,
388
+ readPersistentConversation,
389
+ waitForConversationTurn,
390
+ stopConversation,
391
+ deleteConversation,
392
+ hasActiveTemporaryConversation
393
+ });
394
+ }
@@ -1,7 +1,13 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
 
4
- /** Private transport from the native plugin to the application's shared executor. */
4
+ /**
5
+ * Private transport from the native plugin to the application's shared executor.
6
+ * @param {object} dependencies
7
+ * @param {(input: Record<string, any>, signal: AbortSignal) => Promise<unknown>} dependencies.execute
8
+ * @param {number} dependencies.maxArgumentBytes
9
+ * @param {object[]} dependencies.schemas
10
+ */
5
11
  export async function createOpenCodeToolBridge({ execute, maxArgumentBytes, schemas }) {
6
12
  const token = randomBytes(32).toString("hex");
7
13
  const pending = new Map();
@@ -43,18 +49,26 @@ export async function createOpenCodeToolBridge({ execute, maxArgumentBytes, sche
43
49
  server.once("error", reject);
44
50
  server.listen(0, "127.0.0.1", resolve);
45
51
  });
46
- return {
47
- configuration: { url: `http://127.0.0.1:${server.address().port}/call`, token, schemas },
48
- whenIdle: () => Promise.allSettled([...pending.values()]),
49
- async close() {
50
- if (!closed) {
51
- closed = true;
52
- for (const controller of pending.keys()) controller.abort(new Error("OpenCode application tools closed."));
53
- stopped = new Promise(resolve => server.close(resolve));
54
- server.closeAllConnections();
55
- }
56
- await stopped;
57
- await Promise.allSettled([...pending.values()]);
52
+ /** Settles every call in flight. */
53
+ function whenIdle() {
54
+ return Promise.allSettled([...pending.values()]);
55
+ }
56
+
57
+ /** Refuses new calls, aborts those in flight and closes the listener. */
58
+ async function close() {
59
+ if (!closed) {
60
+ closed = true;
61
+ for (const controller of pending.keys()) controller.abort(new Error("OpenCode application tools closed."));
62
+ stopped = new Promise(resolve => server.close(resolve));
63
+ server.closeAllConnections();
58
64
  }
59
- };
65
+ await stopped;
66
+ await Promise.allSettled([...pending.values()]);
67
+ }
68
+
69
+ return Object.freeze({
70
+ configuration: { url: `http://127.0.0.1:${server.address().port}/call`, token, schemas },
71
+ whenIdle,
72
+ close
73
+ });
60
74
  }