@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.
Files changed (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -0,0 +1,227 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { isCompletedEnvelopeTurn } from "./transcript.js";
3
+ import { conversationNativeIdentity } from "./runtimeStateUpgrade.js";
4
+ import { createConversationTools } from "./tools.js";
5
+ import { readAssistantResponseEnvelope } from "./boundedToolLoop.js";
6
+ import { conversationMessageIdentity, conversationMessageVersion } from "./continuity.js";
7
+ import { failure } from "./conversationRefusals.js";
8
+ import { submittedMessage } from "./conversationReads.js";
9
+ import { saveTools } from "./conversationTurns.js";
10
+ import { serial } from "./conversationEvents.js";
11
+
12
+ /** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
13
+
14
+ /**
15
+ * A completed native response handed to the application worker: its verified text, and
16
+ * either the plain reply it publishes or the one application operation it runs.
17
+ * @param {object} dependencies
18
+ * @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
19
+ * @param {import("../types.js").ConversationTranscript | null} dependencies.transcript
20
+ * @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
21
+ * @param {import("../types.js").ServiceToolCatalog | null} dependencies.catalog
22
+ * @param {() => boolean} dependencies.isClosed Whether the runtime has closed.
23
+ * @param {boolean} dependencies.completedEnvelope
24
+ * @param {number} dependencies.maximumToolCalls
25
+ */
26
+ function createCompletedResponses({ storage, transcript, events, catalog, isClosed, completedEnvelope, maximumToolCalls }) {
27
+ /**
28
+ * @param {ConversationEntry} entry
29
+ * @param {object} context
30
+ * @param {{ messageId: string, turnId: string }} receipt
31
+ * @param {{ signal: AbortSignal, toolSet?: object }} options
32
+ */
33
+ async function prepareCompletedResponse(entry, context, { messageId, turnId }, { signal, toolSet }) {
34
+ if (!completedEnvelope || entry.conversation || !["codex", "claude", "opencode"].includes(entry.engine)) {
35
+ throw failure("This conversation does not own completed envelopes.", "conversation_completed_envelopes_not_owned", 400, { messageCode: "assistant.conversation.completedEnvelopesNotOwned" });
36
+ }
37
+ if (typeof messageId !== "string" || !messageId || typeof turnId !== "string" || !turnId || !(signal instanceof AbortSignal)) {
38
+ throw new TypeError("Completed responses require their exact receipt and the application worker signal.");
39
+ }
40
+ signal.throwIfAborted();
41
+ await events.access(context, entry.id, "tool", entry);
42
+ const owner = entry.bindingOwner;
43
+ const segmentId = entry.segmentId;
44
+ const engine = entry.engine;
45
+ const toolCallId = `${messageId}:operation`;
46
+ if (toolCallId.length > 256) throw new TypeError("The completed response call identity exceeds its limit.");
47
+ let captured;
48
+
49
+ async function exact(transaction, { retry = false } = {}) {
50
+ if (isClosed() || entry.disposed || entry.bindingOwner !== owner || entry.segmentId !== segmentId || entry.engine !== engine) {
51
+ throw failure("The completed response owner changed.", "conversation_response_owner_changed", 409, { messageCode: "assistant.conversation.responseOwnerChanged" });
52
+ }
53
+ if (entry.active) throw failure("Wait for the native response to finish.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForNativeResponse" });
54
+ if (entry.executionFailure) throw entry.executionFailure;
55
+ if (entry.storageFailure && !retry) throw entry.storageFailure;
56
+ const { runtime } = await transaction.readMetadata();
57
+ const turn = await transaction.readTurn(turnId);
58
+ const authored = submittedMessage(turn || {});
59
+ const saved = turn?.metadata?.runtime;
60
+ if (runtime?.version !== 3 || runtime.engine !== engine || runtime.segmentId !== segmentId ||
61
+ runtime.replacement || runtime.request || !conversationNativeIdentity(runtime) || authored?.messageId !== messageId ||
62
+ authored.origin !== "application" || saved?.completedEnvelope !== true || saved.engine !== engine ||
63
+ saved.segmentId !== segmentId || saved.status !== "complete" || saved.supersededBy ||
64
+ typeof turn.assistant?.text !== "string" || !turn.assistant.text || !turn.assistant.messageId) {
65
+ throw failure("The completed response has no current verified receipt.", "conversation_completed_response_no_receipt", 409, { messageCode: "assistant.conversation.completedResponseNoReceipt" });
66
+ }
67
+ const turnIds = await transaction.listTurnIds();
68
+ for (const id of turnIds.slice(turnIds.indexOf(turnId) + 1)) {
69
+ const later = await transaction.readTurn(id);
70
+ if (later.metadata?.runtime?.segmentId === segmentId && later.metadata.runtime.completedEnvelope === true) {
71
+ throw failure("A newer completed response owns this worker.", "conversation_newer_response_owns_worker", 409, { messageCode: "assistant.conversation.newerResponseOwnsWorker" });
72
+ }
73
+ }
74
+ // These verified process references may refresh on cold inspection;
75
+ // all native conversation, path and account custody remains exact.
76
+ const { codexAppServerRun: run, executionId, processDirectory, ...binding } = runtime.binding;
77
+ if (engine === "codex" && (!saved.nativeTurnId || run?.outerTurnId !== messageId ||
78
+ run.providerThreadId !== binding.threadId || run.providerTurnId !== saved.nativeTurnId || run.active || run.state !== "completed")) {
79
+ throw failure("The completed response has no exact native turn receipt.", "conversation_completed_response_no_turn_receipt", 409, { messageCode: "assistant.conversation.completedResponseNoTurnReceipt" });
80
+ }
81
+ if (engine === "claude" && typeof saved.nativeToolAttempt !== "boolean") {
82
+ throw failure("The completed Claude response has no native tool-attempt evidence.", "conversation_completed_claude_no_tool_evidence", 409, { messageCode: "assistant.conversation.completedClaudeNoToolEvidence" });
83
+ }
84
+ const calls = turn.metadata.applicationTools || [];
85
+ if (!Array.isArray(calls) || calls.length > 1 || calls.some(call => call.id !== toolCallId)) {
86
+ throw failure("This response already belongs to a different application operation.", "conversation_response_other_operation", 409, { messageCode: "assistant.conversation.responseOtherOperation" });
87
+ }
88
+ const identity = { configuration: runtime.configuration, binding, assistant: turn.assistant,
89
+ nativeTurnId: saved.nativeTurnId, ...(engine === "claude" ? { nativeToolAttempt: saved.nativeToolAttempt } : {}) };
90
+ if (captured && !isDeepStrictEqual(captured, identity)) {
91
+ throw failure("The completed response identity changed.", "conversation_response_identity_changed", 409, { messageCode: "assistant.conversation.responseIdentityChanged" });
92
+ }
93
+ return { runtime, turn, calls, identity };
94
+ }
95
+
96
+ async function inspect() {
97
+ signal.throwIfAborted();
98
+ await events.access(context, entry.id, "tool", entry);
99
+ const saved = await storage.read(entry.id, transaction => exact(transaction));
100
+ const observation = await entry.provider.inspectAdmission({ messageId,
101
+ threadId: conversationNativeIdentity(saved.runtime), nativeTurnId: saved.turn.metadata.runtime.nativeTurnId,
102
+ configuration: saved.runtime.configuration, receiptBinding: saved.runtime.binding, context, verifyAccount: true });
103
+ signal.throwIfAborted();
104
+ await events.access(context, entry.id, "tool", entry);
105
+ if (!observation?.accepted || observation.recoveryLimitation || observation.conversationTurn &&
106
+ observation.conversationTurn.turnId !== turnId || engine === "codex" && observation.nativeTurnId !== saved.turn.metadata.runtime.nativeTurnId) {
107
+ throw failure("The native owner could not verify this response association.", "conversation_response_association_unverified", 409, { messageCode: "assistant.conversation.responseAssociationUnverified" });
108
+ }
109
+ // Canonical successful completion owns the exact final, including a
110
+ // StructuredOutput carrier whose native history may contain only commentary.
111
+ captured ||= structuredClone(saved.identity);
112
+ return storage.read(entry.id, transaction => exact(transaction));
113
+ }
114
+
115
+ const response = await inspect();
116
+ return { text: response.turn.assistant.text, toolCallId,
117
+ ...(engine === "claude" ? { nativeToolAttempt: response.turn.metadata.runtime.nativeToolAttempt } : {}),
118
+ publishReply(input) {
119
+ return serial(entry, async () => {
120
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some(key => key !== "turnId") ||
121
+ typeof input.turnId !== "string" || !input.turnId || input.turnId !== input.turnId.trim() || input.turnId.length > 128) {
122
+ throw new TypeError("Reply publication requires only its exact existing plain turn id.");
123
+ }
124
+ await inspect();
125
+ return storage.write(entry.id, async transaction => {
126
+ const saved = await exact(transaction);
127
+ signal.throwIfAborted();
128
+ const decoded = readAssistantResponseEnvelope(saved.turn.assistant.text);
129
+ if (decoded.kind !== "reply" || saved.turn.metadata.runtime.nativeToolAttempt === true || saved.calls.length) {
130
+ throw failure("This completed response does not own a plain reply.", "conversation_completed_response_no_plain_reply", 409, { messageCode: "assistant.conversation.completedResponseNoPlainReply" });
131
+ }
132
+ const text = decoded.text.trim();
133
+ const target = await transaction.readTurn(input.turnId);
134
+ if (!text || input.turnId === turnId || !target || isCompletedEnvelopeTurn(target) ||
135
+ target.metadata?.runtime?.engine || !(target.user || target.system) || saved.runtime.lastEngine !== engine ||
136
+ !saved.runtime.seen || typeof saved.runtime.seen !== "object" || Array.isArray(saved.runtime.seen)) {
137
+ throw failure("The completed reply has no current plain authored target.", "conversation_completed_reply_no_target", 409, { messageCode: "assistant.conversation.completedReplyNoTarget" });
138
+ }
139
+ const assistantId = conversationMessageIdentity(input.turnId, { role: "assistant" });
140
+ const published = saved.turn.metadata.runtime.publishedReplyTurnId;
141
+ if (published !== undefined) {
142
+ if (published !== input.turnId || !target.assistant || target.assistant.text !== text ||
143
+ saved.runtime.seen[assistantId] !== conversationMessageVersion(target.assistant, { includeData: true })) {
144
+ throw failure("The published reply target or text changed.", "conversation_published_reply_changed", 409, { messageCode: "assistant.conversation.publishedReplyChanged" });
145
+ }
146
+ signal.throwIfAborted();
147
+ return target;
148
+ }
149
+ if (target.assistant || Object.hasOwn(saved.runtime.seen, assistantId)) {
150
+ throw failure("This plain reply target already has unassociated content or consumption.", "conversation_reply_target_used", 409, { messageCode: "assistant.conversation.replyTargetUsed" });
151
+ }
152
+ // The runtime transcript uses this same reentrant wrapper: its
153
+ // original upsert joins this draft rather than starting a second write.
154
+ const plain = await transcript.upsertConversationAssistantMessage(entry.id, { turnId: input.turnId, text });
155
+ saved.runtime.seen[assistantId] = conversationMessageVersion(plain.assistant, { includeData: true });
156
+ await transaction.updateTurnMetadata(turnId, { runtime: { ...saved.turn.metadata.runtime, publishedReplyTurnId: input.turnId } });
157
+ const metadata = await transaction.readMetadata();
158
+ await transaction.writeMetadata({ ...metadata, runtime: saved.runtime });
159
+ await exact(transaction);
160
+ signal.throwIfAborted();
161
+ return plain;
162
+ });
163
+ });
164
+ },
165
+ tools: { execute(input, options = {}) {
166
+ return serial(entry, async () => {
167
+ if (input?.id !== toolCallId) throw failure("The operation has a different completed response identity.", "conversation_operation_response_identity", 409, { messageCode: "assistant.conversation.operationResponseIdentity" });
168
+ const saved = await inspect();
169
+ if (saved.turn.metadata.runtime.publishedReplyTurnId !== undefined) {
170
+ throw failure("This completed response already published its plain reply.", "conversation_plain_reply_published", 409, { messageCode: "assistant.conversation.plainReplyPublished" });
171
+ }
172
+ const workerSignal = options.signal && options.signal !== signal ? AbortSignal.any([signal, options.signal]) : signal;
173
+ workerSignal.throwIfAborted();
174
+ const request = { turnId, input: { messageId } };
175
+ async function persist(calls, retry = false) {
176
+ await storage.write(entry.id, async transaction => {
177
+ const current = await exact(transaction, { retry });
178
+ const previous = current.calls[0];
179
+ const next = calls[0];
180
+ if (calls.length !== 1 || next?.id !== toolCallId || previous &&
181
+ (previous.name !== next.name || previous.arguments !== next.arguments ||
182
+ previous.status !== "running" && next.status === "running")) {
183
+ throw failure("The saved response operation changed.", "conversation_saved_operation_changed", 409, { messageCode: "assistant.conversation.savedOperationChanged" });
184
+ }
185
+ await saveTools(entry, transaction, request, calls);
186
+ });
187
+ }
188
+ const tools = createConversationTools({ catalog, toolSet, context, signal: workerSignal, previousCalls: saved.calls,
189
+ maximumCalls: maximumToolCalls, discoveryOnly: false,
190
+ authorize: () => events.access(context, entry.id, "tool", entry),
191
+ prepareContext: async () => {
192
+ await storage.read(entry.id, transaction => exact(transaction));
193
+ return context;
194
+ },
195
+ async save(calls) {
196
+ try { await persist(calls); }
197
+ catch (error) {
198
+ if (calls[0]?.status !== "running") {
199
+ const snapshot = structuredClone(calls);
200
+ entry.error = "The application operation result could not be saved. Restore storage access and retry saving.";
201
+ entry.storageFailure = failure(entry.error, "conversation_storage_unavailable", 503);
202
+ entry.storageFailure.cause = error;
203
+ entry.saveProgress = async () => {
204
+ await persist(snapshot, true);
205
+ entry.storageFailure = null;
206
+ entry.saveProgress = null;
207
+ entry.error = "";
208
+ return { type: "tool", turnId, call: snapshot[0] };
209
+ };
210
+ }
211
+ throw error;
212
+ }
213
+ },
214
+ emit: event => events.emit(entry, { ...event, turnId }) });
215
+ return tools.execute(input, { signal: workerSignal });
216
+ });
217
+ } } };
218
+ }
219
+
220
+ return Object.freeze({
221
+ prepareCompletedResponse
222
+ });
223
+ }
224
+
225
+ /** @typedef {ReturnType<typeof createCompletedResponses>} CompletedResponses */
226
+
227
+ export { createCompletedResponses };
@@ -23,8 +23,8 @@ export function validateConversationConfiguration(configuration, { engine, conne
23
23
  throw new TypeError("Conversation configuration requires systemPrompt and optional integrationId, model and effort.");
24
24
  }
25
25
  if ((connectionRequired || configuration.integrationId !== undefined) &&
26
- (!configuration.integrationId || typeof connections?.resolve !== "function") &&
27
- typeof apiClientFactory?.resolveClient !== "function") {
26
+ // readable-check: allow method-probe - input validation: an API conversation needs a connection resolver or a client factory
27
+ (!configuration.integrationId || typeof connections?.resolve !== "function") && typeof apiClientFactory?.resolveClient !== "function") {
28
28
  throw new TypeError(`${engine} requires an authorized integrationId and connection resolver.`);
29
29
  }
30
30
  if (configuration.effort !== undefined && !efforts.includes(configuration.effort)) {
@@ -1,5 +1,7 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
1
2
  import { createHash } from "node:crypto";
2
3
  import { isCompletedEnvelopeTurn } from "./transcript.js";
4
+ import { ignorePromptRejection, ignorePromptSending } from "./nativeCommandCallbacks.js";
3
5
 
4
6
  // A final answer keeps its identity when an application edits its visible text.
5
7
  export function conversationMessageIdentity(turnId, message) {
@@ -14,10 +16,32 @@ export function conversationMessageVersion(message, { includeData = false } = {}
14
16
  })).digest("hex");
15
17
  }
