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