@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,464 @@
|
|
|
1
|
+
import { codexAppServerFrozenTurnInterruptResponse } from "../codexTurn.js";
|
|
2
|
+
import { assertCodexApplicationToolSchemaIdentity, codexApplicationToolConfiguration } from "../codexProviderThreads.js";
|
|
3
|
+
import { codexLocalImageInput } from "../codexProviderTurns.js";
|
|
4
|
+
import { jsonHash as hash } from "../normalize.js";
|
|
5
|
+
|
|
6
|
+
/** The goal a conversation reports, from Codex's native goal. */
|
|
7
|
+
export const goalValue = goal => goal ? {
|
|
8
|
+
id: hash([goal.threadId, goal.createdAt, goal.objective]), objective: goal.objective, status: goal.status,
|
|
9
|
+
tokenBudget: goal.tokenBudget ?? null, tokensUsed: goal.tokensUsed, timeUsedSeconds: goal.timeUsedSeconds,
|
|
10
|
+
...(Number.isFinite(goal.createdAt) ? { createdAt: new Date(goal.createdAt * 1000).toISOString() } : {}),
|
|
11
|
+
...(Number.isFinite(goal.updatedAt) ? { updatedAt: new Date(goal.updatedAt * 1000).toISOString() } : {})
|
|
12
|
+
} : null;
|
|
13
|
+
|
|
14
|
+
const nativeInput = input => [
|
|
15
|
+
...(input.text || input.attachmentManifest ? [{ type: "text", text: `${input.text || ""}${input.attachmentManifest || ""}` }] : []),
|
|
16
|
+
...(input.content || []).map(part => part.type === "text" ? part :
|
|
17
|
+
{ type: "image", url: `data:${part.mediaType};base64,${part.image.toString("base64")}` }),
|
|
18
|
+
...codexLocalImageInput(input.localFiles)
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// A supplied native message without its own rejection step leaves rejection to this conversation.
|
|
22
|
+
async function rejectNoNativePrompt() {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The turns of one driver conversation: message delivery, goal context, interruption and the run of a turn. It
|
|
26
|
+
* reads and updates the conversation's session record and uses its native connection.
|
|
27
|
+
* @param {object} dependencies
|
|
28
|
+
* @param {import("./codexDriver.js").CodexDriverSession} dependencies.session
|
|
29
|
+
* @param {object | undefined} dependencies.supplied
|
|
30
|
+
* @param {object} dependencies.conversation
|
|
31
|
+
* @param {string} dependencies.sessionId
|
|
32
|
+
* @param {string} dependencies.providerKey
|
|
33
|
+
* @param {import("../codexProviderOwner.js").CodexAppServerProviderOwner} dependencies.providerOwner
|
|
34
|
+
* @param {import("../codexRuntimeLifecycle.js").CodexRunOwner} dependencies.owner
|
|
35
|
+
* @param {object} dependencies.runtime
|
|
36
|
+
* @param {(update: (binding: object) => object) => Promise<void>} dependencies.updateBinding
|
|
37
|
+
* @param {() => Promise<void>} dependencies.drainNotifications
|
|
38
|
+
* @param {ReturnType<typeof import("./codexDriverConnection.js").createCodexDriverConnection>} dependencies.connection
|
|
39
|
+
*/
|
|
40
|
+
export function createCodexDriverTurns({
|
|
41
|
+
session,
|
|
42
|
+
supplied,
|
|
43
|
+
conversation,
|
|
44
|
+
sessionId,
|
|
45
|
+
providerKey,
|
|
46
|
+
providerOwner,
|
|
47
|
+
owner,
|
|
48
|
+
runtime,
|
|
49
|
+
updateBinding,
|
|
50
|
+
drainNotifications,
|
|
51
|
+
connection
|
|
52
|
+
}) {
|
|
53
|
+
const { stop, selection, connect, prepareConfiguration, prepare } = connection;
|
|
54
|
+
|
|
55
|
+
function deliveryInput(input, beforeDispatch, rejected) {
|
|
56
|
+
if (supplied) {
|
|
57
|
+
const { onPromptRejected = rejectNoNativePrompt } = input.nativeMessage;
|
|
58
|
+
return { ...input.nativeMessage,
|
|
59
|
+
onPromptSending: beforeDispatch,
|
|
60
|
+
onPromptRejected: async () => { await onPromptRejected(); await rejected(); } };
|
|
61
|
+
}
|
|
62
|
+
return { messageId: input.messageId, message: input.text,
|
|
63
|
+
displayMessage: input.authoredInput?.text ?? input.text,
|
|
64
|
+
displayAttachments: input.attachments, attachments: input.localFiles,
|
|
65
|
+
onPromptSending: value => beforeDispatch({ threadId: value.threadId }), onPromptRejected: rejected };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function deliveredRow(input, nativeIdentity) {
|
|
69
|
+
const written = await owner.writeDeliveredUserMessage(runtime, sessionId,
|
|
70
|
+
input.authoredInput?.text ?? input.text, input.messageId, null, input.attachments,
|
|
71
|
+
nativeIdentity);
|
|
72
|
+
if (written) return written;
|
|
73
|
+
const saved = (await runtime.store.readConversationLog(sessionId)).find(turn =>
|
|
74
|
+
(turn.user || turn.system)?.messageId === input.messageId);
|
|
75
|
+
if (!saved) throw new Error("The accepted native receipt has no authored conversation message.");
|
|
76
|
+
return saved;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function withDelivery(command, operation) {
|
|
80
|
+
const input = command.input;
|
|
81
|
+
return owner.withMessageDelivery(sessionId, input.messageId, {
|
|
82
|
+
attachments: input.attachments, text: input.authoredInput?.text ?? input.text,
|
|
83
|
+
turnMetadata: input.nativeMessage?.turnMetadata || null, actorContext: input.nativeMessage?.actorContext || null,
|
|
84
|
+
authoredInput: input.authoredInput,
|
|
85
|
+
onNativeReceipt: command.onNativeReceipt,
|
|
86
|
+
onReceiptPersisted: command.onReceiptPersisted
|
|
87
|
+
}, operation);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function messagePreparation(configuration, context, signal, tools) {
|
|
91
|
+
if (supplied) return tools ? { ...supplied.messagePreparation, applicationTools: tools,
|
|
92
|
+
providerReady: session.current.providerReady } : supplied.messagePreparation;
|
|
93
|
+
return {
|
|
94
|
+
async readContext() {
|
|
95
|
+
const runtimeSession = await runtime.getSession(sessionId);
|
|
96
|
+
return { runtime, session: runtimeSession, selection: {
|
|
97
|
+
runtime, session: runtimeSession, threadId: () => session.binding.threadId,
|
|
98
|
+
async acquireProvider() {
|
|
99
|
+
const reused = Boolean(session.native?.isAvailable());
|
|
100
|
+
await connect(signal, await selection(configuration, context, signal));
|
|
101
|
+
return { provider: session.native, reused };
|
|
102
|
+
}
|
|
103
|
+
} };
|
|
104
|
+
},
|
|
105
|
+
threadPreparation() {
|
|
106
|
+
return {
|
|
107
|
+
get failureThreadId() { return session.binding.threadId; },
|
|
108
|
+
async provider() {
|
|
109
|
+
const { selected, params, toolSchemaIdentity } = await prepareConfiguration(configuration, context, signal, tools);
|
|
110
|
+
const retainedThreadId = session.binding.threadId;
|
|
111
|
+
return {
|
|
112
|
+
provider: session.native,
|
|
113
|
+
workdir: session.binding.workdir,
|
|
114
|
+
observerOptions: { providerKey },
|
|
115
|
+
requirePaginatedHistory: !retainedThreadId,
|
|
116
|
+
settings: { ...selected, resumeOptions: params },
|
|
117
|
+
preparation: {
|
|
118
|
+
settings: async () => ({ threadSettings: params, threadStartSettings: params }),
|
|
119
|
+
identity: { read: () => retainedThreadId,
|
|
120
|
+
async write({ threadId }) { await updateBinding({ threadId, toolSchemaIdentity }); } }
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
async prepareMessage(input, prepared, { starting }) {
|
|
127
|
+
if (!starting) return null;
|
|
128
|
+
const { settings } = prepared;
|
|
129
|
+
return { renderedPrompt: input.message,
|
|
130
|
+
get turnSettings() { return { cwd: session.binding.workdir,
|
|
131
|
+
...(settings.model ? { model: settings.model } : {}), effort: configuration.effort || null,
|
|
132
|
+
...(configuration.outputSchema ? { outputSchema: configuration.outputSchema } : {}) }; }
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
async finishMessage(input, prepared, outcome) {
|
|
136
|
+
if (Object.hasOwn(outcome, "error")) throw outcome.error;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function acceptMessage(command, nativeIdentity, row, nativeResult) {
|
|
142
|
+
if (typeof nativeIdentity?.threadId !== "string" || !nativeIdentity.threadId.trim() ||
|
|
143
|
+
typeof nativeIdentity?.turnId !== "string" || !nativeIdentity.turnId.trim()) {
|
|
144
|
+
throw Object.assign(new Error("The native delivery has no exact turn identity. Inspect this message's receipt before continuing."), {
|
|
145
|
+
code: "conversation_delivery_uncertain", delivery: "uncertain"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const { turnId } = nativeIdentity;
|
|
149
|
+
const active = session.current;
|
|
150
|
+
if (!active) throw new Error("The native delivery lost its active conversation owner.");
|
|
151
|
+
active.inputId = command.input.messageId;
|
|
152
|
+
if (!active.turnId) active.turnId = turnId;
|
|
153
|
+
await command.accept({ nativeTurnId: turnId, conversationTurn: row || await deliveredRow(command.input, nativeIdentity),
|
|
154
|
+
...(supplied ? { nativeResult } : {}) });
|
|
155
|
+
active.admitted = true;
|
|
156
|
+
active.admission.resolve();
|
|
157
|
+
if (active.pendingGoal !== undefined) {
|
|
158
|
+
const goal = active.pendingGoal;
|
|
159
|
+
delete active.pendingGoal;
|
|
160
|
+
await active.onEvent({ type: "goal", goal: goalValue(goal) });
|
|
161
|
+
}
|
|
162
|
+
if (active.pendingNativeTurn) {
|
|
163
|
+
const next = active.pendingNativeTurn;
|
|
164
|
+
delete active.pendingNativeTurn;
|
|
165
|
+
await active.onNativeTurn({ turnId: next, nativeOwner: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function sendMessage(command, configuration, context, signal, tools) {
|
|
170
|
+
return withDelivery(command, async () => {
|
|
171
|
+
let attempted = false;
|
|
172
|
+
let rejected = false;
|
|
173
|
+
let accepted = false;
|
|
174
|
+
const input = deliveryInput(command.input, async value => {
|
|
175
|
+
await command.beforeDispatch(value);
|
|
176
|
+
signal.throwIfAborted();
|
|
177
|
+
attempted = true;
|
|
178
|
+
session.current.dispatched = true;
|
|
179
|
+
}, () => { rejected = true; });
|
|
180
|
+
try {
|
|
181
|
+
const delivered = await owner.dispatchMessage(sessionId, input, {
|
|
182
|
+
...(supplied ? { ...context, actorContext: input.actorContext || null } : { preparedInput: nativeInput(command.input) })
|
|
183
|
+
}, messagePreparation(configuration, context, signal, tools));
|
|
184
|
+
const { value, nativeIdentity } = delivered;
|
|
185
|
+
if (supplied ? value.delivered === true : value.delivered || nativeIdentity) {
|
|
186
|
+
await acceptMessage(command, nativeIdentity || { threadId: value.threadId, turnId: value.turnId }, value.conversationTurn, delivered);
|
|
187
|
+
accepted = true;
|
|
188
|
+
if (value.ok === false) throw new Error(value.error);
|
|
189
|
+
return delivered;
|
|
190
|
+
}
|
|
191
|
+
// The host's main sender keeps its pending evidence when the
|
|
192
|
+
// command returns delivered:false, even if a native ID is known.
|
|
193
|
+
if (supplied) return delivered;
|
|
194
|
+
throw Object.assign(new Error(value?.error || "Codex is not ready to receive this instruction."), value);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (attempted && !accepted && !rejected && error.delivery !== "not-sent" && !Number.isInteger(error.code)) error.delivery = "uncertain";
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function goalContext(configuration, context, signal, { prepareThread = false, tools } = {}) {
|
|
203
|
+
if (supplied) return owner.prepareGoalContext(sessionId, context, supplied.goalPreparation, { prepareThread });
|
|
204
|
+
let selected;
|
|
205
|
+
if (prepareThread && !session.binding.threadId) selected = await prepare(configuration, context, signal, tools);
|
|
206
|
+
else if (prepareThread) {
|
|
207
|
+
const prepared = await prepareConfiguration(configuration, context, signal, tools);
|
|
208
|
+
selected = { ...prepared.selected, resumeOptions: prepared.params };
|
|
209
|
+
} else {
|
|
210
|
+
selected = await selection(configuration, context, signal);
|
|
211
|
+
await connect(signal, selected);
|
|
212
|
+
}
|
|
213
|
+
return { runtime, session: await runtime.getSession(sessionId), provider: session.native,
|
|
214
|
+
threadId: session.binding.threadId, providerKey, resumeOptions: selected.resumeOptions };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function interrupt({ configuration, context, input = {}, representation } = {}) {
|
|
218
|
+
if (supplied) {
|
|
219
|
+
const admission = supplied.admission();
|
|
220
|
+
if (admission.ok === false) {
|
|
221
|
+
const value = codexAppServerFrozenTurnInterruptResponse({
|
|
222
|
+
threadId: input.threadId || input.codexSessionId, turnId: input.turnId || input.codexTurnId
|
|
223
|
+
});
|
|
224
|
+
return representation === "native" ? { value } : value;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const prepared = await supplied.controlPreparation(context, input);
|
|
228
|
+
if (prepared.ok === false) {
|
|
229
|
+
if (representation === "native") return { value: prepared };
|
|
230
|
+
throw Object.assign(new Error(prepared.error), prepared, { cleanupFailed: true });
|
|
231
|
+
}
|
|
232
|
+
const result = await owner.interruptTurn(sessionId, input, owner.controlContext(sessionId, prepared));
|
|
233
|
+
if (representation === "native") return result;
|
|
234
|
+
if (result.value?.ok === false) throw Object.assign(new Error(result.value.error), result.value, { cleanupFailed: true });
|
|
235
|
+
return result.value;
|
|
236
|
+
} finally { admission.release(); }
|
|
237
|
+
}
|
|
238
|
+
if (session.observationError) {
|
|
239
|
+
await stop();
|
|
240
|
+
return { ok: true, interrupted: true };
|
|
241
|
+
}
|
|
242
|
+
const result = await owner.interruptTurn(sessionId, {}, {
|
|
243
|
+
runtime, admissionError: () => null, threadId: () => session.binding.threadId,
|
|
244
|
+
observationOwner: () => session.native,
|
|
245
|
+
async acquireProvider() {
|
|
246
|
+
if (!session.native && configuration) {
|
|
247
|
+
const signal = AbortSignal.timeout(30_000);
|
|
248
|
+
await connect(signal, await selection(configuration, context, signal));
|
|
249
|
+
}
|
|
250
|
+
if (!session.native) throw new Error("Codex's retained runtime owner is unavailable.");
|
|
251
|
+
await session.native.ensureAvailable();
|
|
252
|
+
return session.native;
|
|
253
|
+
}
|
|
254
|
+
}).catch(error => {
|
|
255
|
+
// A thrown control error also leaves this native owner unconfirmed.
|
|
256
|
+
if (error === session.current?.toolFailure && !session.current.signal.aborted && !error.cleanupFailed) {
|
|
257
|
+
session.current.toolFailureCleanup = error;
|
|
258
|
+
}
|
|
259
|
+
error.cleanupFailed = true;
|
|
260
|
+
throw error;
|
|
261
|
+
});
|
|
262
|
+
if (result.value?.ok === false) throw Object.assign(new Error(result.value.error || "Codex could not confirm that work stopped."), { cleanupFailed: true });
|
|
263
|
+
return result.value;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function run({ configuration, context, input, tools, signal, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted,
|
|
267
|
+
onEvent, onNativeTurn, finishIfCurrent }) {
|
|
268
|
+
if (session.disposed || session.current) throw new Error("This Codex conversation is closed or already working.");
|
|
269
|
+
// input validation: a host's bound application tools need the conversation's schema identity reader.
|
|
270
|
+
if (supplied && tools && !conversation.identity?.readToolSchemaIdentity) {
|
|
271
|
+
throw new TypeError("Bound Codex application tools require the original schema identity reader and writer.");
|
|
272
|
+
}
|
|
273
|
+
const completion = Promise.withResolvers();
|
|
274
|
+
const admission = Promise.withResolvers();
|
|
275
|
+
completion.promise.catch(() => {});
|
|
276
|
+
admission.promise.catch(() => {});
|
|
277
|
+
if (input.goal) throw new Error("Codex goals are controls. Inspect any unresolved goal-message record before continuing.");
|
|
278
|
+
const active = session.current = { configuration, context, inputId: input.messageId,
|
|
279
|
+
onEvent, onNativeTurn, signal, completion, admission, inputReady: admission.promise, finishIfCurrent,
|
|
280
|
+
admitted: false, dispatched: false, turnId: "" };
|
|
281
|
+
let cancellation;
|
|
282
|
+
let toolFailureRecovery;
|
|
283
|
+
let problem;
|
|
284
|
+
let delivered;
|
|
285
|
+
let waitForTurn = false;
|
|
286
|
+
let executingProvider;
|
|
287
|
+
const toolCalls = new Set();
|
|
288
|
+
const toolRegistrations = new Map();
|
|
289
|
+
// This run still owns the original native provider's current turn.
|
|
290
|
+
const ownsTurn = (provider, threadId, turnId) => session.current === active && !session.disposed && !signal.aborted &&
|
|
291
|
+
Boolean(provider) && provider === executingProvider && provider === session.native &&
|
|
292
|
+
threadId === session.binding.threadId && turnId === active.turnId;
|
|
293
|
+
function toolHandler(provider, threadId, isRegistered = () => true) {
|
|
294
|
+
return async ({ method, params }) => {
|
|
295
|
+
if (method !== "item/tool/call" || !tools || params?.threadId !== (threadId || session.binding.threadId) || params.namespace ||
|
|
296
|
+
supplied && (session.disposed || session.current !== active || active.finished || signal.aborted || owner.runtimeLifecycle?.closing === true || !isRegistered())) {
|
|
297
|
+
throw new Error("This conversation does not authorize that native tool request.");
|
|
298
|
+
}
|
|
299
|
+
let owned = !supplied;
|
|
300
|
+
const operation = (async () => {
|
|
301
|
+
await active.inputReady;
|
|
302
|
+
if (supplied && owner.runtimeLifecycle?.closing === true) {
|
|
303
|
+
throw new Error("This conversation does not authorize that native tool request.");
|
|
304
|
+
}
|
|
305
|
+
const ownership = Promise.withResolvers();
|
|
306
|
+
owner.notificationQueue.run({ sessionId, provider }, () => {
|
|
307
|
+
if (supplied && (params.turnId !== active.turnId || session.disposed || session.current !== active || active.finished ||
|
|
308
|
+
signal.aborted || !isRegistered())) {
|
|
309
|
+
ownership.resolve(false);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
owned = true;
|
|
313
|
+
const failure = provider.observationFailure;
|
|
314
|
+
// readable-check: allow no-dual-shape - data: a lost observation's failure wraps its cause or is the cause itself
|
|
315
|
+
if (failure) ownership.reject(failure.cause || failure);
|
|
316
|
+
else ownership.resolve(params.turnId === active.turnId);
|
|
317
|
+
});
|
|
318
|
+
if (!await ownership.promise || supplied && (session.disposed || session.current !== active || active.finished || signal.aborted || !isRegistered())) {
|
|
319
|
+
owned = false;
|
|
320
|
+
throw new Error("This application tool call belongs to another turn.");
|
|
321
|
+
}
|
|
322
|
+
let result;
|
|
323
|
+
try {
|
|
324
|
+
result = await tools.execute({ id: params.callId, name: params.tool, arguments: JSON.stringify(params.arguments) });
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (!supplied && owned && ownsTurn(provider, params.threadId, params.turnId)) active.toolFailure ||= error;
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
return { success: result.ok, contentItems: [{ type: "inputText", text: JSON.stringify(result) }] };
|
|
330
|
+
})();
|
|
331
|
+
toolCalls.add(operation);
|
|
332
|
+
try { return await operation; }
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (!supplied || owned && session.current === active && params.turnId === active.turnId && isRegistered()) {
|
|
335
|
+
if (supplied) active.toolFailure = error;
|
|
336
|
+
active.completion.reject(error);
|
|
337
|
+
}
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
finally { toolCalls.delete(operation); }
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
active.providerReady = async ({ provider, threadId }) => {
|
|
344
|
+
signal.throwIfAborted();
|
|
345
|
+
if (!threadId) return;
|
|
346
|
+
if (await conversation.identity.read() !== threadId) throw new Error("The native application tool thread changed before dispatch.");
|
|
347
|
+
assertCodexApplicationToolSchemaIdentity(threadId, await conversation.identity.readToolSchemaIdentity(),
|
|
348
|
+
codexApplicationToolConfiguration(tools).toolSchemaIdentity);
|
|
349
|
+
signal.throwIfAborted();
|
|
350
|
+
if (session.disposed || session.current !== active) throw new Error("This conversation handle is closed.");
|
|
351
|
+
for (const [previous, owned] of toolRegistrations) {
|
|
352
|
+
if (previous === provider && owned.threadId === threadId && owned.client === provider.client && owned.registration.isCurrent()) return;
|
|
353
|
+
owned.registration.release();
|
|
354
|
+
toolRegistrations.delete(previous);
|
|
355
|
+
}
|
|
356
|
+
const client = provider.client;
|
|
357
|
+
let registration;
|
|
358
|
+
registration = provider.registerThreadRequestHandler(threadId, toolHandler(provider, threadId,
|
|
359
|
+
() => registration?.isCurrent() === true && provider.client === client));
|
|
360
|
+
toolRegistrations.set(provider, { threadId, client, registration });
|
|
361
|
+
};
|
|
362
|
+
function abort() {
|
|
363
|
+
if (cancellation) return;
|
|
364
|
+
cancellation = interrupt();
|
|
365
|
+
cancellation.then(() => active.completion.reject(signal.reason), error => active.completion.reject(error));
|
|
366
|
+
}
|
|
367
|
+
async function submit(command, operation) {
|
|
368
|
+
await admission.promise;
|
|
369
|
+
const ready = Promise.withResolvers();
|
|
370
|
+
const previous = active.inputReady;
|
|
371
|
+
active.inputReady = ready.promise;
|
|
372
|
+
try { return await operation(command, configuration, context, signal, tools); }
|
|
373
|
+
finally { ready.resolve(); active.inputReady = previous; }
|
|
374
|
+
}
|
|
375
|
+
active.steer = command => submit(command, sendMessage);
|
|
376
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
377
|
+
try {
|
|
378
|
+
signal.throwIfAborted();
|
|
379
|
+
// Preparation retains the provider's authenticated account and the
|
|
380
|
+
// stable application-tool entry points. The run owner alone
|
|
381
|
+
// subscribes to notifications and handles native turn state/output.
|
|
382
|
+
if (!supplied) {
|
|
383
|
+
const selected = await selection(configuration, context, signal);
|
|
384
|
+
await connect(signal, selected);
|
|
385
|
+
executingProvider = session.native;
|
|
386
|
+
}
|
|
387
|
+
if (!supplied) session.native.setServerRequestHandler(toolHandler(executingProvider));
|
|
388
|
+
const command = { input, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted };
|
|
389
|
+
delivered = await sendMessage(command, configuration, context, signal, tools);
|
|
390
|
+
waitForTurn = !supplied || delivered.value?.delivered === true;
|
|
391
|
+
if (!finishIfCurrent && waitForTurn) {
|
|
392
|
+
await completion.promise;
|
|
393
|
+
await drainNotifications();
|
|
394
|
+
}
|
|
395
|
+
} catch (error) { problem = error; }
|
|
396
|
+
if (finishIfCurrent) {
|
|
397
|
+
try {
|
|
398
|
+
for (;;) {
|
|
399
|
+
const settling = active.completion;
|
|
400
|
+
if (waitForTurn) {
|
|
401
|
+
try { await settling.promise; }
|
|
402
|
+
catch (error) { problem = error; }
|
|
403
|
+
}
|
|
404
|
+
if (await finishIfCurrent(() => active.completion === settling)) break;
|
|
405
|
+
// The same dispatch established another native turn.
|
|
406
|
+
// Keep its completion and Stop owner until that turn settles.
|
|
407
|
+
problem = undefined;
|
|
408
|
+
waitForTurn = true;
|
|
409
|
+
}
|
|
410
|
+
if (!problem && waitForTurn) await drainNotifications();
|
|
411
|
+
} catch (error) { problem = error; }
|
|
412
|
+
}
|
|
413
|
+
signal.removeEventListener("abort", abort);
|
|
414
|
+
admission.reject(problem || signal.reason);
|
|
415
|
+
const failureThreadId = session.binding.threadId;
|
|
416
|
+
const failureTurnId = active.turnId;
|
|
417
|
+
try {
|
|
418
|
+
// Account invalidation already owns this stop attempt. Its failed
|
|
419
|
+
// proof stays available for explicit cleanup through the same owner.
|
|
420
|
+
if ((!supplied && problem || supplied && active.toolFailure || signal.aborted) && problem?.code !== "codex_runtime_invalidated") {
|
|
421
|
+
await (cancellation || interrupt());
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
problem = error;
|
|
425
|
+
// A direct Stop checkpoint can throw before its native completion
|
|
426
|
+
// reaches the queue. Keep the same admitted tool failure with the
|
|
427
|
+
// provider's observation-stop owner until its pending recovery joins.
|
|
428
|
+
if (!supplied && error === active.toolFailure && ownsTurn(executingProvider, failureThreadId, failureTurnId) &&
|
|
429
|
+
error.code !== "codex_runtime_invalidated") {
|
|
430
|
+
toolFailureRecovery = executingProvider.failObservation(error);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
await drainNotifications();
|
|
435
|
+
// The notification queue starts observation recovery without awaiting
|
|
436
|
+
// it. Join only this provider's pending attempt before settling.
|
|
437
|
+
if (!supplied) await providerOwner.pendingRecovery(providerKey, executingProvider);
|
|
438
|
+
if (toolFailureRecovery) {
|
|
439
|
+
await toolFailureRecovery;
|
|
440
|
+
if (problem === active.toolFailureCleanup && ownsTurn(executingProvider, failureThreadId, failureTurnId) &&
|
|
441
|
+
!session.observationError && session.binding.observationLoss?.stopped === true) delete problem.cleanupFailed;
|
|
442
|
+
}
|
|
443
|
+
} catch (error) { problem = error; }
|
|
444
|
+
finally {
|
|
445
|
+
for (const owned of toolRegistrations.values()) owned.registration.release();
|
|
446
|
+
toolRegistrations.clear();
|
|
447
|
+
await Promise.allSettled([...toolCalls]);
|
|
448
|
+
if (!supplied) {
|
|
449
|
+
session.native?.setServerRequestHandler(null);
|
|
450
|
+
owner.clearSessionRecoveryTimers(sessionId);
|
|
451
|
+
}
|
|
452
|
+
session.current = null;
|
|
453
|
+
}
|
|
454
|
+
if (problem) throw problem;
|
|
455
|
+
return delivered;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return Object.freeze({
|
|
459
|
+
sendMessage,
|
|
460
|
+
goalContext,
|
|
461
|
+
interrupt,
|
|
462
|
+
run
|
|
463
|
+
});
|
|
464
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A native conversation's instruction source: its text, an optional identity that keeps it cached, and where it goes.
|
|
3
|
+
* @typedef {object} OpenCodeInstructionSource
|
|
4
|
+
* @property {() => string | Promise<string>} read
|
|
5
|
+
* @property {string} [identity]
|
|
6
|
+
* @property {"append" | "replace"} [placement]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* OpenCode's native system lane is rebuilt for each inference, without a chat message.
|
|
11
|
+
* @param {{ resolveInstructions: (id: string) => Promise<OpenCodeInstructionSource | null | undefined> }} dependencies
|
|
12
|
+
*/
|
|
13
|
+
export function createOpenCodeConversationAdapter({ resolveInstructions }) {
|
|
4
14
|
const contexts = new Map();
|
|
5
15
|
const contributions = new WeakMap();
|
|
6
16
|
|
|
@@ -9,6 +19,7 @@ export function createOpenCodeConversationAdapter({ resolveInstructions } = {})
|
|
|
9
19
|
if (!id) return;
|
|
10
20
|
const source = await resolveInstructions(id);
|
|
11
21
|
if (!source) return;
|
|
22
|
+
// readable-check: allow method-probe - input validation: the instruction source a host resolves for a native conversation
|
|
12
23
|
if ((source.identity !== undefined && (typeof source.identity !== "string" || !source.identity)) || typeof source.read !== "function" ||
|
|
13
24
|
![undefined, "append", "replace"].includes(source.placement)) {
|
|
14
25
|
throw new TypeError("Conversation instructions require read() and an optional non-empty identity.");
|
|
@@ -45,5 +56,20 @@ export function createOpenCodeConversationAdapter({ resolveInstructions } = {})
|
|
|
45
56
|
if (id) contexts.delete(id);
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
/** Forgets one conversation's cached instructions. */
|
|
60
|
+
function invalidate(id) {
|
|
61
|
+
return contexts.delete(id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Forgets every cached instruction. */
|
|
65
|
+
function close() {
|
|
66
|
+
return contexts.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
transformSystem,
|
|
71
|
+
event,
|
|
72
|
+
invalidate,
|
|
73
|
+
close
|
|
74
|
+
});
|
|
49
75
|
}
|