@jskit-ai/assistant-core 0.1.200 → 0.1.201
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/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { conversationRequestText } from "../continuity.js";
|
|
2
|
+
import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
|
|
3
|
+
import { consumeCompletionStream, sanitizeAssistantMessageText } from "../../lib/assistantCompletion.js";
|
|
4
|
+
import { runAssistantToolLoop } from "../../lib/assistantToolLoop.js";
|
|
5
|
+
import { isAssistantProgressOnlyText } from "../../../shared/assistantResponseText.js";
|
|
6
|
+
|
|
7
|
+
// Preserve the original assistant service's model-facing action result. The
|
|
8
|
+
// durable receipt keeps its full ok/result/error envelope in common storage.
|
|
9
|
+
const toolResultContent = result => JSON.stringify(result.ok ? result.result ?? null : { error: result.error });
|
|
10
|
+
|
|
11
|
+
/** Direct API inference uses the same authorized connection catalogue as other JSKIT services. */
|
|
12
|
+
export function createApiConversationDriver({ connections, apiClientFactory, fetch, limits = {} }) {
|
|
13
|
+
if (apiClientFactory !== undefined && typeof apiClientFactory?.resolveClient !== "function") {
|
|
14
|
+
throw new TypeError("An API client factory requires resolveClient({ context, configuration }).");
|
|
15
|
+
}
|
|
16
|
+
if (!apiClientFactory && typeof connections?.resolve !== "function") throw new TypeError("API conversations require an authorized AI connection resolver.");
|
|
17
|
+
const maximumOutput = limits.maxOutputCharacters ?? 64_000;
|
|
18
|
+
const maximumHistory = limits.maxHistoryCharacters ?? 256_000;
|
|
19
|
+
const preserveWhitespace = apiClientFactory?.preserveWhitespace !== false;
|
|
20
|
+
for (const limit of [maximumOutput, maximumHistory]) {
|
|
21
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError("Invalid API conversation limit.");
|
|
22
|
+
}
|
|
23
|
+
const driver = Object.freeze({
|
|
24
|
+
admissionBeforeDispatch: true,
|
|
25
|
+
attachmentTypes: ["text", "image", "file"],
|
|
26
|
+
capabilities: Object.freeze({ streaming: true, cancellation: true, instructions: true,
|
|
27
|
+
configuration: true, history: true, steering: false, goals: false, attachments: true, tools: true, nativeTools: false, structuredOutput: false }),
|
|
28
|
+
open() { return { run: driver.run, dispose: async () => {} }; },
|
|
29
|
+
validateConfiguration(configuration) {
|
|
30
|
+
validateConversationConfiguration(configuration, { engine: "API", connections, apiClientFactory, connectionRequired: true,
|
|
31
|
+
efforts: ["none", "minimal", "low", "medium", "high", "xhigh"] });
|
|
32
|
+
},
|
|
33
|
+
async run({ configuration, context, input, history, requestHistory, continuity, tools, signal, accept, beforeDispatch, onMessage }) {
|
|
34
|
+
const { createAiConnectionClient } = await import("../../lib/aiConnectionClient.js");
|
|
35
|
+
const content = message => message.content?.length
|
|
36
|
+
? [...(message.text ? [{ type: "text", text: message.text }] : []), ...message.content] : message.text;
|
|
37
|
+
const messages = [
|
|
38
|
+
{ role: "system", content: configuration.systemPrompt },
|
|
39
|
+
...(continuity ? [{ role: "user", content: continuity }] : []),
|
|
40
|
+
...(requestHistory ?? history.flatMap(turn => [
|
|
41
|
+
...(turn.user ? [{ role: "user", content: content({ ...turn.user, text: conversationRequestText(turn.user) }) }] : []),
|
|
42
|
+
...(turn.system ? [{ role: "user", content: content({ ...turn.system, text: conversationRequestText({ ...turn.system, origin: "application" }) }) }] : []),
|
|
43
|
+
...(turn.metadata?.applicationTools || []).filter(call => call.result).flatMap(call => [
|
|
44
|
+
{ role: "assistant", content: "", tool_calls: [{ id: call.id, type: "function", function: { name: call.name, arguments: call.arguments } }] },
|
|
45
|
+
{ role: "tool", tool_call_id: call.id, content: toolResultContent(call.result) }
|
|
46
|
+
]),
|
|
47
|
+
...(turn.assistant ? [{ role: "assistant", content: turn.assistant.text }] : [])
|
|
48
|
+
])),
|
|
49
|
+
{ role: "user", content: content(input) }
|
|
50
|
+
];
|
|
51
|
+
let accepted = false;
|
|
52
|
+
let round = 0;
|
|
53
|
+
let responseId;
|
|
54
|
+
let responseIsCommentary = false;
|
|
55
|
+
async function prepareClient(inferenceMessages = messages) {
|
|
56
|
+
if (inferenceMessages.reduce((size, message) => size + (typeof message.content === "string" ? message.content.length
|
|
57
|
+
: message.content.reduce((count, part) => count + (part.type === "text" ? part.text.length : 0), 0)) +
|
|
58
|
+
(message.tool_calls ? JSON.stringify(message.tool_calls).length : 0), 0) > maximumHistory) {
|
|
59
|
+
throw new Error("This conversation exceeds the configured API history limit. Start another conversation or increase the limit.");
|
|
60
|
+
}
|
|
61
|
+
let client;
|
|
62
|
+
if (apiClientFactory) {
|
|
63
|
+
client = await apiClientFactory.resolveClient({ context, configuration });
|
|
64
|
+
if (!client?.enabled || typeof client.createChatCompletionStream !== "function") {
|
|
65
|
+
throw new Error("Assistant provider is not configured.");
|
|
66
|
+
}
|
|
67
|
+
validateConnectionModel(configuration, { model: client.defaultModel });
|
|
68
|
+
} else {
|
|
69
|
+
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
70
|
+
validateConnectionModel(configuration, connection);
|
|
71
|
+
client = createAiConnectionClient(connection, { fetch, effort: configuration.effort,
|
|
72
|
+
maxOutputTokens: limits.maxOutputTokens, timeoutMs: limits.timeoutMs });
|
|
73
|
+
}
|
|
74
|
+
signal.throwIfAborted();
|
|
75
|
+
await beforeDispatch();
|
|
76
|
+
if (!accepted) { await accept(); accepted = true; }
|
|
77
|
+
return client;
|
|
78
|
+
}
|
|
79
|
+
return runAssistantToolLoop({
|
|
80
|
+
messages, input: input.text, toolSet: { tools: tools?.descriptors || [] }, preserveWhitespace,
|
|
81
|
+
toToolSchema: tool => tools.schemas.find(schema => schema.function.name === tool.name),
|
|
82
|
+
async complete(messages, schemas) {
|
|
83
|
+
const id = responseId = `reply-${++round}`;
|
|
84
|
+
responseIsCommentary = false;
|
|
85
|
+
const client = await prepareClient(messages);
|
|
86
|
+
const completion = await consumeCompletionStream(
|
|
87
|
+
await client.createChatCompletionStream({ signal, messages, tools: schemas }),
|
|
88
|
+
async text => { if (text.trim()) await onMessage({ id, outputId: id, role: "assistant", text, complete: false }); },
|
|
89
|
+
{ signal, preserveWhitespace, requireCallIds: true, callIdPrefix: `${id}:`,
|
|
90
|
+
limits: { maxOutputCharacters: maximumOutput, maxToolCalls: tools ? limits.maxToolCalls ?? 32 : 0,
|
|
91
|
+
maxToolArgumentBytes: tools?.maximumArgumentBytes ?? 0 } }
|
|
92
|
+
);
|
|
93
|
+
const { toolCalls: calls, toolCallSource } = completion;
|
|
94
|
+
// Environment clients expose the provider's original OpenAI spelling;
|
|
95
|
+
// SDK connection clients use the normalized equivalent.
|
|
96
|
+
const finishReason = completion.finishReason === "tool_calls" ? "tool-calls" : completion.finishReason;
|
|
97
|
+
const text = sanitizeAssistantMessageText(completion.assistantText, { preserveWhitespace });
|
|
98
|
+
if (finishReason === "length") throw new Error("The model reached its output token limit before completing the answer. Increase maxOutputTokens in the conversation limits.");
|
|
99
|
+
if (calls.length) {
|
|
100
|
+
if (finishReason !== (toolCallSource === "text" ? "stop" : "tool-calls")) throw new Error("The model stream ended without confirming its application tool calls.");
|
|
101
|
+
} else if (finishReason !== "stop") {
|
|
102
|
+
throw new Error("The model stream ended without confirming a completed answer.");
|
|
103
|
+
}
|
|
104
|
+
if (text.trim() && (calls.length || isAssistantProgressOnlyText(text))) {
|
|
105
|
+
responseIsCommentary = true;
|
|
106
|
+
await onMessage({ id, outputId: id, role: "commentary", text, complete: true });
|
|
107
|
+
}
|
|
108
|
+
return completion;
|
|
109
|
+
},
|
|
110
|
+
async executeToolCalls(calls, { toolFailures, toolSuccesses }) {
|
|
111
|
+
// The original preflight runs before inference, but never before the
|
|
112
|
+
// same authorized connection and durable admission as ordinary sends.
|
|
113
|
+
if (!accepted) await prepareClient();
|
|
114
|
+
const failures = [];
|
|
115
|
+
for (const call of calls) {
|
|
116
|
+
const result = await tools.execute(call);
|
|
117
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: toolResultContent(result) });
|
|
118
|
+
if (result.ok) toolSuccesses.push({ name: call.name, result: result.result });
|
|
119
|
+
else {
|
|
120
|
+
const failure = { name: call.name, error: result.error };
|
|
121
|
+
failures.push(failure);
|
|
122
|
+
toolFailures.push(failure);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return failures;
|
|
126
|
+
},
|
|
127
|
+
async finish(text, { metadata = {} } = {}) {
|
|
128
|
+
const id = responseIsCommentary ? `reply-${++round}` : responseId;
|
|
129
|
+
await onMessage({ id, outputId: id, role: "assistant", text, complete: true });
|
|
130
|
+
return { metadata };
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return driver;
|
|
136
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createConversationSystemPrompt } from "../systemPrompt.js";
|
|
2
|
+
|
|
3
|
+
/** One instance per native Claude conversation; process ownership stays with the host. */
|
|
4
|
+
export function createClaudeConversationAdapter({ getProcess, isActive, startProcess, stopProcess } = {}) {
|
|
5
|
+
for (const callback of [getProcess, isActive, startProcess, stopProcess]) {
|
|
6
|
+
if (typeof callback !== "function") throw new TypeError("Claude requires process and activity callbacks.");
|
|
7
|
+
}
|
|
8
|
+
const instructions = createConversationSystemPrompt();
|
|
9
|
+
let pending = Promise.resolve();
|
|
10
|
+
let updateIdentity;
|
|
11
|
+
let installedMode;
|
|
12
|
+
|
|
13
|
+
function instructionArguments({ systemPrompt, instructionMode = "append" }) {
|
|
14
|
+
if (typeof systemPrompt !== "string" || !systemPrompt.trim()) throw new TypeError("Claude instructions must contain text.");
|
|
15
|
+
if (!["append", "replace"].includes(instructionMode)) throw new TypeError("Invalid instruction mode.");
|
|
16
|
+
// Claude otherwise restores its recorded system prompt on resume, ignoring
|
|
17
|
+
// changed prompt arguments until compaction. The host supplies current text.
|
|
18
|
+
return [instructionMode === "replace" ? "--system-prompt" : "--append-system-prompt", systemPrompt,
|
|
19
|
+
"--system-prompt-snapshot", "off"];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function prepare(input) {
|
|
23
|
+
// Check active work when this request reaches the front of the queue, so
|
|
24
|
+
// a queued change cannot stop a turn admitted during earlier preparation.
|
|
25
|
+
const operation = pending.catch(() => {}).then(async () => {
|
|
26
|
+
const { systemPrompt, settings, model, liveUpdateIdentity } = input;
|
|
27
|
+
const instructionMode = input.instructionMode || "append";
|
|
28
|
+
const contextIdentity = JSON.stringify([input.contextIdentity, instructionMode]);
|
|
29
|
+
if (!getProcess()) instructions.invalidate();
|
|
30
|
+
if (getProcess() && isActive() && !instructions.isCurrent({ systemPrompt, contextIdentity })) {
|
|
31
|
+
throw new Error("Stop the current Claude turn before changing its settings.");
|
|
32
|
+
}
|
|
33
|
+
return instructions.ensure({
|
|
34
|
+
systemPrompt,
|
|
35
|
+
contextIdentity,
|
|
36
|
+
install: async ({ promptChanged }) => {
|
|
37
|
+
let native = getProcess();
|
|
38
|
+
const canUpdate = native && !promptChanged && installedMode === instructionMode &&
|
|
39
|
+
liveUpdateIdentity !== undefined && updateIdentity === liveUpdateIdentity;
|
|
40
|
+
if (!canUpdate) {
|
|
41
|
+
// The host must prove owned cleanup before launching the successor.
|
|
42
|
+
await stopProcess("Claude's previous process was stopped before reconnecting.");
|
|
43
|
+
native = await startProcess({ ...input, instructionArguments: instructionArguments(input) });
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await native.client.request({ subtype: "apply_flag_settings", settings });
|
|
47
|
+
await native.client.request({ subtype: "set_model", model });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await stopProcess(error.message);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
updateIdentity = liveUpdateIdentity;
|
|
53
|
+
installedMode = instructionMode;
|
|
54
|
+
return native;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
pending = operation;
|
|
59
|
+
return operation;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Object.freeze({ prepare, instructionArguments, invalidate: instructions.invalidate, whenIdle: () => pending });
|
|
63
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { realpath } from "node:fs/promises";
|
|
5
|
+
import { bindClaudeConversationAccount, claudeFlagSettings, claudeModelConfiguration, readClaudeCodeAuthStatus } from "../claudeProcess.js";
|
|
6
|
+
import { nativeAiModel, nativeAiProvider } from "../../../shared/nativeProviders.js";
|
|
7
|
+
import { requireClaudeSessionId, listClaudeConversationStorage } from "../claudeHistory.js";
|
|
8
|
+
import { createClaudeConversationOwner, claudeNativeMessageId } from "../claudeTurn.js";
|
|
9
|
+
import { createLocalConversationExecution } from "../localExecution.js";
|
|
10
|
+
import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
|
|
11
|
+
|
|
12
|
+
const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
13
|
+
const nativeInput = input => input.content?.length ? [...(input.text ? [{ type: "text", text: input.text }] : []),
|
|
14
|
+
...input.content.map(part => part.type === "text" ? part : { type: "image",
|
|
15
|
+
source: { type: "base64", media_type: part.mediaType, data: part.image.toString("base64") } })] : input.text;
|
|
16
|
+
const claudeGoalValue = (goal, conversationId) => goal ? { ...goal,
|
|
17
|
+
id: hash([conversationId, goal.createdAt, goal.objective]),
|
|
18
|
+
...(Number.isFinite(goal.createdAt) ? { createdAt: new Date(goal.createdAt * 1000).toISOString() } : {}),
|
|
19
|
+
...(Number.isFinite(goal.updatedAt) ? { updatedAt: new Date(goal.updatedAt * 1000).toISOString() } : {})
|
|
20
|
+
} : null;
|
|
21
|
+
const goalCapabilities = Object.freeze({ goals: true, goalBudgets: false,
|
|
22
|
+
goalCommands: Object.freeze({
|
|
23
|
+
set: Object.freeze({ delivery: "message", interruptsTurn: false }),
|
|
24
|
+
resume: Object.freeze({ delivery: "message", interruptsTurn: false }),
|
|
25
|
+
pause: Object.freeze({ delivery: "control", interruptsTurn: true }),
|
|
26
|
+
cancel: Object.freeze({ delivery: "message", interruptsTurn: true })
|
|
27
|
+
}) });
|
|
28
|
+
|
|
29
|
+
/** The ordinary native host uses the CLI's own login; no application launch callbacks. */
|
|
30
|
+
export function createClaudeConversationDriver({ connections, host = {}, limits = {} } = {}) {
|
|
31
|
+
const env = { ...(host.env || process.env) };
|
|
32
|
+
const workdir = path.resolve(host.workdir || process.cwd());
|
|
33
|
+
const configRoot = path.resolve(workdir, env.CLAUDE_CONFIG_DIR || path.join(env.HOME || homedir(), ".claude"));
|
|
34
|
+
if (env.CLAUDE_CONFIG_DIR) env.CLAUDE_CONFIG_DIR = configRoot;
|
|
35
|
+
const command = host.commands?.claude || "claude";
|
|
36
|
+
const execution = host.execution || createLocalConversationExecution();
|
|
37
|
+
const nativeTools = host.nativeTools === true;
|
|
38
|
+
const pendingAccountStops = new Set();
|
|
39
|
+
const maximumOutput = limits.maxOutputCharacters ?? 64_000;
|
|
40
|
+
if (!Number.isSafeInteger(maximumOutput) || maximumOutput < 1) throw new TypeError("Invalid Claude output limit.");
|
|
41
|
+
|
|
42
|
+
async function stopAccount(id) {
|
|
43
|
+
pendingAccountStops.add(id);
|
|
44
|
+
try {
|
|
45
|
+
const proof = await execution.stop(id);
|
|
46
|
+
if (!proof?.scopeEmpty) throw new Error("Claude account process cleanup could not be confirmed.");
|
|
47
|
+
pendingAccountStops.delete(id);
|
|
48
|
+
} catch (error) { error.cleanupFailed = true; throw error; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function captureAccountStatus({ command, args, cwd, baseEnv, signal, timeout, maxBuffer }) {
|
|
52
|
+
signal.throwIfAborted();
|
|
53
|
+
const native = await execution.start({ command, args, cwd, env: baseEnv, stream: true, limits: host.limits });
|
|
54
|
+
const cancelled = Promise.withResolvers();
|
|
55
|
+
const abort = () => cancelled.reject(signal.reason);
|
|
56
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
57
|
+
if (signal.aborted) abort();
|
|
58
|
+
const result = (async () => {
|
|
59
|
+
native.stdin.end();
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
for await (const chunk of native.stdout) {
|
|
63
|
+
bytes += chunk.length;
|
|
64
|
+
if (bytes > maxBuffer) throw new Error("Claude account status exceeded its size limit.");
|
|
65
|
+
chunks.push(chunk);
|
|
66
|
+
}
|
|
67
|
+
const exit = await native.exited;
|
|
68
|
+
return { ok: !exit.signal && exit.code === 0, exitCode: exit.code,
|
|
69
|
+
signal: exit.signal, stdout: Buffer.concat(chunks).toString("utf8") };
|
|
70
|
+
})();
|
|
71
|
+
let timer;
|
|
72
|
+
try {
|
|
73
|
+
return await Promise.race([result, cancelled.promise, new Promise((_, reject) => {
|
|
74
|
+
timer = setTimeout(() => reject(new Error("Claude account status timed out.")), timeout);
|
|
75
|
+
})]);
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
signal.removeEventListener("abort", abort);
|
|
79
|
+
await stopAccount(native.id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function accountIdentity(signal) {
|
|
84
|
+
signal.throwIfAborted();
|
|
85
|
+
for (const id of pendingAccountStops) await stopAccount(id);
|
|
86
|
+
// Native API credentials already have an explicit identity. Never persist
|
|
87
|
+
// credentials or include them in configuration returned to the browser.
|
|
88
|
+
const key = env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY;
|
|
89
|
+
if (key) return hash([configRoot, env.ANTHROPIC_BASE_URL || "https://api.anthropic.com", key]);
|
|
90
|
+
const account = await readClaudeCodeAuthStatus({ command, env,
|
|
91
|
+
credentialHome: { home: env.HOME || homedir() }, commandRunner: captureAccountStatus, signal });
|
|
92
|
+
signal.throwIfAborted();
|
|
93
|
+
if (!account.loggedIn || !account.email) {
|
|
94
|
+
throw new Error(account.error || "Sign in with claude auth login before using this conversation.");
|
|
95
|
+
}
|
|
96
|
+
return hash([configRoot, account.authMethod, account.email.toLowerCase()]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let standalone;
|
|
100
|
+
function standaloneOwner() {
|
|
101
|
+
if (standalone) return standalone;
|
|
102
|
+
const store = {
|
|
103
|
+
read(context) { return JSON.stringify({ executionId: context.binding.executionId, sent: context.binding.sent }); },
|
|
104
|
+
async save(entry, { execution = false } = {}) {
|
|
105
|
+
if (execution) await entry.context.updateBinding({ executionId: entry.executionId });
|
|
106
|
+
},
|
|
107
|
+
async releaseExecution(entry) {
|
|
108
|
+
await entry.context.updateBinding({ executionId: "" });
|
|
109
|
+
entry.executionId = "";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
standalone = createClaudeConversationOwner({ configRoot, store,
|
|
113
|
+
preparation: {
|
|
114
|
+
async configuration(entry, { configuration, context, signal, tools }) {
|
|
115
|
+
const { selected, identity } = await bindAccount(entry, configuration, context, signal);
|
|
116
|
+
const settings = claudeFlagSettings({ effort: configuration.effort, providerEnv: selected.env });
|
|
117
|
+
return { systemPrompt: configuration.systemPrompt, instructionMode: "replace",
|
|
118
|
+
model: selected.model, settings, external: Boolean(configuration.integrationId),
|
|
119
|
+
contextIdentity: JSON.stringify([configuration, identity, Boolean(tools)]),
|
|
120
|
+
liveUpdateIdentity: JSON.stringify([Boolean(tools), configuration.outputSchema ?? null]),
|
|
121
|
+
outputSchema: configuration.outputSchema };
|
|
122
|
+
},
|
|
123
|
+
message: (_entry, input) => nativeInput(input)
|
|
124
|
+
},
|
|
125
|
+
process: {
|
|
126
|
+
stopExecution: (id, options) => execution.stop(id, options),
|
|
127
|
+
configure(entry, { instructionArguments, model, settings, external, outputSchema }) {
|
|
128
|
+
return { execution, command, env, model: external ? "" : model,
|
|
129
|
+
settings: claudeFlagSettings(), effort: settings.effortLevel || "", instructionArguments,
|
|
130
|
+
toolFree: !nativeTools, isolated: true, permissionMode: nativeTools ? "bypassPermissions" : undefined, outputSchema,
|
|
131
|
+
commandWrapper: host.commandWrapper, applicationTools: Boolean(entry.command.tools),
|
|
132
|
+
executionLimits: host.limits, signal: entry.command.signal };
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
async onEvent(entry, event) {
|
|
136
|
+
if (event.type === "save") return store.save(entry);
|
|
137
|
+
if (event.type === "binding-admitted") {
|
|
138
|
+
await entry.context.updateBinding({ sent: true });
|
|
139
|
+
entry.sent = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const current = entry.command;
|
|
143
|
+
if (!current) return;
|
|
144
|
+
if (event.type === "message") await current.onMessage({ ...event.message, transient: !event.message.complete });
|
|
145
|
+
else if (event.type === "message-complete") await current.onEvent(event);
|
|
146
|
+
else if ((event.type === "admitted" && current.goal) || event.type === "settled") {
|
|
147
|
+
await current.onEvent({ type: "goal", goal: claudeGoalValue(await entry.goals.read(), entry.id) });
|
|
148
|
+
} else if (event.type === "phase") await current.onEvent({ type: "phase", phase: event.phase === "compacting" ? "compacting" : "working" });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return standalone;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function bindAccount(entry, configuration, context, signal = AbortSignal.timeout(30_000)) {
|
|
155
|
+
let selected;
|
|
156
|
+
let providerId;
|
|
157
|
+
let identity;
|
|
158
|
+
if (configuration.integrationId) {
|
|
159
|
+
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
160
|
+
signal.throwIfAborted();
|
|
161
|
+
providerId = connection.providerId;
|
|
162
|
+
const provider = nativeAiProvider(providerId);
|
|
163
|
+
const model = nativeAiModel(connection.model, providerId);
|
|
164
|
+
if (providerId !== "anthropic" && (!provider || !model)) throw new Error("This exact provider/model is not supported by the Claude integration. Choose a supported model without renaming it.");
|
|
165
|
+
validateConnectionModel(configuration, connection);
|
|
166
|
+
if (model && configuration.effort && !model.variants.includes(configuration.effort)) throw new Error("The selected Claude model does not support this reasoning effort.");
|
|
167
|
+
selected = claudeModelConfiguration({ providerId, model: connection.model }, {
|
|
168
|
+
apiKey: connection.apiKey, baseUrl: provider?.claudeBaseUrl || connection.baseURL || "https://api.anthropic.com"
|
|
169
|
+
});
|
|
170
|
+
identity = hash([configRoot, providerId, selected.env.ANTHROPIC_BASE_URL, connection.apiKey]);
|
|
171
|
+
} else {
|
|
172
|
+
identity = await accountIdentity(signal);
|
|
173
|
+
selected = claudeModelConfiguration({ providerId: "anthropic", model: configuration.model || "default" });
|
|
174
|
+
// Returning to the native host clears connection overrides, while
|
|
175
|
+
// retaining explicitly supplied standalone host credentials/routing.
|
|
176
|
+
for (const name of Object.keys(selected.env)) if (Object.hasOwn(env, name)) selected.env[name] = env[name];
|
|
177
|
+
}
|
|
178
|
+
signal.throwIfAborted();
|
|
179
|
+
const previous = providerId ? entry.context.binding.connectionIdentities?.[providerId] : entry.context.binding.accountIdentity;
|
|
180
|
+
await bindClaudeConversationAccount({ accountIdentity: previous,
|
|
181
|
+
accountIdentities: { [providerId || "anthropic"]: previous }
|
|
182
|
+
}, { identity, providerId: providerId || "anthropic",
|
|
183
|
+
stop: (_entry, reason) => entry.nativeTurn.stopProcess(reason),
|
|
184
|
+
save: account => entry.context.updateBinding(providerId
|
|
185
|
+
? { connectionIdentities: { ...entry.context.binding.connectionIdentities, [providerId]: account.accountIdentity } }
|
|
186
|
+
: { accountIdentity: account.accountIdentity })
|
|
187
|
+
});
|
|
188
|
+
return { selected, identity };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function disposeNative({ native, options }) {
|
|
192
|
+
const prepared = native.preparation.cleanup(options);
|
|
193
|
+
return native.owner.closeSession(prepared.context, prepared.application);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
admissionBeforeDispatch: false,
|
|
198
|
+
toolDiscovery: true,
|
|
199
|
+
attachmentTypes: ["text", "image"],
|
|
200
|
+
capabilities: Object.freeze({ streaming: true, cancellation: true, instructions: true,
|
|
201
|
+
configuration: true, history: true, steering: true, ...goalCapabilities,
|
|
202
|
+
attachments: true, tools: true, nativeTools, structuredOutput: true }),
|
|
203
|
+
validateConfiguration(configuration) {
|
|
204
|
+
validateConversationConfiguration(configuration, { engine: "Claude", connections, efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
205
|
+
structuredOutput: true, maxOutputCharacters: maximumOutput });
|
|
206
|
+
},
|
|
207
|
+
async readState({ conversation, context, representation }) {
|
|
208
|
+
let nativeResult = null;
|
|
209
|
+
if (representation === "native" || context) {
|
|
210
|
+
const { owner } = conversation.native;
|
|
211
|
+
const entry = await owner.acquire(context);
|
|
212
|
+
nativeResult = { ok: true, thread: { id: entry.id }, turn: owner.snapshot(entry), workdir: entry.context.workdir };
|
|
213
|
+
}
|
|
214
|
+
return conversation.read(context, representation, nativeResult);
|
|
215
|
+
},
|
|
216
|
+
inspectTemporaryActivity({ native, context }) {
|
|
217
|
+
return native.owner.hasActiveTemporaryConversation(context, { acquire: native.owner.acquire });
|
|
218
|
+
},
|
|
219
|
+
releaseRenewalPredecessorProcessExitProof({ native, context }) {
|
|
220
|
+
return native.owner.closeSession(context, native.application);
|
|
221
|
+
},
|
|
222
|
+
releaseRenewalSuccessorProcessExitProof({ native, context }) {
|
|
223
|
+
return native.owner.closeSession(context, native.application);
|
|
224
|
+
},
|
|
225
|
+
async generateRenewalHandover({ native, input, context }) {
|
|
226
|
+
const prepared = native.preparation.handover(input, context);
|
|
227
|
+
const entry = await native.owner.acquire(context);
|
|
228
|
+
const result = await native.owner.renewal(entry, prepared.input, prepared.options, { context, acquire: native.owner.acquire });
|
|
229
|
+
return prepared.completeResult(result);
|
|
230
|
+
},
|
|
231
|
+
async seedRenewalHandover({ native, input, context }) {
|
|
232
|
+
const prepared = native.preparation.seed(input, context);
|
|
233
|
+
const entry = await native.owner.acquire(context);
|
|
234
|
+
const result = await native.owner.renewal(entry, prepared.input, prepared.options, { context, acquire: native.owner.acquire });
|
|
235
|
+
return prepared.completeResult(result);
|
|
236
|
+
},
|
|
237
|
+
async ensureConversation({ native, context }) {
|
|
238
|
+
const entry = await native.owner.acquire(context);
|
|
239
|
+
return native.owner.ensureReady(entry);
|
|
240
|
+
},
|
|
241
|
+
disposeNative,
|
|
242
|
+
async interruptDetachedConversation({ native, input, context }) {
|
|
243
|
+
const entry = await native.owner.acquire(context, input.conversationId || input.threadId,
|
|
244
|
+
{ cleanupExecutionId: input.cleanupExecutionId });
|
|
245
|
+
return native.owner.stopConversation(entry);
|
|
246
|
+
},
|
|
247
|
+
async deleteDetachedConversation({ native, input, context }) {
|
|
248
|
+
const entry = await native.owner.acquire(context, input.conversationId || input.threadId,
|
|
249
|
+
{ operation: "delete", input, cleanupExecutionId: input.cleanupExecutionId });
|
|
250
|
+
return native.owner.deleteConversation(entry);
|
|
251
|
+
},
|
|
252
|
+
listConversationStorage({ native, binding }) {
|
|
253
|
+
return listClaudeConversationStorage({ configRoot: native.configRoot, binding });
|
|
254
|
+
},
|
|
255
|
+
retireConversationHistory({ native, binding, context }) {
|
|
256
|
+
return native.owner.retireConversationHistory(native.preparation.retirement(binding, context));
|
|
257
|
+
},
|
|
258
|
+
closeProject({ native, input }) {
|
|
259
|
+
return native.owner.closeProject(input, native.application);
|
|
260
|
+
},
|
|
261
|
+
invalidateRuntimes({ native, input }) {
|
|
262
|
+
return native.owner.invalidateRuntimes(input, native.application, native.account);
|
|
263
|
+
},
|
|
264
|
+
reconcileSessions({ native, sessions, options }) {
|
|
265
|
+
return native.owner.reconcileSessions(sessions, options, native.application);
|
|
266
|
+
},
|
|
267
|
+
async createConversation({ native, input, context }) {
|
|
268
|
+
return native.owner.createConversation(input, { context, acquire: native.owner.acquire });
|
|
269
|
+
},
|
|
270
|
+
async createBinding() {
|
|
271
|
+
return { conversationId: randomUUID(), workdir: await realpath(workdir), configRoot, executionId: "", sent: false };
|
|
272
|
+
},
|
|
273
|
+
async open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
|
|
274
|
+
const supplied = conversation?.native;
|
|
275
|
+
const scoped = supplied?.scoped;
|
|
276
|
+
let owner = supplied?.owner;
|
|
277
|
+
let entry;
|
|
278
|
+
if (!supplied) {
|
|
279
|
+
requireClaudeSessionId(binding?.conversationId);
|
|
280
|
+
if (binding.workdir !== await realpath(workdir) || binding.configRoot !== configRoot) {
|
|
281
|
+
throw new Error("This Claude conversation belongs to another working directory or credential home. Restore its original host configuration.");
|
|
282
|
+
}
|
|
283
|
+
owner = standaloneOwner();
|
|
284
|
+
const context = { key: binding.workdir, workdir: binding.workdir, binding, reportFailure,
|
|
285
|
+
async updateBinding(patch) {
|
|
286
|
+
const next = { ...context.binding, ...patch };
|
|
287
|
+
await writeBinding(next);
|
|
288
|
+
context.binding = next;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
entry = await owner.open(context, { conversationId: binding.conversationId, mainId: binding.conversationId, create: true });
|
|
292
|
+
// An unknown execution is never reclaimed by PID or silently abandoned.
|
|
293
|
+
// Managed hosts can prove cleanup using their durable execution identity.
|
|
294
|
+
if (binding.executionId) await entry.nativeTurn.stopProcess("The application reconnected to this conversation.");
|
|
295
|
+
}
|
|
296
|
+
const acquire = supplied ? owner.acquire : (() => entry);
|
|
297
|
+
const control = { current: null, disposed: false, cleanupFailure: null };
|
|
298
|
+
const commandOptions = { acquire, reportFailure, entry,
|
|
299
|
+
maximumOutput, maximumToolCalls: limits.maxToolCalls ?? 32, timeoutMs: limits.admissionTimeoutMs };
|
|
300
|
+
const scopedInput = (input = {}) => {
|
|
301
|
+
if (input.conversationId && String(input.conversationId).trim() !== scoped.conversationId) {
|
|
302
|
+
throw new TypeError("This operation belongs to a different scoped conversation.");
|
|
303
|
+
}
|
|
304
|
+
return { ...input, conversationId: scoped.conversationId };
|
|
305
|
+
};
|
|
306
|
+
const unsupported = () => { throw Object.assign(new Error("This original scoped conversation has no canonical message or goal receipt."), {
|
|
307
|
+
code: "conversation_unsupported"
|
|
308
|
+
}); };
|
|
309
|
+
return Object.freeze({
|
|
310
|
+
...(scoped ? { capabilities: Object.freeze({ streaming: false, instructions: false, configuration: false,
|
|
311
|
+
goals: false, goalBudgets: false, goalCommands: Object.freeze({}), attachments: false, tools: false, nativeTools: false }),
|
|
312
|
+
async sendNative({ input, context }) {
|
|
313
|
+
input = scopedInput(input);
|
|
314
|
+
return owner.startTurn(await acquire(context, input.conversationId, { operation: "start", input }), input);
|
|
315
|
+
},
|
|
316
|
+
async readNative({ input, context }) {
|
|
317
|
+
input = scopedInput(input);
|
|
318
|
+
return owner.read(await acquire(context, input.conversationId), input);
|
|
319
|
+
},
|
|
320
|
+
async waitNative({ input, context }) {
|
|
321
|
+
input = scopedInput(input);
|
|
322
|
+
return owner.wait(await acquire(context, input.conversationId), input, { context, acquire });
|
|
323
|
+
}
|
|
324
|
+
} : {}),
|
|
325
|
+
...(supplied && !scoped ? {
|
|
326
|
+
publishNative: event => conversation.publish({ ...event, goalChanged: event.reason === "claude-goal" })
|
|
327
|
+
} : {}),
|
|
328
|
+
async readGoal({ configuration, context }) {
|
|
329
|
+
if (scoped) return unsupported();
|
|
330
|
+
if (supplied) {
|
|
331
|
+
await acquire(context);
|
|
332
|
+
const completeResult = supplied.completeResult?.(context);
|
|
333
|
+
const nativeResult = await owner.readGoal(await acquire(context));
|
|
334
|
+
await completeResult?.(nativeResult, {
|
|
335
|
+
segmentId: nativeResult.threadId ? `claude:${nativeResult.threadId}` : null, capabilities: goalCapabilities
|
|
336
|
+
});
|
|
337
|
+
return { nativeResult, goal: claudeGoalValue(nativeResult.goal, nativeResult.threadId) };
|
|
338
|
+
}
|
|
339
|
+
await bindAccount(entry, configuration, context);
|
|
340
|
+
return claudeGoalValue(await entry.goals.read(), entry.id);
|
|
341
|
+
},
|
|
342
|
+
async updateGoal({ configuration, context, input, representation, interrupt, send }) {
|
|
343
|
+
if (scoped) return unsupported();
|
|
344
|
+
if (supplied) {
|
|
345
|
+
await acquire(context);
|
|
346
|
+
const completeResult = supplied.completeResult?.(context);
|
|
347
|
+
let command = input;
|
|
348
|
+
let facilities;
|
|
349
|
+
if (representation !== "native") {
|
|
350
|
+
const current = await owner.readGoal(await acquire(context));
|
|
351
|
+
if (input.action !== "set" && claudeGoalValue(current.goal, current.threadId)?.id !== input.expectedGoalId) {
|
|
352
|
+
throw new Error("The Claude goal changed. Refresh before trying again.");
|
|
353
|
+
}
|
|
354
|
+
command = { ...input, threadId: current.threadId,
|
|
355
|
+
...(input.action !== "set" ? { createdAt: current.goal.createdAt, objective: current.goal.objective } : {}) };
|
|
356
|
+
facilities = { interrupt, send: command => send(command, { threadId: current.threadId }) };
|
|
357
|
+
}
|
|
358
|
+
const nativeResult = await owner.updateGoal(await acquire(context), command, facilities);
|
|
359
|
+
await completeResult?.(nativeResult);
|
|
360
|
+
return { nativeResult, goal: claudeGoalValue(nativeResult.goal, nativeResult.threadId) };
|
|
361
|
+
}
|
|
362
|
+
await bindAccount(entry, configuration, context);
|
|
363
|
+
const goal = await entry.goals.read();
|
|
364
|
+
if (input.action !== "set" && claudeGoalValue(goal, entry.id)?.id !== input.expectedGoalId) {
|
|
365
|
+
throw new Error("The Claude goal changed. Refresh before trying again.");
|
|
366
|
+
}
|
|
367
|
+
await entry.goals.update({ ...input, threadId: entry.id,
|
|
368
|
+
...(input.action !== "set" ? { createdAt: goal?.createdAt, objective: goal?.objective } : {}) }, { interrupt, send });
|
|
369
|
+
return claudeGoalValue(await entry.goals.read(), entry.id);
|
|
370
|
+
},
|
|
371
|
+
async inspectAdmission({ context, representation, ...input }) {
|
|
372
|
+
if (scoped) return unsupported();
|
|
373
|
+
if (supplied) {
|
|
374
|
+
await acquire(context);
|
|
375
|
+
const result = await owner.inspectAdmission(await acquire(context), input);
|
|
376
|
+
return representation === "native" ? result : { accepted: result.admission === "accepted" };
|
|
377
|
+
}
|
|
378
|
+
const history = await owner.readHistory(entry);
|
|
379
|
+
const nativeId = claudeNativeMessageId(input.messageId);
|
|
380
|
+
return { accepted: history.userIds.includes(nativeId),
|
|
381
|
+
messages: history.messages.filter(message => message.userId === nativeId) };
|
|
382
|
+
},
|
|
383
|
+
steer: command => owner.steer(control, command, commandOptions),
|
|
384
|
+
async run(command) {
|
|
385
|
+
if (entry && (entry.disposed || entry.command)) throw new Error("This Claude conversation is closed or already working.");
|
|
386
|
+
return owner.run(control, command, commandOptions);
|
|
387
|
+
},
|
|
388
|
+
async cancel({ input, context, representation } = {}) {
|
|
389
|
+
if (scoped) {
|
|
390
|
+
input = scopedInput(input);
|
|
391
|
+
return owner.stopConversation(await acquire(context, input.conversationId, { cleanupExecutionId: input.cleanupExecutionId }));
|
|
392
|
+
}
|
|
393
|
+
if (!supplied) for (const id of pendingAccountStops) await stopAccount(id);
|
|
394
|
+
const value = await owner.cancel(control, { context, acquire, reportFailure, entry });
|
|
395
|
+
return supplied && representation === "native" ? { value } : value;
|
|
396
|
+
},
|
|
397
|
+
async dispose(options) {
|
|
398
|
+
try {
|
|
399
|
+
let result;
|
|
400
|
+
if (scoped && options) {
|
|
401
|
+
const input = scopedInput(options.input);
|
|
402
|
+
result = await owner.deleteConversation(await acquire(options.context, input.conversationId, {
|
|
403
|
+
operation: "delete", input, cleanupExecutionId: input.cleanupExecutionId }));
|
|
404
|
+
} else if (supplied) {
|
|
405
|
+
result = await disposeNative({ native: supplied, options: scoped ? scoped.context : options });
|
|
406
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || (scoped ? "Scoped cleanup could not be confirmed." : "Claude cleanup could not be confirmed.")), result);
|
|
407
|
+
} else {
|
|
408
|
+
await entry.nativeTurn.whenIdle().catch(() => {});
|
|
409
|
+
for (const id of pendingAccountStops) await stopAccount(id);
|
|
410
|
+
await owner.close(entry);
|
|
411
|
+
}
|
|
412
|
+
control.disposed = true;
|
|
413
|
+
return result;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (supplied && !scoped) {
|
|
416
|
+
error.cleanupFailed = true;
|
|
417
|
+
control.current?.failure.reject(error);
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|