@jskit-ai/assistant-core 0.2.7 → 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 +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,227 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { isCompletedEnvelopeTurn } from "./transcript.js";
|
|
3
|
+
import { conversationNativeIdentity } from "./runtimeStateUpgrade.js";
|
|
4
|
+
import { createConversationTools } from "./tools.js";
|
|
5
|
+
import { readAssistantResponseEnvelope } from "./boundedToolLoop.js";
|
|
6
|
+
import { conversationMessageIdentity, conversationMessageVersion } from "./continuity.js";
|
|
7
|
+
import { failure } from "./conversationRefusals.js";
|
|
8
|
+
import { submittedMessage } from "./conversationReads.js";
|
|
9
|
+
import { saveTools } from "./conversationTurns.js";
|
|
10
|
+
import { serial } from "./conversationEvents.js";
|
|
11
|
+
|
|
12
|
+
/** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A completed native response handed to the application worker: its verified text, and
|
|
16
|
+
* either the plain reply it publishes or the one application operation it runs.
|
|
17
|
+
* @param {object} dependencies
|
|
18
|
+
* @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
|
|
19
|
+
* @param {import("../types.js").ConversationTranscript | null} dependencies.transcript
|
|
20
|
+
* @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
|
|
21
|
+
* @param {import("../types.js").ServiceToolCatalog | null} dependencies.catalog
|
|
22
|
+
* @param {() => boolean} dependencies.isClosed Whether the runtime has closed.
|
|
23
|
+
* @param {boolean} dependencies.completedEnvelope
|
|
24
|
+
* @param {number} dependencies.maximumToolCalls
|
|
25
|
+
*/
|
|
26
|
+
function createCompletedResponses({ storage, transcript, events, catalog, isClosed, completedEnvelope, maximumToolCalls }) {
|
|
27
|
+
/**
|
|
28
|
+
* @param {ConversationEntry} entry
|
|
29
|
+
* @param {object} context
|
|
30
|
+
* @param {{ messageId: string, turnId: string }} receipt
|
|
31
|
+
* @param {{ signal: AbortSignal, toolSet?: object }} options
|
|
32
|
+
*/
|
|
33
|
+
async function prepareCompletedResponse(entry, context, { messageId, turnId }, { signal, toolSet }) {
|
|
34
|
+
if (!completedEnvelope || entry.conversation || !["codex", "claude", "opencode"].includes(entry.engine)) {
|
|
35
|
+
throw failure("This conversation does not own completed envelopes.", "conversation_completed_envelopes_not_owned", 400, { messageCode: "assistant.conversation.completedEnvelopesNotOwned" });
|
|
36
|
+
}
|
|
37
|
+
if (typeof messageId !== "string" || !messageId || typeof turnId !== "string" || !turnId || !(signal instanceof AbortSignal)) {
|
|
38
|
+
throw new TypeError("Completed responses require their exact receipt and the application worker signal.");
|
|
39
|
+
}
|
|
40
|
+
signal.throwIfAborted();
|
|
41
|
+
await events.access(context, entry.id, "tool", entry);
|
|
42
|
+
const owner = entry.bindingOwner;
|
|
43
|
+
const segmentId = entry.segmentId;
|
|
44
|
+
const engine = entry.engine;
|
|
45
|
+
const toolCallId = `${messageId}:operation`;
|
|
46
|
+
if (toolCallId.length > 256) throw new TypeError("The completed response call identity exceeds its limit.");
|
|
47
|
+
let captured;
|
|
48
|
+
|
|
49
|
+
async function exact(transaction, { retry = false } = {}) {
|
|
50
|
+
if (isClosed() || entry.disposed || entry.bindingOwner !== owner || entry.segmentId !== segmentId || entry.engine !== engine) {
|
|
51
|
+
throw failure("The completed response owner changed.", "conversation_response_owner_changed", 409, { messageCode: "assistant.conversation.responseOwnerChanged" });
|
|
52
|
+
}
|
|
53
|
+
if (entry.active) throw failure("Wait for the native response to finish.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForNativeResponse" });
|
|
54
|
+
if (entry.executionFailure) throw entry.executionFailure;
|
|
55
|
+
if (entry.storageFailure && !retry) throw entry.storageFailure;
|
|
56
|
+
const { runtime } = await transaction.readMetadata();
|
|
57
|
+
const turn = await transaction.readTurn(turnId);
|
|
58
|
+
const authored = submittedMessage(turn || {});
|
|
59
|
+
const saved = turn?.metadata?.runtime;
|
|
60
|
+
if (runtime?.version !== 3 || runtime.engine !== engine || runtime.segmentId !== segmentId ||
|
|
61
|
+
runtime.replacement || runtime.request || !conversationNativeIdentity(runtime) || authored?.messageId !== messageId ||
|
|
62
|
+
authored.origin !== "application" || saved?.completedEnvelope !== true || saved.engine !== engine ||
|
|
63
|
+
saved.segmentId !== segmentId || saved.status !== "complete" || saved.supersededBy ||
|
|
64
|
+
typeof turn.assistant?.text !== "string" || !turn.assistant.text || !turn.assistant.messageId) {
|
|
65
|
+
throw failure("The completed response has no current verified receipt.", "conversation_completed_response_no_receipt", 409, { messageCode: "assistant.conversation.completedResponseNoReceipt" });
|
|
66
|
+
}
|
|
67
|
+
const turnIds = await transaction.listTurnIds();
|
|
68
|
+
for (const id of turnIds.slice(turnIds.indexOf(turnId) + 1)) {
|
|
69
|
+
const later = await transaction.readTurn(id);
|
|
70
|
+
if (later.metadata?.runtime?.segmentId === segmentId && later.metadata.runtime.completedEnvelope === true) {
|
|
71
|
+
throw failure("A newer completed response owns this worker.", "conversation_newer_response_owns_worker", 409, { messageCode: "assistant.conversation.newerResponseOwnsWorker" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// These verified process references may refresh on cold inspection;
|
|
75
|
+
// all native conversation, path and account custody remains exact.
|
|
76
|
+
const { codexAppServerRun: run, executionId, processDirectory, ...binding } = runtime.binding;
|
|
77
|
+
if (engine === "codex" && (!saved.nativeTurnId || run?.outerTurnId !== messageId ||
|
|
78
|
+
run.providerThreadId !== binding.threadId || run.providerTurnId !== saved.nativeTurnId || run.active || run.state !== "completed")) {
|
|
79
|
+
throw failure("The completed response has no exact native turn receipt.", "conversation_completed_response_no_turn_receipt", 409, { messageCode: "assistant.conversation.completedResponseNoTurnReceipt" });
|
|
80
|
+
}
|
|
81
|
+
if (engine === "claude" && typeof saved.nativeToolAttempt !== "boolean") {
|
|
82
|
+
throw failure("The completed Claude response has no native tool-attempt evidence.", "conversation_completed_claude_no_tool_evidence", 409, { messageCode: "assistant.conversation.completedClaudeNoToolEvidence" });
|
|
83
|
+
}
|
|
84
|
+
const calls = turn.metadata.applicationTools || [];
|
|
85
|
+
if (!Array.isArray(calls) || calls.length > 1 || calls.some(call => call.id !== toolCallId)) {
|
|
86
|
+
throw failure("This response already belongs to a different application operation.", "conversation_response_other_operation", 409, { messageCode: "assistant.conversation.responseOtherOperation" });
|
|
87
|
+
}
|
|
88
|
+
const identity = { configuration: runtime.configuration, binding, assistant: turn.assistant,
|
|
89
|
+
nativeTurnId: saved.nativeTurnId, ...(engine === "claude" ? { nativeToolAttempt: saved.nativeToolAttempt } : {}) };
|
|
90
|
+
if (captured && !isDeepStrictEqual(captured, identity)) {
|
|
91
|
+
throw failure("The completed response identity changed.", "conversation_response_identity_changed", 409, { messageCode: "assistant.conversation.responseIdentityChanged" });
|
|
92
|
+
}
|
|
93
|
+
return { runtime, turn, calls, identity };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function inspect() {
|
|
97
|
+
signal.throwIfAborted();
|
|
98
|
+
await events.access(context, entry.id, "tool", entry);
|
|
99
|
+
const saved = await storage.read(entry.id, transaction => exact(transaction));
|
|
100
|
+
const observation = await entry.provider.inspectAdmission({ messageId,
|
|
101
|
+
threadId: conversationNativeIdentity(saved.runtime), nativeTurnId: saved.turn.metadata.runtime.nativeTurnId,
|
|
102
|
+
configuration: saved.runtime.configuration, receiptBinding: saved.runtime.binding, context, verifyAccount: true });
|
|
103
|
+
signal.throwIfAborted();
|
|
104
|
+
await events.access(context, entry.id, "tool", entry);
|
|
105
|
+
if (!observation?.accepted || observation.recoveryLimitation || observation.conversationTurn &&
|
|
106
|
+
observation.conversationTurn.turnId !== turnId || engine === "codex" && observation.nativeTurnId !== saved.turn.metadata.runtime.nativeTurnId) {
|
|
107
|
+
throw failure("The native owner could not verify this response association.", "conversation_response_association_unverified", 409, { messageCode: "assistant.conversation.responseAssociationUnverified" });
|
|
108
|
+
}
|
|
109
|
+
// Canonical successful completion owns the exact final, including a
|
|
110
|
+
// StructuredOutput carrier whose native history may contain only commentary.
|
|
111
|
+
captured ||= structuredClone(saved.identity);
|
|
112
|
+
return storage.read(entry.id, transaction => exact(transaction));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const response = await inspect();
|
|
116
|
+
return { text: response.turn.assistant.text, toolCallId,
|
|
117
|
+
...(engine === "claude" ? { nativeToolAttempt: response.turn.metadata.runtime.nativeToolAttempt } : {}),
|
|
118
|
+
publishReply(input) {
|
|
119
|
+
return serial(entry, async () => {
|
|
120
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some(key => key !== "turnId") ||
|
|
121
|
+
typeof input.turnId !== "string" || !input.turnId || input.turnId !== input.turnId.trim() || input.turnId.length > 128) {
|
|
122
|
+
throw new TypeError("Reply publication requires only its exact existing plain turn id.");
|
|
123
|
+
}
|
|
124
|
+
await inspect();
|
|
125
|
+
return storage.write(entry.id, async transaction => {
|
|
126
|
+
const saved = await exact(transaction);
|
|
127
|
+
signal.throwIfAborted();
|
|
128
|
+
const decoded = readAssistantResponseEnvelope(saved.turn.assistant.text);
|
|
129
|
+
if (decoded.kind !== "reply" || saved.turn.metadata.runtime.nativeToolAttempt === true || saved.calls.length) {
|
|
130
|
+
throw failure("This completed response does not own a plain reply.", "conversation_completed_response_no_plain_reply", 409, { messageCode: "assistant.conversation.completedResponseNoPlainReply" });
|
|
131
|
+
}
|
|
132
|
+
const text = decoded.text.trim();
|
|
133
|
+
const target = await transaction.readTurn(input.turnId);
|
|
134
|
+
if (!text || input.turnId === turnId || !target || isCompletedEnvelopeTurn(target) ||
|
|
135
|
+
target.metadata?.runtime?.engine || !(target.user || target.system) || saved.runtime.lastEngine !== engine ||
|
|
136
|
+
!saved.runtime.seen || typeof saved.runtime.seen !== "object" || Array.isArray(saved.runtime.seen)) {
|
|
137
|
+
throw failure("The completed reply has no current plain authored target.", "conversation_completed_reply_no_target", 409, { messageCode: "assistant.conversation.completedReplyNoTarget" });
|
|
138
|
+
}
|
|
139
|
+
const assistantId = conversationMessageIdentity(input.turnId, { role: "assistant" });
|
|
140
|
+
const published = saved.turn.metadata.runtime.publishedReplyTurnId;
|
|
141
|
+
if (published !== undefined) {
|
|
142
|
+
if (published !== input.turnId || !target.assistant || target.assistant.text !== text ||
|
|
143
|
+
saved.runtime.seen[assistantId] !== conversationMessageVersion(target.assistant, { includeData: true })) {
|
|
144
|
+
throw failure("The published reply target or text changed.", "conversation_published_reply_changed", 409, { messageCode: "assistant.conversation.publishedReplyChanged" });
|
|
145
|
+
}
|
|
146
|
+
signal.throwIfAborted();
|
|
147
|
+
return target;
|
|
148
|
+
}
|
|
149
|
+
if (target.assistant || Object.hasOwn(saved.runtime.seen, assistantId)) {
|
|
150
|
+
throw failure("This plain reply target already has unassociated content or consumption.", "conversation_reply_target_used", 409, { messageCode: "assistant.conversation.replyTargetUsed" });
|
|
151
|
+
}
|
|
152
|
+
// The runtime transcript uses this same reentrant wrapper: its
|
|
153
|
+
// original upsert joins this draft rather than starting a second write.
|
|
154
|
+
const plain = await transcript.upsertConversationAssistantMessage(entry.id, { turnId: input.turnId, text });
|
|
155
|
+
saved.runtime.seen[assistantId] = conversationMessageVersion(plain.assistant, { includeData: true });
|
|
156
|
+
await transaction.updateTurnMetadata(turnId, { runtime: { ...saved.turn.metadata.runtime, publishedReplyTurnId: input.turnId } });
|
|
157
|
+
const metadata = await transaction.readMetadata();
|
|
158
|
+
await transaction.writeMetadata({ ...metadata, runtime: saved.runtime });
|
|
159
|
+
await exact(transaction);
|
|
160
|
+
signal.throwIfAborted();
|
|
161
|
+
return plain;
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
tools: { execute(input, options = {}) {
|
|
166
|
+
return serial(entry, async () => {
|
|
167
|
+
if (input?.id !== toolCallId) throw failure("The operation has a different completed response identity.", "conversation_operation_response_identity", 409, { messageCode: "assistant.conversation.operationResponseIdentity" });
|
|
168
|
+
const saved = await inspect();
|
|
169
|
+
if (saved.turn.metadata.runtime.publishedReplyTurnId !== undefined) {
|
|
170
|
+
throw failure("This completed response already published its plain reply.", "conversation_plain_reply_published", 409, { messageCode: "assistant.conversation.plainReplyPublished" });
|
|
171
|
+
}
|
|
172
|
+
const workerSignal = options.signal && options.signal !== signal ? AbortSignal.any([signal, options.signal]) : signal;
|
|
173
|
+
workerSignal.throwIfAborted();
|
|
174
|
+
const request = { turnId, input: { messageId } };
|
|
175
|
+
async function persist(calls, retry = false) {
|
|
176
|
+
await storage.write(entry.id, async transaction => {
|
|
177
|
+
const current = await exact(transaction, { retry });
|
|
178
|
+
const previous = current.calls[0];
|
|
179
|
+
const next = calls[0];
|
|
180
|
+
if (calls.length !== 1 || next?.id !== toolCallId || previous &&
|
|
181
|
+
(previous.name !== next.name || previous.arguments !== next.arguments ||
|
|
182
|
+
previous.status !== "running" && next.status === "running")) {
|
|
183
|
+
throw failure("The saved response operation changed.", "conversation_saved_operation_changed", 409, { messageCode: "assistant.conversation.savedOperationChanged" });
|
|
184
|
+
}
|
|
185
|
+
await saveTools(entry, transaction, request, calls);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const tools = createConversationTools({ catalog, toolSet, context, signal: workerSignal, previousCalls: saved.calls,
|
|
189
|
+
maximumCalls: maximumToolCalls, discoveryOnly: false,
|
|
190
|
+
authorize: () => events.access(context, entry.id, "tool", entry),
|
|
191
|
+
prepareContext: async () => {
|
|
192
|
+
await storage.read(entry.id, transaction => exact(transaction));
|
|
193
|
+
return context;
|
|
194
|
+
},
|
|
195
|
+
async save(calls) {
|
|
196
|
+
try { await persist(calls); }
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (calls[0]?.status !== "running") {
|
|
199
|
+
const snapshot = structuredClone(calls);
|
|
200
|
+
entry.error = "The application operation result could not be saved. Restore storage access and retry saving.";
|
|
201
|
+
entry.storageFailure = failure(entry.error, "conversation_storage_unavailable", 503);
|
|
202
|
+
entry.storageFailure.cause = error;
|
|
203
|
+
entry.saveProgress = async () => {
|
|
204
|
+
await persist(snapshot, true);
|
|
205
|
+
entry.storageFailure = null;
|
|
206
|
+
entry.saveProgress = null;
|
|
207
|
+
entry.error = "";
|
|
208
|
+
return { type: "tool", turnId, call: snapshot[0] };
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
emit: event => events.emit(entry, { ...event, turnId }) });
|
|
215
|
+
return tools.execute(input, { signal: workerSignal });
|
|
216
|
+
});
|
|
217
|
+
} } };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
prepareCompletedResponse
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @typedef {ReturnType<typeof createCompletedResponses>} CompletedResponses */
|
|
226
|
+
|
|
227
|
+
export { createCompletedResponses };
|
|
@@ -23,8 +23,8 @@ export function validateConversationConfiguration(configuration, { engine, conne
|
|
|
23
23
|
throw new TypeError("Conversation configuration requires systemPrompt and optional integrationId, model and effort.");
|
|
24
24
|
}
|
|
25
25
|
if ((connectionRequired || configuration.integrationId !== undefined) &&
|
|
26
|
-
|
|
27
|
-
typeof apiClientFactory?.resolveClient !== "function") {
|
|
26
|
+
// readable-check: allow method-probe - input validation: an API conversation needs a connection resolver or a client factory
|
|
27
|
+
(!configuration.integrationId || typeof connections?.resolve !== "function") && typeof apiClientFactory?.resolveClient !== "function") {
|
|
28
28
|
throw new TypeError(`${engine} requires an authorized integrationId and connection resolver.`);
|
|
29
29
|
}
|
|
30
30
|
if (configuration.effort !== undefined && !efforts.includes(configuration.effort)) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { isCompletedEnvelopeTurn } from "./transcript.js";
|
|
4
|
+
import { ignorePromptRejection, ignorePromptSending } from "./nativeCommandCallbacks.js";
|
|
3
5
|
|
|
4
6
|
// A final answer keeps its identity when an application edits its visible text.
|
|
5
7
|
export function conversationMessageIdentity(turnId, message) {
|
|
@@ -14,10 +16,32 @@ export function conversationMessageVersion(message, { includeData = false } = {}
|
|
|
14
16
|
})).digest("hex");
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
// A changeover whose owner keeps no delivery log.
|
|
20
|
+
function logNothing() {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Keep native delivery and its history cursor in the same durable record.
|
|
24
|
+
* @param {object} options
|
|
25
|
+
* @param {{ read(): Promise<object | null>, write(value: object): Promise<void>,
|
|
26
|
+
* transaction?(callback: (transaction: object) => Promise<void>): Promise<void> }} options.state
|
|
27
|
+
* The saved delivery record, and the transaction that releases a replaced binding.
|
|
28
|
+
* @param {{ hasMessage(messageId: string): Promise<boolean>, writeUserMessage(message: object): Promise<void> }} [options.transcript]
|
|
29
|
+
* The conversation's own transcript, for messages delivered natively.
|
|
30
|
+
* @param {{ closeSession?(): Promise<{ ok: boolean, error?: string }>, sendMessage?(input: object): Promise<object>,
|
|
31
|
+
* inspectMessageAdmission?(request: { messageId: string }): Promise<object> }} [options.agent] The native engine's session.
|
|
32
|
+
* @param {{ inspect(expectedId: string): Promise<object> }} [options.identity] The native predecessor's identity.
|
|
33
|
+
* @param {object} [options.presentation] How history is rendered for the native engine.
|
|
34
|
+
* @param {(event: object) => void} [options.log] Receives each delivery event.
|
|
35
|
+
* @param {boolean} [options.captureContext]
|
|
36
|
+
* @param {boolean} [options.applicationMessages]
|
|
37
|
+
* @param {number} [options.maximumInitialMessages]
|
|
38
|
+
* @param {number} [options.maximumInitialMessageCharacters]
|
|
39
|
+
* @param {number | ((request: object) => Promise<number>)} [options.maximumHistoryBytes]
|
|
40
|
+
*/
|
|
41
|
+
export function createConversationChangeover({ state, transcript, agent, identity, presentation = {}, log = logNothing,
|
|
19
42
|
captureContext = false, applicationMessages = false, maximumInitialMessages = 30,
|
|
20
43
|
maximumInitialMessageCharacters = Infinity, maximumHistoryBytes = Infinity }) {
|
|
44
|
+
// readable-check: allow method-probe - data: the history budget is a byte count or a resolver of one
|
|
21
45
|
if (typeof maximumHistoryBytes !== "function" && maximumHistoryBytes !== Infinity &&
|
|
22
46
|
(!Number.isSafeInteger(maximumHistoryBytes) || maximumHistoryBytes < 1) ||
|
|
23
47
|
maximumInitialMessages === Infinity && maximumHistoryBytes === Infinity) {
|
|
@@ -72,7 +96,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
|
|
|
72
96
|
|
|
73
97
|
async function replace({ engineId, messages, operationId, expectedId, handover }) {
|
|
74
98
|
const fail = (message, translation = {}) => {
|
|
75
|
-
throw
|
|
99
|
+
throw new AppError(409, message, { code: replacementUnavailableCode, ...translation });
|
|
76
100
|
};
|
|
77
101
|
const value = await readState({ engineId, messages });
|
|
78
102
|
let replacement = value.replacement;
|
|
@@ -285,6 +309,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
|
|
|
285
309
|
returning, messageCount: catchup.length, correctionCount: catchup.filter((m) => binding.seen[m.id]).length,
|
|
286
310
|
removedCount: deleted.length, promptCharacters: preamble.length });
|
|
287
311
|
}
|
|
312
|
+
const { onPromptSending = ignorePromptSending, onPromptRejected = ignorePromptRejection } = input;
|
|
288
313
|
const delivered = await agent.sendMessage(pending ? {
|
|
289
314
|
...input, message: pending.message, displayMessage: pending.displayMessage,
|
|
290
315
|
displayAttachments: pending.displayAttachments, attachmentIds: pending.attachmentIds,
|
|
@@ -296,7 +321,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
|
|
|
296
321
|
(!threadId || threadId === replacement.previous.conversationId)) {
|
|
297
322
|
throw new Error("Native replacement must use a fresh successor before sending its briefing.");
|
|
298
323
|
}
|
|
299
|
-
await
|
|
324
|
+
await onPromptSending({ threadId, displayAttachments, turnMetadata });
|
|
300
325
|
pending.threadId = threadId;
|
|
301
326
|
pending.displayAttachments = displayAttachments || pending.displayAttachments;
|
|
302
327
|
pending.turnMetadata = turnMetadata || pending.turnMetadata;
|
|
@@ -304,7 +329,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
|
|
|
304
329
|
await persist();
|
|
305
330
|
},
|
|
306
331
|
onPromptRejected: async () => {
|
|
307
|
-
await
|
|
332
|
+
await onPromptRejected();
|
|
308
333
|
pending.attempted = false;
|
|
309
334
|
await persist();
|
|
310
335
|
}
|
|
@@ -328,7 +353,19 @@ export function createConversationChangeover({ state, transcript, agent, identit
|
|
|
328
353
|
return true;
|
|
329
354
|
}
|
|
330
355
|
|
|
331
|
-
return {
|
|
356
|
+
return Object.freeze({
|
|
357
|
+
|
|
358
|
+
readState,
|
|
359
|
+
|
|
360
|
+
remember,
|
|
361
|
+
|
|
362
|
+
replace,
|
|
363
|
+
|
|
364
|
+
send,
|
|
365
|
+
|
|
366
|
+
discard
|
|
367
|
+
|
|
368
|
+
});
|
|
332
369
|
}
|
|
333
370
|
|
|
334
371
|
function writtenMessages(history, native = false) {
|