@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
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { jsonHash as hash } from "../normalize.js";
|
|
2
|
+
import { realpath, rm } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { createOpenCodeConversationServer, openCodeConversationAgent } from "../openCodeProcess.js";
|
|
6
|
+
import { createOpenCodeSharedRuntime } from "../openCodeRuntime.js";
|
|
7
|
+
import { ensureOpenCodeSession, openCodeServerForDirectory } from "../openCodeNativeSession.js";
|
|
8
|
+
import { openCodeAssistantMessageText } from "../openCodeClient.js";
|
|
9
|
+
import { inspectOpenCodeMessageAdmission, observeOpenCodeEvents, openCodeRowsForInput,
|
|
10
|
+
openCodeDetachedPrompt, openCodeStructuredOutput, readOpenCodeMessagesUntil } from "../openCodeTurn.js";
|
|
11
|
+
import { createOpenCodeToolBridge } from "../openCodeTools.js";
|
|
12
|
+
import { validateConnectionModel } from "../configuration.js";
|
|
13
|
+
import { ignoreReceipt } from "../nativeCommandCallbacks.js";
|
|
14
|
+
|
|
15
|
+
/** A standalone conversation's tool call has no further owner to confirm. */
|
|
16
|
+
function acceptCurrentTurn() {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const nativeMessageId = id => `msg_jskit_${hash(id).slice(0, 40)}`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Standalone native-use validation and authored admission, shared by
|
|
24
|
+
* explicitly supplied bound tools without transferring native process ownership.
|
|
25
|
+
*/
|
|
26
|
+
export async function executeOpenCodeApplicationTool({ active, turn, target, sessionId, assertCurrent = acceptCurrentTurn }, input, signal) {
|
|
27
|
+
if (!active?.tools || input.sessionId !== sessionId ||
|
|
28
|
+
[input.id, input.name, input.messageId].some(value => typeof value !== "string" || !value || value.length > 256)) {
|
|
29
|
+
throw new Error("This OpenCode tool call does not belong to the active conversation.");
|
|
30
|
+
}
|
|
31
|
+
if (!turn?.abortController) {
|
|
32
|
+
throw new Error("This OpenCode tool call does not belong to an admitted conversation.");
|
|
33
|
+
}
|
|
34
|
+
assertCurrent();
|
|
35
|
+
const observed = AbortSignal.any([signal, turn.abortController.signal]);
|
|
36
|
+
await turn.admission?.promise;
|
|
37
|
+
observed.throwIfAborted();
|
|
38
|
+
// Read back to an admitted input that precedes the native tool use.
|
|
39
|
+
const messages = await readOpenCodeMessagesUntil(target.server.client, sessionId, rows => {
|
|
40
|
+
const index = rows.findIndex(row => row.id === input.messageId);
|
|
41
|
+
return index >= 0 && rows.slice(0, index).some(row => active.messageIds.has(row.id));
|
|
42
|
+
}, { signal: observed });
|
|
43
|
+
let message;
|
|
44
|
+
let owner;
|
|
45
|
+
for (const [nativeId, authored] of active.messageIds) {
|
|
46
|
+
message = openCodeRowsForInput(messages, nativeId).find(message => message.id === input.messageId && message.type === "assistant");
|
|
47
|
+
if (message) { owner = authored; break; }
|
|
48
|
+
}
|
|
49
|
+
const tool = message?.content.find(part => part.type === "tool" && part.callID === input.id);
|
|
50
|
+
if (!tool || tool.tool !== input.name || !isDeepStrictEqual(tool.state?.input, input.input)) {
|
|
51
|
+
throw new Error("OpenCode's application call does not match an admitted native tool use.");
|
|
52
|
+
}
|
|
53
|
+
if (!owner.committed) throw new Error("This OpenCode tool call has no durable authored admission.");
|
|
54
|
+
await owner.committed;
|
|
55
|
+
observed.throwIfAborted();
|
|
56
|
+
assertCurrent(true, owner);
|
|
57
|
+
try {
|
|
58
|
+
return await active.tools.execute({ id: input.id, name: input.name, arguments: JSON.stringify(input.input) }, { signal: observed, messageId: owner.messageId });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (assertCurrent(false, owner)) turn.abortController.abort(error);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Opens a conversation on the driver's own native OpenCode process and saved
|
|
67
|
+
* binding: its preparation, its shared runtime, its messages, its tools and its
|
|
68
|
+
* stop.
|
|
69
|
+
* @param {{
|
|
70
|
+
* connections: { resolve(request: { context: object, integrationId: string }): Promise<object> },
|
|
71
|
+
* host: { env?: import("@jskit-ai/kernel/server/types").RuntimeEnv, opencode?: Function, commands?: { opencode?: string },
|
|
72
|
+
* limits?: object, commandWrapper?: object },
|
|
73
|
+
* limits: Record<string, number>,
|
|
74
|
+
* workdir: string,
|
|
75
|
+
* stateDirectory: string,
|
|
76
|
+
* execution: import("../localExecution.js").ConversationExecution,
|
|
77
|
+
* nativeTools: boolean,
|
|
78
|
+
* maximumOutput: number
|
|
79
|
+
* }} settings
|
|
80
|
+
* @param {{ binding: object, writeBinding(binding: object): Promise<void>, reportFailure(error: Error, options?: object): Promise<void> }} opening
|
|
81
|
+
*/
|
|
82
|
+
export async function openStandaloneOpenCodeConversation({
|
|
83
|
+
connections,
|
|
84
|
+
host,
|
|
85
|
+
limits,
|
|
86
|
+
workdir,
|
|
87
|
+
stateDirectory,
|
|
88
|
+
execution,
|
|
89
|
+
nativeTools,
|
|
90
|
+
maximumOutput
|
|
91
|
+
}, { binding, writeBinding, reportFailure }) {
|
|
92
|
+
function prompt(input, configuration) {
|
|
93
|
+
const text = `${input.text || ""}${input.attachmentManifest || ""}`;
|
|
94
|
+
if (!configuration.outputSchema) return { text, content: input.content };
|
|
95
|
+
const prepared = openCodeDetachedPrompt({ prompt: text, outputSchema: configuration.outputSchema });
|
|
96
|
+
if (prepared.length > (limits.maxInputCharacters ?? 32_000)) throw new Error("OpenCode structured input exceeded the configured limit.");
|
|
97
|
+
return { text: prepared, content: input.content };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!binding || typeof binding.sessionId !== "string" || binding.workdir !== await realpath(workdir) ||
|
|
101
|
+
path.dirname(binding.directory || "") !== stateDirectory) {
|
|
102
|
+
throw new Error("This OpenCode conversation belongs to another working directory or native state directory. Restore its original host configuration.");
|
|
103
|
+
}
|
|
104
|
+
let native;
|
|
105
|
+
let toolBridge;
|
|
106
|
+
let facilities;
|
|
107
|
+
let processVariants = {};
|
|
108
|
+
let current;
|
|
109
|
+
let stopping;
|
|
110
|
+
let disposed = false;
|
|
111
|
+
async function updateBinding(patch) {
|
|
112
|
+
const next = { ...binding, ...patch };
|
|
113
|
+
await writeBinding(next);
|
|
114
|
+
binding = next;
|
|
115
|
+
}
|
|
116
|
+
async function stopRecordedExecution() {
|
|
117
|
+
const proof = await execution.stop(binding.executionId);
|
|
118
|
+
if (!proof?.scopeEmpty) throw new Error("OpenCode process cleanup could not be confirmed. Restore its execution host before continuing.");
|
|
119
|
+
if (binding.processDirectory) {
|
|
120
|
+
if (path.dirname(binding.processDirectory) !== (binding.runtimeDirectory || stateDirectory) || !path.basename(binding.processDirectory).startsWith("process-")) {
|
|
121
|
+
throw new Error("OpenCode's private process directory does not belong to this host.");
|
|
122
|
+
}
|
|
123
|
+
await rm(binding.processDirectory, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
await updateBinding({ executionId: "", processDirectory: "" });
|
|
126
|
+
}
|
|
127
|
+
async function stop() {
|
|
128
|
+
if (stopping) return stopping;
|
|
129
|
+
stopping = (async () => {
|
|
130
|
+
await toolBridge?.close();
|
|
131
|
+
toolBridge = null;
|
|
132
|
+
if (facilities?.runtime.cleanupPending) {
|
|
133
|
+
await facilities.runtime.stop("opencode-startup-cleanup");
|
|
134
|
+
await updateBinding({ executionId: "", processDirectory: "" });
|
|
135
|
+
}
|
|
136
|
+
if (native) {
|
|
137
|
+
await facilities.runtime.releaseTarget(native, {
|
|
138
|
+
onRemoved: () => facilities.runtime.writeBindings(facilities.registryPath, binding.directory, [])
|
|
139
|
+
});
|
|
140
|
+
facilities.runtime.turns.delete(binding.directory);
|
|
141
|
+
await updateBinding({ executionId: "", processDirectory: "" });
|
|
142
|
+
native = null;
|
|
143
|
+
} else if (binding.executionId && facilities && !host.opencode &&
|
|
144
|
+
!facilities.runtime.current && !facilities.runtime.processStarts.size) {
|
|
145
|
+
// Only our unfinished standalone startup belongs to this handle.
|
|
146
|
+
// A saved shared-server reference is not authority to stop a peer's
|
|
147
|
+
// service before this conversation has acquired its owner.
|
|
148
|
+
await stopRecordedExecution();
|
|
149
|
+
}
|
|
150
|
+
if (current?.finished) current = null;
|
|
151
|
+
})().catch(error => {
|
|
152
|
+
error.cleanupFailed = true;
|
|
153
|
+
if (error.code === "assistant_opencode_stop_unverified") {
|
|
154
|
+
error.message = "OpenCode process cleanup could not be confirmed. Restore its execution host before continuing.";
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}).finally(() => { stopping = null; });
|
|
158
|
+
return stopping;
|
|
159
|
+
}
|
|
160
|
+
async function resolvePreparation(configuration, context, signal, requireBound = false, receiptBinding, authoredPreparation = false) {
|
|
161
|
+
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
162
|
+
signal.throwIfAborted();
|
|
163
|
+
validateConnectionModel(configuration, connection);
|
|
164
|
+
const identity = hash([connection.providerId, connection.baseURL || "", connection.apiKey]);
|
|
165
|
+
if (requireBound && !receiptBinding?.accountIdentity) throw new Error("The completed OpenCode response has no saved account fingerprint.");
|
|
166
|
+
if (requireBound && (binding.accountIdentity !== receiptBinding.accountIdentity || identity !== receiptBinding.accountIdentity)) {
|
|
167
|
+
throw new Error("The completed OpenCode response belongs to a different account binding.");
|
|
168
|
+
}
|
|
169
|
+
// A managed host reauthorizes its own connection on every preparation.
|
|
170
|
+
// A key change can rotate an authored run only inside the same native storage/runtime scope.
|
|
171
|
+
const preparedFacilities = host.opencode ? await host.opencode({ connection, context }) : facilities;
|
|
172
|
+
signal.throwIfAborted();
|
|
173
|
+
if (host.opencode && (
|
|
174
|
+
binding.databasePath && binding.databasePath !== preparedFacilities.databasePath ||
|
|
175
|
+
binding.runtimeDirectory && binding.runtimeDirectory !== preparedFacilities.runtimeDirectory ||
|
|
176
|
+
facilities && facilities.runtime !== preparedFacilities.runtime
|
|
177
|
+
)) {
|
|
178
|
+
throw new Error("This OpenCode conversation belongs to another native history store or runtime. Restore its original host configuration.");
|
|
179
|
+
}
|
|
180
|
+
const allowCredentialRefresh = authoredPreparation && host.opencode && preparedFacilities.allowCredentialRefresh === true;
|
|
181
|
+
if (binding.accountIdentity && identity !== binding.accountIdentity && !allowCredentialRefresh) {
|
|
182
|
+
throw new Error("This conversation belongs to another OpenCode account. Restore that connection or start a new conversation.");
|
|
183
|
+
}
|
|
184
|
+
if (binding.accountIdentity !== identity) await updateBinding({ accountIdentity: identity });
|
|
185
|
+
const model = { providerID: connection.providerId, id: connection.model };
|
|
186
|
+
const agent = openCodeConversationAgent({ nativeTools, tools: Boolean(current?.tools) });
|
|
187
|
+
if (current) current.agent = agent;
|
|
188
|
+
if (binding.sessionId && !binding.databasePath) {
|
|
189
|
+
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.");
|
|
190
|
+
}
|
|
191
|
+
if (!facilities || host.opencode) {
|
|
192
|
+
if (host.opencode) facilities = preparedFacilities;
|
|
193
|
+
else {
|
|
194
|
+
const runtimeDirectory = path.join(stateDirectory, "shared", hash([identity, host.commands?.opencode || "opencode"]));
|
|
195
|
+
const selected = { modelProviderId: connection.providerId, canonicalUrl: connection.baseURL || "",
|
|
196
|
+
endpointCode: connection.sdkPackage || "", fingerprint: identity };
|
|
197
|
+
facilities = { runtime: createOpenCodeSharedRuntime({ scope: runtimeDirectory }), runtimeDirectory,
|
|
198
|
+
registryPath: path.join(runtimeDirectory, "session-environments.json"),
|
|
199
|
+
databasePath: path.join(runtimeDirectory, "opencode.db"), selected,
|
|
200
|
+
async start({ signal, connection }) {
|
|
201
|
+
if (binding.executionId && !facilities.runtime.processes.has(binding.directory)) await stopRecordedExecution();
|
|
202
|
+
const starting = new AbortController();
|
|
203
|
+
const abortUnusedStartup = () => {
|
|
204
|
+
if (facilities.runtime.processStarts.size === 1 && facilities.runtime.processes.size === 0) starting.abort(signal.reason);
|
|
205
|
+
};
|
|
206
|
+
signal.addEventListener("abort", abortUnusedStartup, { once: true });
|
|
207
|
+
if (signal.aborted) abortUnusedStartup();
|
|
208
|
+
try {
|
|
209
|
+
const server = await createOpenCodeConversationServer({ command: host.commands?.opencode || "opencode",
|
|
210
|
+
execution, env: host.env, workdir: binding.workdir, stateDirectory: runtimeDirectory,
|
|
211
|
+
databasePath: facilities.databasePath, sessionEnvironmentRegistry: facilities.registryPath, connection,
|
|
212
|
+
maxOutputTokens: limits.maxOutputTokens, executionLimits: host.limits, signal: starting.signal,
|
|
213
|
+
onStarted: (executionId, processDirectory) => updateBinding({ executionId, processDirectory }),
|
|
214
|
+
async onFailure(error) {
|
|
215
|
+
for (const target of facilities.runtime.processes.values()) target.abortController.abort(error);
|
|
216
|
+
try { await facilities.runtime.stop("opencode-process-exited"); }
|
|
217
|
+
catch (cleanupError) { await reportFailure(cleanupError); }
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return { server, connections: [selected] };
|
|
221
|
+
} finally { signal.removeEventListener("abort", abortUnusedStartup); }
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (binding.databasePath && binding.databasePath !== facilities.databasePath) {
|
|
226
|
+
throw new Error("This OpenCode conversation belongs to another native history store. Restore its original host configuration.");
|
|
227
|
+
}
|
|
228
|
+
await updateBinding({ databasePath: facilities.databasePath, runtimeDirectory: facilities.runtimeDirectory });
|
|
229
|
+
}
|
|
230
|
+
if (!toolBridge && current?.tools) {
|
|
231
|
+
toolBridge = await createOpenCodeToolBridge({
|
|
232
|
+
schemas: current.tools.schemas, maxArgumentBytes: current.tools.maximumArgumentBytes,
|
|
233
|
+
async execute(input, signal) {
|
|
234
|
+
return executeOpenCodeApplicationTool({ active: current,
|
|
235
|
+
turn: facilities.runtime.turns.get(binding.directory), target: native, sessionId: binding.sessionId }, input, signal);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return { configuration, signal, model, agent, connection };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function prepare({ configuration, signal, model, agent, connection }) {
|
|
243
|
+
try {
|
|
244
|
+
const start = host.opencode
|
|
245
|
+
? async () => facilities.runtime.startPreparedProcess(await facilities.prepareServer())
|
|
246
|
+
: () => facilities.start({ signal, connection });
|
|
247
|
+
// A managed host's saved connections reach its running server in place.
|
|
248
|
+
const savedConnections = host.opencode ? async () => (await facilities.prepareServer()).connections : null;
|
|
249
|
+
if (!native) {
|
|
250
|
+
native = await facilities.runtime.acquire(binding.directory, async () => {
|
|
251
|
+
const shared = await facilities.runtime.ensure(facilities.selected, start, savedConnections);
|
|
252
|
+
return { key: binding.directory, workdir: binding.workdir, upstreamSessionId: binding.sessionId,
|
|
253
|
+
abortController: new AbortController(), server: openCodeServerForDirectory(shared.server, binding.workdir) };
|
|
254
|
+
});
|
|
255
|
+
} else await facilities.runtime.ensure(facilities.selected, start, savedConnections);
|
|
256
|
+
signal.throwIfAborted();
|
|
257
|
+
await updateBinding({ executionId: native.server.executionId, processDirectory: native.server.privateRoot || "" });
|
|
258
|
+
const catalogue = await native.server.client.providers({ signal });
|
|
259
|
+
const selected = catalogue.all.find(provider => provider.id === connection.providerId)?.models?.[connection.model];
|
|
260
|
+
if (selected?.id !== connection.model) throw new Error("OpenCode could not load the configured model definition. Check the selected integration before sending.");
|
|
261
|
+
processVariants = selected.variants || {};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error.stopProof?.scopeEmpty) await updateBinding({ executionId: "", processDirectory: "" });
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
if (configuration.effort) {
|
|
267
|
+
if (!Object.hasOwn(processVariants, configuration.effort)) throw new Error("The selected OpenCode model does not support this reasoning effort.");
|
|
268
|
+
model.variant = configuration.effort;
|
|
269
|
+
}
|
|
270
|
+
const retainedId = binding.sessionId;
|
|
271
|
+
if (retainedId && (await native.server.client.sessionStatus(retainedId, { signal })).type !== "idle") {
|
|
272
|
+
throw new Error("The native OpenCode conversation is still working. Stop or inspect it before sending another message.");
|
|
273
|
+
}
|
|
274
|
+
const session = {
|
|
275
|
+
selection: { agentId: agent, modelId: model.id, modelProviderId: model.providerID, variantId: model.variant || "" },
|
|
276
|
+
model, workdir: binding.workdir, signal,
|
|
277
|
+
invalidIdentity: () => new Error("OpenCode did not acknowledge a native conversation."),
|
|
278
|
+
identity: {
|
|
279
|
+
async write(nativeId) {
|
|
280
|
+
if (retainedId && nativeId !== retainedId) throw new Error("The saved OpenCode conversation is unavailable. Restore its native history before continuing.");
|
|
281
|
+
if (binding.sessionId !== nativeId) await updateBinding({ sessionId: nativeId });
|
|
282
|
+
},
|
|
283
|
+
publish(nativeId) {
|
|
284
|
+
return facilities.runtime.writeBindings(facilities.registryPath, binding.directory, [{
|
|
285
|
+
upstreamSessionId: nativeId, workdir: binding.workdir, modelProviderId: connection.providerId,
|
|
286
|
+
...(nativeTools ? { env: host.env } : {}),
|
|
287
|
+
conversation: { systemPrompt: configuration.systemPrompt, nativeTools,
|
|
288
|
+
commandWrapper: host.commandWrapper, tools: current?.tools ? toolBridge?.configuration : undefined }
|
|
289
|
+
}]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
return { target: native, session, model };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function messageMonitor(active, command) {
|
|
297
|
+
return {
|
|
298
|
+
prepare() { return { fields: {} }; },
|
|
299
|
+
create(target, turn, _metadata, options) {
|
|
300
|
+
const abort = () => turn.abortController.abort(command.signal.reason);
|
|
301
|
+
command.signal.addEventListener("abort", abort, { once: true });
|
|
302
|
+
if (command.signal.aborted) abort();
|
|
303
|
+
const recoveryMessageId = nativeMessageId(`${command.input.messageId}:final-response`);
|
|
304
|
+
return {
|
|
305
|
+
observe(_turn, signal) {
|
|
306
|
+
return observeOpenCodeEvents(target.server.client, target.upstreamSessionId, {
|
|
307
|
+
abortController: turn.abortController, signal, eventStartedAt: turn.eventStartedAt,
|
|
308
|
+
async onEvent(summary, event) {
|
|
309
|
+
if (summary.type === "session.status") {
|
|
310
|
+
const status = event.data.properties?.status?.type;
|
|
311
|
+
if (status === "retry" || status === "busy") {
|
|
312
|
+
await turn.admission?.promise;
|
|
313
|
+
await active.deliveryCommitted;
|
|
314
|
+
signal.throwIfAborted();
|
|
315
|
+
await active.onEvent({ type: "phase", phase: status === "retry" ? "retrying" : "working" });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
eventReady: options.eventReady,
|
|
322
|
+
readiness: { timeoutMs: limits.admissionTimeoutMs || 120_000,
|
|
323
|
+
timeoutError: new Error("OpenCode's event connection was not ready before the admission deadline.") },
|
|
324
|
+
finalResponse: {
|
|
325
|
+
agent: active.agent, model: active.prepared.model, recoveryMessageId,
|
|
326
|
+
beforeRecovery() {
|
|
327
|
+
active.messageIds.set(recoveryMessageId, active.messageIds.get(turn.inputMessageId));
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
async writeRun(observed, state) {
|
|
331
|
+
if (state === "active") await active.onEvent({ type: "phase", phase: observed.phase || "working" });
|
|
332
|
+
},
|
|
333
|
+
async projectMessages(messages, { inputMessageId }) {
|
|
334
|
+
await active.deliveryCommitted;
|
|
335
|
+
if (inputMessageId !== turn.inputMessageId) return { failure: "", providerApiFailure: false };
|
|
336
|
+
return facilities.runtime.projectConversationMessages(messages, inputMessageId, {
|
|
337
|
+
maximumOutput, outputSchema: active.configuration.outputSchema,
|
|
338
|
+
previous: active.previous, onMessage: active.onMessage
|
|
339
|
+
});
|
|
340
|
+
},
|
|
341
|
+
completeResult(_turn, { failure }) { return failure; },
|
|
342
|
+
onRetired() { command.signal.removeEventListener("abort", abort); }
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
onError(error) { active.failure ||= error; }
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function send(command) {
|
|
350
|
+
const { onNativeReceipt = ignoreReceipt, onReceiptPersisted = ignoreReceipt } = command;
|
|
351
|
+
const active = current;
|
|
352
|
+
const id = nativeMessageId(command.input.messageId);
|
|
353
|
+
const previousOwner = active.messageIds.get(id);
|
|
354
|
+
let accepted = false;
|
|
355
|
+
let value;
|
|
356
|
+
try {
|
|
357
|
+
value = await facilities.runtime.sendMessage(binding.directory, {
|
|
358
|
+
id, threadId: binding.sessionId, workdir: binding.workdir
|
|
359
|
+
}, { promptTimeoutMs: limits.admissionTimeoutMs || 30_000 }, {
|
|
360
|
+
async writeRun() {},
|
|
361
|
+
async prepare({ overwrite }) {
|
|
362
|
+
const signal = active.prepared
|
|
363
|
+
? AbortSignal.any([command.signal, AbortSignal.timeout(120_000)])
|
|
364
|
+
: active.resolved.signal;
|
|
365
|
+
if (overwrite || !active.prepared) active.prepared = await prepare({ ...active.resolved, signal });
|
|
366
|
+
return { ...active.prepared, session: { ...active.prepared.session, signal } };
|
|
367
|
+
},
|
|
368
|
+
prompt() {
|
|
369
|
+
native.abortController.signal.throwIfAborted();
|
|
370
|
+
return {
|
|
371
|
+
agent: active.agent, model: active.prepared.model,
|
|
372
|
+
prompt: prompt(command.input, active.configuration), attachments: command.input.localFiles,
|
|
373
|
+
authorizeAttachments: true,
|
|
374
|
+
async beforeDispatch(threadId) {
|
|
375
|
+
await command.beforeDispatch({ threadId });
|
|
376
|
+
command.signal.throwIfAborted();
|
|
377
|
+
active.messageIds.set(id, {
|
|
378
|
+
messageId: command.input.messageId, committed: command.deliveryCommitted
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
async commit() {
|
|
384
|
+
onNativeReceipt();
|
|
385
|
+
await command.accept();
|
|
386
|
+
onReceiptPersisted();
|
|
387
|
+
active.deliveryCommitted = command.deliveryCommitted;
|
|
388
|
+
accepted = true;
|
|
389
|
+
},
|
|
390
|
+
monitor: messageMonitor(active, command)
|
|
391
|
+
});
|
|
392
|
+
if (value.failure) throw value.failure.error;
|
|
393
|
+
if (!value.delivered) throw new Error("OpenCode did not acknowledge the message.");
|
|
394
|
+
} catch (error) {
|
|
395
|
+
const turn = facilities.runtime.turns.get(binding.directory);
|
|
396
|
+
const attempted = value?.failure ? value.failure.attempted : turn?.promptAttempted === true;
|
|
397
|
+
if (attempted && !accepted && !(error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 408)) {
|
|
398
|
+
error.delivery = "uncertain";
|
|
399
|
+
turn?.abortController?.abort(error);
|
|
400
|
+
// Join the same owner's recovery before releasing its process target.
|
|
401
|
+
await facilities.runtime.monitors.get(binding.directory);
|
|
402
|
+
} else if (previousOwner) active.messageIds.set(id, previousOwner);
|
|
403
|
+
else active.messageIds.delete(id);
|
|
404
|
+
if (turn?.active && turn.observationError) error.cleanupFailed = true;
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
active.delivery = { turn: facilities.runtime.turns.get(binding.directory),
|
|
408
|
+
completion: facilities.runtime.monitors.get(binding.directory) };
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return Object.freeze({
|
|
413
|
+
async inspectAdmission({ messageId, threadId, configuration, context, verifyAccount = false, receiptBinding }) {
|
|
414
|
+
if (verifyAccount === true && (!receiptBinding || threadId !== binding.sessionId || receiptBinding.sessionId !== binding.sessionId ||
|
|
415
|
+
receiptBinding.workdir !== binding.workdir || receiptBinding.directory !== binding.directory ||
|
|
416
|
+
receiptBinding.databasePath !== binding.databasePath || receiptBinding.runtimeDirectory !== binding.runtimeDirectory)) {
|
|
417
|
+
throw new Error("The completed OpenCode response belongs to a different native conversation or host scope.");
|
|
418
|
+
}
|
|
419
|
+
if (!binding.sessionId) return { accepted: false, messages: [] };
|
|
420
|
+
const prepared = await prepare(await resolvePreparation(configuration, context, AbortSignal.timeout(120_000), verifyAccount === true, receiptBinding));
|
|
421
|
+
await ensureOpenCodeSession(prepared.target, prepared.session);
|
|
422
|
+
const id = nativeMessageId(messageId);
|
|
423
|
+
const { accepted = false, messages = [] } = await inspectOpenCodeMessageAdmission(native.server.client, binding.sessionId, id) || {};
|
|
424
|
+
return { accepted,
|
|
425
|
+
messages: openCodeRowsForInput(messages, id).filter(message => message.type === "assistant" && !message.summary)
|
|
426
|
+
.map(message => ({ id: message.id, outputId: `${message.id}:assistant`, role: "assistant",
|
|
427
|
+
text: configuration.outputSchema ? openCodeStructuredOutput(openCodeAssistantMessageText(message)) : openCodeAssistantMessageText(message) })) };
|
|
428
|
+
},
|
|
429
|
+
steer(command) {
|
|
430
|
+
if (!current || current.finished) throw new Error("OpenCode has no active turn to steer.");
|
|
431
|
+
return send(command);
|
|
432
|
+
},
|
|
433
|
+
async run(command) {
|
|
434
|
+
if (disposed || current) throw new Error("This OpenCode conversation is closed or already working.");
|
|
435
|
+
const active = current = { tools: command.tools, configuration: command.configuration,
|
|
436
|
+
onMessage: command.onMessage, onEvent: command.onEvent, messageIds: new Map(), previous: new Map() };
|
|
437
|
+
let problem;
|
|
438
|
+
try {
|
|
439
|
+
active.resolved = await resolvePreparation(command.configuration, command.context,
|
|
440
|
+
AbortSignal.any([command.signal, AbortSignal.timeout(120_000)]), false, undefined, true);
|
|
441
|
+
let dispatchFailure;
|
|
442
|
+
try { await send(command); }
|
|
443
|
+
catch (error) { dispatchFailure = error; }
|
|
444
|
+
for (;;) {
|
|
445
|
+
const settling = active.delivery;
|
|
446
|
+
let result;
|
|
447
|
+
let failure = dispatchFailure || active.failure;
|
|
448
|
+
if (!failure && settling?.completion) {
|
|
449
|
+
try { result = await settling.completion; }
|
|
450
|
+
catch (error) { failure = error; }
|
|
451
|
+
} else if (!failure && settling) result = facilities.runtime.turnSnapshot(settling.turn, binding.sessionId);
|
|
452
|
+
if (command.finishIfCurrent && !await command.finishIfCurrent(() => active.delivery === settling)) continue;
|
|
453
|
+
failure = dispatchFailure || active.failure || failure;
|
|
454
|
+
if (!failure && result?.error) {
|
|
455
|
+
// readable-check: allow patched-error - data: status is the native turn's state, not an HTTP status
|
|
456
|
+
failure = Object.assign(new Error(result.error), { status: result.status });
|
|
457
|
+
if (result.active && result.status === "observation_lost") failure.cleanupFailed = true;
|
|
458
|
+
}
|
|
459
|
+
if (failure) throw failure;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
} catch (error) {
|
|
463
|
+
problem = error.cleanupFailed ? error : command.signal.aborted ? command.signal.reason : error;
|
|
464
|
+
if (problem.cleanupFailed) problem.message = "OpenCode process cleanup could not be confirmed. Restore its execution host before continuing.";
|
|
465
|
+
}
|
|
466
|
+
active.finished = true;
|
|
467
|
+
try { if (!problem?.cleanupFailed && (problem || command.signal.aborted)) await stop(); }
|
|
468
|
+
catch (error) { problem = error; }
|
|
469
|
+
finally { await toolBridge?.whenIdle(); if (!problem?.cleanupFailed) current = null; }
|
|
470
|
+
if (problem) throw problem;
|
|
471
|
+
},
|
|
472
|
+
cancel: stop,
|
|
473
|
+
async dispose() { await stop(); disposed = true; }
|
|
474
|
+
});
|
|
475
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { normalizeNullableText } from "../normalize.js";
|
|
2
2
|
import { createConversationChangeover } from "../continuity.js";
|
|
3
|
+
import { ignorePromptRejection } from "../nativeCommandCallbacks.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
/** A scoped turn whose caller follows no thread or turn events. */
|
|
6
|
+
function ignoreScopedEvent() {}
|
|
7
|
+
|
|
8
|
+
// The host retains profile authority and durable parent receipts. This
|
|
5
9
|
// coordinator uses its admitted operations before and after native creation.
|
|
6
10
|
export async function runScopedConversationTurn({ operations, scope = {}, input = {}, context: options = {},
|
|
7
11
|
executionProfile: profileSnapshot } = {}) {
|
|
8
12
|
const request = { ...input, ephemeral: true, message: input.prompt || input.message };
|
|
13
|
+
const { onEvent = ignoreScopedEvent } = options;
|
|
9
14
|
options.signal?.throwIfAborted();
|
|
10
15
|
let conversationId = normalizeNullableText(input.conversationId || input.threadId);
|
|
11
16
|
if (!conversationId) {
|
|
@@ -16,12 +21,12 @@ export async function runScopedConversationTurn({ operations, scope = {}, input
|
|
|
16
21
|
}
|
|
17
22
|
// Await ownership before starting: Stop or a failed parent write must not
|
|
18
23
|
// leave an unidentified native turn running.
|
|
19
|
-
await
|
|
24
|
+
await onEvent({ type: "thread", threadId: conversationId });
|
|
20
25
|
options.signal?.throwIfAborted();
|
|
21
26
|
const started = await operations.startEphemeralConversationTurn(scope, { ...request, conversationId }, options);
|
|
22
27
|
if (started?.ok === false) return { ...started, threadId: conversationId };
|
|
23
28
|
const runId = normalizeNullableText(started.runId);
|
|
24
|
-
await
|
|
29
|
+
await onEvent({ type: "turn", threadId: conversationId, turnId: runId, status: started.status });
|
|
25
30
|
if (options.signal?.aborted) {
|
|
26
31
|
const stopped = await operations.stopEphemeralConversation(scope, { ...request, conversationId, runId }, options);
|
|
27
32
|
if (stopped?.ok === false) return { ...stopped, threadId: conversationId, turnId: runId };
|
|
@@ -76,27 +81,65 @@ export function unsupportedScopedConversation() {
|
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
/**
|
|
79
|
-
*
|
|
84
|
+
* One engine's native owner call for a scoped conversation.
|
|
85
|
+
* @typedef {(input: Record<string, any>, context: Record<string, any>) => Promise<any>} ScopedNativeCall
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Driver handle for a scoped conversation. Each engine supplies its
|
|
80
90
|
* native owner calls; every call receives input bound to `conversationId`.
|
|
81
91
|
* `disposeOpening` releases the opening without deleting the native conversation.
|
|
92
|
+
* @param {object} owner
|
|
93
|
+
* @param {string} owner.conversationId
|
|
94
|
+
* @param {ScopedNativeCall} owner.sendNative
|
|
95
|
+
* @param {ScopedNativeCall} owner.readNative
|
|
96
|
+
* @param {ScopedNativeCall} owner.waitNative
|
|
97
|
+
* @param {ScopedNativeCall} owner.cancel
|
|
98
|
+
* @param {ScopedNativeCall} owner.deleteConversation
|
|
99
|
+
* @param {() => Promise<any>} owner.disposeOpening
|
|
82
100
|
*/
|
|
83
101
|
export function createScopedConversationHandle({ conversationId, sendNative, readNative, waitNative, cancel,
|
|
84
102
|
deleteConversation, disposeOpening }) {
|
|
85
103
|
const inputFor = scopedConversationInput(conversationId);
|
|
104
|
+
|
|
105
|
+
/** Sends to the native conversation. */
|
|
106
|
+
async function sendScoped({ input, context }) {
|
|
107
|
+
return sendNative(inputFor(input), context);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Reads the native conversation. */
|
|
111
|
+
async function readScoped({ input, context }) {
|
|
112
|
+
return readNative(inputFor(input), context);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Waits for the native conversation's turn. */
|
|
116
|
+
async function waitScoped({ input, context }) {
|
|
117
|
+
return waitNative(inputFor(input), context);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Stops the native conversation's turn. */
|
|
121
|
+
async function cancelScoped({ input, context }) {
|
|
122
|
+
return cancel(inputFor(input), context);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Deletes the conversation when given its input, or releases only the opening. */
|
|
126
|
+
async function dispose(options) {
|
|
127
|
+
if (options) return deleteConversation(inputFor(options.input), options.context);
|
|
128
|
+
const result = await disposeOpening();
|
|
129
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || "Scoped cleanup could not be confirmed."), result);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
86
133
|
return Object.freeze({
|
|
87
134
|
capabilities: SCOPED_CONVERSATION_CAPABILITIES,
|
|
88
|
-
sendNative:
|
|
89
|
-
readNative:
|
|
90
|
-
waitNative:
|
|
91
|
-
cancel:
|
|
92
|
-
readGoal: unsupportedScopedConversation,
|
|
135
|
+
sendNative: sendScoped,
|
|
136
|
+
readNative: readScoped,
|
|
137
|
+
waitNative: waitScoped,
|
|
138
|
+
cancel: cancelScoped,
|
|
139
|
+
readGoal: unsupportedScopedConversation,
|
|
140
|
+
updateGoal: unsupportedScopedConversation,
|
|
93
141
|
inspectAdmission: unsupportedScopedConversation,
|
|
94
|
-
|
|
95
|
-
if (options) return deleteConversation(inputFor(options.input), options.context);
|
|
96
|
-
const result = await disposeOpening();
|
|
97
|
-
if (result?.ok === false) throw Object.assign(new Error(result.error || "Scoped cleanup could not be confirmed."), result);
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
142
|
+
dispose
|
|
100
143
|
});
|
|
101
144
|
}
|
|
102
145
|
|
|
@@ -188,8 +231,8 @@ export async function projectStoredPersistentConversation(options) {
|
|
|
188
231
|
}, turns);
|
|
189
232
|
}
|
|
190
233
|
|
|
191
|
-
// These
|
|
192
|
-
//
|
|
234
|
+
// These persistent-conversation operations run inside the host's
|
|
235
|
+
// write lease. Their record, transcript and native operations are the
|
|
193
236
|
// same facilities used by its retained conversation; no state is copied here.
|
|
194
237
|
export async function readPersistentConversation(options) {
|
|
195
238
|
const { sessionId, operations, store, records, projection, publish } = options;
|
|
@@ -283,10 +326,11 @@ export async function startPersistentConversationTurn(options) {
|
|
|
283
326
|
runId: steering ? previous.runId : "",
|
|
284
327
|
error: ""
|
|
285
328
|
});
|
|
329
|
+
const { onEvent: forwardEvent = ignoreScopedEvent } = context;
|
|
286
330
|
try {
|
|
287
331
|
const result = requireSuccess(await operations.startConversationTurn(sessionId,
|
|
288
332
|
records.input(record, { ...input, ...prepared.input, steer: steering }), { ...context, attachmentsPrepared: true,
|
|
289
|
-
onEvent(event) { streamMessage(options, context, record, event); return
|
|
333
|
+
onEvent(event) { streamMessage(options, context, record, event); return forwardEvent(event); }
|
|
290
334
|
}));
|
|
291
335
|
if (receipts.afterAdmission(input)) await writeReceipt();
|
|
292
336
|
await records.bind(context, record, { runId: result.runId, status: result.status || "inProgress", draft: "", attachments: [] },
|
|
@@ -299,7 +343,8 @@ export async function startPersistentConversationTurn(options) {
|
|
|
299
343
|
// its authored row, but restore the admitted identity before reconciliation.
|
|
300
344
|
if (notSent) {
|
|
301
345
|
record = await records.save(context, record, priorInput);
|
|
302
|
-
|
|
346
|
+
const { onPromptRejected = ignorePromptRejection } = input;
|
|
347
|
+
await onPromptRejected();
|
|
303
348
|
}
|
|
304
349
|
const observed = await readPersistentConversation({ ...options, context, record });
|
|
305
350
|
if (!notSent && observed.admitted) {
|