@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,388 @@
|
|
|
1
|
+
// Observing a session's native turn until its final response: the turn's
|
|
2
|
+
// public snapshot, its failure, its event observation, the projection of its
|
|
3
|
+
// messages into the store, and the retained final-answer evidence.
|
|
4
|
+
import {
|
|
5
|
+
waitForOpenCodeFinalResponse, openCodeLastAssistantResult, observeOpenCodeEvents, openCodeMessageRows, openCodeRowsForInput,
|
|
6
|
+
recoverWithoutProjection
|
|
7
|
+
} from "./openCodeTurn.js";
|
|
8
|
+
import { openCodeAssistantMessageText as assistantMessageText } from "./openCodeClient.js";
|
|
9
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
10
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
11
|
+
|
|
12
|
+
/** A projection that keeps no native output identities. */
|
|
13
|
+
function noOutputId() {}
|
|
14
|
+
|
|
15
|
+
/** A turn's public state, or null before it has one. */
|
|
16
|
+
export function openCodeTurnSnapshot(turn = null, threadId = "") {
|
|
17
|
+
const source = isRecord(turn) ? turn : {};
|
|
18
|
+
const active = source.active === true;
|
|
19
|
+
return active || normalizeNullableText(source.id)
|
|
20
|
+
? {
|
|
21
|
+
active,
|
|
22
|
+
error: normalizeNullableText(source.error),
|
|
23
|
+
id: normalizeNullableText(source.id),
|
|
24
|
+
phase: active && !source.observationError ? normalizeNullableText(source.phase) : "",
|
|
25
|
+
startedAt: normalizeNullableText(source.startedAt),
|
|
26
|
+
state: normalizeNullableText(source.state) || (active ? "active" : "completed"),
|
|
27
|
+
status: source.observationError ? "observation_lost" : normalizeNullableText(source.state) || (active ? "active" : "completed"),
|
|
28
|
+
threadId: normalizeNullableText(source.threadId || threadId),
|
|
29
|
+
updatedAt: normalizeNullableText(source.updatedAt)
|
|
30
|
+
}
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Whether a failure reads as a rejected or missing credential. */
|
|
35
|
+
function openCodeCredentialFailure(value = "") {
|
|
36
|
+
const failure = normalizeNullableText(value);
|
|
37
|
+
return (
|
|
38
|
+
/\b(?:401|403)\b/u.test(failure) ||
|
|
39
|
+
(
|
|
40
|
+
/(?:api[-_\s]?key|authentication|authorization|credential|access[-_\s]?token|unauthori[sz]ed|forbidden)/iu.test(failure) &&
|
|
41
|
+
/(?:denied|expired|failed|forbidden|incorrect|invalid|missing|rejected|revoked|unauthori[sz]ed)/iu.test(failure)
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether a native error is the provider API's own failure. */
|
|
47
|
+
export function openCodeProviderApiFailure(error) {
|
|
48
|
+
return normalizeNullableText(error?.name) === "APIError";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Observes a target's native events for a projection that reports their failure and close. */
|
|
52
|
+
export function observePreparedEvents(target, turn, abortController, signal, projection) {
|
|
53
|
+
const observation = observeOpenCodeEvents(target.server.client, target.upstreamSessionId, {
|
|
54
|
+
abortController, signal,
|
|
55
|
+
eventStartedAt: turn ? turn.eventStartedAt : null,
|
|
56
|
+
readError: projection.readError,
|
|
57
|
+
onEvent: summary => projection.onEvent(summary, target)
|
|
58
|
+
});
|
|
59
|
+
void observation.completion.then(() => {
|
|
60
|
+
projection.failure(observation.readFailure());
|
|
61
|
+
projection.closed();
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
close: observation.close,
|
|
65
|
+
readFailure: observation.readFailure,
|
|
66
|
+
waitUntilReady(options) {
|
|
67
|
+
const ready = observation.waitUntilReady(options);
|
|
68
|
+
void ready.catch(projection.failure);
|
|
69
|
+
return ready;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Projects a turn's native assistant messages and reasoning into the conversation store. */
|
|
75
|
+
export async function writeConversationProjection(sessionId, messages = null, {
|
|
76
|
+
inputMessageId = "",
|
|
77
|
+
streaming = false,
|
|
78
|
+
captureFinalAssistantResult = false
|
|
79
|
+
} = {}, projection) {
|
|
80
|
+
const { outputId: nativeOutputId = noOutputId } = projection;
|
|
81
|
+
let failure = "";
|
|
82
|
+
let finalAssistantResult = null;
|
|
83
|
+
let providerApiFailure = false;
|
|
84
|
+
const rows = (inputMessageId
|
|
85
|
+
? openCodeRowsForInput(messages, inputMessageId)
|
|
86
|
+
: openCodeMessageRows(messages)).filter((message) => message?.type === "assistant");
|
|
87
|
+
const finalNativeMessage = captureFinalAssistantResult && !streaming
|
|
88
|
+
? openCodeLastAssistantResult(rows, { readError: projection.readError }).message : null;
|
|
89
|
+
for (const [index, message] of rows.entries()) {
|
|
90
|
+
failure ||= projection.readError(message);
|
|
91
|
+
providerApiFailure ||= openCodeProviderApiFailure(message.error);
|
|
92
|
+
if (message.summary === true) continue;
|
|
93
|
+
const assistantText = assistantMessageText(message);
|
|
94
|
+
const messageId = assistantText ? projection.messageId(message.id, "assistant") : "";
|
|
95
|
+
const outputId = assistantText ? nativeOutputId(message.id, "assistant") : "";
|
|
96
|
+
const inFlight = streaming && index === rows.length - 1;
|
|
97
|
+
const reasoning = (message.content || []).filter((part) => part.type === "reasoning" && normalizeNullableText(part.text))
|
|
98
|
+
.map((part) => ({
|
|
99
|
+
messageId: message.id,
|
|
100
|
+
partId: part.id,
|
|
101
|
+
text: part.text,
|
|
102
|
+
createdAt: message.time?.created,
|
|
103
|
+
complete: Boolean(part.time?.end)
|
|
104
|
+
}));
|
|
105
|
+
await projection.reasoning(reasoning, {
|
|
106
|
+
complete: !inFlight || Boolean(message.time?.completed || message.finish),
|
|
107
|
+
flush: !streaming || Boolean(assistantText && !inFlight)
|
|
108
|
+
});
|
|
109
|
+
if (streaming && assistantText && !inFlight) {
|
|
110
|
+
// A completed round persists as soon as the next one starts, so
|
|
111
|
+
// superseded narration never evaporates from the transcript.
|
|
112
|
+
const turn = await projection.store.writeConversationAssistantMessage(sessionId, {
|
|
113
|
+
messageId,
|
|
114
|
+
...(outputId ? { outputId } : {}),
|
|
115
|
+
text: assistantText
|
|
116
|
+
});
|
|
117
|
+
projection.store.completeConversationStreamMessage(sessionId, messageId);
|
|
118
|
+
await projection.publishTurn(turn);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (streaming && assistantText && inFlight) {
|
|
122
|
+
const conversationStream = projection.store.updateConversationStream(sessionId, {
|
|
123
|
+
turnId: inputMessageId,
|
|
124
|
+
messageId,
|
|
125
|
+
...(outputId ? { outputId } : {}),
|
|
126
|
+
text: assistantText
|
|
127
|
+
});
|
|
128
|
+
if (conversationStream) {
|
|
129
|
+
await projection.publishStream(conversationStream);
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (assistantText && !streaming) {
|
|
134
|
+
const turn = await projection.store.writeConversationAssistantMessage(sessionId, {
|
|
135
|
+
messageId,
|
|
136
|
+
...(outputId ? { outputId } : {}),
|
|
137
|
+
text: assistantText
|
|
138
|
+
});
|
|
139
|
+
projection.store.completeConversationStreamMessage(sessionId, messageId);
|
|
140
|
+
await projection.publishTurn(turn);
|
|
141
|
+
if (message === finalNativeMessage && !projection.readError(message) &&
|
|
142
|
+
(message.time?.completed || message.finish) && turn) {
|
|
143
|
+
finalAssistantResult = { inputMessageId: normalizeNullableText(inputMessageId), itemId: normalizeNullableText(message.id),
|
|
144
|
+
text: assistantText, conversationTurn: turn };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { failure, providerApiFailure, ...(captureFinalAssistantResult ? { finalAssistantResult } : {}) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Starts and retires the monitor that observes a session's native turn, and
|
|
153
|
+
* reads the final-answer evidence it retains.
|
|
154
|
+
* @param {{
|
|
155
|
+
* processes: Map<string, object>,
|
|
156
|
+
* monitors: Map<string, Promise<object>>,
|
|
157
|
+
* turns: Map<string, object>,
|
|
158
|
+
* processOwner: import("./openCodeProcessOwner.js").OpenCodeProcessOwner
|
|
159
|
+
* }} options
|
|
160
|
+
*/
|
|
161
|
+
export function createOpenCodeTurnMonitor({ processes, monitors, turns, processOwner }) {
|
|
162
|
+
// Evidence belongs to the exact retained native turn. This read never creates,
|
|
163
|
+
// observes, resumes or projects a native conversation.
|
|
164
|
+
function readFinalAssistantResult(key, threadId = "", turnId = "") {
|
|
165
|
+
const turn = turns.get(key);
|
|
166
|
+
const result = turn?.finalAssistantResult;
|
|
167
|
+
const target = turn?.finalAssistantResultTarget;
|
|
168
|
+
if (!target || processes.get(key) !== target || target.abortController.signal.aborted ||
|
|
169
|
+
!turn.finalAssistantResultProcess || processOwner.current !== turn.finalAssistantResultProcess ||
|
|
170
|
+
target.upstreamSessionId !== turn.threadId) return null;
|
|
171
|
+
if (!normalizeNullableText(threadId) || !normalizeNullableText(turnId) || !result || turn.active || turn.state !== "completed" ||
|
|
172
|
+
turn.error || turn.observationError || turn.interruptRequested || turn.interruptAcknowledged ||
|
|
173
|
+
turn.threadId !== normalizeNullableText(threadId) || turn.id !== normalizeNullableText(turnId) ||
|
|
174
|
+
result.threadId !== normalizeNullableText(threadId) || result.turnId !== normalizeNullableText(turnId) ||
|
|
175
|
+
result.inputMessageId !== normalizeNullableText(turn.inputMessageId) || !result.conversationTurn) return null;
|
|
176
|
+
return structuredClone(result);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function beginMessageMonitor(key, target, admitted, options, projection) {
|
|
180
|
+
const start = () => {
|
|
181
|
+
target = { ...target };
|
|
182
|
+
const existing = monitors.get(key);
|
|
183
|
+
if (existing) {
|
|
184
|
+
return existing;
|
|
185
|
+
}
|
|
186
|
+
const metadata = projection.prepare(admitted, options);
|
|
187
|
+
const eventStartedAt = Number(admitted.eventStartedAt) || Date.now();
|
|
188
|
+
const startedAt = normalizeNullableText(admitted.startedAt) || new Date(eventStartedAt).toISOString();
|
|
189
|
+
const turn = {
|
|
190
|
+
abortController: new AbortController(),
|
|
191
|
+
admission: options.admission,
|
|
192
|
+
active: true,
|
|
193
|
+
error: "",
|
|
194
|
+
eventStartedAt,
|
|
195
|
+
id: normalizeNullableText(admitted.id),
|
|
196
|
+
inputMessageId: normalizeNullableText(admitted.id),
|
|
197
|
+
startedAt,
|
|
198
|
+
state: options.admission ? "starting" : "active",
|
|
199
|
+
...metadata.fields,
|
|
200
|
+
threadId: target.upstreamSessionId,
|
|
201
|
+
updatedAt: startedAt
|
|
202
|
+
};
|
|
203
|
+
const monitor = beginMonitor(key, target, turn, projection.create(target, turn, metadata, options));
|
|
204
|
+
void monitor.catch(error => projection.onError(error));
|
|
205
|
+
return monitor;
|
|
206
|
+
};
|
|
207
|
+
// readable-check: allow method-probe - optional operation: a monitor that runs inside its request's context supplies runInContext
|
|
208
|
+
if (typeof projection.runInContext !== "function") return start();
|
|
209
|
+
let monitor, startFailure;
|
|
210
|
+
// Context entry invokes this callback synchronously. Do not return the
|
|
211
|
+
// monitor to an async scope wrapper: retain its identity and error owner.
|
|
212
|
+
const scope = projection.runInContext(() => {
|
|
213
|
+
try { monitor = start(); }
|
|
214
|
+
catch (error) { startFailure = { error }; }
|
|
215
|
+
});
|
|
216
|
+
void Promise.resolve(scope).catch(error => projection.onError(error));
|
|
217
|
+
if (startFailure) throw startFailure.error;
|
|
218
|
+
return monitor;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A turn's failure and whether it is a credential failure. A removed
|
|
222
|
+
// connection's models are gone from its reloaded directory.
|
|
223
|
+
function turnFailure(failure) {
|
|
224
|
+
const providerId = /\bModel not found: ([^/\s]+)\//u.exec(failure)?.[1];
|
|
225
|
+
if (providerId && processOwner.current && !processOwner.current.connections.has(providerId)) {
|
|
226
|
+
return { failure: `${providerId} isn't connected. Connect it again, then send your message again.`, credentialFailure: true };
|
|
227
|
+
}
|
|
228
|
+
return { failure, credentialFailure: openCodeCredentialFailure(failure) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function beginMonitor(key, target, turn, {
|
|
232
|
+
observe, observation, eventReady, readiness, finalResponse,
|
|
233
|
+
writeRun, projectMessages, completeResult, onRetired
|
|
234
|
+
}) {
|
|
235
|
+
delete turn.finalAssistantResult;
|
|
236
|
+
delete turn.finalAssistantResultTarget;
|
|
237
|
+
delete turn.finalAssistantResultProcess;
|
|
238
|
+
const registeredTarget = processes.get(key);
|
|
239
|
+
const finalAssistantResultTarget = registeredTarget &&
|
|
240
|
+
registeredTarget.abortController === target.abortController &&
|
|
241
|
+
registeredTarget.server === target.server &&
|
|
242
|
+
registeredTarget.upstreamSessionId === target.upstreamSessionId ? registeredTarget : null;
|
|
243
|
+
const finalAssistantResultProcess = finalAssistantResultTarget ? processOwner.current : null;
|
|
244
|
+
turns.set(key, turn);
|
|
245
|
+
const releaseDirectory = processOwner.holdDirectory(target.workdir);
|
|
246
|
+
const signal = AbortSignal.any([target.abortController.signal, turn.abortController.signal]);
|
|
247
|
+
const monitor = Promise.resolve().then(async () => {
|
|
248
|
+
const events = observation
|
|
249
|
+
? observePreparedEvents(target, turn, turn.abortController, signal, observation())
|
|
250
|
+
: observe(turn, signal);
|
|
251
|
+
if (eventReady) {
|
|
252
|
+
void events.waitUntilReady(readiness).then(eventReady.resolve, eventReady.reject);
|
|
253
|
+
}
|
|
254
|
+
let finalState = "completed";
|
|
255
|
+
let failure = "";
|
|
256
|
+
let finalAssistantResult = null;
|
|
257
|
+
let credentialFailure = false;
|
|
258
|
+
let providerApiFailure = false;
|
|
259
|
+
try {
|
|
260
|
+
await turn.admission?.promise;
|
|
261
|
+
signal.throwIfAborted();
|
|
262
|
+
turn.state = "active";
|
|
263
|
+
await writeRun(turn, "active");
|
|
264
|
+
for (;;) {
|
|
265
|
+
const completion = await waitForOpenCodeFinalResponse(
|
|
266
|
+
target.server.client,
|
|
267
|
+
target.upstreamSessionId,
|
|
268
|
+
turn,
|
|
269
|
+
{
|
|
270
|
+
...finalResponse,
|
|
271
|
+
// Preserve the reasoning-only completion before the native recovery
|
|
272
|
+
// input replaces the projection boundary. Replayed writes are no-ops.
|
|
273
|
+
beforeRecovery: async completion => {
|
|
274
|
+
await projectMessages(completion.messages, { inputMessageId: completion.inputMessageId });
|
|
275
|
+
const { beforeRecovery = recoverWithoutProjection } = finalResponse;
|
|
276
|
+
await beforeRecovery(completion);
|
|
277
|
+
},
|
|
278
|
+
onMessages: async (messages, inputMessageId) => {
|
|
279
|
+
const latest = openCodeLastAssistantResult(messages, { readError: finalResponse.readError }).message;
|
|
280
|
+
const phase = latest?.summary === true && Number(latest.time?.created) >= turn.eventStartedAt &&
|
|
281
|
+
!latest.time?.completed && !latest.finish && !latest.error ? "compacting" : "";
|
|
282
|
+
if (turn.active && !signal.aborted && normalizeNullableText(turn.phase) !== phase) {
|
|
283
|
+
turn.phase = phase;
|
|
284
|
+
turn.updatedAt = new Date().toISOString();
|
|
285
|
+
await writeRun(turn, turn.state);
|
|
286
|
+
}
|
|
287
|
+
return projectMessages(messages, { inputMessageId, streaming: true });
|
|
288
|
+
},
|
|
289
|
+
readFailure: events.readFailure,
|
|
290
|
+
signal
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
const admission = turn.admission;
|
|
294
|
+
await admission?.promise;
|
|
295
|
+
signal.throwIfAborted();
|
|
296
|
+
if (admission !== turn.admission || completion.inputMessageId !== normalizeNullableText(turn.inputMessageId)) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const projection = await projectMessages(completion.messages, {
|
|
300
|
+
inputMessageId: completion.inputMessageId,
|
|
301
|
+
captureFinalAssistantResult: true
|
|
302
|
+
});
|
|
303
|
+
await turn.admission?.promise;
|
|
304
|
+
signal.throwIfAborted();
|
|
305
|
+
if (admission !== turn.admission || completion.inputMessageId !== normalizeNullableText(turn.inputMessageId)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
failure = projection.failure || normalizeNullableText(events.readFailure()?.message);
|
|
309
|
+
providerApiFailure = projection.providerApiFailure || openCodeProviderApiFailure(events.readFailure());
|
|
310
|
+
if (!failure && !turn.interruptRequested && !normalizeNullableText(completion.result?.text)) {
|
|
311
|
+
failure = "OpenCode finished without a user-facing final response. Please send your message again.";
|
|
312
|
+
finalState = "failed";
|
|
313
|
+
} else if (failure) {
|
|
314
|
+
({ failure, credentialFailure } = turnFailure(failure));
|
|
315
|
+
finalState = "failed";
|
|
316
|
+
} else if (turn.interruptRequested) {
|
|
317
|
+
finalState = "interrupted";
|
|
318
|
+
}
|
|
319
|
+
const published = projection.finalAssistantResult;
|
|
320
|
+
if (finalState === "completed" && !failure && published &&
|
|
321
|
+
published.inputMessageId === completion.inputMessageId &&
|
|
322
|
+
published.itemId === completion.result?.turnId &&
|
|
323
|
+
normalizeNullableText(published.text) === normalizeNullableText(completion.result?.text)) {
|
|
324
|
+
finalAssistantResult = { ...published, threadId: turn.threadId, turnId: turn.id };
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (turn.interruptAcknowledged) {
|
|
330
|
+
finalState = "interrupted";
|
|
331
|
+
} else {
|
|
332
|
+
const cause = signal.aborted ? signal.reason : error;
|
|
333
|
+
({ failure, credentialFailure } = turnFailure(normalizeNullableText(cause?.message) || "OpenCode turn failed."));
|
|
334
|
+
providerApiFailure = openCodeProviderApiFailure(cause);
|
|
335
|
+
finalState = target.abortController.signal.aborted
|
|
336
|
+
? "cancelled"
|
|
337
|
+
: "failed";
|
|
338
|
+
if (!target.abortController.signal.aborted) {
|
|
339
|
+
turn.observationError = failure;
|
|
340
|
+
try {
|
|
341
|
+
await writeRun(turn, turn.state, failure);
|
|
342
|
+
} catch {
|
|
343
|
+
// A failed store must not prevent stopping native work.
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await processOwner.stopUnobservedSession(target, turn.threadId);
|
|
347
|
+
} catch (stopError) {
|
|
348
|
+
failure = `${failure} ${stopError.message}`;
|
|
349
|
+
finalState = "active";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} finally {
|
|
354
|
+
// Claim retirement before awaited cleanup/checkpointing. A new Send
|
|
355
|
+
// joins this monitor's cleanup instead of steering an unobserved turn.
|
|
356
|
+
turn.active = finalState === "active";
|
|
357
|
+
await events.close();
|
|
358
|
+
if (credentialFailure || finalState === "failed") {
|
|
359
|
+
failure = await completeResult(turn, { credentialFailure, providerApiFailure, finalState, failure });
|
|
360
|
+
}
|
|
361
|
+
turn.error = failure;
|
|
362
|
+
turn.state = finalState;
|
|
363
|
+
turn.finalAssistantResult = finalState === "completed" && !failure ? finalAssistantResult : null;
|
|
364
|
+
turn.finalAssistantResultTarget = turn.finalAssistantResult ? finalAssistantResultTarget : null;
|
|
365
|
+
turn.finalAssistantResultProcess = turn.finalAssistantResult ? finalAssistantResultProcess : null;
|
|
366
|
+
turn.updatedAt = new Date().toISOString();
|
|
367
|
+
await writeRun(turn, finalState, failure).catch(() => null);
|
|
368
|
+
}
|
|
369
|
+
return openCodeTurnSnapshot(turn, target.upstreamSessionId);
|
|
370
|
+
}).finally(() => {
|
|
371
|
+
if (monitors.get(key) === monitor) {
|
|
372
|
+
monitors.delete(key);
|
|
373
|
+
}
|
|
374
|
+
releaseDirectory();
|
|
375
|
+
onRetired();
|
|
376
|
+
});
|
|
377
|
+
monitors.set(key, monitor);
|
|
378
|
+
return monitor;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return Object.freeze({
|
|
382
|
+
beginMessageMonitor,
|
|
383
|
+
beginMonitor,
|
|
384
|
+
readFinalAssistantResult
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** @typedef {ReturnType<typeof createOpenCodeTurnMonitor>} OpenCodeTurnMonitor */
|