@jskit-ai/assistant-core 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +18 -12
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { notSentDeliveryReceipt } from "./providers/scoped.js";
|
|
3
|
+
import { isCompletedEnvelopeTurn } from "./transcript.js";
|
|
4
|
+
import { conversationNativeIdentity } from "./runtimeStateUpgrade.js";
|
|
5
|
+
import { createConversationChangeover, conversationNativeMessages } from "./continuity.js";
|
|
6
|
+
import { deliveryFailure, failure, stateErrorFields } from "./conversationRefusals.js";
|
|
7
|
+
import { pendingSuccessor, retainedSegments, submittedMessage } from "./conversationReads.js";
|
|
8
|
+
|
|
9
|
+
/** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
|
|
10
|
+
/** @typedef {import("./conversationSending.js").ConversationRequest} ConversationRequest */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Native delivery receipts: the changeover state a native owner sees, the changeover that
|
|
14
|
+
* sends a message with the history its engine has not seen, and recovery of a message
|
|
15
|
+
* whose delivery is unconfirmed.
|
|
16
|
+
* @param {object} dependencies
|
|
17
|
+
* @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
|
|
18
|
+
* @param {import("../types.js").ConversationTranscript | null} dependencies.transcript
|
|
19
|
+
* @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
|
|
20
|
+
* @param {import("./conversationEntries.js").ConversationEntries} dependencies.entries
|
|
21
|
+
* @param {import("./conversationReads.js").ConversationReads} dependencies.reads
|
|
22
|
+
* @param {import("../types.js").ConversationLimits} dependencies.limits
|
|
23
|
+
* @param {boolean} dependencies.persistCommentary
|
|
24
|
+
* @param {boolean} dependencies.completedEnvelope
|
|
25
|
+
* @param {number} dependencies.maximumFinalReply
|
|
26
|
+
* @param {number} dependencies.maximumCompletedOutput
|
|
27
|
+
*/
|
|
28
|
+
function createConversationDelivery({ storage, transcript, events, entries, reads, limits, persistCommentary, completedEnvelope, maximumFinalReply,
|
|
29
|
+
maximumCompletedOutput }) {
|
|
30
|
+
/**
|
|
31
|
+
* The changeover owner sees its usual state contract. Only this storage adapter knows about the
|
|
32
|
+
* runtime's segment registry and its one authored request record; provider binding writes stay
|
|
33
|
+
* fenced on their own.
|
|
34
|
+
* @param {ConversationEntry} entry
|
|
35
|
+
* @param {ConversationRequest | null} [request]
|
|
36
|
+
* @param {object} [transaction] The storage transaction already open, if any.
|
|
37
|
+
*/
|
|
38
|
+
function changeoverState(entry, request, transaction) {
|
|
39
|
+
const readMetadata = () => transaction ? transaction.readMetadata()
|
|
40
|
+
: storage.read(entry.id, current => current.readMetadata());
|
|
41
|
+
const write = callback => transaction ? callback(transaction) : storage.write(entry.id, callback);
|
|
42
|
+
return {
|
|
43
|
+
async read() {
|
|
44
|
+
const { runtime } = await readMetadata();
|
|
45
|
+
const engines = Object.fromEntries(retainedSegments(runtime).filter(segment => segment.engine !== "api").map(segment => {
|
|
46
|
+
const pending = segment.request;
|
|
47
|
+
return [segment.engine, { seen: structuredClone(segment.seen),
|
|
48
|
+
...(typeof pending?.message === "string" || pending?.inspectionOnly ? { pending: structuredClone({ ...pending,
|
|
49
|
+
...(pending.inspectionOnly ? { attempted: true, displayMessage: pending.text, displayAttachments: pending.attachments } : {}) }) } : {}) }];
|
|
50
|
+
}));
|
|
51
|
+
const predecessor = pendingSuccessor(runtime);
|
|
52
|
+
const preparing = runtime.replacement?.request?.operation === "replace" ? runtime.replacement : null;
|
|
53
|
+
return { lastEngine: runtime.lastEngine, engines,
|
|
54
|
+
...(preparing ? { replacement: { engineId: preparing.engine, status: "preparing",
|
|
55
|
+
operationId: preparing.request.operationId, handover: preparing.request.briefing || "",
|
|
56
|
+
bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: conversationNativeIdentity(runtime) },
|
|
57
|
+
...(preparing.preparedAt ? { preparedAt: preparing.preparedAt } : {}) } }
|
|
58
|
+
: predecessor ? { replacement: { engineId: runtime.engine, status: "ready",
|
|
59
|
+
operationId: predecessor.replacement.operationId, handover: predecessor.replacement.briefing || "",
|
|
60
|
+
previous: { engineId: predecessor.engine, conversationId: conversationNativeIdentity(predecessor) },
|
|
61
|
+
...(predecessor.preparedAt ? { preparedAt: predecessor.preparedAt } : {}) } } : {}) };
|
|
62
|
+
},
|
|
63
|
+
write(value) {
|
|
64
|
+
return write(async current => {
|
|
65
|
+
const metadata = await current.readMetadata();
|
|
66
|
+
const runtime = metadata.runtime;
|
|
67
|
+
if (runtime.segmentId !== entry.segmentId || runtime.engine !== entry.engine) throw new Error("The native conversation changed before its delivery was saved.");
|
|
68
|
+
const binding = value.engines[entry.engine];
|
|
69
|
+
if (binding.seen !== undefined) runtime.seen = binding.seen;
|
|
70
|
+
else if (!runtime.request?.inspectionOnly) throw new Error("Native delivery has no saved history cursor.");
|
|
71
|
+
// A pre-preparation v2 reservation has no snapshot. Its confirmed
|
|
72
|
+
// receipt settles only that request; it cannot acknowledge extra history.
|
|
73
|
+
runtime.lastEngine = value.lastEngine;
|
|
74
|
+
if (binding.pending) {
|
|
75
|
+
const authored = runtime.request?.messageId === binding.pending.messageId ? runtime.request
|
|
76
|
+
: request?.input.messageId === binding.pending.messageId ? { ...request.input, at: request.at } : null;
|
|
77
|
+
if (!authored) throw new Error("A native prompt has no matching authored request.");
|
|
78
|
+
// Inspection-only predecessors retain exactly their old authored
|
|
79
|
+
// record. Their absent prompt must never be manufactured on read.
|
|
80
|
+
runtime.request = authored.inspectionOnly ? authored : { ...authored, ...binding.pending };
|
|
81
|
+
} else if (runtime.request && (typeof runtime.request.message === "string" || runtime.request.inspectionOnly ||
|
|
82
|
+
runtime.request.messageId === request?.input.messageId)) {
|
|
83
|
+
delete runtime.request;
|
|
84
|
+
}
|
|
85
|
+
const predecessor = pendingSuccessor(runtime);
|
|
86
|
+
if (predecessor && value.replacement?.status === "accepted") {
|
|
87
|
+
predecessor.acceptedAt = value.replacement.acceptedAt;
|
|
88
|
+
predecessor.successorConversationId = value.replacement.successorConversationId;
|
|
89
|
+
delete runtime.continuity;
|
|
90
|
+
delete runtime.continuityAttachments;
|
|
91
|
+
}
|
|
92
|
+
await current.writeMetadata(metadata);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The changeover that sends a message with the history its native engine has not seen.
|
|
100
|
+
* @param {ConversationEntry} entry
|
|
101
|
+
* @param {object} context
|
|
102
|
+
* @param {ConversationRequest | null} request
|
|
103
|
+
* @param {(input: object) => Promise<unknown>} sendMessage
|
|
104
|
+
* @param {AbortSignal} [signal]
|
|
105
|
+
*/
|
|
106
|
+
function nativeChangeover(entry, context, request, sendMessage, signal) {
|
|
107
|
+
let observation;
|
|
108
|
+
const initialWindow = limits.maxInitialNativeHistoryMessages;
|
|
109
|
+
const budget = entry.conversation?.maximumHistoryBytes;
|
|
110
|
+
if (initialWindow !== undefined && (!Number.isSafeInteger(initialWindow) || initialWindow < 0)) {
|
|
111
|
+
throw new TypeError("Initial native history requires a non-negative message limit.");
|
|
112
|
+
}
|
|
113
|
+
return createConversationChangeover({
|
|
114
|
+
state: entry.conversation?.state || changeoverState(entry, request),
|
|
115
|
+
captureContext: !entry.conversation, applicationMessages: !entry.conversation,
|
|
116
|
+
maximumInitialMessages: initialWindow === undefined ? (budget ? Infinity : undefined)
|
|
117
|
+
: Math.max(0, initialWindow - (request?.input.origin === "user" ? 1 : 0)),
|
|
118
|
+
maximumInitialMessageCharacters: limits.maxInitialNativeHistoryMessageCharacters,
|
|
119
|
+
...(budget ? { maximumHistoryBytes: async options => {
|
|
120
|
+
try {
|
|
121
|
+
await events.access(context, entry.id, request.operation, entry);
|
|
122
|
+
signal.throwIfAborted();
|
|
123
|
+
// A host may free native context first; show that as compacting.
|
|
124
|
+
const bytes = await budget({ ...options, context, signal,
|
|
125
|
+
// readable-check: allow setter-seam - not a slot: the history budget resolver reports compacting through setPhase
|
|
126
|
+
setPhase: phase => events.setPhase(entry, phase === "compacting" ? "compacting" : "preparing") });
|
|
127
|
+
signal.throwIfAborted();
|
|
128
|
+
return bytes;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Sizing missed history precedes native dispatch, so this message
|
|
131
|
+
// was definitely not sent. Hosts and clients report it as such.
|
|
132
|
+
throw Object.assign(error, { delivery: "not-sent" });
|
|
133
|
+
}
|
|
134
|
+
} } : {}),
|
|
135
|
+
presentation: entry.conversation?.presentation || { unconfirmedCode: "conversation_delivery_uncertain",
|
|
136
|
+
unconfirmedMessage: "The previous submission has uncertain delivery. Retry this message to check its native receipt; it will not be resent automatically." },
|
|
137
|
+
transcript: entry.conversation?.transcript || {
|
|
138
|
+
hasMessage: messageId => transcript.conversationMessageIdExists(entry.id, messageId),
|
|
139
|
+
async writeUserMessage({ messageId }) {
|
|
140
|
+
if (observation) await recoverDelivery(entry, context, messageId, { observation, allowActive: true });
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
agent: {
|
|
144
|
+
sendMessage,
|
|
145
|
+
async inspectMessageAdmission({ messageId, threadId }) {
|
|
146
|
+
if (entry.conversation) {
|
|
147
|
+
observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
|
|
148
|
+
await events.access(context, entry.id, request?.operation || "inspectDelivery", entry);
|
|
149
|
+
return { admission: observation.accepted ? "accepted" : "unknown" };
|
|
150
|
+
}
|
|
151
|
+
const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
|
|
152
|
+
if (!threadId || threadId !== conversationNativeIdentity(runtime) || runtime.request?.messageId !== messageId) return null;
|
|
153
|
+
observation = await entry.provider.inspectAdmission({ ...runtime.request, context, configuration: runtime.configuration });
|
|
154
|
+
await events.access(context, entry.id, request?.operation || "inspectDelivery", entry);
|
|
155
|
+
return { admission: observation.accepted ? "accepted" : "unknown" };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Records saved turns as seen by the entry's native engine.
|
|
163
|
+
* @param {ConversationEntry} entry
|
|
164
|
+
* @param {object} transaction
|
|
165
|
+
* @param {object[]} turns
|
|
166
|
+
*/
|
|
167
|
+
async function rememberNative(entry, transaction, turns) {
|
|
168
|
+
const changeover = createConversationChangeover({ state: changeoverState(entry, null, transaction) });
|
|
169
|
+
await changeover.remember({ engineId: entry.engine, messages: conversationNativeMessages(turns) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Recovers a message whose delivery is unconfirmed, from its engine's own admission record.
|
|
174
|
+
* @param {ConversationEntry} entry
|
|
175
|
+
* @param {object} context
|
|
176
|
+
* @param {string} messageId
|
|
177
|
+
* @param {{ observation?: object, allowActive?: boolean }} [options]
|
|
178
|
+
*/
|
|
179
|
+
async function recoverDelivery(entry, context, messageId, { observation: suppliedObservation, allowActive = false } = {}) {
|
|
180
|
+
await events.access(context, entry.id, "inspectDelivery", entry);
|
|
181
|
+
if (entry.active && !allowActive) throw failure("Wait for the current submission before checking delivery.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForSubmission" });
|
|
182
|
+
if (entry.storageFailure) throw entry.storageFailure;
|
|
183
|
+
if (entry.executionFailure) throw entry.executionFailure;
|
|
184
|
+
const history = await reads.readTranscript(entry);
|
|
185
|
+
const existing = history.find(turn => submittedMessage(turn)?.messageId === messageId);
|
|
186
|
+
const receipt = existing && { status: "accepted", messageId, turnId: existing.turnId, duplicate: true };
|
|
187
|
+
// Turns written before the first runtime segment carry no runtime metadata.
|
|
188
|
+
const existingRuntime = existing?.metadata?.runtime;
|
|
189
|
+
const goalMessageId = existingRuntime?.goalMessageId;
|
|
190
|
+
const goalRequest = Boolean(submittedMessage(existing || {})?.goal || goalMessageId === messageId);
|
|
191
|
+
if (existing && (existingRuntime?.segmentId !== entry.segmentId || existingRuntime.supersededBy ||
|
|
192
|
+
existingRuntime.status === "complete" && !goalRequest)) return receipt;
|
|
193
|
+
if (goalMessageId && goalMessageId !== messageId) {
|
|
194
|
+
const owner = history.find(turn => submittedMessage(turn)?.messageId === goalMessageId);
|
|
195
|
+
const ownerRuntime = owner?.metadata?.runtime;
|
|
196
|
+
if (ownerRuntime?.goalMessageId !== goalMessageId || ownerRuntime.segmentId !== entry.segmentId) throw new Error("The goal command for this native continuation is missing.");
|
|
197
|
+
const result = await inspectDelivery(entry, context, goalMessageId);
|
|
198
|
+
return { ...receipt, recovered: result.recovered === true };
|
|
199
|
+
}
|
|
200
|
+
const metadata = { runtime: await entries.readState(entry) };
|
|
201
|
+
if (metadata.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending_before_recovery", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
|
|
202
|
+
const savedRequests = [...metadata.runtime.predecessors, metadata.runtime].filter(segment => segment.request?.messageId === messageId);
|
|
203
|
+
if (!existing && savedRequests.length && savedRequests.every(segment =>
|
|
204
|
+
["codex", "claude", "opencode"].includes(segment.engine) && segment.request.attempted === false && !segment.request.inspectionOnly &&
|
|
205
|
+
isDeepStrictEqual(segment.request, savedRequests[0].request))) {
|
|
206
|
+
await events.access(context, entry.id, "inspectDelivery", entry);
|
|
207
|
+
return notSentDeliveryReceipt(messageId);
|
|
208
|
+
}
|
|
209
|
+
const request = existing ? submittedMessage(existing) : metadata.runtime.request;
|
|
210
|
+
if (!request || request.messageId !== messageId || !entry.provider.inspectAdmission) return receipt || { status: "unknown", messageId };
|
|
211
|
+
const pending = metadata.runtime.request?.messageId === messageId ? metadata.runtime.request : null;
|
|
212
|
+
if (pending?.threadId && pending.threadId !== conversationNativeIdentity(metadata.runtime)) return receipt || { status: "unknown", messageId };
|
|
213
|
+
const observation = suppliedObservation || await entry.provider.inspectAdmission({ ...request, nativeTurnId: existing?.metadata?.runtime?.nativeTurnId,
|
|
214
|
+
context, configuration: metadata.runtime.configuration });
|
|
215
|
+
await events.access(context, entry.id, "inspectDelivery", entry);
|
|
216
|
+
// The engine proved it never admitted this saved request. Record that it
|
|
217
|
+
// was not sent, as a definite rejection would, so Retry may send it.
|
|
218
|
+
if (!existing && observation.notAdmitted && pending && !pending.inspectionOnly && !entry.active) {
|
|
219
|
+
await storage.write(entry.id, async transaction => {
|
|
220
|
+
const current = await transaction.readMetadata();
|
|
221
|
+
if (current.runtime.request?.messageId !== messageId) return;
|
|
222
|
+
current.runtime.request.attempted = false;
|
|
223
|
+
await transaction.writeMetadata(current);
|
|
224
|
+
});
|
|
225
|
+
return notSentDeliveryReceipt(messageId);
|
|
226
|
+
}
|
|
227
|
+
if (!observation.accepted) return { ...(receipt || { status: "unknown", messageId }),
|
|
228
|
+
...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
|
|
229
|
+
if (entry.driver.canonicalTranscript) {
|
|
230
|
+
const latest = await reads.readTranscript(entry);
|
|
231
|
+
const saved = latest.find(turn => submittedMessage(turn)?.messageId === messageId);
|
|
232
|
+
if (!saved) throw new Error("The native receipt did not persist its authored message.");
|
|
233
|
+
const state = saved.metadata?.runtime;
|
|
234
|
+
if (state?.segmentId !== entry.segmentId) throw new Error("The native receipt belongs to another conversation segment.");
|
|
235
|
+
const unknownTool = saved.metadata.applicationTools?.some(call => !call.result || call.status === "unknown");
|
|
236
|
+
const status = ["cancelled", "failed"].includes(state.status) ? state.status : unknownTool ? "interrupted"
|
|
237
|
+
: observation.recoveryLimitation ? "interrupted" : ["completed", "idle"].includes(observation.status) ? "complete"
|
|
238
|
+
: observation.status === "failed" ? "failed" : "interrupted";
|
|
239
|
+
const error = unknownTool ? "Native output was recovered, but an application tool has no verified result. Inspect its target before requesting another execution."
|
|
240
|
+
: observation.recoveryLimitation || (status === "complete" ? "" : state.error || "Delivery was confirmed. Completion could not be verified; this request was not replayed.");
|
|
241
|
+
await storage.write(entry.id, async transaction => {
|
|
242
|
+
const current = await transaction.readMetadata();
|
|
243
|
+
if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) {
|
|
244
|
+
throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending_before_recovery", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
|
|
245
|
+
}
|
|
246
|
+
const exact = await transaction.readTurn(saved.turnId);
|
|
247
|
+
const { error: _previousError, ...runtime } = exact.metadata.runtime;
|
|
248
|
+
await transaction.updateTurnMetadata(saved.turnId, { runtime: { ...runtime, status, ...(error ? { error } : {}) } });
|
|
249
|
+
});
|
|
250
|
+
if (Object.hasOwn(observation, "goal")) entry.goal = observation.goal;
|
|
251
|
+
entry.error = error;
|
|
252
|
+
const result = { ...(receipt || { status: "accepted", messageId, turnId: saved.turnId }),
|
|
253
|
+
recovered: observation.recovered === true, ...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
|
|
254
|
+
if (!existing) await events.emit(entry, { type: "accepted", ...result });
|
|
255
|
+
if (result.recovered) await events.emit(entry, { type: "settled", turnId: saved.turnId, messageId, status, error, ...stateErrorFields(error) });
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
const recovered = await storage.write(entry.id, async transaction => {
|
|
259
|
+
const current = await transaction.readMetadata();
|
|
260
|
+
if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending_before_recovery", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
|
|
261
|
+
const native = { ...observation, messageId };
|
|
262
|
+
const results = [];
|
|
263
|
+
const id = messageId;
|
|
264
|
+
let turn = history.find(turn => submittedMessage(turn)?.messageId === id);
|
|
265
|
+
if (turn?.metadata?.runtime?.supersededBy || turn && turn.metadata?.runtime?.segmentId !== entry.segmentId) {
|
|
266
|
+
throw new Error("Native history refers to a turn outside the active conversation segment.");
|
|
267
|
+
}
|
|
268
|
+
if (turn?.metadata?.runtime?.status !== "complete") {
|
|
269
|
+
if (!turn) {
|
|
270
|
+
const input = request;
|
|
271
|
+
const turnId = await transaction.nextTurnId();
|
|
272
|
+
await transaction.appendMessage(turnId, { role: input.origin === "application" ? "system" : "user", origin: input.origin || "user",
|
|
273
|
+
messageId: id, text: input.text, attachments: input.attachments, at: input.at,
|
|
274
|
+
...(input.data !== undefined ? { data: input.data } : {}), ...(input.goal ? { goal: input.goal } : {}),
|
|
275
|
+
turnMetadata: { runtime: { engine: entry.engine, segmentId: entry.segmentId, origin: input.origin || "user",
|
|
276
|
+
...(input.completedEnvelope === true ? { completedEnvelope: true } : {}),
|
|
277
|
+
...(typeof input.nativeToolAttempt === "boolean" ? { nativeToolAttempt: input.nativeToolAttempt } : {}) } } });
|
|
278
|
+
turn = await transaction.readTurn(turnId);
|
|
279
|
+
history.push(turn);
|
|
280
|
+
}
|
|
281
|
+
const runtime = turn.metadata.runtime;
|
|
282
|
+
if (runtime.nativeTurnId && native.nativeTurnId && runtime.nativeTurnId !== native.nativeTurnId) throw new Error("Native history changed the accepted turn's identity.");
|
|
283
|
+
const replies = [];
|
|
284
|
+
for (const message of native.messages || []) {
|
|
285
|
+
if (!persistCommentary && message.role === "commentary") continue;
|
|
286
|
+
if (message.role === "assistant") replies.push(message.text);
|
|
287
|
+
else {
|
|
288
|
+
const messageId = `${turn.turnId}:${message.id}`;
|
|
289
|
+
if (!await transaction.hasMessage(messageId)) await transaction.appendMessage(turn.turnId,
|
|
290
|
+
{ messageId, ...(message.outputId ? { outputId: `${turn.turnId}:${message.outputId}` } : {}),
|
|
291
|
+
role: message.role, text: message.text, at: submittedMessage(turn).at });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const nativeReplies = (native.messages || []).filter(message => message.role === "assistant");
|
|
295
|
+
const outputId = nativeReplies.length === 1 && nativeReplies[0].outputId;
|
|
296
|
+
const replyText = replies.join("\n\n");
|
|
297
|
+
const finalReplyLimit = completedEnvelope && isCompletedEnvelopeTurn(turn) && !submittedMessage(turn)?.goal
|
|
298
|
+
? maximumCompletedOutput : maximumFinalReply;
|
|
299
|
+
const oversizedReply = replyText.length > finalReplyLimit;
|
|
300
|
+
if (replies.length && !oversizedReply) await transaction.replaceAssistant(turn.turnId, { messageId: turn.assistant?.messageId || `${turn.turnId}:assistant`,
|
|
301
|
+
...(!turn.assistant?.outputId && outputId ? { outputId: `${turn.turnId}:${outputId}` } : {}),
|
|
302
|
+
role: "assistant", text: replyText, at: submittedMessage(turn).at });
|
|
303
|
+
const unknownTool = turn.metadata.applicationTools?.some(call => !call.result || call.status === "unknown");
|
|
304
|
+
const retainedFailure = ["cancelled", "failed"].includes(runtime.status);
|
|
305
|
+
const status = retainedFailure ? runtime.status : oversizedReply ? "failed" : unknownTool ? "interrupted" : native.status || "interrupted";
|
|
306
|
+
const { error: previousError, ...saved } = runtime;
|
|
307
|
+
const error = unknownTool ? "Native output was recovered, but an application tool has no verified result. Inspect its target before requesting another execution."
|
|
308
|
+
: oversizedReply ? retainedFailure && (previousError || native.error) || "The assistant exceeded the configured final reply limit."
|
|
309
|
+
: status === "complete" ? "" : previousError || native.error || "Delivery was confirmed from native history. Completion was not confirmed; this request was not replayed.";
|
|
310
|
+
await transaction.updateTurnMetadata(turn.turnId, { runtime: { ...saved, status,
|
|
311
|
+
...(request.goal || goalRequest ? { goalMessageId: messageId } : {}),
|
|
312
|
+
...(native.nativeTurnId ? { nativeTurnId: native.nativeTurnId } : {}), ...(error ? { error } : {}) } });
|
|
313
|
+
results.push({ turnId: turn.turnId, messageId: id, status, error });
|
|
314
|
+
}
|
|
315
|
+
await transaction.writeMetadata(current);
|
|
316
|
+
return results;
|
|
317
|
+
});
|
|
318
|
+
if (Object.hasOwn(observation, "goal")) entry.goal = observation.goal;
|
|
319
|
+
entry.error = recovered.find(turn => turn.error)?.error || "";
|
|
320
|
+
const result = { ...(receipt || { status: "accepted", messageId, turnId: recovered[0].turnId }), recovered: recovered.length > 0 };
|
|
321
|
+
if (!existing) await events.emit(entry, { type: "accepted", ...result });
|
|
322
|
+
for (const turn of recovered) await events.emit(entry, { type: "settled", ...turn, ...stateErrorFields(turn.error) });
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {ConversationEntry} entry
|
|
328
|
+
* @param {object} context
|
|
329
|
+
* @param {string} messageId
|
|
330
|
+
*/
|
|
331
|
+
async function inspectDelivery(entry, context, messageId) {
|
|
332
|
+
if (entry.conversation?.commands) {
|
|
333
|
+
await events.access(context, entry.id, "inspectDelivery", entry);
|
|
334
|
+
const result = await entry.conversation.commands.inspectDelivery({ messageId }, context);
|
|
335
|
+
await events.access(context, entry.id, "inspectDelivery", entry);
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
if (entry.conversation) {
|
|
339
|
+
await events.access(context, entry.id, "inspectDelivery", entry);
|
|
340
|
+
const state = await entries.readState(entry);
|
|
341
|
+
// A running owner completes its own receipt. Only an idle owner recovers
|
|
342
|
+
// the saved changeover, preserving write-receipt-before-clear ordering.
|
|
343
|
+
if (!entry.active && state.request?.messageId === messageId && state.request.attempted) {
|
|
344
|
+
const sender = nativeChangeover(entry, context, null, () => {
|
|
345
|
+
throw new Error("Receipt inspection cannot submit another native prompt.");
|
|
346
|
+
});
|
|
347
|
+
const delivered = await sender.send({ engineId: entry.engine,
|
|
348
|
+
messages: await entry.conversation.transcript.history(), input: { messageId }, inspectOnly: true });
|
|
349
|
+
if (delivered?.notSent) return notSentDeliveryReceipt(messageId);
|
|
350
|
+
if (!delivered?.delivered) throw deliveryFailure(delivered);
|
|
351
|
+
}
|
|
352
|
+
const row = (await reads.readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === messageId);
|
|
353
|
+
if (row) return { status: "accepted", messageId, turnId: row.turnId, duplicate: true };
|
|
354
|
+
const threadId = state.request?.messageId === messageId ? state.request.threadId : conversationNativeIdentity(state);
|
|
355
|
+
if (!threadId) return { status: "unknown", messageId };
|
|
356
|
+
const observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
|
|
357
|
+
await events.access(context, entry.id, "inspectDelivery", entry);
|
|
358
|
+
// Native-only acceptance is evidence of delivery, not permission to
|
|
359
|
+
// manufacture an authored row or use a native turn ID as its receipt.
|
|
360
|
+
if (observation.accepted) return { status: "accepted", messageId, duplicate: true };
|
|
361
|
+
return observation.notAdmitted && !entry.active ? notSentDeliveryReceipt(messageId) : { status: "unknown", messageId };
|
|
362
|
+
}
|
|
363
|
+
const result = await recoverDelivery(entry, context, messageId);
|
|
364
|
+
if (result.status !== "accepted" || entry.engine === "api") return result;
|
|
365
|
+
if (entry.driver.canonicalTranscript && result.recoveryLimitation) {
|
|
366
|
+
const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
|
|
367
|
+
if (runtime.request?.goal || runtime.binding?.goalRequest?.messageId === messageId ||
|
|
368
|
+
runtime.binding?.goalReceipt?.messageId === messageId) return result;
|
|
369
|
+
}
|
|
370
|
+
const messages = conversationNativeMessages((await reads.readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy));
|
|
371
|
+
const sender = nativeChangeover(entry, context, null, () => {
|
|
372
|
+
throw new Error("Receipt inspection cannot submit another native prompt.");
|
|
373
|
+
});
|
|
374
|
+
const pending = (await entries.readState(entry)).request;
|
|
375
|
+
if (pending?.messageId === messageId) {
|
|
376
|
+
const delivered = await sender.send({ engineId: entry.engine, messages, input: { messageId } });
|
|
377
|
+
if (!delivered?.delivered) throw deliveryFailure(delivered);
|
|
378
|
+
}
|
|
379
|
+
await sender.remember({ engineId: entry.engine, messages });
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return Object.freeze({
|
|
384
|
+
changeoverState,
|
|
385
|
+
nativeChangeover,
|
|
386
|
+
rememberNative,
|
|
387
|
+
recoverDelivery,
|
|
388
|
+
inspectDelivery
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** @typedef {ReturnType<typeof createConversationDelivery>} ConversationDelivery */
|
|
393
|
+
|
|
394
|
+
export { createConversationDelivery };
|