16
18
 
17
- /** Keep native delivery and its history cursor in the same durable record. */
18
- export function createConversationChangeover({ state, transcript, agent, identity, presentation = {}, log = () => {},
19
+ // A changeover whose owner keeps no delivery log.
20
+ function logNothing() {}
21
+
22
+ /**
23
+ * Keep native delivery and its history cursor in the same durable record.
24
+ * @param {object} options
25
+ * @param {{ read(): Promise<object | null>, write(value: object): Promise<void>,
26
+ * transaction?(callback: (transaction: object) => Promise<void>): Promise<void> }} options.state
27
+ * The saved delivery record, and the transaction that releases a replaced binding.
28
+ * @param {{ hasMessage(messageId: string): Promise<boolean>, writeUserMessage(message: object): Promise<void> }} [options.transcript]
29
+ * The conversation's own transcript, for messages delivered natively.
30
+ * @param {{ closeSession?(): Promise<{ ok: boolean, error?: string }>, sendMessage?(input: object): Promise<object>,
31
+ * inspectMessageAdmission?(request: { messageId: string }): Promise<object> }} [options.agent] The native engine's session.
32
+ * @param {{ inspect(expectedId: string): Promise<object> }} [options.identity] The native predecessor's identity.
33
+ * @param {object} [options.presentation] How history is rendered for the native engine.
34
+ * @param {(event: object) => void} [options.log] Receives each delivery event.
35
+ * @param {boolean} [options.captureContext]
36
+ * @param {boolean} [options.applicationMessages]
37
+ * @param {number} [options.maximumInitialMessages]
38
+ * @param {number} [options.maximumInitialMessageCharacters]
39
+ * @param {number | ((request: object) => Promise<number>)} [options.maximumHistoryBytes]
40
+ */
41
+ export function createConversationChangeover({ state, transcript, agent, identity, presentation = {}, log = logNothing,
19
42
  captureContext = false, applicationMessages = false, maximumInitialMessages = 30,
20
43
  maximumInitialMessageCharacters = Infinity, maximumHistoryBytes = Infinity }) {
44
+ // readable-check: allow method-probe - data: the history budget is a byte count or a resolver of one
21
45
  if (typeof maximumHistoryBytes !== "function" && maximumHistoryBytes !== Infinity &&
22
46
  (!Number.isSafeInteger(maximumHistoryBytes) || maximumHistoryBytes < 1) ||
23
47
  maximumInitialMessages === Infinity && maximumHistoryBytes === Infinity) {
@@ -72,7 +96,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
72
96
 
73
97
  async function replace({ engineId, messages, operationId, expectedId, handover }) {
74
98
  const fail = (message, translation = {}) => {
75
- throw Object.assign(new Error(message), { code: replacementUnavailableCode, statusCode: 409 }, translation);
99
+ throw new AppError(409, message, { code: replacementUnavailableCode, ...translation });
76
100
  };
77
101
  const value = await readState({ engineId, messages });
78
102
  let replacement = value.replacement;
@@ -285,6 +309,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
285
309
  returning, messageCount: catchup.length, correctionCount: catchup.filter((m) => binding.seen[m.id]).length,
286
310
  removedCount: deleted.length, promptCharacters: preamble.length });
287
311
  }
312
+ const { onPromptSending = ignorePromptSending, onPromptRejected = ignorePromptRejection } = input;
288
313
  const delivered = await agent.sendMessage(pending ? {
289
314
  ...input, message: pending.message, displayMessage: pending.displayMessage,
290
315
  displayAttachments: pending.displayAttachments, attachmentIds: pending.attachmentIds,
@@ -296,7 +321,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
296
321
  (!threadId || threadId === replacement.previous.conversationId)) {
297
322
  throw new Error("Native replacement must use a fresh successor before sending its briefing.");
298
323
  }
299
- await input.onPromptSending?.({ threadId, displayAttachments, turnMetadata });
324
+ await onPromptSending({ threadId, displayAttachments, turnMetadata });
300
325
  pending.threadId = threadId;
301
326
  pending.displayAttachments = displayAttachments || pending.displayAttachments;
302
327
  pending.turnMetadata = turnMetadata || pending.turnMetadata;
@@ -304,7 +329,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
304
329
  await persist();
305
330
  },
306
331
  onPromptRejected: async () => {
307
- await input.onPromptRejected?.();
332
+ await onPromptRejected();
308
333
  pending.attempted = false;
309
334
  await persist();
310
335
  }
@@ -328,7 +353,19 @@ export function createConversationChangeover({ state, transcript, agent, identit
328
353
  return true;
329
354
  }
330
355
 
331
- return { readState, remember, replace, send, discard };
356
+ return Object.freeze({
357
+
358
+ readState,
359
+
360
+ remember,
361
+
362
+ replace,
363
+
364
+ send,
365
+
366
+ discard
367
+
368
+ });
332
369
  }
333
370
 
334
371
  function writtenMessages(history, native = false) {