@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.
- package/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -1,81 +1,57 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { mkdir, realpath, rm } from "node:fs/promises";
|
|
2
|
+
import { mkdir, realpath } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import { isDeepStrictEqual } from "node:util";
|
|
6
4
|
import { createLocalConversationExecution } from "../localExecution.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
openCodeDetachedPrompt, openCodeStructuredOutput, readOpenCodeMessagesUntil } from "../openCodeTurn.js";
|
|
5
|
+
import { ignoreReceipt } from "../nativeCommandCallbacks.js";
|
|
6
|
+
import { openCodeServerForDirectory } from "../openCodeNativeSession.js";
|
|
7
|
+
import { retireOpenCodeConversationHistory } from "../openCodeNativeHistory.js";
|
|
8
|
+
import { recoverWithoutProjection } from "../openCodeTurn.js";
|
|
12
9
|
import { createOpenCodeToolBridge } from "../openCodeTools.js";
|
|
13
|
-
import { validateConversationConfiguration
|
|
10
|
+
import { validateConversationConfiguration } from "../configuration.js";
|
|
14
11
|
import { createScopedConversationHandle } from "./scoped.js";
|
|
12
|
+
import { executeOpenCodeApplicationTool, openStandaloneOpenCodeConversation } from "./opencodeStandalone.js";
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
/** A runtime given no host runs OpenCode in the process's working directory, with local execution and the `opencode` command. */
|
|
15
|
+
const LOCAL_HOST = Object.freeze({});
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const tool = message?.content.find(part => part.type === "tool" && part.callID === input.id);
|
|
44
|
-
if (!tool || tool.tool !== input.name || !isDeepStrictEqual(tool.state?.input, input.input)) {
|
|
45
|
-
throw new Error("OpenCode's application call does not match an admitted native tool use.");
|
|
46
|
-
}
|
|
47
|
-
if (!owner.committed) throw new Error("This OpenCode tool call has no durable authored admission.");
|
|
48
|
-
await owner.committed;
|
|
49
|
-
observed.throwIfAborted();
|
|
50
|
-
assertCurrent?.(true, owner);
|
|
51
|
-
try {
|
|
52
|
-
return await active.tools.execute({ id: input.id, name: input.name, arguments: JSON.stringify(input.input) }, { signal: observed, messageId: owner.messageId });
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if (!assertCurrent || assertCurrent(false, owner)) turn.abortController.abort(error);
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Uses the application's authorized API connection; native state stays server-side. */
|
|
60
|
-
export function createOpenCodeConversationDriver({ connections, host = {}, limits = {} } = {}) {
|
|
17
|
+
/**
|
|
18
|
+
* Uses the application's authorized API connection; native state stays server-side.
|
|
19
|
+
* @param {{
|
|
20
|
+
* connections?: { resolve(request: { context: object, integrationId: string }): Promise<object> },
|
|
21
|
+
* host?: {
|
|
22
|
+
* env?: import("@jskit-ai/kernel/server/types").RuntimeEnv,
|
|
23
|
+
* workdir?: string,
|
|
24
|
+
* stateDirectory?: string,
|
|
25
|
+
* execution?: import("../localExecution.js").ConversationExecution,
|
|
26
|
+
* nativeTools?: boolean,
|
|
27
|
+
* conversation?: Function,
|
|
28
|
+
* opencode?: Function,
|
|
29
|
+
* commands?: { opencode?: string },
|
|
30
|
+
* limits?: object,
|
|
31
|
+
* commandWrapper?: object
|
|
32
|
+
* },
|
|
33
|
+
* limits?: Record<string, number>
|
|
34
|
+
* }} options
|
|
35
|
+
*/
|
|
36
|
+
export function createOpenCodeConversationDriver({
|
|
37
|
+
connections,
|
|
38
|
+
host = LOCAL_HOST,
|
|
39
|
+
limits = {}
|
|
40
|
+
}) {
|
|
41
|
+
// readable-check: allow method-probe - input validation: a host that does not open its own conversations requires the application's AI connection resolver
|
|
61
42
|
if (!host.conversation && typeof connections?.resolve !== "function") throw new TypeError("OpenCode conversations require an authorized AI connection resolver.");
|
|
62
43
|
const workdir = path.resolve(host.workdir || process.cwd());
|
|
63
44
|
const stateDirectory = path.resolve(host.stateDirectory || path.join(workdir, ".assistant", "native", "opencode"));
|
|
64
45
|
const execution = host.execution || createLocalConversationExecution();
|
|
65
46
|
const nativeTools = host.nativeTools === true;
|
|
47
|
+
if (nativeTools && !host.env) {
|
|
48
|
+
throw new TypeError("An OpenCode conversation's host requires the application's environment, such as process.env, for native tools.");
|
|
49
|
+
}
|
|
66
50
|
const maximumOutput = limits.maxOutputCharacters ?? 64_000;
|
|
67
51
|
if (!Number.isSafeInteger(maximumOutput) || maximumOutput < 1) throw new TypeError("Invalid OpenCode output limit.");
|
|
68
52
|
if (limits.maxOutputTokens !== undefined && (!Number.isSafeInteger(limits.maxOutputTokens) || limits.maxOutputTokens < 1)) {
|
|
69
53
|
throw new TypeError("Invalid OpenCode token limit.");
|
|
70
54
|
}
|
|
71
|
-
function prompt(input, configuration) {
|
|
72
|
-
const text = `${input.text || ""}${input.attachmentManifest || ""}`;
|
|
73
|
-
if (!configuration.outputSchema) return { text, content: input.content };
|
|
74
|
-
const prepared = openCodeDetachedPrompt({ prompt: text, outputSchema: configuration.outputSchema });
|
|
75
|
-
if (prepared.length > (limits.maxInputCharacters ?? 32_000)) throw new Error("OpenCode structured input exceeded the configured limit.");
|
|
76
|
-
return { text: prepared, content: input.content };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
55
|
function withNativeStorageTarget({ native, context }, operation) {
|
|
80
56
|
const prepared = native.preparation.storage(context);
|
|
81
57
|
return native.owner.withStorageProcess(async () =>
|
|
@@ -87,635 +63,321 @@ export function createOpenCodeConversationDriver({ connections, host = {}, limit
|
|
|
87
63
|
return native.owner.closeSession(prepared.sessionId, prepared.options, prepared.application);
|
|
88
64
|
}
|
|
89
65
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
66
|
+
function validateConfiguration(configuration) {
|
|
67
|
+
validateConversationConfiguration(configuration, { engine: "OpenCode", connections, connectionRequired: true,
|
|
68
|
+
structuredOutput: true, maxOutputCharacters: maximumOutput });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readState({ conversation, context, representation }) {
|
|
72
|
+
const { owner, preparation } = conversation.native;
|
|
73
|
+
const nativeResult = await owner.readSessionState(context, preparation.state);
|
|
74
|
+
return conversation.read(context, representation, nativeResult);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inspectTemporaryActivity({ native }) {
|
|
78
|
+
return native.owner.hasActiveTemporaryConversation(native.sessionId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function releaseRenewalPredecessorProcessExitProof({ native }) {
|
|
82
|
+
return native.owner.releaseRenewalPredecessorProcessExitProof(native.preparation);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function releaseRenewalSuccessorProcessExitProof({ native }) {
|
|
86
|
+
return native.owner.releaseProcessExitProof(native.preparation.sessionId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function generateRenewalHandover({ native, input, context }) {
|
|
90
|
+
return native.owner.runPreparedRenewalTurn(await native.preparation.handover(input, context));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function seedRenewalHandover({ native, input, context }) {
|
|
94
|
+
return native.owner.runPreparedRenewalTurn(await native.preparation.seed(input, context));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function ensureConversation({ native, context }) {
|
|
98
|
+
return native.owner.ensurePreparedSessionReadiness(await native.preparation.readiness(context));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function interruptDetachedConversation({ native, input, context }) {
|
|
102
|
+
return native.owner.stopPreparedConversation(await native.preparation.existing(input, context), input);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function deleteDetachedConversation({ native, input, context }) {
|
|
106
|
+
const prepared = await native.preparation.existing(input, context, { operation: "delete" });
|
|
107
|
+
return native.owner.deletePreparedConversation(prepared, input);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function listConversationStorage(prepared) {
|
|
111
|
+
const { binding, context } = prepared;
|
|
112
|
+
return withNativeStorageTarget(prepared, async target =>
|
|
113
|
+
(await target.server.client.listConversationsForDirectory(binding.workdir, { signal: context.signal }))
|
|
114
|
+
.map((row) => ({ ...binding, conversationId: row.id })));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function retireConversationHistory(prepared) {
|
|
118
|
+
return withNativeStorageTarget(prepared, target => {
|
|
119
|
+
// Native status is scoped to the saved project instance, not the control server.
|
|
120
|
+
const client = openCodeServerForDirectory(target.server, prepared.binding.workdir).client;
|
|
121
|
+
return retireOpenCodeConversationHistory(target.server.client, client, prepared.binding, prepared.context);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function closeProject({ native, input }) {
|
|
126
|
+
return native.owner.closeProject(input, native.preparation.projectCleanup());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function invalidateRuntimes({ native, input }) {
|
|
130
|
+
return native.owner.invalidateRuntimes(input, native.preparation);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function reconcileSessions({ native, sessions, options }) {
|
|
134
|
+
return native.owner.reconcileSessions(sessions, options, native.preparation);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function createConversation({ native, input, context }) {
|
|
138
|
+
return native.owner.createPreparedConversation(await native.preparation.creation(input, context));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function runDetachedConversation({ native, input, options }) {
|
|
142
|
+
return native.owner.runPreparedConversationTurn(await native.preparation.turn(input, options));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function createBinding() {
|
|
146
|
+
const directory = path.join(stateDirectory, randomUUID());
|
|
147
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
148
|
+
return { sessionId: "", workdir: await realpath(workdir), directory, executionId: "", processDirectory: "" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
|
|
152
|
+
if (conversation?.native?.scoped) {
|
|
153
|
+
const supplied = conversation.native;
|
|
154
|
+
const { owner, preparation } = supplied;
|
|
155
|
+
const { conversationId, context: openingContext } = supplied.scoped;
|
|
156
|
+
return createScopedConversationHandle({ conversationId,
|
|
157
|
+
sendNative: async (input, context) => owner.runPreparedConversationTurn(await preparation.turn(input, context)),
|
|
158
|
+
readNative: async (input, context) => owner.readPreparedConversation(await preparation.existing(input, context), input),
|
|
159
|
+
waitNative: async (input, context) => owner.waitPreparedConversationTurn(await preparation.existing(input, context), input),
|
|
160
|
+
cancel: async (input, context) => owner.stopPreparedConversation(await preparation.existing(input, context), input),
|
|
161
|
+
deleteConversation: async (input, context) => owner.deletePreparedConversation(
|
|
162
|
+
await preparation.existing(input, context, { operation: "delete" }), input),
|
|
163
|
+
disposeOpening: () => disposeNative({ native: supplied, options: openingContext })
|
|
141
164
|
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
},
|
|
146
|
-
invalidateRuntimes({ native, input }) {
|
|
147
|
-
return native.owner.invalidateRuntimes(input, native.preparation);
|
|
148
|
-
},
|
|
149
|
-
reconcileSessions({ native, sessions, options }) {
|
|
150
|
-
return native.owner.reconcileSessions(sessions, options, native.preparation);
|
|
151
|
-
},
|
|
152
|
-
async createConversation({ native, input, context }) {
|
|
153
|
-
return native.owner.createPreparedConversation(await native.preparation.creation(input, context));
|
|
154
|
-
},
|
|
155
|
-
async runDetachedConversation({ native, input, options }) {
|
|
156
|
-
return native.owner.runPreparedConversationTurn(await native.preparation.turn(input, options));
|
|
157
|
-
},
|
|
158
|
-
async createBinding() {
|
|
159
|
-
const directory = path.join(stateDirectory, randomUUID());
|
|
160
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
161
|
-
return { sessionId: "", workdir: await realpath(workdir), directory, executionId: "", processDirectory: "" };
|
|
162
|
-
},
|
|
163
|
-
async open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
|
|
164
|
-
if (conversation?.native?.scoped) {
|
|
165
|
-
const supplied = conversation.native;
|
|
166
|
-
const { owner, preparation } = supplied;
|
|
167
|
-
const { conversationId, context: openingContext } = supplied.scoped;
|
|
168
|
-
return createScopedConversationHandle({ conversationId,
|
|
169
|
-
sendNative: async (input, context) => owner.runPreparedConversationTurn(await preparation.turn(input, context)),
|
|
170
|
-
readNative: async (input, context) => owner.readPreparedConversation(await preparation.existing(input, context), input),
|
|
171
|
-
waitNative: async (input, context) => owner.waitPreparedConversationTurn(await preparation.existing(input, context), input),
|
|
172
|
-
cancel: async (input, context) => owner.stopPreparedConversation(await preparation.existing(input, context), input),
|
|
173
|
-
deleteConversation: async (input, context) => owner.deletePreparedConversation(
|
|
174
|
-
await preparation.existing(input, context, { operation: "delete" }), input),
|
|
175
|
-
disposeOpening: () => disposeNative({ native: supplied, options: openingContext })
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (conversation?.native) {
|
|
179
|
-
const supplied = conversation.native;
|
|
180
|
-
let current;
|
|
181
|
-
let disposed = false;
|
|
182
|
-
let cleanupFailure;
|
|
183
|
-
let toolBridge;
|
|
184
|
-
async function interrupt(context) {
|
|
185
|
-
try {
|
|
186
|
-
const acquired = await supplied.acquire(context || current?.context);
|
|
187
|
-
const value = await supplied.owner.interruptPreparedTurn(await supplied.preparation.interruption(acquired.options));
|
|
188
|
-
if (toolBridge) { await toolBridge.close(); toolBridge = null; }
|
|
189
|
-
if (cleanupFailure) {
|
|
190
|
-
await reportFailure(cleanupFailure, { recovered: true });
|
|
191
|
-
cleanupFailure = null;
|
|
192
|
-
}
|
|
193
|
-
return value;
|
|
194
|
-
} catch (error) {
|
|
195
|
-
error.cleanupFailed = true;
|
|
196
|
-
cleanupFailure = error;
|
|
197
|
-
current?.failure.reject(error);
|
|
198
|
-
await reportFailure(error);
|
|
199
|
-
throw error;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
async function send(command) {
|
|
203
|
-
const { input, context, signal, beforeDispatch, accept } = command;
|
|
204
|
-
signal.throwIfAborted();
|
|
205
|
-
const acquired = await supplied.acquire(context);
|
|
206
|
-
const active = current;
|
|
207
|
-
if (active?.tools) signal.throwIfAborted();
|
|
208
|
-
if (active?.tools && !toolBridge) {
|
|
209
|
-
toolBridge = await createOpenCodeToolBridge({ schemas: active.tools.schemas,
|
|
210
|
-
maxArgumentBytes: active.tools.maximumArgumentBytes, execute(input, signal) {
|
|
211
|
-
const turn = supplied.owner.turns.get(acquired.key);
|
|
212
|
-
const target = supplied.owner.processes.get(acquired.key);
|
|
213
|
-
const execute = () => executeOpenCodeApplicationTool({ active: current, turn, target,
|
|
214
|
-
sessionId: target?.upstreamSessionId,
|
|
215
|
-
assertCurrent(throwFailure = true, authored) {
|
|
216
|
-
const owned = !disposed && current === active && active.turn === turn && turn?.active &&
|
|
217
|
-
(!authored || active.messageIds.get(turn.inputMessageId) === authored) &&
|
|
218
|
-
supplied.owner.turns.get(acquired.key) === turn && supplied.owner.processes.get(acquired.key) === target;
|
|
219
|
-
if (!owned && throwFailure) throw new Error("This OpenCode tool call belongs to a retired native turn.");
|
|
220
|
-
return owned;
|
|
221
|
-
}
|
|
222
|
-
}, input, signal);
|
|
223
|
-
return typeof acquired.runInContext === "function" ? acquired.runInContext(execute) : execute();
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
const options = active?.tools ? { ...acquired.options, applicationTools: toolBridge.configuration } : acquired.options;
|
|
228
|
-
let prepared;
|
|
229
|
-
const dispatch = active?.tools ? async identity => {
|
|
230
|
-
await beforeDispatch(identity);
|
|
231
|
-
signal.throwIfAborted();
|
|
232
|
-
active.turn = supplied.owner.turns.get(acquired.key);
|
|
233
|
-
active.messageIds.set(prepared.input.id, { messageId: input.messageId, committed: command.deliveryCommitted });
|
|
234
|
-
} : beforeDispatch;
|
|
235
|
-
prepared = await supplied.preparation.message({ ...input.nativeMessage, onPromptSending: dispatch }, options);
|
|
236
|
-
if (prepared.application?.commit) {
|
|
237
|
-
const application = prepared.application;
|
|
238
|
-
prepared.application = { ...application, async commit(admitted) {
|
|
239
|
-
command.onNativeReceipt?.();
|
|
240
|
-
const result = await application.commit(admitted);
|
|
241
|
-
command.onReceiptPersisted?.();
|
|
242
|
-
return result;
|
|
243
|
-
} };
|
|
244
|
-
}
|
|
245
|
-
if (active?.tools && prepared.application?.monitor) {
|
|
246
|
-
const monitor = prepared.application.monitor;
|
|
247
|
-
const create = monitor.create;
|
|
248
|
-
prepared.application = { ...prepared.application, monitor: { ...monitor,
|
|
249
|
-
create(...args) {
|
|
250
|
-
const projection = create(...args);
|
|
251
|
-
const finalResponse = projection.finalResponse;
|
|
252
|
-
return { ...projection, finalResponse: { ...finalResponse,
|
|
253
|
-
async beforeRecovery(completion) {
|
|
254
|
-
active.messageIds.set(finalResponse.recoveryMessageId, active.messageIds.get(completion.inputMessageId));
|
|
255
|
-
await finalResponse.beforeRecovery?.(completion);
|
|
256
|
-
}
|
|
257
|
-
} };
|
|
258
|
-
}
|
|
259
|
-
} };
|
|
260
|
-
}
|
|
261
|
-
const value = await supplied.owner.sendPreparedMessage(prepared, options);
|
|
262
|
-
const delivered = { value, completion: value.conversationTurn ? supplied.owner.monitors.get(acquired.key) : null };
|
|
263
|
-
if (value.delivered === true && value.conversationTurn) {
|
|
264
|
-
if (current) current.delivery = delivered;
|
|
265
|
-
await accept({ nativeResult: { value }, conversationTurn: value.conversationTurn, nativeTurnId: value.turn?.id });
|
|
266
|
-
} else if (active?.tools) active.messageIds.delete(prepared.input.id);
|
|
267
|
-
return delivered;
|
|
268
|
-
}
|
|
269
|
-
return Object.freeze({
|
|
270
|
-
publishNative: event => conversation.publish(event),
|
|
271
|
-
async inspectAdmission({ context, representation, ...input }) {
|
|
272
|
-
const acquired = await supplied.acquire(context);
|
|
273
|
-
const result = await supplied.owner.inspectPreparedMessage(await supplied.preparation.inspection(input, acquired.options));
|
|
274
|
-
return representation === "native" ? result : { accepted: result.admission === "accepted" };
|
|
275
|
-
},
|
|
276
|
-
async run(command) {
|
|
277
|
-
if (disposed || current) throw new Error("This OpenCode conversation is closed or already working.");
|
|
278
|
-
const active = { context: command.context, failure: Promise.withResolvers(),
|
|
279
|
-
...(command.tools ? { tools: command.tools, messageIds: new Map() } : {}) };
|
|
280
|
-
let failure;
|
|
281
|
-
active.failure.promise.catch(error => { failure = error; });
|
|
282
|
-
current = active;
|
|
283
|
-
let cancellation;
|
|
284
|
-
const abort = () => {
|
|
285
|
-
cancellation ||= interrupt(command.context).then(() => { throw command.signal.reason; });
|
|
286
|
-
cancellation.catch(active.failure.reject);
|
|
287
|
-
};
|
|
288
|
-
command.signal.addEventListener("abort", abort, { once: true });
|
|
289
|
-
try {
|
|
290
|
-
let delivered;
|
|
291
|
-
let dispatchFailure;
|
|
292
|
-
try { delivered = await send(command); }
|
|
293
|
-
catch (error) { dispatchFailure = error; }
|
|
294
|
-
for (;;) {
|
|
295
|
-
const settling = active.delivery;
|
|
296
|
-
let result;
|
|
297
|
-
let problem = dispatchFailure || failure;
|
|
298
|
-
if (!problem && settling?.completion) {
|
|
299
|
-
try { result = await Promise.race([settling.completion, active.failure.promise]); }
|
|
300
|
-
catch (error) { problem = error; }
|
|
301
|
-
}
|
|
302
|
-
if (command.finishIfCurrent && !await command.finishIfCurrent(() => active.delivery === settling)) {
|
|
303
|
-
// The original sender admitted another invocation and returned
|
|
304
|
-
// its exact monitor. Keep the same completion and Stop owner.
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
problem = dispatchFailure || failure || problem;
|
|
308
|
-
if (!problem && result?.error) {
|
|
309
|
-
problem = Object.assign(new Error(result.error), { status: result.status });
|
|
310
|
-
if (result.active && result.status === "observation_lost") {
|
|
311
|
-
problem.cleanupFailed = true;
|
|
312
|
-
cleanupFailure = problem;
|
|
313
|
-
await reportFailure(problem);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (problem) throw problem;
|
|
317
|
-
return { value: delivered.value };
|
|
318
|
-
}
|
|
319
|
-
} finally {
|
|
320
|
-
command.signal.removeEventListener("abort", abort);
|
|
321
|
-
await cancellation?.catch(() => {});
|
|
322
|
-
if (toolBridge) { await toolBridge.close(); toolBridge = null; }
|
|
323
|
-
if (current === active) current = null;
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
async steer(command) { return { value: (await send(command)).value }; },
|
|
327
|
-
async cancel({ context, representation } = {}) {
|
|
328
|
-
const value = await interrupt(context);
|
|
329
|
-
return representation === "native" ? { value } : value;
|
|
330
|
-
},
|
|
331
|
-
async dispose(options) {
|
|
332
|
-
try {
|
|
333
|
-
const result = await disposeNative({ native: supplied, options });
|
|
334
|
-
if (result?.ok === false) throw Object.assign(new Error(result.error || "OpenCode cleanup could not be confirmed."), result);
|
|
335
|
-
if (toolBridge) { await toolBridge.close(); toolBridge = null; }
|
|
336
|
-
disposed = true;
|
|
337
|
-
return result;
|
|
338
|
-
} catch (error) {
|
|
339
|
-
error.cleanupFailed = true;
|
|
340
|
-
current?.failure.reject(error);
|
|
341
|
-
throw error;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
if (!binding || typeof binding.sessionId !== "string" || binding.workdir !== await realpath(workdir) ||
|
|
347
|
-
path.dirname(binding.directory || "") !== stateDirectory) {
|
|
348
|
-
throw new Error("This OpenCode conversation belongs to another working directory or native state directory. Restore its original host configuration.");
|
|
349
|
-
}
|
|
350
|
-
let native;
|
|
351
|
-
let toolBridge;
|
|
352
|
-
let facilities;
|
|
353
|
-
let processVariants = {};
|
|
165
|
+
}
|
|
166
|
+
if (conversation?.native) {
|
|
167
|
+
const supplied = conversation.native;
|
|
354
168
|
let current;
|
|
355
|
-
let stopping;
|
|
356
169
|
let disposed = false;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
throw new Error("OpenCode's private process directory does not belong to this host.");
|
|
368
|
-
}
|
|
369
|
-
await rm(binding.processDirectory, { recursive: true, force: true });
|
|
370
|
-
}
|
|
371
|
-
await updateBinding({ executionId: "", processDirectory: "" });
|
|
372
|
-
}
|
|
373
|
-
async function stop() {
|
|
374
|
-
if (stopping) return stopping;
|
|
375
|
-
stopping = (async () => {
|
|
376
|
-
await toolBridge?.close();
|
|
377
|
-
toolBridge = null;
|
|
378
|
-
if (facilities?.runtime.cleanupPending) {
|
|
379
|
-
await facilities.runtime.stop("opencode-startup-cleanup");
|
|
380
|
-
await updateBinding({ executionId: "", processDirectory: "" });
|
|
381
|
-
}
|
|
382
|
-
if (native) {
|
|
383
|
-
await facilities.runtime.releaseTarget(native, {
|
|
384
|
-
onRemoved: () => facilities.runtime.writeBindings(facilities.registryPath, binding.directory, [])
|
|
385
|
-
});
|
|
386
|
-
facilities.runtime.turns.delete(binding.directory);
|
|
387
|
-
await updateBinding({ executionId: "", processDirectory: "" });
|
|
388
|
-
native = null;
|
|
389
|
-
} else if (binding.executionId && facilities && !host.opencode &&
|
|
390
|
-
!facilities.runtime.current && !facilities.runtime.processStarts.size) {
|
|
391
|
-
// Only our unfinished standalone startup belongs to this handle.
|
|
392
|
-
// A saved shared-server reference is not authority to stop a peer's
|
|
393
|
-
// service before this conversation has acquired its owner.
|
|
394
|
-
await stopRecordedExecution();
|
|
170
|
+
let cleanupFailure;
|
|
171
|
+
let toolBridge;
|
|
172
|
+
async function interrupt(context) {
|
|
173
|
+
try {
|
|
174
|
+
const acquired = await supplied.acquire(context || current?.context);
|
|
175
|
+
const value = await supplied.owner.interruptPreparedTurn(await supplied.preparation.interruption(acquired.options));
|
|
176
|
+
if (toolBridge) { await toolBridge.close(); toolBridge = null; }
|
|
177
|
+
if (cleanupFailure) {
|
|
178
|
+
await reportFailure(cleanupFailure, { recovered: true });
|
|
179
|
+
cleanupFailure = null;
|
|
395
180
|
}
|
|
396
|
-
|
|
397
|
-
}
|
|
181
|
+
return value;
|
|
182
|
+
} catch (error) {
|
|
398
183
|
error.cleanupFailed = true;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
184
|
+
cleanupFailure = error;
|
|
185
|
+
current?.failure.reject(error);
|
|
186
|
+
await reportFailure(error);
|
|
402
187
|
throw error;
|
|
403
|
-
}).finally(() => { stopping = null; });
|
|
404
|
-
return stopping;
|
|
405
|
-
}
|
|
406
|
-
async function resolvePreparation(configuration, context, signal, requireBound = false, receiptBinding, authoredPreparation = false) {
|
|
407
|
-
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
408
|
-
signal.throwIfAborted();
|
|
409
|
-
validateConnectionModel(configuration, connection);
|
|
410
|
-
const identity = hash([connection.providerId, connection.baseURL || "", connection.apiKey]);
|
|
411
|
-
if (requireBound && !receiptBinding?.accountIdentity) throw new Error("The completed OpenCode response has no saved account fingerprint.");
|
|
412
|
-
if (requireBound && (binding.accountIdentity !== receiptBinding.accountIdentity || identity !== receiptBinding.accountIdentity)) {
|
|
413
|
-
throw new Error("The completed OpenCode response belongs to a different account binding.");
|
|
414
188
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
189
|
+
}
|
|
190
|
+
async function send(command) {
|
|
191
|
+
const {
|
|
192
|
+
input,
|
|
193
|
+
context,
|
|
194
|
+
signal,
|
|
195
|
+
beforeDispatch,
|
|
196
|
+
accept,
|
|
197
|
+
onNativeReceipt = ignoreReceipt,
|
|
198
|
+
onReceiptPersisted = ignoreReceipt
|
|
199
|
+
} = command;
|
|
418
200
|
signal.throwIfAborted();
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
else {
|
|
440
|
-
const runtimeDirectory = path.join(stateDirectory, "shared", hash([identity, host.commands?.opencode || "opencode"]));
|
|
441
|
-
const selected = { modelProviderId: connection.providerId, canonicalUrl: connection.baseURL || "",
|
|
442
|
-
endpointCode: connection.sdkPackage || "", fingerprint: identity };
|
|
443
|
-
facilities = { runtime: createOpenCodeSharedRuntime({ scope: runtimeDirectory }), runtimeDirectory,
|
|
444
|
-
registryPath: path.join(runtimeDirectory, "session-environments.json"),
|
|
445
|
-
databasePath: path.join(runtimeDirectory, "opencode.db"), selected,
|
|
446
|
-
async start({ signal, connection }) {
|
|
447
|
-
if (binding.executionId && !facilities.runtime.processes.has(binding.directory)) await stopRecordedExecution();
|
|
448
|
-
const starting = new AbortController();
|
|
449
|
-
const abortUnusedStartup = () => {
|
|
450
|
-
if (facilities.runtime.processStarts.size === 1 && facilities.runtime.processes.size === 0) starting.abort(signal.reason);
|
|
451
|
-
};
|
|
452
|
-
signal.addEventListener("abort", abortUnusedStartup, { once: true });
|
|
453
|
-
if (signal.aborted) abortUnusedStartup();
|
|
454
|
-
try {
|
|
455
|
-
const server = await createOpenCodeConversationServer({ command: host.commands?.opencode || "opencode",
|
|
456
|
-
execution, env: host.env || process.env, workdir: binding.workdir, stateDirectory: runtimeDirectory,
|
|
457
|
-
databasePath: facilities.databasePath, sessionEnvironmentRegistry: facilities.registryPath, connection,
|
|
458
|
-
maxOutputTokens: limits.maxOutputTokens, executionLimits: host.limits, signal: starting.signal,
|
|
459
|
-
onStarted: (executionId, processDirectory) => updateBinding({ executionId, processDirectory }),
|
|
460
|
-
async onFailure(error) {
|
|
461
|
-
for (const target of facilities.runtime.processes.values()) target.abortController.abort(error);
|
|
462
|
-
try { await facilities.runtime.stop("opencode-process-exited"); }
|
|
463
|
-
catch (cleanupError) { await reportFailure(cleanupError); }
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
return { server, connections: [selected] };
|
|
467
|
-
} finally { signal.removeEventListener("abort", abortUnusedStartup); }
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
if (binding.databasePath && binding.databasePath !== facilities.databasePath) {
|
|
472
|
-
throw new Error("This OpenCode conversation belongs to another native history store. Restore its original host configuration.");
|
|
473
|
-
}
|
|
474
|
-
await updateBinding({ databasePath: facilities.databasePath, runtimeDirectory: facilities.runtimeDirectory });
|
|
475
|
-
}
|
|
476
|
-
if (!toolBridge && current?.tools) {
|
|
477
|
-
toolBridge = await createOpenCodeToolBridge({
|
|
478
|
-
schemas: current.tools.schemas, maxArgumentBytes: current.tools.maximumArgumentBytes,
|
|
479
|
-
async execute(input, signal) {
|
|
480
|
-
return executeOpenCodeApplicationTool({ active: current,
|
|
481
|
-
turn: facilities.runtime.turns.get(binding.directory), target: native, sessionId: binding.sessionId }, input, signal);
|
|
201
|
+
const acquired = await supplied.acquire(context);
|
|
202
|
+
const active = current;
|
|
203
|
+
if (active?.tools) signal.throwIfAborted();
|
|
204
|
+
if (active?.tools && !toolBridge) {
|
|
205
|
+
toolBridge = await createOpenCodeToolBridge({ schemas: active.tools.schemas,
|
|
206
|
+
maxArgumentBytes: active.tools.maximumArgumentBytes, execute(input, signal) {
|
|
207
|
+
const turn = supplied.owner.turns.get(acquired.key);
|
|
208
|
+
const target = supplied.owner.processes.get(acquired.key);
|
|
209
|
+
const execute = () => executeOpenCodeApplicationTool({ active: current, turn, target,
|
|
210
|
+
sessionId: target?.upstreamSessionId,
|
|
211
|
+
assertCurrent(throwFailure = true, authored) {
|
|
212
|
+
const owned = !disposed && current === active && active.turn === turn && turn?.active &&
|
|
213
|
+
(!authored || active.messageIds.get(turn.inputMessageId) === authored) &&
|
|
214
|
+
supplied.owner.turns.get(acquired.key) === turn && supplied.owner.processes.get(acquired.key) === target;
|
|
215
|
+
if (!owned && throwFailure) throw new Error("This OpenCode tool call belongs to a retired native turn.");
|
|
216
|
+
return owned;
|
|
217
|
+
}
|
|
218
|
+
}, input, signal);
|
|
219
|
+
// readable-check: allow method-probe - optional operation: an acquisition that runs inside its request's context supplies runInContext
|
|
220
|
+
return typeof acquired.runInContext === "function" ? acquired.runInContext(execute) : execute();
|
|
482
221
|
}
|
|
483
222
|
});
|
|
484
223
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
try {
|
|
490
|
-
const start = host.opencode
|
|
491
|
-
? async () => facilities.runtime.startPreparedProcess(await facilities.prepareServer())
|
|
492
|
-
: () => facilities.start({ signal, connection });
|
|
493
|
-
// A managed host's saved connections reach its running server in place.
|
|
494
|
-
const savedConnections = host.opencode ? async () => (await facilities.prepareServer()).connections : null;
|
|
495
|
-
if (!native) {
|
|
496
|
-
native = await facilities.runtime.acquire(binding.directory, async () => {
|
|
497
|
-
const shared = await facilities.runtime.ensure(facilities.selected, start, savedConnections);
|
|
498
|
-
return { key: binding.directory, workdir: binding.workdir, upstreamSessionId: binding.sessionId,
|
|
499
|
-
abortController: new AbortController(), server: openCodeServerForDirectory(shared.server, binding.workdir) };
|
|
500
|
-
});
|
|
501
|
-
} else await facilities.runtime.ensure(facilities.selected, start, savedConnections);
|
|
224
|
+
const options = active?.tools ? { ...acquired.options, applicationTools: toolBridge.configuration } : acquired.options;
|
|
225
|
+
let prepared;
|
|
226
|
+
const dispatch = active?.tools ? async identity => {
|
|
227
|
+
await beforeDispatch(identity);
|
|
502
228
|
signal.throwIfAborted();
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
229
|
+
active.turn = supplied.owner.turns.get(acquired.key);
|
|
230
|
+
active.messageIds.set(prepared.input.id, { messageId: input.messageId, committed: command.deliveryCommitted });
|
|
231
|
+
} : beforeDispatch;
|
|
232
|
+
prepared = await supplied.preparation.message({ ...input.nativeMessage, onPromptSending: dispatch }, options);
|
|
233
|
+
if (prepared.application?.commit) {
|
|
234
|
+
const application = prepared.application;
|
|
235
|
+
prepared.application = { ...application, async commit(admitted) {
|
|
236
|
+
onNativeReceipt();
|
|
237
|
+
const result = await application.commit(admitted);
|
|
238
|
+
onReceiptPersisted();
|
|
239
|
+
return result;
|
|
240
|
+
} };
|
|
515
241
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
},
|
|
529
|
-
publish(nativeId) {
|
|
530
|
-
return facilities.runtime.writeBindings(facilities.registryPath, binding.directory, [{
|
|
531
|
-
upstreamSessionId: nativeId, workdir: binding.workdir, modelProviderId: connection.providerId,
|
|
532
|
-
...(nativeTools ? { env: host.env || process.env } : {}),
|
|
533
|
-
conversation: { systemPrompt: configuration.systemPrompt, nativeTools,
|
|
534
|
-
commandWrapper: host.commandWrapper, tools: current?.tools ? toolBridge?.configuration : undefined }
|
|
535
|
-
}]);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
return { target: native, session, model };
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function messageMonitor(active, command) {
|
|
543
|
-
return {
|
|
544
|
-
prepare() { return { fields: {} }; },
|
|
545
|
-
create(target, turn, _metadata, options) {
|
|
546
|
-
const abort = () => turn.abortController.abort(command.signal.reason);
|
|
547
|
-
command.signal.addEventListener("abort", abort, { once: true });
|
|
548
|
-
if (command.signal.aborted) abort();
|
|
549
|
-
const recoveryMessageId = nativeMessageId(`${command.input.messageId}:final-response`);
|
|
550
|
-
return {
|
|
551
|
-
observe(_turn, signal) {
|
|
552
|
-
return observeOpenCodeEvents(target.server.client, target.upstreamSessionId, {
|
|
553
|
-
abortController: turn.abortController, signal, eventStartedAt: turn.eventStartedAt,
|
|
554
|
-
async onEvent(summary, event) {
|
|
555
|
-
if (summary.type === "session.status") {
|
|
556
|
-
const status = event.data.properties?.status?.type;
|
|
557
|
-
if (status === "retry" || status === "busy") {
|
|
558
|
-
await turn.admission?.promise;
|
|
559
|
-
await active.deliveryCommitted;
|
|
560
|
-
signal.throwIfAborted();
|
|
561
|
-
await active.onEvent({ type: "phase", phase: status === "retry" ? "retrying" : "working" });
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
},
|
|
567
|
-
eventReady: options.eventReady,
|
|
568
|
-
readiness: { timeoutMs: limits.admissionTimeoutMs || 120_000,
|
|
569
|
-
timeoutError: new Error("OpenCode's event connection was not ready before the admission deadline.") },
|
|
570
|
-
finalResponse: {
|
|
571
|
-
agent: active.agent, model: active.prepared.model, recoveryMessageId,
|
|
572
|
-
beforeRecovery() {
|
|
573
|
-
active.messageIds.set(recoveryMessageId, active.messageIds.get(turn.inputMessageId));
|
|
574
|
-
}
|
|
575
|
-
},
|
|
576
|
-
async writeRun(observed, state) {
|
|
577
|
-
if (state === "active") await active.onEvent({ type: "phase", phase: observed.phase || "working" });
|
|
578
|
-
},
|
|
579
|
-
async projectMessages(messages, { inputMessageId }) {
|
|
580
|
-
await active.deliveryCommitted;
|
|
581
|
-
if (inputMessageId !== turn.inputMessageId) return { failure: "", providerApiFailure: false };
|
|
582
|
-
return facilities.runtime.projectConversationMessages(messages, inputMessageId, {
|
|
583
|
-
maximumOutput, outputSchema: active.configuration.outputSchema,
|
|
584
|
-
previous: active.previous, onMessage: active.onMessage
|
|
585
|
-
});
|
|
586
|
-
},
|
|
587
|
-
completeResult(_turn, { failure }) { return failure; },
|
|
588
|
-
onRetired() { command.signal.removeEventListener("abort", abort); }
|
|
589
|
-
};
|
|
590
|
-
},
|
|
591
|
-
onError(error) { active.failure ||= error; }
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
async function send(command) {
|
|
596
|
-
const active = current;
|
|
597
|
-
const id = nativeMessageId(command.input.messageId);
|
|
598
|
-
const previousOwner = active.messageIds.get(id);
|
|
599
|
-
let accepted = false;
|
|
600
|
-
let value;
|
|
601
|
-
try {
|
|
602
|
-
value = await facilities.runtime.sendMessage(binding.directory, {
|
|
603
|
-
id, threadId: binding.sessionId, workdir: binding.workdir
|
|
604
|
-
}, { promptTimeoutMs: limits.admissionTimeoutMs || 30_000 }, {
|
|
605
|
-
async writeRun() {},
|
|
606
|
-
async prepare({ overwrite }) {
|
|
607
|
-
const signal = active.prepared
|
|
608
|
-
? AbortSignal.any([command.signal, AbortSignal.timeout(120_000)])
|
|
609
|
-
: active.resolved.signal;
|
|
610
|
-
if (overwrite || !active.prepared) active.prepared = await prepare({ ...active.resolved, signal });
|
|
611
|
-
return { ...active.prepared, session: { ...active.prepared.session, signal } };
|
|
612
|
-
},
|
|
613
|
-
prompt() {
|
|
614
|
-
native.abortController.signal.throwIfAborted();
|
|
615
|
-
return {
|
|
616
|
-
agent: active.agent, model: active.prepared.model,
|
|
617
|
-
prompt: prompt(command.input, active.configuration), attachments: command.input.localFiles,
|
|
618
|
-
authorizeAttachments: true,
|
|
619
|
-
async beforeDispatch(threadId) {
|
|
620
|
-
await command.beforeDispatch({ threadId });
|
|
621
|
-
command.signal.throwIfAborted();
|
|
622
|
-
active.messageIds.set(id, {
|
|
623
|
-
messageId: command.input.messageId, committed: command.deliveryCommitted
|
|
624
|
-
});
|
|
242
|
+
if (active?.tools && prepared.application?.monitor) {
|
|
243
|
+
const monitor = prepared.application.monitor;
|
|
244
|
+
const create = monitor.create;
|
|
245
|
+
prepared.application = { ...prepared.application, monitor: { ...monitor,
|
|
246
|
+
create(...args) {
|
|
247
|
+
const projection = create(...args);
|
|
248
|
+
const finalResponse = projection.finalResponse;
|
|
249
|
+
return { ...projection, finalResponse: { ...finalResponse,
|
|
250
|
+
async beforeRecovery(completion) {
|
|
251
|
+
active.messageIds.set(finalResponse.recoveryMessageId, active.messageIds.get(completion.inputMessageId));
|
|
252
|
+
const { beforeRecovery = recoverWithoutProjection } = finalResponse;
|
|
253
|
+
await beforeRecovery(completion);
|
|
625
254
|
}
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
command.onNativeReceipt?.();
|
|
630
|
-
await command.accept();
|
|
631
|
-
command.onReceiptPersisted?.();
|
|
632
|
-
active.deliveryCommitted = command.deliveryCommitted;
|
|
633
|
-
accepted = true;
|
|
634
|
-
},
|
|
635
|
-
monitor: messageMonitor(active, command)
|
|
636
|
-
});
|
|
637
|
-
if (value.failure) throw value.failure.error;
|
|
638
|
-
if (!value.delivered) throw new Error("OpenCode did not acknowledge the message.");
|
|
639
|
-
} catch (error) {
|
|
640
|
-
const turn = facilities.runtime.turns.get(binding.directory);
|
|
641
|
-
const attempted = value?.failure ? value.failure.attempted : turn?.promptAttempted === true;
|
|
642
|
-
if (attempted && !accepted && !(error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 408)) {
|
|
643
|
-
error.delivery = "uncertain";
|
|
644
|
-
turn?.abortController?.abort(error);
|
|
645
|
-
// Join the same owner's recovery before releasing its process target.
|
|
646
|
-
await facilities.runtime.monitors.get(binding.directory);
|
|
647
|
-
} else if (previousOwner) active.messageIds.set(id, previousOwner);
|
|
648
|
-
else active.messageIds.delete(id);
|
|
649
|
-
if (turn?.active && turn.observationError) error.cleanupFailed = true;
|
|
650
|
-
throw error;
|
|
255
|
+
} };
|
|
256
|
+
}
|
|
257
|
+
} };
|
|
651
258
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
259
|
+
const value = await supplied.owner.sendPreparedMessage(prepared, options);
|
|
260
|
+
const delivered = { value, completion: value.conversationTurn ? supplied.owner.monitors.get(acquired.key) : null };
|
|
261
|
+
if (value.delivered === true && value.conversationTurn) {
|
|
262
|
+
if (current) current.delivery = delivered;
|
|
263
|
+
await accept({ nativeResult: { value }, conversationTurn: value.conversationTurn, nativeTurnId: value.turn?.id });
|
|
264
|
+
} else if (active?.tools) active.messageIds.delete(prepared.input.id);
|
|
265
|
+
return delivered;
|
|
655
266
|
}
|
|
656
|
-
|
|
657
267
|
return Object.freeze({
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
if (!binding.sessionId) return { accepted: false, messages: [] };
|
|
665
|
-
const prepared = await prepare(await resolvePreparation(configuration, context, AbortSignal.timeout(120_000), verifyAccount === true, receiptBinding));
|
|
666
|
-
await ensureOpenCodeSession(prepared.target, prepared.session);
|
|
667
|
-
const id = nativeMessageId(messageId);
|
|
668
|
-
const { accepted = false, messages = [] } = await inspectOpenCodeMessageAdmission(native.server.client, binding.sessionId, id) || {};
|
|
669
|
-
return { accepted,
|
|
670
|
-
messages: openCodeRowsForInput(messages, id).filter(message => message.type === "assistant" && !message.summary)
|
|
671
|
-
.map(message => ({ id: message.id, outputId: `${message.id}:assistant`, role: "assistant",
|
|
672
|
-
text: configuration.outputSchema ? openCodeStructuredOutput(openCodeAssistantMessageText(message)) : openCodeAssistantMessageText(message) })) };
|
|
673
|
-
},
|
|
674
|
-
steer(command) {
|
|
675
|
-
if (!current || current.finished) throw new Error("OpenCode has no active turn to steer.");
|
|
676
|
-
return send(command);
|
|
268
|
+
publishNative: event => conversation.publish(event),
|
|
269
|
+
async inspectAdmission({ context, representation, ...input }) {
|
|
270
|
+
const acquired = await supplied.acquire(context);
|
|
271
|
+
const result = await supplied.owner.inspectPreparedMessage(await supplied.preparation.inspection(input, acquired.options));
|
|
272
|
+
return representation === "native" ? result : { accepted: result.admission === "accepted" };
|
|
677
273
|
},
|
|
678
274
|
async run(command) {
|
|
679
275
|
if (disposed || current) throw new Error("This OpenCode conversation is closed or already working.");
|
|
680
|
-
const active =
|
|
681
|
-
|
|
682
|
-
let
|
|
276
|
+
const active = { context: command.context, failure: Promise.withResolvers(),
|
|
277
|
+
...(command.tools ? { tools: command.tools, messageIds: new Map() } : {}) };
|
|
278
|
+
let failure;
|
|
279
|
+
active.failure.promise.catch(error => { failure = error; });
|
|
280
|
+
current = active;
|
|
281
|
+
let cancellation;
|
|
282
|
+
const abort = () => {
|
|
283
|
+
cancellation ||= interrupt(command.context).then(() => { throw command.signal.reason; });
|
|
284
|
+
cancellation.catch(active.failure.reject);
|
|
285
|
+
};
|
|
286
|
+
command.signal.addEventListener("abort", abort, { once: true });
|
|
683
287
|
try {
|
|
684
|
-
|
|
685
|
-
AbortSignal.any([command.signal, AbortSignal.timeout(120_000)]), false, undefined, true);
|
|
288
|
+
let delivered;
|
|
686
289
|
let dispatchFailure;
|
|
687
|
-
try { await send(command); }
|
|
290
|
+
try { delivered = await send(command); }
|
|
688
291
|
catch (error) { dispatchFailure = error; }
|
|
689
292
|
for (;;) {
|
|
690
293
|
const settling = active.delivery;
|
|
691
294
|
let result;
|
|
692
|
-
let
|
|
693
|
-
if (!
|
|
694
|
-
try { result = await settling.completion; }
|
|
695
|
-
catch (error) {
|
|
696
|
-
} else if (!failure && settling) result = facilities.runtime.turnSnapshot(settling.turn, binding.sessionId);
|
|
697
|
-
if (command.finishIfCurrent && !await command.finishIfCurrent(() => active.delivery === settling)) continue;
|
|
698
|
-
failure = dispatchFailure || active.failure || failure;
|
|
699
|
-
if (!failure && result?.error) {
|
|
700
|
-
failure = Object.assign(new Error(result.error), { status: result.status });
|
|
701
|
-
if (result.active && result.status === "observation_lost") failure.cleanupFailed = true;
|
|
295
|
+
let problem = dispatchFailure || failure;
|
|
296
|
+
if (!problem && settling?.completion) {
|
|
297
|
+
try { result = await Promise.race([settling.completion, active.failure.promise]); }
|
|
298
|
+
catch (error) { problem = error; }
|
|
702
299
|
}
|
|
703
|
-
if (
|
|
704
|
-
|
|
300
|
+
if (command.finishIfCurrent && !await command.finishIfCurrent(() => active.delivery === settling)) {
|
|
301
|
+
// The original sender admitted another invocation and returned
|
|
302
|
+
// its exact monitor. Keep the same completion and Stop owner.
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
problem = dispatchFailure || failure || problem;
|
|
306
|
+
if (!problem && result?.error) {
|
|
307
|
+
// readable-check: allow patched-error - data: status is the native turn's state, not an HTTP status
|
|
308
|
+
problem = Object.assign(new Error(result.error), { status: result.status });
|
|
309
|
+
if (result.active && result.status === "observation_lost") {
|
|
310
|
+
problem.cleanupFailed = true;
|
|
311
|
+
cleanupFailure = problem;
|
|
312
|
+
await reportFailure(problem);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (problem) throw problem;
|
|
316
|
+
return { value: delivered.value };
|
|
705
317
|
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
318
|
+
} finally {
|
|
319
|
+
command.signal.removeEventListener("abort", abort);
|
|
320
|
+
await cancellation?.catch(() => {});
|
|
321
|
+
if (toolBridge) { await toolBridge.close(); toolBridge = null; }
|
|
322
|
+
if (current === active) current = null;
|
|
709
323
|
}
|
|
710
|
-
active.finished = true;
|
|
711
|
-
try { if (!problem?.cleanupFailed && (problem || command.signal.aborted)) await stop(); }
|
|
712
|
-
catch (error) { problem = error; }
|
|
713
|
-
finally { await toolBridge?.whenIdle(); if (!problem?.cleanupFailed) current = null; }
|
|
714
|
-
if (problem) throw problem;
|
|
715
324
|
},
|
|
716
|
-
|
|
717
|
-
async
|
|
325
|
+
async steer(command) { return { value: (await send(command)).value }; },
|
|
326
|
+
async cancel({ context, representation } = {}) {
|
|
327
|
+
const value = await interrupt(context);
|
|
328
|
+
return representation === "native" ? { value } : value;
|
|
329
|
+
},
|
|
330
|
+
async dispose(options) {
|
|
331
|
+
try {
|
|
332
|
+
const result = await disposeNative({ native: supplied, options });
|
|
333
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || "OpenCode cleanup could not be confirmed."), result);
|
|
334
|
+
if (toolBridge) { await toolBridge.close(); toolBridge = null; }
|
|
335
|
+
disposed = true;
|
|
336
|
+
return result;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
error.cleanupFailed = true;
|
|
339
|
+
current?.failure.reject(error);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
718
343
|
});
|
|
719
344
|
}
|
|
345
|
+
return openStandaloneOpenCodeConversation({
|
|
346
|
+
connections,
|
|
347
|
+
host,
|
|
348
|
+
limits,
|
|
349
|
+
workdir,
|
|
350
|
+
stateDirectory,
|
|
351
|
+
execution,
|
|
352
|
+
nativeTools,
|
|
353
|
+
maximumOutput
|
|
354
|
+
}, { binding, writeBinding, reportFailure });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return Object.freeze({
|
|
358
|
+
toolDiscovery: true,
|
|
359
|
+
attachmentTypes: ["text", "image", "localFile"],
|
|
360
|
+
capabilities: Object.freeze({ streaming: true, cancellation: true, history: true,
|
|
361
|
+
instructions: true, configuration: true, steering: true, goals: false, attachments: true, tools: true, nativeTools, structuredOutput: true }),
|
|
362
|
+
validateConfiguration,
|
|
363
|
+
readState,
|
|
364
|
+
inspectTemporaryActivity,
|
|
365
|
+
releaseRenewalPredecessorProcessExitProof,
|
|
366
|
+
releaseRenewalSuccessorProcessExitProof,
|
|
367
|
+
generateRenewalHandover,
|
|
368
|
+
seedRenewalHandover,
|
|
369
|
+
ensureConversation,
|
|
370
|
+
disposeNative,
|
|
371
|
+
interruptDetachedConversation,
|
|
372
|
+
deleteDetachedConversation,
|
|
373
|
+
listConversationStorage,
|
|
374
|
+
retireConversationHistory,
|
|
375
|
+
closeProject,
|
|
376
|
+
invalidateRuntimes,
|
|
377
|
+
reconcileSessions,
|
|
378
|
+
createConversation,
|
|
379
|
+
runDetachedConversation,
|
|
380
|
+
createBinding,
|
|
381
|
+
open
|
|
720
382
|
});
|
|
721
383
|
}
|