@jskit-ai/assistant-core 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { conversationAttachmentIds } from "./attachments.js";
|
|
3
|
+
import { failure } from "./conversationRefusals.js";
|
|
4
|
+
import { pendingSuccessor, submittedMessage } from "./conversationReads.js";
|
|
5
|
+
import { serial } from "./conversationEvents.js";
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reading and changing an open conversation's native goal.
|
|
11
|
+
* @param {object} dependencies
|
|
12
|
+
* @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
|
|
13
|
+
* @param {import("./conversationEntries.js").ConversationEntries} dependencies.entries
|
|
14
|
+
* @param {import("./conversationReads.js").ConversationReads} dependencies.reads
|
|
15
|
+
* @param {import("./conversationSending.js").ConversationSending} dependencies.sending
|
|
16
|
+
* @param {import("./conversationStopping.js").ConversationStopping} dependencies.stopping
|
|
17
|
+
* @param {import("../types.js").ServiceToolCatalog | null} dependencies.catalog
|
|
18
|
+
*/
|
|
19
|
+
function createConversationGoals({ events, entries, reads, sending, stopping, catalog }) {
|
|
20
|
+
/**
|
|
21
|
+
* @param {ConversationEntry} entry
|
|
22
|
+
* @param {object} context
|
|
23
|
+
* @param {string} representation
|
|
24
|
+
*/
|
|
25
|
+
async function readGoal(entry, context, representation) {
|
|
26
|
+
await events.access(context, entry.id, "readGoal", entry);
|
|
27
|
+
if (!reads.capabilities(entry).goals) return null;
|
|
28
|
+
const configuration = representation === "native" ? undefined : (await entries.readState(entry)).configuration;
|
|
29
|
+
const result = await entry.provider.readGoal({ context, configuration });
|
|
30
|
+
if (entry.conversation && representation !== "native" && result.nativeResult.status === "unavailable") {
|
|
31
|
+
throw failure("The native goal is unavailable. Try reading its status again.", "conversation_goal_unavailable", 503);
|
|
32
|
+
}
|
|
33
|
+
entry.goal = entry.conversation ? result.goal : result;
|
|
34
|
+
return representation === "native" ? result.nativeResult : structuredClone(entry.goal);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {ConversationEntry} entry
|
|
39
|
+
* @param {object} context
|
|
40
|
+
* @param {object} input
|
|
41
|
+
* @param {string} representation
|
|
42
|
+
* @param {boolean} [retryFinishedTurn] Whether a goal that Claude's completion refused may be admitted once more.
|
|
43
|
+
*/
|
|
44
|
+
async function updateGoal(entry, context, input, representation, retryFinishedTurn = true) {
|
|
45
|
+
await events.access(context, entry.id, "goal", entry);
|
|
46
|
+
context?.signal?.throwIfAborted();
|
|
47
|
+
if (!reads.capabilities(entry).goals) throw failure("This connection does not support native goals.", "conversation_goals_unsupported", 400, { messageCode: "assistant.conversation.goalsUnsupported" });
|
|
48
|
+
if (representation === "native") {
|
|
49
|
+
const { operation } = await serial(entry, async () => {
|
|
50
|
+
await events.access(context, entry.id, "goal", entry);
|
|
51
|
+
// The original goal owner has its own admission. Its native message
|
|
52
|
+
// acknowledgement must not hold the common queue ahead of Stop.
|
|
53
|
+
return { operation: entry.provider.updateGoal({ context, input, representation }) };
|
|
54
|
+
});
|
|
55
|
+
const result = await operation;
|
|
56
|
+
if (result.nativeResult.ok !== false) {
|
|
57
|
+
entry.goal = result.goal;
|
|
58
|
+
await events.emit(entry, { type: "goal", goal: entry.goal });
|
|
59
|
+
}
|
|
60
|
+
return result.nativeResult;
|
|
61
|
+
}
|
|
62
|
+
if (!input || !["set", "resume", "pause", "cancel"].includes(input.action) ||
|
|
63
|
+
(typeof input.expectedSegmentId !== "string" && !(entry.conversation && input.expectedSegmentId === null)) ||
|
|
64
|
+
input.expectedGoalId != null && (typeof input.expectedGoalId !== "string" || !input.expectedGoalId)) {
|
|
65
|
+
throw failure("A goal action and current conversation segment are required.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalActionRequired" });
|
|
66
|
+
}
|
|
67
|
+
const starting = input.action === "set" || input.action === "resume";
|
|
68
|
+
const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId, expectedGoalId: input.expectedGoalId || null };
|
|
69
|
+
if (starting) {
|
|
70
|
+
if (input.action === "set") {
|
|
71
|
+
if (input.tokenBudget != null && !entry.driver.capabilities.goalBudgets) throw failure("This connection does not support goal token budgets.", "conversation_goal_token_budgets_unsupported", 400, { messageCode: "assistant.conversation.goalTokenBudgetsUnsupported" });
|
|
72
|
+
if (typeof input.objective !== "string" || !input.objective.trim() || input.objective.length > 4000 ||
|
|
73
|
+
input.tokenBudget != null && (!Number.isSafeInteger(input.tokenBudget) || input.tokenBudget < 1)) {
|
|
74
|
+
throw failure("A bounded objective and optional positive token budget are required.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalObjectiveInvalid" });
|
|
75
|
+
}
|
|
76
|
+
goal.objective = input.objective.trim();
|
|
77
|
+
if (input.tokenBudget != null) goal.tokenBudget = input.tokenBudget;
|
|
78
|
+
} else if (!goal.expectedGoalId) throw failure("Select the goal to resume.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalToResumeRequired" });
|
|
79
|
+
}
|
|
80
|
+
if (!starting && !input.expectedGoalId) throw failure("Select the goal to stop.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalToStopRequired" });
|
|
81
|
+
const delivery = entry.driver.capabilities.goalCommands?.[input.action]?.delivery;
|
|
82
|
+
if (!["message", "control"].includes(delivery)) throw failure("This connection does not support that goal command.", "conversation_goal_command_unsupported", 400, { messageCode: "assistant.conversation.goalCommandUnsupported" });
|
|
83
|
+
const messageCommand = delivery === "message";
|
|
84
|
+
if (messageCommand && (typeof input.messageId !== "string" || !/^[\w-]{1,128}$/u.test(input.messageId))) {
|
|
85
|
+
throw failure("A stable messageId is required for this goal command.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.goalMessageIdRequired" });
|
|
86
|
+
}
|
|
87
|
+
if (conversationAttachmentIds(input.attachmentIds).length || input.attachments?.length) {
|
|
88
|
+
throw failure("This engine's native goal commands do not accept attachments.", "conversation_goal_attachments_unsupported", 400, { messageCode: "assistant.conversation.goalAttachmentsUnsupported" });
|
|
89
|
+
}
|
|
90
|
+
const result = await serial(entry, async () => {
|
|
91
|
+
await events.access(context, entry.id, "goal", entry);
|
|
92
|
+
if (entry.conversation) await entries.readState(entry);
|
|
93
|
+
if (entry.segmentId !== input.expectedSegmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalConversationChanged" });
|
|
94
|
+
if (entry.conversation && input.expectedSegmentId === null && input.action !== "set") {
|
|
95
|
+
throw failure("This command requires an existing native conversation.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalNeedsNativeConversation" });
|
|
96
|
+
}
|
|
97
|
+
if (entry.storageFailure) throw entry.storageFailure;
|
|
98
|
+
if (entry.executionFailure) throw entry.executionFailure;
|
|
99
|
+
if (messageCommand) {
|
|
100
|
+
const history = await reads.readTranscript(entry);
|
|
101
|
+
const existing = history.find(turn => submittedMessage(turn)?.messageId === input.messageId);
|
|
102
|
+
if (existing) {
|
|
103
|
+
if (!entry.conversation && !isDeepStrictEqual(submittedMessage(existing).goal, goal)) throw failure("This messageId already belongs to different content.", "conversation_message_id_content_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
104
|
+
return { receipt: { status: "accepted", messageId: input.messageId, turnId: existing.turnId, origin: "user", duplicate: true } };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (entry.active && !entry.active.request.accepted) throw failure("Wait for the goal command to be admitted.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForGoalCommand" });
|
|
108
|
+
const metadata = { runtime: await entries.readState(entry) };
|
|
109
|
+
if (metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly)) {
|
|
110
|
+
throw failure("Check the uncertain command's delivery before changing the goal.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.goalCommandDeliveryUncertain" });
|
|
111
|
+
}
|
|
112
|
+
if (entry.driver.canonicalTranscript && metadata.runtime.binding?.goalRequest) {
|
|
113
|
+
throw failure("A goal command from an earlier runtime has unresolved delivery. Inspect its saved state before changing the goal.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.earlierGoalCommandUnresolved" });
|
|
114
|
+
}
|
|
115
|
+
if (starting && (metadata.runtime.replacement || !entry.conversation && pendingSuccessor(metadata.runtime) || metadata.runtime.lastEngine !== entry.engine)) {
|
|
116
|
+
throw failure("Send a message to catch this AI up before starting or resuming its goal.", "conversation_briefing_before_goal", 409, { messageCode: "assistant.conversation.briefingBeforeGoal" });
|
|
117
|
+
}
|
|
118
|
+
// Definitions keep native thread discovery stable. A control has no
|
|
119
|
+
// admitted request, application-tool actor or executor of its own.
|
|
120
|
+
const tools = catalog && entry.driver.toolDiscovery
|
|
121
|
+
? { schemas: catalog.resolveToolSet(context, { discoveryOnly: true }).tools.map(catalog.toOpenAiToolSchema) } : undefined;
|
|
122
|
+
if (entry.conversation) {
|
|
123
|
+
let receipt;
|
|
124
|
+
// Preserve the original goal owner's acknowledgement wait outside the
|
|
125
|
+
// invocation queue so the same Stop command can still interrupt it.
|
|
126
|
+
const operation = entry.provider.updateGoal({ context, configuration: metadata.runtime.configuration,
|
|
127
|
+
input: { ...input, ...goal }, representation, tools, interrupt: () => stopping.stop(entry, context),
|
|
128
|
+
async send({ message }, { threadId }) {
|
|
129
|
+
const admission = await serial(entry, async () => {
|
|
130
|
+
const state = await entries.readState(entry);
|
|
131
|
+
const selectedSegment = `${entry.engine}:${threadId}`;
|
|
132
|
+
if (!threadId || state.segmentId !== selectedSegment ||
|
|
133
|
+
input.expectedSegmentId !== null && input.expectedSegmentId !== selectedSegment) {
|
|
134
|
+
throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalConversationChanged" });
|
|
135
|
+
}
|
|
136
|
+
return sending.admitSend(entry, context, { messageId: input.messageId, text: message }, "user",
|
|
137
|
+
{ ...goal, expectedSegmentId: selectedSegment });
|
|
138
|
+
});
|
|
139
|
+
const accepted = await (admission.receipt || admission.promise);
|
|
140
|
+
if (accepted.error) throw accepted.error;
|
|
141
|
+
receipt = accepted.receipt;
|
|
142
|
+
}
|
|
143
|
+
}).then(async updated => {
|
|
144
|
+
if (updated.nativeResult.ok === false) throw Object.assign(new Error(updated.nativeResult.error), updated.nativeResult);
|
|
145
|
+
entry.goal = updated.goal;
|
|
146
|
+
await events.emit(entry, { type: "goal", goal: entry.goal });
|
|
147
|
+
return receipt ? { receipt } : { goal: structuredClone(entry.goal) };
|
|
148
|
+
});
|
|
149
|
+
return { operation };
|
|
150
|
+
}
|
|
151
|
+
let delivery;
|
|
152
|
+
const updated = await entry.provider.updateGoal({ context, configuration: metadata.runtime.configuration, input: { ...input, ...goal }, tools,
|
|
153
|
+
interrupt: () => stopping.stop(entry, context),
|
|
154
|
+
async send({ message }) {
|
|
155
|
+
delivery = await sending.admitSend(entry, context, { messageId: input.messageId, text: message }, "user", goal);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// Release the queue once Send is reserved, just as an ordinary message
|
|
159
|
+
// does. Stop remains available while native acknowledgement is pending.
|
|
160
|
+
if (delivery) return delivery;
|
|
161
|
+
entry.goal = updated;
|
|
162
|
+
await events.emit(entry, { type: "goal", goal: entry.goal });
|
|
163
|
+
return { goal: structuredClone(entry.goal) };
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
const completed = result.operation ? await result.operation : result;
|
|
167
|
+
return await (completed.receipt || completed.promise || completed.goal);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (!retryFinishedTurn || !entry.conversation || !starting || !messageCommand ||
|
|
170
|
+
error.code !== "conversation_steer_turn_finished" || error.delivery !== "not-sent") throw error;
|
|
171
|
+
// Completion can retire Claude's generation during asynchronous native
|
|
172
|
+
// preparation. Its exact pre-write rejection permits one re-admission of
|
|
173
|
+
// this same goal/message ID, after the original cleanup and save finish.
|
|
174
|
+
await entry.active?.done;
|
|
175
|
+
return updateGoal(entry, context, input, representation, false);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
readGoal,
|
|
181
|
+
updateGoal
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** @typedef {ReturnType<typeof createConversationGoals>} ConversationGoals */
|
|
186
|
+
|
|
187
|
+
export { createConversationGoals };
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { normalizeConversationConfiguration } from "./configuration.js";
|
|
3
|
+
import { failure } from "./conversationRefusals.js";
|
|
4
|
+
import { serial } from "./conversationEvents.js";
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
|
|
7
|
+
/** @typedef {import("./conversationEntries.js").HostConversation} HostConversation */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks the conversation a host supplies before the runtime binds it: a logical conversation
|
|
11
|
+
* brings its command, read and stream owners; a scoped one its native representation, identity
|
|
12
|
+
* and operations; a bound native one its identity, store, preparation and native owners.
|
|
13
|
+
* @param {HostConversation | null} conversation
|
|
14
|
+
* @param {{ id: string, configuration: object | undefined, representation: string }} request
|
|
15
|
+
*/
|
|
16
|
+
function assertHostConversation(conversation, { id, configuration, representation }) {
|
|
17
|
+
// readable-check: allow method-probe - input validation: the conversation a host supplies is checked before the runtime binds it
|
|
18
|
+
const offers = (owner, names) => names.every(name => typeof owner?.[name] === "function");
|
|
19
|
+
const scoped = conversation?.native?.scoped;
|
|
20
|
+
const commands = conversation?.commands;
|
|
21
|
+
const owner = conversation?.native?.owner;
|
|
22
|
+
const preparation = conversation?.native?.preparation;
|
|
23
|
+
const invalidClaudeOwner = conversation?.engine === "claude" && (!owner || typeof owner !== "object" || !offers(owner, ["acquire"]) ||
|
|
24
|
+
!offers(preparation, ["cleanup"]));
|
|
25
|
+
const invalidOpenCodeOwner = conversation?.engine === "opencode" && (!owner || typeof owner !== "object" ||
|
|
26
|
+
!offers(preparation, ["cleanup", ...(scoped ? ["turn", "existing"] : ["message", "inspection", "interruption"])]) ||
|
|
27
|
+
!scoped && !offers(conversation.native, ["acquire"]));
|
|
28
|
+
if (commands && (representation !== "canonical" || configuration !== undefined ||
|
|
29
|
+
!offers(commands, ["send", "cancel", "select", "inspectDelivery"]) || !offers(conversation, ["read", "readStream"]))) {
|
|
30
|
+
throw new TypeError("A logical conversation requires its complete original command, read and stream owners.");
|
|
31
|
+
}
|
|
32
|
+
if (scoped && (representation !== "native" || configuration !== undefined || !["codex", "claude", "opencode"].includes(conversation.engine) ||
|
|
33
|
+
typeof scoped.conversationId !== "string" || !scoped.conversationId ||
|
|
34
|
+
(conversation.engine === "claude" ? invalidClaudeOwner : conversation.engine === "opencode" ? invalidOpenCodeOwner
|
|
35
|
+
: !offers(preparation, ["cleanup"]) ||
|
|
36
|
+
!offers(conversation.native.runOwner, ["startConversationTurn", "readConversation", "waitForConversationTurn", "stopConversation", "deleteConversation"])))) {
|
|
37
|
+
throw new TypeError("An original scoped conversation requires its native representation, identity and existing operations.");
|
|
38
|
+
}
|
|
39
|
+
if (conversation && (conversation.sessionId !== id || typeof conversation.namespace !== "string" || !conversation.namespace ||
|
|
40
|
+
!["codex", "claude", "opencode"].includes(conversation.engine) || !scoped && !commands && (
|
|
41
|
+
(conversation.engine === "codex" ? !conversation.native?.runOwner || !conversation.native?.providerOwner
|
|
42
|
+
: conversation.engine === "claude" ? invalidClaudeOwner
|
|
43
|
+
: invalidOpenCodeOwner) ||
|
|
44
|
+
!offers(conversation, ["prepareInput", "read"])))) {
|
|
45
|
+
throw new TypeError("A bound native conversation requires its original identity, store, preparation and native owners.");
|
|
46
|
+
}
|
|
47
|
+
if (conversation?.maximumHistoryBytes !== undefined && !offers(conversation, ["maximumHistoryBytes"])) {
|
|
48
|
+
throw new TypeError("A bound native history budget requires a server-owned resolver.");
|
|
49
|
+
}
|
|
50
|
+
if (!["canonical", "native"].includes(representation) || representation === "native" && !conversation) {
|
|
51
|
+
throw new TypeError("Native result representation requires an original bound conversation.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Opening a conversation handle: the host's conversation is checked, its entry is created or
|
|
57
|
+
* reused, and the handle's operations are bound to that entry and caller context.
|
|
58
|
+
* @param {object} dependencies
|
|
59
|
+
* @param {import("./conversationEntries.js").ConversationEntries} dependencies.entries
|
|
60
|
+
* @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
|
|
61
|
+
* @param {import("../types.js").ConversationHost | undefined} dependencies.defaultHost
|
|
62
|
+
* @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
|
|
63
|
+
* @param {import("./conversationReads.js").ConversationReads} dependencies.reads
|
|
64
|
+
* @param {import("./conversationSending.js").ConversationSending} dependencies.sending
|
|
65
|
+
* @param {import("./conversationGoals.js").ConversationGoals} dependencies.goals
|
|
66
|
+
* @param {import("./conversationDelivery.js").ConversationDelivery} dependencies.delivery
|
|
67
|
+
* @param {import("./conversationStopping.js").ConversationStopping} dependencies.stopping
|
|
68
|
+
* @param {import("./conversationReplacement.js").ConversationReplacement} dependencies.replacement
|
|
69
|
+
* @param {import("./completedResponses.js").CompletedResponses} dependencies.completedResponses
|
|
70
|
+
* @param {boolean} dependencies.completedEnvelope
|
|
71
|
+
* @param {boolean} dependencies.persistCommentary
|
|
72
|
+
* @param {string | undefined} dependencies.defaultIntegrationId
|
|
73
|
+
*/
|
|
74
|
+
function createConversationHandles({ entries, storage, defaultHost, events, reads, sending, goals, delivery, stopping, replacement,
|
|
75
|
+
completedResponses, completedEnvelope, persistCommentary, defaultIntegrationId }) {
|
|
76
|
+
/**
|
|
77
|
+
* What a handle reads: a scoped conversation's own result, a bound conversation's native result,
|
|
78
|
+
* a logical conversation's snapshot from its own read, or the snapshot of its saved state.
|
|
79
|
+
* @param {ConversationEntry} entry
|
|
80
|
+
* @param {string} representation
|
|
81
|
+
* @param {object} context
|
|
82
|
+
* @param {object} [query]
|
|
83
|
+
*/
|
|
84
|
+
async function readSnapshot(entry, representation, context, query) {
|
|
85
|
+
if (entry.conversation?.native?.scoped) return entry.provider.readNative({ input: query, context });
|
|
86
|
+
if (representation === "native") return reads.readNativeResult(entry, context);
|
|
87
|
+
if (entry.conversation?.commands) {
|
|
88
|
+
const logical = await entry.conversation.read(context, query);
|
|
89
|
+
entries.followEngine(entry, logical.engine);
|
|
90
|
+
return reads.logicalSnapshot(entry, logical);
|
|
91
|
+
}
|
|
92
|
+
return reads.snapshot(entry, await entries.readState(entry, context), query);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function open({ id, context, configuration, engine, host, representation = "canonical" }) {
|
|
96
|
+
if (typeof id !== "string" || !id.trim()) throw new TypeError("A conversation id is required.");
|
|
97
|
+
await events.access(context, id, "open");
|
|
98
|
+
const selectedHost = host || defaultHost;
|
|
99
|
+
const conversation = selectedHost?.conversation ? await selectedHost.conversation({ id, context }) : null;
|
|
100
|
+
if (completedEnvelope && (conversation || representation !== "canonical")) {
|
|
101
|
+
throw failure("Completed envelopes require runtime-owned canonical storage.", "conversation_completed_envelopes_need_storage", 400, { messageCode: "assistant.conversation.completedEnvelopesNeedStorage" });
|
|
102
|
+
}
|
|
103
|
+
if (conversation && !persistCommentary) {
|
|
104
|
+
throw new TypeError("Transient commentary requires a runtime-owned transcript; supplied conversations retain their host's policy.");
|
|
105
|
+
}
|
|
106
|
+
assertHostConversation(conversation, { id, configuration, representation });
|
|
107
|
+
const entry = entries.acquire({ id, conversation, host: selectedHost, configuration, engine });
|
|
108
|
+
await entry.initializing;
|
|
109
|
+
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_handle_disposed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
110
|
+
if (host && !isDeepStrictEqual(host, entry.host)) throw failure("This open conversation already belongs to another host scope. Dispose its handle before reopening it on the original host.", "conversation_host_scope_mismatch", 409, { messageCode: "assistant.conversation.hostScopeMismatch" });
|
|
111
|
+
if (engine && engine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_use_replace_for_engine", 409, { messageCode: "assistant.conversation.useReplaceForEngine" });
|
|
112
|
+
if (configuration) {
|
|
113
|
+
const state = await entries.readState(entry);
|
|
114
|
+
const supplied = entry.conversation ? configuration
|
|
115
|
+
: normalizeConversationConfiguration(configuration, { engine: entry.engine, defaultIntegrationId });
|
|
116
|
+
if (!isDeepStrictEqual(supplied, state.configuration)) {
|
|
117
|
+
throw failure("Use configure() to change a saved conversation's settings.", "conversation_configuration_mismatch");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
id,
|
|
122
|
+
get capabilities() { return reads.capabilities(entry); },
|
|
123
|
+
replace: input => serial(entry, () => replacement.replace(entry, context, input, false, representation)),
|
|
124
|
+
select: input => serial(entry, () => replacement.replace(entry, context, input, true, representation)),
|
|
125
|
+
async read(query) {
|
|
126
|
+
await events.access(context, id, "read", entry);
|
|
127
|
+
const result = await readSnapshot(entry, representation, context, query);
|
|
128
|
+
if (entry.conversation?.commands) await events.access(context, id, "read", entry);
|
|
129
|
+
return result;
|
|
130
|
+
},
|
|
131
|
+
send: input => sending.send(entry, context, input, "user", undefined, representation),
|
|
132
|
+
wake: (input, options) => sending.send(entry, context, input, "application", undefined, representation, options),
|
|
133
|
+
...(completedEnvelope ? { prepareCompletedResponse: (input, options) =>
|
|
134
|
+
serial(entry, () => completedResponses.prepareCompletedResponse(entry, context, input, options)) } : {}),
|
|
135
|
+
readGoal: () => serial(entry, () => goals.readGoal(entry, context, representation)),
|
|
136
|
+
updateGoal: input => goals.updateGoal(entry, context, input, representation),
|
|
137
|
+
inspectDelivery({ messageId, threadId }) {
|
|
138
|
+
if (typeof messageId !== "string" || !messageId) throw new TypeError("A message id is required.");
|
|
139
|
+
return serial(entry, async () => {
|
|
140
|
+
if (representation !== "native") return delivery.inspectDelivery(entry, context, messageId);
|
|
141
|
+
await events.access(context, id, "inspectDelivery", entry);
|
|
142
|
+
return entry.provider.inspectAdmission({ messageId, threadId, context, representation });
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
async wait(input) {
|
|
146
|
+
await events.access(context, id, "read", entry);
|
|
147
|
+
if (entry.conversation?.commands) throw failure("This conversation uses its original read and subscription operations.", "conversation_original_read_only", 400, { messageCode: "assistant.conversation.originalReadOnly" });
|
|
148
|
+
if (entry.conversation?.native?.scoped) return entry.provider.waitNative({ input, context });
|
|
149
|
+
await entry.pending;
|
|
150
|
+
await entry.active?.done;
|
|
151
|
+
await events.access(context, id, "read", entry);
|
|
152
|
+
return reads.snapshot(entry, await entries.readState(entry));
|
|
153
|
+
},
|
|
154
|
+
// Cancel of one unsent or unconfirmed message always records it; an
|
|
155
|
+
// application conversation supplies its own operation for that.
|
|
156
|
+
async cancelDelivery({ messageId }) {
|
|
157
|
+
if (typeof messageId !== "string" || !messageId) throw new TypeError("A message id is required.");
|
|
158
|
+
await events.access(context, id, "cancel", entry);
|
|
159
|
+
if (entry.conversation?.commands) {
|
|
160
|
+
// readable-check: allow method-probe - optional operation: cancelling one message is optional in a logical conversation's commands
|
|
161
|
+
if (typeof entry.conversation.commands.cancelDelivery !== "function") {
|
|
162
|
+
throw failure("This conversation cannot cancel one message.", "conversation_message_cancel_unsupported", 400,
|
|
163
|
+
{ messageCode: "assistant.conversation.messageCancelUnsupported" });
|
|
164
|
+
}
|
|
165
|
+
return entry.conversation.commands.cancelDelivery({ messageId }, context);
|
|
166
|
+
}
|
|
167
|
+
return stopping.cancelDelivery(entry, context, messageId, representation);
|
|
168
|
+
},
|
|
169
|
+
async cancel(input) {
|
|
170
|
+
await events.access(context, id, "cancel", entry);
|
|
171
|
+
if (entry.conversation?.commands) return entry.conversation.commands.cancel(input, context);
|
|
172
|
+
await entry.pending;
|
|
173
|
+
await events.access(context, id, "cancel", entry);
|
|
174
|
+
return stopping.stop(entry, context, input, representation);
|
|
175
|
+
},
|
|
176
|
+
async retrySave() {
|
|
177
|
+
await events.access(context, id, "retrySave", entry);
|
|
178
|
+
return serial(entry, async () => {
|
|
179
|
+
await events.access(context, id, "retrySave", entry);
|
|
180
|
+
return stopping.retrySave(entry);
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
async configure(patch) {
|
|
184
|
+
await events.access(context, id, "configure", entry);
|
|
185
|
+
if (entry.conversation?.commands) throw failure("Use the conversation's authorized selection operation.", "conversation_authorized_selection_required", 400, { messageCode: "assistant.conversation.authorizedSelectionRequired" });
|
|
186
|
+
return serial(entry, async () => {
|
|
187
|
+
await events.access(context, id, "configure", entry);
|
|
188
|
+
if (entry.active) throw failure("Stop the current turn before changing its settings.", "conversation_busy", 409, { messageCode: "assistant.conversation.stopBeforeSettings" });
|
|
189
|
+
if (entry.storageFailure) throw entry.storageFailure;
|
|
190
|
+
if (entry.executionFailure) throw entry.executionFailure;
|
|
191
|
+
if (entry.conversation) {
|
|
192
|
+
if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_host_selection_required", 409, { messageCode: "assistant.conversation.hostSelectionRequired" });
|
|
193
|
+
return entry.conversation.selection.write({ configuration: patch }, context);
|
|
194
|
+
}
|
|
195
|
+
return storage.write(id, async transaction => {
|
|
196
|
+
const metadata = await transaction.readMetadata();
|
|
197
|
+
if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before changing settings.", "conversation_replacement_pending_before_settings", 409, { messageCode: "assistant.conversation.replacementPendingBeforeSettings" });
|
|
198
|
+
if (metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly)) {
|
|
199
|
+
throw failure("Resolve uncertain delivery before changing settings.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.deliveryUncertainBeforeSettings" });
|
|
200
|
+
}
|
|
201
|
+
const next = normalizeConversationConfiguration({ ...metadata.runtime.configuration, ...patch }, {
|
|
202
|
+
engine: metadata.runtime.engine, defaultIntegrationId
|
|
203
|
+
});
|
|
204
|
+
entry.driver.validateConfiguration(next);
|
|
205
|
+
metadata.runtime.configuration = next;
|
|
206
|
+
await transaction.writeMetadata(metadata);
|
|
207
|
+
return structuredClone(next);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
async subscribe(listener) {
|
|
212
|
+
await events.access(context, id, "subscribe", entry);
|
|
213
|
+
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original event delivery.", "conversation_event_delivery_retained", 409, { messageCode: "assistant.conversation.eventDeliveryRetained" });
|
|
214
|
+
if (typeof listener !== "function") throw new TypeError("A conversation listener is required.");
|
|
215
|
+
const subscription = { context, listener };
|
|
216
|
+
entry.listeners.add(subscription);
|
|
217
|
+
return () => entry.listeners.delete(subscription);
|
|
218
|
+
},
|
|
219
|
+
async dispose(options) {
|
|
220
|
+
await events.access(context, id, "dispose", entry);
|
|
221
|
+
return serial(entry, async () => {
|
|
222
|
+
await events.access(context, id, "dispose", entry);
|
|
223
|
+
const result = await stopping.disposeEntry(entry, context, options, representation);
|
|
224
|
+
if (result?.ok === false || representation === "native") return result;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
open
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @typedef {ReturnType<typeof createConversationHandles>} ConversationHandles */
|
|
236
|
+
|
|
237
|
+
export { createConversationHandles };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { codexAppServerTurnStateFromAgentRun } from "./codexTurn.js";
|
|
2
|
+
import { failure, stateErrorFields } from "./conversationRefusals.js";
|
|
3
|
+
|
|
4
|
+
/** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
|
|
5
|
+
/** @typedef {import("./conversationEntries.js").ConversationState} ConversationState */
|
|
6
|
+
|
|
7
|
+
/** The authored message of a saved turn: a user's message, or the application's. */
|
|
8
|
+
const submittedMessage = turn => turn.user || turn.system;
|
|
9
|
+
|
|
10
|
+
/** Every runtime segment a saved state retains: its predecessors and itself, once each. */
|
|
11
|
+
const retainedSegments = state => [...new Map([...state.predecessors, state].map(segment => [segment.segmentId, segment])).values()];
|
|
12
|
+
|
|
13
|
+
// readable-check: allow method-probe - optional operation: a host's conversation transcript may offer paged history
|
|
14
|
+
const offersPages = entry => typeof entry.conversation.transcript.readConversationLogPage === "function";
|
|
15
|
+
|
|
16
|
+
/** The predecessor whose replacement still waits for its successor's first accepted message. */
|
|
17
|
+
const pendingSuccessor = state => state.engine !== "api" && state.predecessors.findLast(segment => segment.successorId === state.segmentId &&
|
|
18
|
+
segment.segmentId !== state.segmentId && segment.replacement?.operation === "replace" && !segment.acceptedAt);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What an open conversation reads: its capabilities, its transcript, and the snapshot a reader
|
|
22
|
+
* sees. Nothing here changes the conversation.
|
|
23
|
+
* @param {object} dependencies
|
|
24
|
+
* @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
|
|
25
|
+
* @param {import("../types.js").ConversationTranscript | null} dependencies.transcript
|
|
26
|
+
* @param {import("../types.js").ConversationStreams} dependencies.streams
|
|
27
|
+
* @param {boolean} dependencies.attachmentsOffered Whether the application resolves message attachments.
|
|
28
|
+
* @param {boolean} dependencies.completedEnvelope
|
|
29
|
+
*/
|
|
30
|
+
function createConversationReads({ storage, transcript, streams, attachmentsOffered, completedEnvelope }) {
|
|
31
|
+
/** @param {ConversationEntry} entry */
|
|
32
|
+
function capabilities(entry) {
|
|
33
|
+
const value = { ...entry.driver.capabilities, ...entry.provider?.capabilities,
|
|
34
|
+
attachments: Boolean((attachmentsOffered || entry.conversation?.prepareInput || entry.conversation?.commands) && entry.driver.capabilities.attachments) };
|
|
35
|
+
if (entry.conversation?.commands) {
|
|
36
|
+
delete value.goalCommands;
|
|
37
|
+
Object.assign(value, { goals: false, configuration: false, wake: false, replacement: false });
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The saved turns; `afterTurnId` reads only the turns a stored summary doesn't cover.
|
|
44
|
+
* @param {ConversationEntry} entry
|
|
45
|
+
* @param {string} [afterTurnId]
|
|
46
|
+
*/
|
|
47
|
+
const readTranscript = (entry, afterTurnId = "") => entry.conversation
|
|
48
|
+
? entry.conversation.transcript.readConversationLog() : transcript.readConversationLog(entry.id, { afterTurnId });
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The stored compaction summary, when it belongs to this history. A bound conversation's host
|
|
52
|
+
* owns its transcript, so it has none.
|
|
53
|
+
* @param {ConversationEntry} entry
|
|
54
|
+
* @param {(turn: object) => boolean} inHistory
|
|
55
|
+
*/
|
|
56
|
+
async function readSummary(entry, inHistory) {
|
|
57
|
+
if (entry.conversation) return null;
|
|
58
|
+
return storage.read(entry.id, async transaction => {
|
|
59
|
+
const summary = await transaction.readSummary();
|
|
60
|
+
const covered = summary && await transaction.readTurn(summary.throughTurnId);
|
|
61
|
+
return covered && inHistory(covered) ? summary : null;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Identity lookups avoid reading the whole log when the transcript supports them. A bound host
|
|
67
|
+
* may supply findSubmittedTurn(messageId) and readTurn(turnId).
|
|
68
|
+
* @param {ConversationEntry} entry
|
|
69
|
+
* @param {string} messageId
|
|
70
|
+
*/
|
|
71
|
+
async function findSubmittedTurn(entry, messageId) {
|
|
72
|
+
const find = entry.conversation ? entry.conversation.transcript.findSubmittedTurn : transcript.findSubmittedTurn &&
|
|
73
|
+
(id => transcript.findSubmittedTurn(entry.id, id));
|
|
74
|
+
if (typeof find === "function") return (await find(messageId)) || null;
|
|
75
|
+
return (await readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === messageId) || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {ConversationEntry} entry
|
|
80
|
+
* @param {string} turnId
|
|
81
|
+
*/
|
|
82
|
+
async function readBoundTurn(entry, turnId) {
|
|
83
|
+
// readable-check: allow method-probe - optional operation: a host's conversation transcript may offer direct turn reads
|
|
84
|
+
if (typeof entry.conversation.transcript.readTurn === "function") return (await entry.conversation.transcript.readTurn(turnId)) || null;
|
|
85
|
+
return (await readTranscript(entry)).find(turn => turn.turnId === turnId) || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @param {ConversationEntry} entry */
|
|
89
|
+
async function lastTranscriptTurn(entry) {
|
|
90
|
+
const page = entry.conversation && !offersPages(entry) ? null
|
|
91
|
+
: entry.conversation ? await entry.conversation.transcript.readConversationLogPage({ limit: 1 })
|
|
92
|
+
: await transcript.readConversationLogPage(entry.id, { limit: 1 });
|
|
93
|
+
return page?.conversationLog.at(-1) || (await readTranscript(entry)).at(-1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {ConversationEntry} entry
|
|
98
|
+
* @param {object} query
|
|
99
|
+
*/
|
|
100
|
+
function readTranscriptPage(entry, query) {
|
|
101
|
+
if (!entry.conversation) return transcript.readConversationLogPage(entry.id, { ...query, presentation: completedEnvelope });
|
|
102
|
+
if (!offersPages(entry)) {
|
|
103
|
+
throw failure("This conversation does not supply paged history.", "conversation_paged_history_unsupported", 400, { messageCode: "assistant.conversation.pagedHistoryUnsupported" });
|
|
104
|
+
}
|
|
105
|
+
return entry.conversation.transcript.readConversationLogPage(query);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A bound conversation's own result, in its native representation.
|
|
110
|
+
* @param {ConversationEntry} entry
|
|
111
|
+
* @param {object} context
|
|
112
|
+
*/
|
|
113
|
+
async function readNativeResult(entry, context) {
|
|
114
|
+
const value = entry.driver.readState
|
|
115
|
+
? await entry.driver.readState({ conversation: entry.conversation, context, representation: "native" })
|
|
116
|
+
: await entry.conversation.read(context, "native");
|
|
117
|
+
return value.nativeResult;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* What a reader sees of a logical conversation, from the result of its own read.
|
|
122
|
+
* @param {ConversationEntry} entry
|
|
123
|
+
* @param {object} logical
|
|
124
|
+
*/
|
|
125
|
+
async function logicalSnapshot(entry, logical) {
|
|
126
|
+
const streaming = await entry.conversation.readStream();
|
|
127
|
+
const error = logical.error || "";
|
|
128
|
+
return {
|
|
129
|
+
id: entry.id, engine: entry.engine, capabilities: capabilities(entry), configuration: logical.configuration,
|
|
130
|
+
segmentId: logical.threadId ? `${logical.engine}:${logical.threadId}` : null,
|
|
131
|
+
replacement: null,
|
|
132
|
+
status: logical.status,
|
|
133
|
+
phase: logical.phase || "",
|
|
134
|
+
error,
|
|
135
|
+
...stateErrorFields(error),
|
|
136
|
+
goal: entry.goal || null,
|
|
137
|
+
pendingRequest: logical.pendingRequest || null,
|
|
138
|
+
...(logical.presentation ? { presentation: logical.presentation } : {}),
|
|
139
|
+
conversationLog: logical.conversationLog.filter(turn => !turn.metadata?.runtime?.supersededBy),
|
|
140
|
+
...(logical.pagination ? { pagination: logical.pagination } : {}),
|
|
141
|
+
streaming
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* What a reader sees of a conversation from its saved state: a page of its transcript, or all
|
|
147
|
+
* of it, with the current turn's status, phase, error and live stream.
|
|
148
|
+
* @param {ConversationEntry} entry
|
|
149
|
+
* @param {ConversationState} state
|
|
150
|
+
* @param {object} [query]
|
|
151
|
+
*/
|
|
152
|
+
async function snapshot(entry, state, query) {
|
|
153
|
+
const page = query ? await readTranscriptPage(entry, query) : null;
|
|
154
|
+
const conversationLog = page ? page.conversationLog
|
|
155
|
+
: completedEnvelope ? await transcript.readConversationLog(entry.id, { presentation: true }) : await readTranscript(entry);
|
|
156
|
+
// Browsing an older page does not change the current turn's control/error state.
|
|
157
|
+
const latest = completedEnvelope || query?.beforeTurnId
|
|
158
|
+
? entry.conversation ? (await readTranscriptPage(entry, { limit: 1 })).conversationLog
|
|
159
|
+
: (await transcript.readConversationLogPage(entry.id, { limit: 1 })).conversationLog : conversationLog;
|
|
160
|
+
const last = latest.at(-1)?.metadata?.runtime;
|
|
161
|
+
const savedRequest = state.request;
|
|
162
|
+
const nativeTurn = state.nativeTurn || (entry.driver.canonicalTranscript && state.binding?.codexAppServerRun
|
|
163
|
+
? codexAppServerTurnStateFromAgentRun(state.binding.codexAppServerRun) : null);
|
|
164
|
+
const request = savedRequest && (entry.engine === "api" || savedRequest.attempted || savedRequest.inspectionOnly ||
|
|
165
|
+
entry.active?.requests.has(savedRequest.messageId)) ? savedRequest : null;
|
|
166
|
+
const streaming = entry.conversation ? await entry.conversation.readStream() : streams.read(entry.id);
|
|
167
|
+
const error = entry.error || nativeTurn?.error || request?.error || last?.error || "";
|
|
168
|
+
const pendingRequest = request && !(completedEnvelope && request.origin === "application" && request.completedEnvelope === true) ? Object.fromEntries([
|
|
169
|
+
"messageId", "text", "origin", "data", "attachments", "at", "goal", "steering", "error"
|
|
170
|
+
].filter(name => Object.hasOwn(request, name)).map(name => [name, request[name]])) : null;
|
|
171
|
+
if (pendingRequest?.error) Object.assign(pendingRequest, stateErrorFields(pendingRequest.error));
|
|
172
|
+
return {
|
|
173
|
+
id: entry.id, engine: entry.engine, capabilities: capabilities(entry), configuration: state.configuration,
|
|
174
|
+
segmentId: state.segmentId,
|
|
175
|
+
replacement: state.replacement ? { operationId: state.replacement.request?.operationId || state.replacement.operationId,
|
|
176
|
+
reason: state.replacement.reason, segmentId: state.replacement.segmentId } : null,
|
|
177
|
+
status: entry.storageFailure || entry.executionFailure ? "unavailable" : entry.active || nativeTurn?.active ? "working" : state.replacement ? "replacement-pending"
|
|
178
|
+
: state.request && (entry.engine === "api" || state.request.attempted || state.request.inspectionOnly) ? "unconfirmed" : "ready",
|
|
179
|
+
phase: entry.phase || (nativeTurn?.active && nativeTurn.status !== "observation_lost" ? ["compacting", "background"].includes(nativeTurn.phase) ? nativeTurn.phase : "working" : ""),
|
|
180
|
+
error,
|
|
181
|
+
...stateErrorFields(error),
|
|
182
|
+
goal: entry.goal || null,
|
|
183
|
+
pendingRequest,
|
|
184
|
+
conversationLog: conversationLog.filter(turn => !turn.metadata?.runtime?.supersededBy),
|
|
185
|
+
...(page ? { pagination: page.pagination } : {}),
|
|
186
|
+
streaming: completedEnvelope && !entry.conversation ? { ...streaming,
|
|
187
|
+
messages: streaming.messages.filter(message => !(message.origin === "application" && message.completedEnvelope === true)) } : streaming
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
capabilities,
|
|
193
|
+
readTranscript,
|
|
194
|
+
readSummary,
|
|
195
|
+
findSubmittedTurn,
|
|
196
|
+
readBoundTurn,
|
|
197
|
+
lastTranscriptTurn,
|
|
198
|
+
readNativeResult,
|
|
199
|
+
logicalSnapshot,
|
|
200
|
+
snapshot
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @typedef {ReturnType<typeof createConversationReads>} ConversationReads */
|
|
205
|
+
|
|
206
|
+
export { createConversationReads, pendingSuccessor, retainedSegments, submittedMessage };
|