@jskit-ai/assistant-core 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -7,6 +7,7 @@ export function createConversationOperationLease() {
7
7
  const idle = new Promise((resolve) => {
8
8
  resolveIdle = resolve;
9
9
  });
10
+ // readable-check: allow unfrozen-result - data: a lease counts its running operations in place
10
11
  return {
11
12
  idle,
12
13
  operations: 0,
@@ -29,10 +30,13 @@ export function finishConversationOperation(lease, participant) {
29
30
  }
30
31
  }
31
32
 
32
- /** Nested native store operations share the host's current transaction. The
33
+ /**
34
+ * Nested native store operations share the host's current transaction. The
33
35
  * supplied storage remains the only writer queue and commit/rollback owner.
36
+ * @param {import("../types.js").ConversationStorage} storage
34
37
  */
35
38
  export function createReentrantConversationStorage(storage) {
39
+ // readable-check: allow method-probe - input validation: the application's storage adapter must offer read and write
36
40
  if (typeof storage?.read !== "function" || typeof storage?.write !== "function") {
37
41
  throw new TypeError("Conversation storage requires read(scope, callback) and write(scope, callback).");
38
42
  }
@@ -52,26 +56,31 @@ export function createReentrantConversationStorage(storage) {
52
56
  }
53
57
  }
54
58
 
59
+ function read(scope, operation) {
60
+ const context = inherited(scope);
61
+ return context ? participate(context, operation) : storage.read(scope, operation);
62
+ }
63
+
64
+ function write(scope, operation) {
65
+ const context = inherited(scope);
66
+ if (context) return participate(context, operation);
67
+ return storage.write(scope, async transaction => {
68
+ const lease = createConversationOperationLease();
69
+ const participant = beginConversationOperation(lease);
70
+ try {
71
+ return await mutationContext.run({ scope, transaction, lease, participant }, () => operation(transaction));
72
+ } finally {
73
+ finishConversationOperation(lease, participant);
74
+ await lease.idle;
75
+ }
76
+ });
77
+ }
78
+
55
79
  return Object.freeze({
56
80
  ...storage,
57
- read(scope, operation) {
58
- const context = inherited(scope);
59
- return context ? participate(context, operation) : storage.read(scope, operation);
60
- },
61
- write(scope, operation) {
62
- const context = inherited(scope);
63
- if (context) return participate(context, operation);
64
- return storage.write(scope, async transaction => {
65
- const lease = createConversationOperationLease();
66
- const participant = beginConversationOperation(lease);
67
- try {
68
- return await mutationContext.run({ scope, transaction, lease, participant }, () => operation(transaction));
69
- } finally {
70
- finishConversationOperation(lease, participant);
71
- await lease.idle;
72
- }
73
- });
74
- },
81
+ read,
82
+ write,
83
+ // readable-check: allow method-probe - optional operation: a storage adapter may also delete a conversation
75
84
  ...(typeof storage.deleteConversation === "function"
76
85
  ? { deleteConversation: scope => storage.deleteConversation(scope) } : {})
77
86
  });
@@ -85,8 +94,8 @@ function conversationMessageStorageKey({ role, at, messageId } = {}) {
85
94
  const timestamp = date.toISOString().replace(/[-:]/gu, "").replace(".", "");
86
95
  const normalizedMessageId = String(messageId || "").trim();
87
96
  const idSuffix = normalizedMessageId ? `.${normalizedMessageId}` : "";
88
- // The original transcript overwrote this same role/timestamp/id file. Its
89
- // filesystem suffix restrictions do not apply to record-native message IDs.
97
+ // One role, timestamp and message ID name one message, so saving it again
98
+ // replaces it. Record-native message IDs need no filesystem-safe suffix.
90
99
  return `${normalizedRole}.${timestamp}${idSuffix}.md`;
91
100
  }
92
101
 
@@ -102,13 +111,21 @@ function conversationMessageIndex(record) {
102
111
  return record.index;
103
112
  }
104
113
 
105
- /** Transaction implementation for single-writer record adapters. This serializes
114
+ /**
115
+ * Transaction implementation for single-writer record adapters. This serializes
106
116
  * one instance's writes; a database with multiple writers must also hold its own
107
117
  * transaction/lock across readRecord and writeRecord, or implement read/write directly.
108
118
  * An adapter may supply recordVersion(id), a cheap token that changes whenever
109
119
  * the saved record changes; the parsed record is then reused until it changes.
120
+ * @param {object} adapter
121
+ * @param {(id: string) => Promise<{ turns: Map<string, object>, metadata: object, summary?: object }>} adapter.readRecord
122
+ * @param {(id: string, record: object, view: object) => Promise<void>} adapter.writeRecord
123
+ * @param {(id: string) => Promise<void>} adapter.deleteRecord
124
+ * @param {(id: string) => Promise<unknown>} [adapter.recordVersion]
110
125
  */
111
126
  export function createConversationStorage({ readRecord, writeRecord, deleteRecord, recordVersion }) {
127
+ // readable-check: allow method-probe - optional operation: an adapter without recordVersion reads its record every time and keeps no cache
128
+ const versioned = typeof recordVersion === "function";
112
129
  const pending = new Map();
113
130
  const cache = new Map();
114
131
 
@@ -118,7 +135,7 @@ export function createConversationStorage({ readRecord, writeRecord, deleteRecor
118
135
  }
119
136
 
120
137
  async function readSaved(id) {
121
- if (typeof recordVersion !== "function") return readRecord(id);
138
+ if (!versioned) return readRecord(id);
122
139
  const version = await recordVersion(id);
123
140
  const cached = cache.get(id);
124
141
  if (version != null && cached?.version === version) return cached.record;
@@ -130,7 +147,8 @@ export function createConversationStorage({ readRecord, writeRecord, deleteRecor
130
147
 
131
148
  // Saved records are shared and never changed in place. A transaction copies
132
149
  // the turn map and each turn it changes, then commits that new record.
133
- // Adapter fields the transaction does not manage are carried unchanged.
150
+ // Adapter fields the transaction does not manage are carried unchanged. The
151
+ // record's optional `summary` is { text, throughTurnId }, written by compaction.
134
152
  function transaction(saved) {
135
153
  const { index: _index, ...draft } = saved;
136
154
  const added = new Map();
@@ -157,6 +175,24 @@ export function createConversationStorage({ readRecord, writeRecord, deleteRecor
157
175
  return { ...draft, index };
158
176
  }
159
177
  const turns = () => draft.turns;
178
+ function readTurn(id) {
179
+ const turn = turns().get(id);
180
+ if (!turn) return null;
181
+ const find = (role) => turn.messages.find((message) => message.role === role) || null;
182
+ const thinking = turn.messages.filter((message) => message.role === "thinking");
183
+ const commentary = turn.messages.filter((message) => message.role === "commentary");
184
+ // Native continuations (for example Claude background-task notifications)
185
+ // can add later replies to the same request turn. Every saved reply stays
186
+ // in the ordered message list; the latest one still closes it.
187
+ const replies = turn.messages.filter((message) => message.role === "assistant");
188
+ const activity = [...thinking, ...commentary, ...replies.slice(0, -1)].sort((a, b) => a.at.localeCompare(b.at));
189
+ const system = find("system"), user = find("user"), assistant = find("assistant");
190
+ return structuredClone({
191
+ turnId: id, user, assistant, thinking, commentary,
192
+ ...(system ? { system } : {}), ...(turn.metadata ? { metadata: turn.metadata } : {}),
193
+ messages: [system, user, ...activity, replies.at(-1)].filter(Boolean)
194
+ });
195
+ }
160
196
  function messageTurnId(id) {
161
197
  if (!id) return "";
162
198
  const turnId = added.get(id) ?? conversationMessageIndex(saved).get(id);
@@ -180,23 +216,20 @@ export function createConversationStorage({ readRecord, writeRecord, deleteRecor
180
216
  async hasMessage(id) { return Boolean(messageTurnId(id)); },
181
217
  // Optional transaction extension: the turn holding this message ID.
182
218
  async findMessageTurnId(id) { return messageTurnId(id); },
183
- async readTurn(id) {
184
- const turn = turns().get(id);
185
- if (!turn) return null;
186
- const find = (role) => turn.messages.find((message) => message.role === role) || null;
187
- const thinking = turn.messages.filter((message) => message.role === "thinking");
188
- const commentary = turn.messages.filter((message) => message.role === "commentary");
189
- // Native continuations (for example Claude background-task notifications)
190
- // can add later replies to the same request turn. Every saved reply stays
191
- // in the ordered message list; the latest one still closes it.
192
- const replies = turn.messages.filter((message) => message.role === "assistant");
193
- const activity = [...thinking, ...commentary, ...replies.slice(0, -1)].sort((a, b) => a.at.localeCompare(b.at));
194
- const system = find("system"), user = find("user"), assistant = find("assistant");
195
- return structuredClone({
196
- turnId: id, user, assistant, thinking, commentary,
197
- ...(system ? { system } : {}), ...(turn.metadata ? { metadata: turn.metadata } : {}),
198
- messages: [system, user, ...activity, replies.at(-1)].filter(Boolean)
199
- });
219
+ async readTurn(id) { return readTurn(id); },
220
+ // The turns after `turnId`, in order ("" is all of them).
221
+ async readTurnsAfter(turnId) {
222
+ const ids = [...turns().keys()];
223
+ const start = turnId ? ids.indexOf(turnId) + 1 : 0;
224
+ if (!start && turnId) throw new Error("The conversation turn does not exist.");
225
+ return ids.slice(start).map(readTurn);
226
+ },
227
+ // The compaction summary: its text and the last turn it covers.
228
+ async readSummary() { return draft.summary ? structuredClone(draft.summary) : null; },
229
+ async writeSummary({ text, throughTurnId }) {
230
+ if (typeof text !== "string" || !text.trim()) throw new TypeError("A conversation summary requires text.");
231
+ if (!turns().get(throughTurnId)) throw new Error("The conversation turn does not exist.");
232
+ draft.summary = { text, throughTurnId };
200
233
  },
201
234
  async appendMessage(id, { turnMetadata, ...message }) {
202
235
  const turn = ownTurn(id);
@@ -234,7 +267,7 @@ export function createConversationStorage({ readRecord, writeRecord, deleteRecor
234
267
  else {
235
268
  const record = draft.record();
236
269
  await writeRecord(id, record, draft.view);
237
- const version = typeof recordVersion === "function" ? await recordVersion(id) : null;
270
+ const version = versioned ? await recordVersion(id) : null;
238
271
  if (version != null) cache.set(id, { version, record });
239
272
  }
240
273
  return result;
@@ -245,15 +278,23 @@ export function createConversationStorage({ readRecord, writeRecord, deleteRecor
245
278
  finally { if (pending.get(id) === settled) pending.delete(id); }
246
279
  }
247
280
 
281
+ async function read(scope, callback) {
282
+ const id = key(scope);
283
+ await pending.get(id);
284
+ return callback(transaction(await readSaved(id)).view);
285
+ }
286
+
287
+ async function deleteConversation(scope) {
288
+ await write(scope, async () => {}, true);
289
+ }
290
+
248
291
  return Object.freeze({
292
+
249
293
  write,
250
- async read(scope, callback) {
251
- const id = key(scope);
252
- await pending.get(id);
253
- return callback(transaction(await readSaved(id)).view);
254
- },
255
- async deleteConversation(scope) {
256
- await write(scope, async () => {}, true);
257
- }
294
+
295
+ read,
296
+
297
+ deleteConversation
298
+
258
299
  });
259
300
  }
@@ -1,4 +1,7 @@
1
- /** Transient provider output. Applications own scope, admission and final persistence. */
1
+ /**
2
+ * Transient provider output. Applications own scope, admission and final persistence.
3
+ * @param {{ clock?: () => Date }} [options]
4
+ */
2
5
  export function createConversationStreams({ clock = () => new Date() } = {}) {
3
6
  const streams = new Map();
4
7
  let revision = 0;
@@ -77,5 +80,15 @@ export function createConversationStreams({ clock = () => new Date() } = {}) {
77
80
  return read(scope);
78
81
  }
79
82
 
80
- return { read, update, complete, clear };
83
+ return Object.freeze({
84
+
85
+ read,
86
+
87
+ update,
88
+
89
+ complete,
90
+
91
+ clear
92
+
93
+ });
81
94
  }
@@ -1,23 +1,19 @@
1
+ import { isPlainRecord } from "./normalize.js";
2
+
1
3
  const OUTPUT_SCHEMA_MAX_BYTES = 64 * 1024;
2
4
  const OUTPUT_SCHEMA_MAX_DEPTH = 8;
3
5
  const OUTPUT_SCHEMA_MAX_PROPERTIES = 64;
4
6
  const OUTPUT_SCHEMA_MAX_ENUM_VALUES = 64;
5
7
  const OUTPUT_SCHEMA_MAX_ALTERNATIVES = 64;
6
8
 
7
- function isPlainRecord(value) {
8
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9
- return false;
10
- }
11
- const prototype = Object.getPrototypeOf(value);
12
- return prototype === Object.prototype || prototype === null;
9
+ /** The refusal of an output schema whose host supplies no error policy of its own. */
10
+ function outputSchemaTypeError(message, details) {
11
+ return Object.assign(new TypeError(message), { details });
13
12
  }
14
13
 
15
- // Original bounded native-helper schema contract. The host supplies its existing
14
+ // The bounded schema contract of structured output. The host supplies its own
16
15
  // error policy; native and API process ownership is outside this validator.
17
- export function validateConversationOutputSchema(outputSchema, {
18
- maxOutputCharacters,
19
- createError = (message, details) => Object.assign(new TypeError(message), { details })
20
- } = {}) {
16
+ export function validateConversationOutputSchema(outputSchema, { maxOutputCharacters, createError = outputSchemaTypeError }) {
21
17
  if (!Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 1) {
22
18
  throw new TypeError("Structured output requires a finite positive output limit.");
23
19
  }
@@ -50,5 +50,17 @@ export function createConversationSystemPrompt() {
50
50
  return operation;
51
51
  }
52
52
 
53
- return Object.freeze({ ensure, isCurrent, invalidate, whenIdle: () => pending });
53
+ const whenIdle = () => pending;
54
+
55
+ return Object.freeze({
56
+
57
+ ensure,
58
+
59
+ isCurrent,
60
+
61
+ invalidate,
62
+
63
+ whenIdle
64
+
65
+ });
54
66
  }
@@ -2,15 +2,44 @@ import { isDeepStrictEqual } from "node:util";
2
2
 
3
3
  const unknownOutcome = "The application operation did not return a verified result. Inspect its target before requesting another execution.";
4
4
 
5
- /** One turn's application calls. Native/API adapters only transport these calls. */
6
- export function createConversationTools({ catalog, toolSet: retainedToolSet, context, prepareContext, signal: turnSignal, previousCalls = [], authorize, save, emit,
7
- maximumCalls = 32, discoveryOnly = false, transient = false, propagateErrors = false }) {
8
- if (transient !== true && [authorize, save, emit].some(hook => typeof hook !== "function")) {
5
+ /** A transient tool set's admission check: it has no admission to recheck. */
6
+ function authorizeTransientCall() {}
7
+
8
+ /** A transient tool set's save: its calls live only in memory. */
9
+ function saveTransientCalls() {}
10
+
11
+ /** A transient tool set's announcement: it publishes no events. */
12
+ function announceTransientCall() {}
13
+
14
+ const TRANSIENT_HOOKS = new Set([authorizeTransientCall, saveTransientCalls, announceTransientCall]);
15
+
16
+ /**
17
+ * One turn's application calls. Native/API adapters only transport these calls.
18
+ * @param {object} options
19
+ * @param {import("../types.js").ServiceToolCatalog} options.catalog
20
+ * @param {{ tools: object[] }} [options.toolSet] A tool set resolved earlier for this turn.
21
+ * @param {object} options.context The caller context the calls run as.
22
+ * @param {() => Promise<object>} [options.prepareContext] Maps the context again before each call.
23
+ * @param {AbortSignal} options.signal
24
+ * @param {object[]} [options.previousCalls] The calls this turn already recorded.
25
+ * @param {() => Promise<void> | void} [options.authorize] Rechecks admission; required unless the tool set is transient.
26
+ * @param {(calls: object[]) => Promise<void> | void} [options.save] Saves the turn's calls; required unless the tool set is transient.
27
+ * @param {(event: object) => Promise<void> | void} [options.emit] Announces a call; required unless the tool set is transient.
28
+ * @param {number} [options.maximumCalls]
29
+ * @param {boolean} [options.discoveryOnly]
30
+ * @param {boolean} [options.transient]
31
+ * @param {boolean} [options.propagateErrors]
32
+ */
33
+ export function createConversationTools({ catalog, toolSet: retainedToolSet, context, prepareContext, signal: turnSignal, previousCalls = [],
34
+ authorize = authorizeTransientCall, save = saveTransientCalls, emit = announceTransientCall, maximumCalls = 32, discoveryOnly = false,
35
+ transient = false, propagateErrors = false }) {
36
+ if (transient !== true && [authorize, save, emit].some(hook => typeof hook !== "function" || TRANSIENT_HOOKS.has(hook))) {
9
37
  throw new TypeError("Conversation tools require authorization, durable save and event facilities.");
10
38
  }
11
39
  if (transient === true && previousCalls.length) {
12
40
  throw new TypeError("Transient application tools cannot restore a previous execution.");
13
41
  }
42
+ // readable-check: allow method-probe - input validation: an optional context mapper must be a function
14
43
  if (prepareContext !== undefined && typeof prepareContext !== "function") throw new TypeError("Application tool context requires a server-owned mapper.");
15
44
  const toolSet = retainedToolSet ?? catalog.resolveToolSet(context, { discoveryOnly });
16
45
  const calls = new Map(structuredClone(previousCalls).map(call => [call.id, call]));
@@ -27,7 +56,7 @@ export function createConversationTools({ catalog, toolSet: retainedToolSet, con
27
56
  }
28
57
 
29
58
  async function run({ id, name, arguments: argumentsText }, signal) {
30
- await authorize?.();
59
+ await authorize();
31
60
  let executionContext = prepareContext ? await prepareContext() : context;
32
61
  signal.throwIfAborted();
33
62
  if (typeof id !== "string" || !id || id.length > 256 || typeof name !== "string" || !name || name.length > 256 ||
@@ -49,17 +78,17 @@ export function createConversationTools({ catalog, toolSet: retainedToolSet, con
49
78
  calls.set(id, call);
50
79
  // Canonical calls reserve durably before the effect. Explicit transient
51
80
  // calls retain the same once-only state only for their non-resumable lifetime.
52
- try { await save?.([...calls.values()]); }
81
+ try { await save([...calls.values()]); }
53
82
  catch (error) { calls.delete(id); throw error; }
54
- await emit?.({ type: "tool", call: structuredClone(call) });
83
+ await emit({ type: "tool", call: structuredClone(call) });
55
84
  try {
56
- await authorize?.();
85
+ await authorize();
57
86
  if (prepareContext) executionContext = await prepareContext();
58
87
  signal.throwIfAborted();
59
88
  } catch (error) {
60
89
  call.status = "not-executed";
61
90
  call.result = { ok: false, error: { code: "conversation_tool_not_executed", message: "Execution stopped before the action began." } };
62
- await save?.([...calls.values()]);
91
+ await save([...calls.values()]);
63
92
  throw error;
64
93
  }
65
94
  let failed = false;
@@ -69,8 +98,8 @@ export function createConversationTools({ catalog, toolSet: retainedToolSet, con
69
98
  call.status = !call.result.ok && call.result.error?.status >= 500 ? "unknown" : "complete";
70
99
  // Keep the result in memory as well: retrySave can persist it after a failed
71
100
  // write without executing the action again or repeating model inference.
72
- await save?.([...calls.values()]);
73
- await emit?.({ type: "tool", call: structuredClone(call) });
101
+ await save([...calls.values()]);
102
+ await emit({ type: "tool", call: structuredClone(call) });
74
103
  if (failed) throw failure;
75
104
  if (!call.result.ok && call.result.error?.status >= 500) {
76
105
  throw Object.assign(new Error(unknownOutcome), { code: "conversation_tool_outcome_unknown" });
@@ -78,10 +107,13 @@ export function createConversationTools({ catalog, toolSet: retainedToolSet, con
78
107
  return structuredClone(call.result);
79
108
  }
80
109
 
81
- return {
110
+ const records = () => structuredClone([...calls.values()]);
111
+
112
+ return Object.freeze({
82
113
  descriptors: toolSet.tools,
83
- schemas: toolSet.tools.map(catalog.toOpenAiToolSchema), execute,
114
+ schemas: toolSet.tools.map(catalog.toOpenAiToolSchema),
115
+ execute,
84
116
  maximumArgumentBytes: catalog.limits.maxToolArgumentBytes,
85
- records: () => structuredClone([...calls.values()])
86
- };
117
+ records
118
+ });
87
119
  }
@@ -1,17 +1,31 @@
1
1
  import { normalizeNullableText } from "./normalize.js";
2
2
  import { conversationLogPage } from "../../shared/conversation/pagination.js";
3
3
 
4
- /** Conversation policy, independent of database, filesystem, identity and HTTP. */
5
- export function createConversationTranscript({ storage, clock = () => new Date(), applicationTurns = false } = {}) {
4
+ /** The time a transcript records when its owner supplies no clock. */
5
+ function currentDate() {
6
+ return new Date();
7
+ }
8
+
9
+ /**
10
+ * Conversation policy, independent of database, filesystem, identity and HTTP.
11
+ * @param {object} options
12
+ * @param {import("../types.js").ConversationStorage} options.storage
13
+ * @param {() => Date} [options.clock]
14
+ * @param {boolean} [options.applicationTurns]
15
+ */
16
+ export function createConversationTranscript({ storage, clock = currentDate, applicationTurns = false }) {
17
+ // readable-check: allow method-probe - input validation: the application's storage adapter must offer read and write
6
18
  if (typeof storage?.read !== "function" || typeof storage?.write !== "function") {
7
19
  throw new TypeError("Conversation storage requires read(scope, callback) and write(scope, callback).");
8
20
  }
9
21
 
10
- async function readConversationLog(scope, { presentation = false } = {}) {
22
+ // With `afterTurnId`, only the turns after it (those a summary doesn't cover).
23
+ async function readConversationLog(scope, { presentation = false, afterTurnId = "" } = {}) {
11
24
  if (typeof presentation !== "boolean") throw new TypeError("Transcript presentation requires an explicit boolean.");
12
25
  return storage.read(scope, async (transaction) => {
13
- const ids = await transaction.listTurnIds();
14
- const turns = (await Promise.all(ids.map((id) => transaction.readTurn(id)))).filter(hasMessages);
26
+ const read = afterTurnId ? transaction.readTurnsAfter(afterTurnId)
27
+ : Promise.all((await transaction.listTurnIds()).map((id) => transaction.readTurn(id)));
28
+ const turns = (await read).filter(hasMessages);
15
29
  return presentation ? turns.filter(turn => !isCompletedEnvelopeTurn(turn)) : turns;
16
30
  });
17
31
  }
@@ -98,6 +112,7 @@ export function createConversationTranscript({ storage, clock = () => new Date()
98
112
  if (!id) return null;
99
113
  return storage.read(scope, async (transaction) => {
100
114
  const submitted = turn => hasMessages(turn) && (turn.user || turn.system)?.messageId === id ? turn : null;
115
+ // readable-check: allow method-probe - optional operation: a storage transaction may find a message's turn directly
101
116
  if (typeof transaction.findMessageTurnId === "function") {
102
117
  const turnId = await transaction.findMessageTurnId(id);
103
118
  if (!turnId) return null;
@@ -112,25 +127,39 @@ export function createConversationTranscript({ storage, clock = () => new Date()
112
127
  });
113
128
  }
114
129
 
130
+ const conversationMessageIdExists = (scope, messageId) => storage.read(scope, (transaction) => transaction.hasMessage(normalizeNullableText(messageId)));
131
+
132
+ const writeConversationUserMessage = (scope, input) => append(scope, "user", input);
133
+
134
+ const writeConversationAssistantMessage = (scope, input) => append(scope, "assistant", input);
135
+
136
+ const writeConversationCommentaryMessage = (scope, input) => append(scope, "commentary", input);
137
+
138
+ const writeConversationThinkingMessage = (scope, input) => append(scope, "thinking", input);
139
+
140
+ const writeConversationSystemMessage = (scope, input) => append(scope, "system", input);
141
+
142
+ async function upsertConversationAssistantMessage(scope, { turnId = "", text = "" } = {}) {
143
+ const messageText = normalizeNullableText(text);
144
+ if (!messageText) return null;
145
+ return storage.write(scope, async (transaction) => {
146
+ const id = normalizeNullableText(turnId);
147
+ await transaction.replaceAssistant(id, { role: "assistant", text: messageText, at: new Date(clock()).toISOString() });
148
+ return transaction.readTurn(id);
149
+ });
150
+ }
151
+
115
152
  return Object.freeze({
116
153
  readConversationLog,
117
154
  findSubmittedTurn,
118
155
  readConversationLogPage,
119
- conversationMessageIdExists: (scope, messageId) => storage.read(scope, (transaction) => transaction.hasMessage(normalizeNullableText(messageId))),
120
- writeConversationUserMessage: (scope, input) => append(scope, "user", input),
121
- writeConversationAssistantMessage: (scope, input) => append(scope, "assistant", input),
122
- writeConversationCommentaryMessage: (scope, input) => append(scope, "commentary", input),
123
- writeConversationThinkingMessage: (scope, input) => append(scope, "thinking", input),
124
- writeConversationSystemMessage: (scope, input) => append(scope, "system", input),
125
- async upsertConversationAssistantMessage(scope, { turnId = "", text = "" } = {}) {
126
- const messageText = normalizeNullableText(text);
127
- if (!messageText) return null;
128
- return storage.write(scope, async (transaction) => {
129
- const id = normalizeNullableText(turnId);
130
- await transaction.replaceAssistant(id, { role: "assistant", text: messageText, at: new Date(clock()).toISOString() });
131
- return transaction.readTurn(id);
132
- });
133
- }
156
+ conversationMessageIdExists,
157
+ writeConversationUserMessage,
158
+ writeConversationAssistantMessage,
159
+ writeConversationCommentaryMessage,
160
+ writeConversationThinkingMessage,
161
+ writeConversationSystemMessage,
162
+ upsertConversationAssistantMessage
134
163
  });
135
164
  }
136
165
 
@@ -1,4 +1,2 @@
1
- export {
2
- createClaudeConversationOwner,
3
- claudeNativeMessageId
4
- } from "../conversation/claudeTurn.js";
1
+ export { createClaudeConversationOwner } from "../conversation/claudeTurn.js";
2
+ export { claudeNativeMessageId } from "../conversation/claudeHistory.js";
@@ -5,21 +5,19 @@ export {
5
5
  codexInteractiveArguments,
6
6
  normalizeCodexThreadId,
7
7
  codexAppServerProjectHookTrustConfig,
8
- codexAuthOutputRequiresReconnect,
9
- codexProviderConfiguration,
10
- codexProviderFileConfiguration,
11
- codexProviderCatalogPath,
12
- verifyCodexProviderKey,
13
8
  codexProviderModelCatalog,
14
9
  codexCatalogRows,
15
10
  codexCatalogReasoningEfforts,
16
11
  codexCatalogModels,
17
12
  codexConfiguredModelCatalog,
18
13
  readCodexModelContextWindow,
19
- createCodexAccountReader,
20
- createCodexAppServerIsolation,
21
- readCodexToolFreeConfiguration
14
+ codexProviderConfiguration,
15
+ codexProviderFileConfiguration,
16
+ codexProviderCatalogPath,
17
+ verifyCodexProviderKey
22
18
  } from "../conversation/codexConfiguration.js";
19
+ export { codexAuthOutputRequiresReconnect, createCodexAccountReader } from "../conversation/codexAccountReader.js";
20
+ export { createCodexAppServerIsolation, readCodexToolFreeConfiguration } from "../conversation/codexIsolation.js";
23
21
  export {
24
22
  codexDisabledProcessFeatureArguments,
25
23
  codexHomeDisposablePaths
@@ -19,10 +19,12 @@ export {
19
19
  codexAppServerNotificationEvent,
20
20
  codexAppServerNotificationItemId,
21
21
  codexAppServerNotificationTurnId,
22
- codexAppServerProviderThreadAssistantSegments,
22
+ codexAppServerProviderThreadAssistantSegments
23
+ } from "../conversation/codexEvents.js";
24
+ export {
23
25
  codexAppServerProviderThreadTurns,
24
26
  codexAppServerProviderTurnForOperation,
25
27
  codexAppServerProviderTurnId,
26
28
  codexAppServerThreadError,
27
29
  codexAppServerThreadTurnId
28
- } from "../conversation/codexEvents.js";
30
+ } from "../conversation/codexThreadSnapshot.js";
@@ -6,7 +6,7 @@ export {
6
6
  codexAppServerHelperHomeDir,
7
7
  codexAppServerHelperWorkspaceDir,
8
8
  codexAppServerIsHelper,
9
- createCodexAppServerRuntime,
10
9
  normalizeCodexAppServerTerminalEnv,
11
10
  runCodexAppServerProcess
12
11
  } from "../conversation/codexProcess.js";
12
+ export { createCodexAppServerRuntime } from "../conversation/codexRuntime.js";
@@ -1,22 +1,24 @@
1
1
  export {
2
2
  CODEX_APP_SERVER_INVALID_REQUEST_CODE,
3
- CodexAppServerAgentProvider,
4
- codexAppServerEndpointForTarget,
5
3
  codexAppServerRequestIsInvalid,
6
- codexAppServerRuntimeStopWasVerified,
7
- codexCliResumeCommand,
8
- codexTextInput,
9
- codexTurnInput,
10
- createCodexAppServerProviderOwner,
11
4
  deleteCodexAppServerHelperThread,
12
5
  deleteCodexAppServerThread,
13
6
  ensureCodexAppServerThread,
14
7
  inspectCodexAppServerMessageAdmission,
15
8
  retireCodexConversationHistory,
16
- readCodexNativeGoal,
9
+ readCodexNativeGoal
10
+ } from "../conversation/codexProviderThreads.js";
11
+ export { CodexAppServerAgentProvider } from "../conversation/codexProvider.js";
12
+ export {
13
+ codexAppServerEndpointForTarget,
14
+ codexCliResumeCommand,
15
+ codexTextInput,
16
+ codexTurnInput,
17
17
  sendCodexAppServerPrompt,
18
18
  shellQuote
19
- } from "../conversation/codexProvider.js";
19
+ } from "../conversation/codexProviderTurns.js";
20
+ export { codexAppServerRuntimeStopWasVerified } from "../conversation/codexRuntimeInventory.js";
21
+ export { createCodexAppServerProviderOwner } from "../conversation/codexProviderOwner.js";
20
22
  export { assertCodexAuthPreflightReady } from "../conversation/codexAuthPreflight.js";
21
23
  export {
22
24
  codexRenewalThreadError,
@@ -20,6 +20,6 @@ export {
20
20
 
21
21
  export {
22
22
  CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
23
- CODEX_HELPER_THREAD_LIFECYCLES,
24
- createCodexHelperThreadLedgerOwner
25
- } from "../conversation/codexHelperThreadLedger.js";
23
+ CODEX_HELPER_THREAD_LIFECYCLES
24
+ } from "../conversation/codexHelperThreadRecord.js";
25
+ export { createCodexHelperThreadLedgerOwner } from "../conversation/codexHelperThreadLedger.js";
@@ -2,7 +2,9 @@ export {
2
2
  OPENCODE_RESPONSE_LIMIT_BYTES,
3
3
  createOpenCodeServerClient,
4
4
  openCodeAssistantMessageText,
5
- readBoundedResponse,
5
+ readBoundedResponse
6
+ } from "../conversation/openCodeClient.js";
7
+ export {
6
8
  readOpenCodeConversationDatabase,
7
9
  retireOpenCodeConversationHistory
8
- } from "../conversation/openCodeClient.js";
10
+ } from "../conversation/openCodeNativeHistory.js";
@@ -14,6 +14,4 @@ export {
14
14
  } from "./lib/ndjson.js";
15
15
  export { resolveWorkspaceSlug } from "./lib/resolveWorkspaceSlug.js";
16
16
  export { createServiceToolCatalog } from "./lib/serviceToolCatalog.js";
17
- export { consumeCompletionStream, sanitizeAssistantMessageText } from "./lib/assistantCompletion.js";
18
- export { runAssistantToolLoop } from "./lib/assistantToolLoop.js";
19
17
  export { runBoundedAssistantToolLoop, readAssistantResponseEnvelope, readPartialAssistantReply } from "./conversation/boundedToolLoop.js";