@jskit-ai/assistant-core 0.1.199 → 0.1.201
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.
- package/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { createLocalConversationExecution } from "../localExecution.js";
|
|
6
|
+
import { createOpenCodeConversationServer, openCodeConversationAgent } from "../openCodeProcess.js";
|
|
7
|
+
import { createOpenCodeSharedRuntime, ensureOpenCodeSession, openCodeServerForDirectory } from "../openCodeRuntime.js";
|
|
8
|
+
import { openCodeAssistantMessageText, retireOpenCodeConversationHistory } from "../openCodeClient.js";
|
|
9
|
+
import { inspectOpenCodeMessageAdmission, observeOpenCodeEvents, openCodeRowsForInput,
|
|
10
|
+
openCodeDetachedPrompt, openCodeStructuredOutput } from "../openCodeTurn.js";
|
|
11
|
+
import { createOpenCodeToolBridge } from "../openCodeTools.js";
|
|
12
|
+
import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
|
|
13
|
+
|
|
14
|
+
const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
15
|
+
const nativeMessageId = id => `msg_jskit_${hash(id).slice(0, 40)}`;
|
|
16
|
+
|
|
17
|
+
/** Uses the application's authorized API connection; native state stays server-side. */
|
|
18
|
+
export function createOpenCodeConversationDriver({ connections, host = {}, limits = {} } = {}) {
|
|
19
|
+
if (!host.conversation && typeof connections?.resolve !== "function") throw new TypeError("OpenCode conversations require an authorized AI connection resolver.");
|
|
20
|
+
const workdir = path.resolve(host.workdir || process.cwd());
|
|
21
|
+
const stateDirectory = path.resolve(host.stateDirectory || path.join(workdir, ".assistant", "native", "opencode"));
|
|
22
|
+
const execution = host.execution || createLocalConversationExecution();
|
|
23
|
+
const nativeTools = host.nativeTools === true;
|
|
24
|
+
const maximumOutput = limits.maxOutputCharacters ?? 64_000;
|
|
25
|
+
if (!Number.isSafeInteger(maximumOutput) || maximumOutput < 1) throw new TypeError("Invalid OpenCode output limit.");
|
|
26
|
+
if (limits.maxOutputTokens !== undefined && (!Number.isSafeInteger(limits.maxOutputTokens) || limits.maxOutputTokens < 1)) {
|
|
27
|
+
throw new TypeError("Invalid OpenCode token limit.");
|
|
28
|
+
}
|
|
29
|
+
function prompt(input, configuration) {
|
|
30
|
+
const text = `${input.text || ""}${input.attachmentManifest || ""}`;
|
|
31
|
+
if (!configuration.outputSchema) return { text, content: input.content };
|
|
32
|
+
const prepared = openCodeDetachedPrompt({ prompt: text, outputSchema: configuration.outputSchema });
|
|
33
|
+
if (prepared.length > (limits.maxInputCharacters ?? 32_000)) throw new Error("OpenCode structured input exceeded the configured limit.");
|
|
34
|
+
return { text: prepared, content: input.content };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function nativeStorageTarget({ native, context }) {
|
|
38
|
+
const prepared = native.preparation.storage(context);
|
|
39
|
+
return native.owner.ensure(null, async () =>
|
|
40
|
+
native.owner.startPreparedProcess(await prepared.process())).catch(prepared.failure);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function disposeNative({ native, options }) {
|
|
44
|
+
const prepared = native.preparation.cleanup(options);
|
|
45
|
+
return native.owner.closeSession(prepared.sessionId, prepared.options, prepared.application);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
toolDiscovery: true,
|
|
50
|
+
attachmentTypes: ["text", "image", "localFile"],
|
|
51
|
+
capabilities: Object.freeze({ streaming: true, cancellation: true, history: true,
|
|
52
|
+
instructions: true, configuration: true, steering: true, goals: false, attachments: true, tools: true, nativeTools, structuredOutput: true }),
|
|
53
|
+
validateConfiguration(configuration) {
|
|
54
|
+
validateConversationConfiguration(configuration, { engine: "OpenCode", connections, connectionRequired: true,
|
|
55
|
+
structuredOutput: true, maxOutputCharacters: maximumOutput });
|
|
56
|
+
},
|
|
57
|
+
async readState({ conversation, context, representation }) {
|
|
58
|
+
const { owner, preparation } = conversation.native;
|
|
59
|
+
const nativeResult = await owner.readSessionState(context, preparation.state);
|
|
60
|
+
return conversation.read(context, representation, nativeResult);
|
|
61
|
+
},
|
|
62
|
+
inspectTemporaryActivity({ native }) {
|
|
63
|
+
return native.owner.hasActiveTemporaryConversation(native.sessionId);
|
|
64
|
+
},
|
|
65
|
+
releaseRenewalPredecessorProcessExitProof({ native }) {
|
|
66
|
+
return native.owner.releaseRenewalPredecessorProcessExitProof(native.preparation);
|
|
67
|
+
},
|
|
68
|
+
releaseRenewalSuccessorProcessExitProof({ native }) {
|
|
69
|
+
return native.owner.releaseProcessExitProof(native.preparation.sessionId);
|
|
70
|
+
},
|
|
71
|
+
async generateRenewalHandover({ native, input, context }) {
|
|
72
|
+
return native.owner.runPreparedRenewalTurn(await native.preparation.handover(input, context));
|
|
73
|
+
},
|
|
74
|
+
async seedRenewalHandover({ native, input, context }) {
|
|
75
|
+
return native.owner.runPreparedRenewalTurn(await native.preparation.seed(input, context));
|
|
76
|
+
},
|
|
77
|
+
async ensureConversation({ native, context }) {
|
|
78
|
+
return native.owner.ensurePreparedSessionReadiness(await native.preparation.readiness(context));
|
|
79
|
+
},
|
|
80
|
+
disposeNative,
|
|
81
|
+
async interruptDetachedConversation({ native, input, context }) {
|
|
82
|
+
return native.owner.stopPreparedConversation(await native.preparation.existing(input, context), input);
|
|
83
|
+
},
|
|
84
|
+
async deleteDetachedConversation({ native, input, context }) {
|
|
85
|
+
const prepared = await native.preparation.existing(input, context, { operation: "delete" });
|
|
86
|
+
return native.owner.deletePreparedConversation(prepared, input);
|
|
87
|
+
},
|
|
88
|
+
async listConversationStorage(prepared) {
|
|
89
|
+
const target = await nativeStorageTarget(prepared);
|
|
90
|
+
const { binding, context } = prepared;
|
|
91
|
+
return (await target.server.client.listConversationsForDirectory(binding.workdir, { signal: context.signal }))
|
|
92
|
+
.map((row) => ({ ...binding, conversationId: row.id }));
|
|
93
|
+
},
|
|
94
|
+
async retireConversationHistory(prepared) {
|
|
95
|
+
const target = await nativeStorageTarget(prepared);
|
|
96
|
+
// Native status is scoped to the saved project instance, not the control server.
|
|
97
|
+
const client = openCodeServerForDirectory(target.server, prepared.binding.workdir).client;
|
|
98
|
+
return retireOpenCodeConversationHistory(target.server.client, client, prepared.binding, prepared.context);
|
|
99
|
+
},
|
|
100
|
+
closeProject({ native, input }) {
|
|
101
|
+
return native.owner.closeProject(input, native.preparation.projectCleanup());
|
|
102
|
+
},
|
|
103
|
+
invalidateRuntimes({ native, input }) {
|
|
104
|
+
return native.owner.invalidateRuntimes(input, native.preparation);
|
|
105
|
+
},
|
|
106
|
+
reconcileSessions({ native, sessions, options }) {
|
|
107
|
+
return native.owner.reconcileSessions(sessions, options, native.preparation);
|
|
108
|
+
},
|
|
109
|
+
async createConversation({ native, input, context }) {
|
|
110
|
+
return native.owner.createPreparedConversation(await native.preparation.creation(input, context));
|
|
111
|
+
},
|
|
112
|
+
async createBinding() {
|
|
113
|
+
const directory = path.join(stateDirectory, randomUUID());
|
|
114
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
115
|
+
return { sessionId: "", workdir: await realpath(workdir), directory, executionId: "", processDirectory: "" };
|
|
116
|
+
},
|
|
117
|
+
async open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
|
|
118
|
+
if (conversation?.native?.scoped) {
|
|
119
|
+
const supplied = conversation.native;
|
|
120
|
+
const { owner, preparation } = supplied;
|
|
121
|
+
const { conversationId, context: openingContext } = supplied.scoped;
|
|
122
|
+
const inputFor = (input = {}) => {
|
|
123
|
+
if (input.conversationId && String(input.conversationId).trim() !== conversationId) {
|
|
124
|
+
throw new TypeError("This operation belongs to a different scoped conversation.");
|
|
125
|
+
}
|
|
126
|
+
return { ...input, conversationId };
|
|
127
|
+
};
|
|
128
|
+
const unsupported = () => { throw Object.assign(new Error("This original scoped conversation has no canonical message or goal receipt."), {
|
|
129
|
+
code: "conversation_unsupported"
|
|
130
|
+
}); };
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
capabilities: Object.freeze({ streaming: false, instructions: false, configuration: false,
|
|
133
|
+
goals: false, goalBudgets: false, goalCommands: Object.freeze({}), attachments: false, tools: false, nativeTools: false }),
|
|
134
|
+
async sendNative({ input, context }) {
|
|
135
|
+
return owner.runPreparedConversationTurn(await preparation.turn(inputFor(input), context));
|
|
136
|
+
},
|
|
137
|
+
async readNative({ input, context }) {
|
|
138
|
+
const request = inputFor(input);
|
|
139
|
+
return owner.readPreparedConversation(await preparation.existing(request, context), request);
|
|
140
|
+
},
|
|
141
|
+
async waitNative({ input, context }) {
|
|
142
|
+
const request = inputFor(input);
|
|
143
|
+
return owner.waitPreparedConversationTurn(await preparation.existing(request, context), request);
|
|
144
|
+
},
|
|
145
|
+
async cancel({ input, context }) {
|
|
146
|
+
const request = inputFor(input);
|
|
147
|
+
return owner.stopPreparedConversation(await preparation.existing(request, context), request);
|
|
148
|
+
},
|
|
149
|
+
readGoal: unsupported, updateGoal: unsupported, inspectAdmission: unsupported,
|
|
150
|
+
async dispose(options) {
|
|
151
|
+
if (options) {
|
|
152
|
+
const request = inputFor(options.input);
|
|
153
|
+
const prepared = await preparation.existing(request, options.context, { operation: "delete" });
|
|
154
|
+
return owner.deletePreparedConversation(prepared, request);
|
|
155
|
+
}
|
|
156
|
+
const result = await disposeNative({ native: supplied, options: openingContext });
|
|
157
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || "Scoped cleanup could not be confirmed."), result);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (conversation?.native) {
|
|
163
|
+
const supplied = conversation.native;
|
|
164
|
+
let current;
|
|
165
|
+
let disposed = false;
|
|
166
|
+
let cleanupFailure;
|
|
167
|
+
async function interrupt(context) {
|
|
168
|
+
try {
|
|
169
|
+
const acquired = await supplied.acquire(context || current?.context);
|
|
170
|
+
const value = await supplied.owner.interruptPreparedTurn(await supplied.preparation.interruption(acquired.options));
|
|
171
|
+
if (cleanupFailure) {
|
|
172
|
+
await reportFailure(cleanupFailure, { recovered: true });
|
|
173
|
+
cleanupFailure = null;
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
error.cleanupFailed = true;
|
|
178
|
+
cleanupFailure = error;
|
|
179
|
+
current?.failure.reject(error);
|
|
180
|
+
await reportFailure(error);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function send({ input, context, signal, beforeDispatch, accept }) {
|
|
185
|
+
signal.throwIfAborted();
|
|
186
|
+
const acquired = await supplied.acquire(context);
|
|
187
|
+
const prepared = await supplied.preparation.message({ ...input.nativeMessage, onPromptSending: beforeDispatch }, acquired.options);
|
|
188
|
+
const value = await supplied.owner.sendPreparedMessage(prepared, acquired.options);
|
|
189
|
+
const delivered = { value, completion: value.conversationTurn ? supplied.owner.monitors.get(acquired.key) : null };
|
|
190
|
+
if (value.delivered === true && value.conversationTurn) {
|
|
191
|
+
if (current) current.delivery = delivered;
|
|
192
|
+
await accept({ nativeResult: { value }, conversationTurn: value.conversationTurn, nativeTurnId: value.turn?.id });
|
|
193
|
+
}
|
|
194
|
+
return delivered;
|
|
195
|
+
}
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
publishNative: event => conversation.publish(event),
|
|
198
|
+
async inspectAdmission({ context, representation, ...input }) {
|
|
199
|
+
const acquired = await supplied.acquire(context);
|
|
200
|
+
const result = await supplied.owner.inspectPreparedMessage(await supplied.preparation.inspection(input, acquired.options));
|
|
201
|
+
return representation === "native" ? result : { accepted: result.admission === "accepted" };
|
|
202
|
+
},
|
|
203
|
+
async run(command) {
|
|
204
|
+
if (disposed || current) throw new Error("This OpenCode conversation is closed or already working.");
|
|
205
|
+
const active = { context: command.context, failure: Promise.withResolvers() };
|
|
206
|
+
let failure;
|
|
207
|
+
active.failure.promise.catch(error => { failure = error; });
|
|
208
|
+
current = active;
|
|
209
|
+
let cancellation;
|
|
210
|
+
const abort = () => {
|
|
211
|
+
cancellation ||= interrupt(command.context).then(() => { throw command.signal.reason; });
|
|
212
|
+
cancellation.catch(active.failure.reject);
|
|
213
|
+
};
|
|
214
|
+
command.signal.addEventListener("abort", abort, { once: true });
|
|
215
|
+
try {
|
|
216
|
+
let delivered;
|
|
217
|
+
let dispatchFailure;
|
|
218
|
+
try { delivered = await send(command); }
|
|
219
|
+
catch (error) { dispatchFailure = error; }
|
|
220
|
+
for (;;) {
|
|
221
|
+
const settling = active.delivery;
|
|
222
|
+
let result;
|
|
223
|
+
let problem = dispatchFailure || failure;
|
|
224
|
+
if (!problem && settling?.completion) {
|
|
225
|
+
try { result = await Promise.race([settling.completion, active.failure.promise]); }
|
|
226
|
+
catch (error) { problem = error; }
|
|
227
|
+
}
|
|
228
|
+
if (command.finishIfCurrent && !await command.finishIfCurrent(() => active.delivery === settling)) {
|
|
229
|
+
// The original sender admitted another invocation and returned
|
|
230
|
+
// its exact monitor. Keep the same completion and Stop owner.
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
problem = dispatchFailure || failure || problem;
|
|
234
|
+
if (!problem && result?.error) {
|
|
235
|
+
problem = Object.assign(new Error(result.error), { status: result.status });
|
|
236
|
+
if (result.active && result.status === "observation_lost") {
|
|
237
|
+
problem.cleanupFailed = true;
|
|
238
|
+
cleanupFailure = problem;
|
|
239
|
+
await reportFailure(problem);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (problem) throw problem;
|
|
243
|
+
return { value: delivered.value };
|
|
244
|
+
}
|
|
245
|
+
} finally {
|
|
246
|
+
command.signal.removeEventListener("abort", abort);
|
|
247
|
+
await cancellation?.catch(() => {});
|
|
248
|
+
if (current === active) current = null;
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
async steer(command) { return { value: (await send(command)).value }; },
|
|
252
|
+
async cancel({ context, representation } = {}) {
|
|
253
|
+
const value = await interrupt(context);
|
|
254
|
+
return representation === "native" ? { value } : value;
|
|
255
|
+
},
|
|
256
|
+
async dispose(options) {
|
|
257
|
+
try {
|
|
258
|
+
const result = await disposeNative({ native: supplied, options });
|
|
259
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || "OpenCode cleanup could not be confirmed."), result);
|
|
260
|
+
disposed = true;
|
|
261
|
+
return result;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
error.cleanupFailed = true;
|
|
264
|
+
current?.failure.reject(error);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (!binding || typeof binding.sessionId !== "string" || binding.workdir !== await realpath(workdir) ||
|
|
271
|
+
path.dirname(binding.directory || "") !== stateDirectory) {
|
|
272
|
+
throw new Error("This OpenCode conversation belongs to another working directory or native state directory. Restore its original host configuration.");
|
|
273
|
+
}
|
|
274
|
+
let native;
|
|
275
|
+
let toolBridge;
|
|
276
|
+
let facilities;
|
|
277
|
+
let processVariants = {};
|
|
278
|
+
let current;
|
|
279
|
+
let stopping;
|
|
280
|
+
let disposed = false;
|
|
281
|
+
async function updateBinding(patch) {
|
|
282
|
+
const next = { ...binding, ...patch };
|
|
283
|
+
await writeBinding(next);
|
|
284
|
+
binding = next;
|
|
285
|
+
}
|
|
286
|
+
async function stopRecordedExecution() {
|
|
287
|
+
const proof = await execution.stop(binding.executionId);
|
|
288
|
+
if (!proof?.scopeEmpty) throw new Error("OpenCode process cleanup could not be confirmed. Restore its execution host before continuing.");
|
|
289
|
+
if (binding.processDirectory) {
|
|
290
|
+
if (path.dirname(binding.processDirectory) !== (binding.runtimeDirectory || stateDirectory) || !path.basename(binding.processDirectory).startsWith("process-")) {
|
|
291
|
+
throw new Error("OpenCode's private process directory does not belong to this host.");
|
|
292
|
+
}
|
|
293
|
+
await rm(binding.processDirectory, { recursive: true, force: true });
|
|
294
|
+
}
|
|
295
|
+
await updateBinding({ executionId: "", processDirectory: "" });
|
|
296
|
+
}
|
|
297
|
+
async function stop() {
|
|
298
|
+
if (stopping) return stopping;
|
|
299
|
+
stopping = (async () => {
|
|
300
|
+
await toolBridge?.close();
|
|
301
|
+
toolBridge = null;
|
|
302
|
+
if (facilities?.runtime.cleanupPending) {
|
|
303
|
+
await facilities.runtime.stop("opencode-startup-cleanup");
|
|
304
|
+
await updateBinding({ executionId: "", processDirectory: "" });
|
|
305
|
+
}
|
|
306
|
+
if (native) {
|
|
307
|
+
await facilities.runtime.releaseTarget(native, {
|
|
308
|
+
onRemoved: () => facilities.runtime.writeBindings(facilities.registryPath, binding.directory, [])
|
|
309
|
+
});
|
|
310
|
+
facilities.runtime.turns.delete(binding.directory);
|
|
311
|
+
await updateBinding({ executionId: "", processDirectory: "" });
|
|
312
|
+
native = null;
|
|
313
|
+
} else if (binding.executionId && facilities && !host.opencode &&
|
|
314
|
+
!facilities.runtime.current && !facilities.runtime.processStarts.size) {
|
|
315
|
+
// Only our unfinished standalone startup belongs to this handle.
|
|
316
|
+
// A saved shared-server reference is not authority to stop a peer's
|
|
317
|
+
// service before this conversation has acquired its owner.
|
|
318
|
+
await stopRecordedExecution();
|
|
319
|
+
}
|
|
320
|
+
if (current?.finished) current = null;
|
|
321
|
+
})().catch(error => {
|
|
322
|
+
error.cleanupFailed = true;
|
|
323
|
+
if (error.code === "assistant_opencode_stop_unverified") {
|
|
324
|
+
error.message = "OpenCode process cleanup could not be confirmed. Restore its execution host before continuing.";
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}).finally(() => { stopping = null; });
|
|
328
|
+
return stopping;
|
|
329
|
+
}
|
|
330
|
+
async function resolvePreparation(configuration, context, signal) {
|
|
331
|
+
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
332
|
+
signal.throwIfAborted();
|
|
333
|
+
validateConnectionModel(configuration, connection);
|
|
334
|
+
const identity = hash([connection.providerId, connection.baseURL || "", connection.apiKey]);
|
|
335
|
+
if (binding.accountIdentity && identity !== binding.accountIdentity) {
|
|
336
|
+
throw new Error("This conversation belongs to another OpenCode account. Restore that connection or start a new conversation.");
|
|
337
|
+
}
|
|
338
|
+
if (!binding.accountIdentity) await updateBinding({ accountIdentity: identity });
|
|
339
|
+
const model = { providerID: connection.providerId, id: connection.model };
|
|
340
|
+
const agent = openCodeConversationAgent({ nativeTools, tools: Boolean(current?.tools) });
|
|
341
|
+
if (current) current.agent = agent;
|
|
342
|
+
if (binding.sessionId && !binding.databasePath) {
|
|
343
|
+
throw new Error("This OpenCode conversation uses the earlier private native history layout. Run the offline native-history upgrade before continuing; no history was changed.");
|
|
344
|
+
}
|
|
345
|
+
if (!facilities) {
|
|
346
|
+
if (host.opencode) facilities = await host.opencode({ connection, context });
|
|
347
|
+
else {
|
|
348
|
+
const runtimeDirectory = path.join(stateDirectory, "shared", hash([identity, host.commands?.opencode || "opencode"]));
|
|
349
|
+
const selected = { modelProviderId: connection.providerId, canonicalUrl: connection.baseURL || "",
|
|
350
|
+
endpointCode: connection.sdkPackage || "", fingerprint: identity };
|
|
351
|
+
facilities = { runtime: createOpenCodeSharedRuntime({ scope: runtimeDirectory }), runtimeDirectory,
|
|
352
|
+
registryPath: path.join(runtimeDirectory, "session-environments.json"),
|
|
353
|
+
databasePath: path.join(runtimeDirectory, "opencode.db"), selected,
|
|
354
|
+
async start({ signal, connection }) {
|
|
355
|
+
if (binding.executionId && !facilities.runtime.processes.has(binding.directory)) await stopRecordedExecution();
|
|
356
|
+
const starting = new AbortController();
|
|
357
|
+
const abortUnusedStartup = () => {
|
|
358
|
+
if (facilities.runtime.processStarts.size === 1 && facilities.runtime.processes.size === 0) starting.abort(signal.reason);
|
|
359
|
+
};
|
|
360
|
+
signal.addEventListener("abort", abortUnusedStartup, { once: true });
|
|
361
|
+
if (signal.aborted) abortUnusedStartup();
|
|
362
|
+
try {
|
|
363
|
+
const server = await createOpenCodeConversationServer({ command: host.commands?.opencode || "opencode",
|
|
364
|
+
execution, env: host.env || process.env, workdir: binding.workdir, stateDirectory: runtimeDirectory,
|
|
365
|
+
databasePath: facilities.databasePath, sessionEnvironmentRegistry: facilities.registryPath, connection,
|
|
366
|
+
maxOutputTokens: limits.maxOutputTokens, executionLimits: host.limits, signal: starting.signal,
|
|
367
|
+
onStarted: (executionId, processDirectory) => updateBinding({ executionId, processDirectory }),
|
|
368
|
+
async onFailure(error) {
|
|
369
|
+
for (const target of facilities.runtime.processes.values()) target.abortController.abort(error);
|
|
370
|
+
try { await facilities.runtime.stop("opencode-process-exited"); }
|
|
371
|
+
catch (cleanupError) { await reportFailure(cleanupError); }
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return { server, connections: [selected] };
|
|
375
|
+
} finally { signal.removeEventListener("abort", abortUnusedStartup); }
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (binding.databasePath && binding.databasePath !== facilities.databasePath) {
|
|
380
|
+
throw new Error("This OpenCode conversation belongs to another native history store. Restore its original host configuration.");
|
|
381
|
+
}
|
|
382
|
+
await updateBinding({ databasePath: facilities.databasePath, runtimeDirectory: facilities.runtimeDirectory });
|
|
383
|
+
}
|
|
384
|
+
if (!toolBridge && current?.tools) {
|
|
385
|
+
toolBridge = await createOpenCodeToolBridge({
|
|
386
|
+
schemas: current.tools.schemas, maxArgumentBytes: current.tools.maximumArgumentBytes,
|
|
387
|
+
async execute(input, signal) {
|
|
388
|
+
const active = current;
|
|
389
|
+
if (!active?.tools || input.sessionId !== binding.sessionId ||
|
|
390
|
+
[input.id, input.name, input.messageId].some(value => typeof value !== "string" || !value || value.length > 256)) {
|
|
391
|
+
throw new Error("This OpenCode tool call does not belong to the active conversation.");
|
|
392
|
+
}
|
|
393
|
+
const turn = facilities.runtime.turns.get(binding.directory);
|
|
394
|
+
if (!turn?.abortController) {
|
|
395
|
+
throw new Error("This OpenCode tool call does not belong to an admitted conversation.");
|
|
396
|
+
}
|
|
397
|
+
const observed = AbortSignal.any([signal, turn.abortController.signal]);
|
|
398
|
+
await turn.admission?.promise;
|
|
399
|
+
observed.throwIfAborted();
|
|
400
|
+
const messages = await native.server.client.messages(binding.sessionId, { limit: 100 }, { signal: observed });
|
|
401
|
+
let message;
|
|
402
|
+
let owner;
|
|
403
|
+
for (const [nativeId, authored] of active.messageIds) {
|
|
404
|
+
message = openCodeRowsForInput(messages, nativeId).find(message => message.id === input.messageId && message.type === "assistant");
|
|
405
|
+
if (message) { owner = authored; break; }
|
|
406
|
+
}
|
|
407
|
+
const tool = message?.content.find(part => part.type === "tool" && part.callID === input.id);
|
|
408
|
+
if (!tool || tool.tool !== input.name || !isDeepStrictEqual(tool.state?.input, input.input)) {
|
|
409
|
+
throw new Error("OpenCode's application call does not match an admitted native tool use.");
|
|
410
|
+
}
|
|
411
|
+
if (!owner.committed) throw new Error("This OpenCode tool call has no durable authored admission.");
|
|
412
|
+
await owner.committed;
|
|
413
|
+
observed.throwIfAborted();
|
|
414
|
+
try {
|
|
415
|
+
return await active.tools.execute({ id: input.id, name: input.name, arguments: JSON.stringify(input.input) }, { signal: observed, messageId: owner.messageId });
|
|
416
|
+
} catch (error) {
|
|
417
|
+
turn.abortController.abort(error);
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return { configuration, signal, model, agent, connection };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function prepare({ configuration, signal, model, agent, connection }) {
|
|
427
|
+
try {
|
|
428
|
+
const start = host.opencode
|
|
429
|
+
? async () => facilities.runtime.startPreparedProcess(await facilities.prepareServer())
|
|
430
|
+
: () => facilities.start({ signal, connection });
|
|
431
|
+
if (!native) {
|
|
432
|
+
native = await facilities.runtime.acquire(binding.directory, async () => {
|
|
433
|
+
const shared = await facilities.runtime.ensure(facilities.selected, start);
|
|
434
|
+
return { key: binding.directory, workdir: binding.workdir, upstreamSessionId: binding.sessionId,
|
|
435
|
+
abortController: new AbortController(), server: openCodeServerForDirectory(shared.server, binding.workdir) };
|
|
436
|
+
});
|
|
437
|
+
} else await facilities.runtime.ensure(facilities.selected, start);
|
|
438
|
+
signal.throwIfAborted();
|
|
439
|
+
await updateBinding({ executionId: native.server.executionId, processDirectory: native.server.privateRoot || "" });
|
|
440
|
+
const catalogue = await native.server.client.providers({ signal });
|
|
441
|
+
const selected = catalogue.all.find(provider => provider.id === connection.providerId)?.models?.[connection.model];
|
|
442
|
+
if (selected?.id !== connection.model) throw new Error("OpenCode could not load the configured model definition. Check the selected integration before sending.");
|
|
443
|
+
processVariants = selected.variants || {};
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (error.stopProof?.scopeEmpty) await updateBinding({ executionId: "", processDirectory: "" });
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
if (configuration.effort) {
|
|
449
|
+
if (!Object.hasOwn(processVariants, configuration.effort)) throw new Error("The selected OpenCode model does not support this reasoning effort.");
|
|
450
|
+
model.variant = configuration.effort;
|
|
451
|
+
}
|
|
452
|
+
const retainedId = binding.sessionId;
|
|
453
|
+
if (retainedId && (await native.server.client.sessionStatus(retainedId, { signal })).type !== "idle") {
|
|
454
|
+
throw new Error("The native OpenCode conversation is still working. Stop or inspect it before sending another message.");
|
|
455
|
+
}
|
|
456
|
+
const session = {
|
|
457
|
+
selection: { agentId: agent, modelId: model.id, modelProviderId: model.providerID, variantId: model.variant || "" },
|
|
458
|
+
model, workdir: binding.workdir, signal,
|
|
459
|
+
invalidIdentity: () => new Error("OpenCode did not acknowledge a native conversation."),
|
|
460
|
+
identity: {
|
|
461
|
+
async write(nativeId) {
|
|
462
|
+
if (retainedId && nativeId !== retainedId) throw new Error("The saved OpenCode conversation is unavailable. Restore its native history before continuing.");
|
|
463
|
+
if (binding.sessionId !== nativeId) await updateBinding({ sessionId: nativeId });
|
|
464
|
+
},
|
|
465
|
+
publish(nativeId) {
|
|
466
|
+
return facilities.runtime.writeBindings(facilities.registryPath, binding.directory, [{
|
|
467
|
+
upstreamSessionId: nativeId, workdir: binding.workdir, modelProviderId: connection.providerId,
|
|
468
|
+
...(nativeTools ? { env: host.env || process.env } : {}),
|
|
469
|
+
conversation: { systemPrompt: configuration.systemPrompt, nativeTools,
|
|
470
|
+
commandWrapper: host.commandWrapper, tools: current?.tools ? toolBridge?.configuration : undefined }
|
|
471
|
+
}]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
return { target: native, session, model };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function messageMonitor(active, command) {
|
|
479
|
+
return {
|
|
480
|
+
prepare() { return { fields: {} }; },
|
|
481
|
+
create(target, turn, _metadata, options) {
|
|
482
|
+
const abort = () => turn.abortController.abort(command.signal.reason);
|
|
483
|
+
command.signal.addEventListener("abort", abort, { once: true });
|
|
484
|
+
if (command.signal.aborted) abort();
|
|
485
|
+
const recoveryMessageId = nativeMessageId(`${command.input.messageId}:final-response`);
|
|
486
|
+
return {
|
|
487
|
+
observe(_turn, signal) {
|
|
488
|
+
return observeOpenCodeEvents(target.server.client, target.upstreamSessionId, {
|
|
489
|
+
abortController: turn.abortController, signal, eventStartedAt: turn.eventStartedAt,
|
|
490
|
+
async onEvent(summary, event) {
|
|
491
|
+
if (summary.type === "session.status") {
|
|
492
|
+
const status = event.data.properties?.status?.type;
|
|
493
|
+
if (status === "retry" || status === "busy") {
|
|
494
|
+
await turn.admission?.promise;
|
|
495
|
+
await active.deliveryCommitted;
|
|
496
|
+
signal.throwIfAborted();
|
|
497
|
+
await active.onEvent({ type: "phase", phase: status === "retry" ? "retrying" : "working" });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
eventReady: options.eventReady,
|
|
504
|
+
readiness: { timeoutMs: limits.admissionTimeoutMs || 120_000,
|
|
505
|
+
timeoutError: new Error("OpenCode's event connection was not ready before the admission deadline.") },
|
|
506
|
+
finalResponse: {
|
|
507
|
+
agent: active.agent, model: active.prepared.model, recoveryMessageId,
|
|
508
|
+
beforeRecovery() {
|
|
509
|
+
active.messageIds.set(recoveryMessageId, active.messageIds.get(turn.inputMessageId));
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
async writeRun(observed, state) {
|
|
513
|
+
if (state === "active") await active.onEvent({ type: "phase", phase: observed.phase || "working" });
|
|
514
|
+
},
|
|
515
|
+
async projectMessages(messages, { inputMessageId }) {
|
|
516
|
+
await active.deliveryCommitted;
|
|
517
|
+
if (inputMessageId !== turn.inputMessageId) return { failure: "", providerApiFailure: false };
|
|
518
|
+
return facilities.runtime.projectConversationMessages(messages, inputMessageId, {
|
|
519
|
+
maximumOutput, outputSchema: active.configuration.outputSchema,
|
|
520
|
+
previous: active.previous, onMessage: active.onMessage
|
|
521
|
+
});
|
|
522
|
+
},
|
|
523
|
+
completeResult(_turn, { failure }) { return failure; },
|
|
524
|
+
onRetired() { command.signal.removeEventListener("abort", abort); }
|
|
525
|
+
};
|
|
526
|
+
},
|
|
527
|
+
onError(error) { active.failure ||= error; }
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function send(command) {
|
|
532
|
+
const active = current;
|
|
533
|
+
const id = nativeMessageId(command.input.messageId);
|
|
534
|
+
const previousOwner = active.messageIds.get(id);
|
|
535
|
+
let accepted = false;
|
|
536
|
+
let value;
|
|
537
|
+
try {
|
|
538
|
+
value = await facilities.runtime.sendMessage(binding.directory, {
|
|
539
|
+
id, threadId: binding.sessionId, workdir: binding.workdir
|
|
540
|
+
}, { promptTimeoutMs: limits.admissionTimeoutMs || 30_000 }, {
|
|
541
|
+
async writeRun() {},
|
|
542
|
+
async prepare({ overwrite }) {
|
|
543
|
+
const signal = active.prepared
|
|
544
|
+
? AbortSignal.any([command.signal, AbortSignal.timeout(120_000)])
|
|
545
|
+
: active.resolved.signal;
|
|
546
|
+
if (overwrite || !active.prepared) active.prepared = await prepare({ ...active.resolved, signal });
|
|
547
|
+
return { ...active.prepared, session: { ...active.prepared.session, signal } };
|
|
548
|
+
},
|
|
549
|
+
prompt() {
|
|
550
|
+
native.abortController.signal.throwIfAborted();
|
|
551
|
+
return {
|
|
552
|
+
agent: active.agent, model: active.prepared.model,
|
|
553
|
+
prompt: prompt(command.input, active.configuration), attachments: command.input.localFiles,
|
|
554
|
+
authorizeAttachments: true,
|
|
555
|
+
async beforeDispatch(threadId) {
|
|
556
|
+
await command.beforeDispatch({ threadId });
|
|
557
|
+
command.signal.throwIfAborted();
|
|
558
|
+
active.messageIds.set(id, {
|
|
559
|
+
messageId: command.input.messageId, committed: command.deliveryCommitted
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
async commit() {
|
|
565
|
+
await command.accept();
|
|
566
|
+
active.deliveryCommitted = command.deliveryCommitted;
|
|
567
|
+
accepted = true;
|
|
568
|
+
},
|
|
569
|
+
monitor: messageMonitor(active, command)
|
|
570
|
+
});
|
|
571
|
+
if (value.failure) throw value.failure.error;
|
|
572
|
+
if (!value.delivered) throw new Error("OpenCode did not acknowledge the message.");
|
|
573
|
+
} catch (error) {
|
|
574
|
+
const turn = facilities.runtime.turns.get(binding.directory);
|
|
575
|
+
const attempted = value?.failure ? value.failure.attempted : turn?.promptAttempted === true;
|
|
576
|
+
if (attempted && !accepted && !(error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 408)) {
|
|
577
|
+
error.delivery = "uncertain";
|
|
578
|
+
turn?.abortController?.abort(error);
|
|
579
|
+
// Join the same owner's recovery before releasing its process target.
|
|
580
|
+
await facilities.runtime.monitors.get(binding.directory);
|
|
581
|
+
} else if (previousOwner) active.messageIds.set(id, previousOwner);
|
|
582
|
+
else active.messageIds.delete(id);
|
|
583
|
+
if (turn?.active && turn.observationError) error.cleanupFailed = true;
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
active.delivery = { turn: facilities.runtime.turns.get(binding.directory),
|
|
587
|
+
completion: facilities.runtime.monitors.get(binding.directory) };
|
|
588
|
+
return value;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return Object.freeze({
|
|
592
|
+
async inspectAdmission({ messageId, configuration, context }) {
|
|
593
|
+
if (!binding.sessionId) return { accepted: false, messages: [] };
|
|
594
|
+
const prepared = await prepare(await resolvePreparation(configuration, context, AbortSignal.timeout(120_000)));
|
|
595
|
+
await ensureOpenCodeSession(prepared.target, prepared.session);
|
|
596
|
+
const id = nativeMessageId(messageId);
|
|
597
|
+
const { accepted = false, messages = [] } = await inspectOpenCodeMessageAdmission(native.server.client, binding.sessionId, id) || {};
|
|
598
|
+
return { accepted,
|
|
599
|
+
messages: openCodeRowsForInput(messages, id).filter(message => message.type === "assistant" && !message.summary)
|
|
600
|
+
.map(message => ({ id: message.id, outputId: `${message.id}:assistant`, role: "assistant",
|
|
601
|
+
text: configuration.outputSchema ? openCodeStructuredOutput(openCodeAssistantMessageText(message)) : openCodeAssistantMessageText(message) })) };
|
|
602
|
+
},
|
|
603
|
+
steer(command) {
|
|
604
|
+
if (!current || current.finished) throw new Error("OpenCode has no active turn to steer.");
|
|
605
|
+
return send(command);
|
|
606
|
+
},
|
|
607
|
+
async run(command) {
|
|
608
|
+
if (disposed || current) throw new Error("This OpenCode conversation is closed or already working.");
|
|
609
|
+
const active = current = { tools: command.tools, configuration: command.configuration,
|
|
610
|
+
onMessage: command.onMessage, onEvent: command.onEvent, messageIds: new Map(), previous: new Map() };
|
|
611
|
+
let problem;
|
|
612
|
+
try {
|
|
613
|
+
active.resolved = await resolvePreparation(command.configuration, command.context,
|
|
614
|
+
AbortSignal.any([command.signal, AbortSignal.timeout(120_000)]));
|
|
615
|
+
let dispatchFailure;
|
|
616
|
+
try { await send(command); }
|
|
617
|
+
catch (error) { dispatchFailure = error; }
|
|
618
|
+
for (;;) {
|
|
619
|
+
const settling = active.delivery;
|
|
620
|
+
let result;
|
|
621
|
+
let failure = dispatchFailure || active.failure;
|
|
622
|
+
if (!failure && settling?.completion) {
|
|
623
|
+
try { result = await settling.completion; }
|
|
624
|
+
catch (error) { failure = error; }
|
|
625
|
+
} else if (!failure && settling) result = facilities.runtime.turnSnapshot(settling.turn, binding.sessionId);
|
|
626
|
+
if (command.finishIfCurrent && !await command.finishIfCurrent(() => active.delivery === settling)) continue;
|
|
627
|
+
failure = dispatchFailure || active.failure || failure;
|
|
628
|
+
if (!failure && result?.error) {
|
|
629
|
+
failure = Object.assign(new Error(result.error), { status: result.status });
|
|
630
|
+
if (result.active && result.status === "observation_lost") failure.cleanupFailed = true;
|
|
631
|
+
}
|
|
632
|
+
if (failure) throw failure;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
problem = error.cleanupFailed ? error : command.signal.aborted ? command.signal.reason : error;
|
|
637
|
+
if (problem.cleanupFailed) problem.message = "OpenCode process cleanup could not be confirmed. Restore its execution host before continuing.";
|
|
638
|
+
}
|
|
639
|
+
active.finished = true;
|
|
640
|
+
try { if (!problem?.cleanupFailed && (problem || command.signal.aborted)) await stop(); }
|
|
641
|
+
catch (error) { problem = error; }
|
|
642
|
+
finally { await toolBridge?.whenIdle(); if (!problem?.cleanupFailed) current = null; }
|
|
643
|
+
if (problem) throw problem;
|
|
644
|
+
},
|
|
645
|
+
cancel: stop,
|
|
646
|
+
async dispose() { await stop(); disposed = true; }
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|