@jskit-ai/assistant-core 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +18 -12
  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,270 @@
1
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
4
+
5
+ // Which application actions become assistant tools: an action on the automation channel with
6
+ // input and output schemas, minus the barred and excluded ones, each under one unique tool name.
7
+
8
+ const AUTOMATION_CHANNEL = "automation";
9
+
10
+ function normalizeAssistantExtension(value) {
11
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
12
+ // readable-check: allow method-probe - input validation: an action's assistant extension may bring its own result transform
13
+ if (source.transformResult != null && typeof source.transformResult !== "function") {
14
+ throw new TypeError("extensions.assistant.transformResult must be a function when provided.");
15
+ }
16
+
17
+ return Object.freeze({
18
+ description: normalizeText(source.description),
19
+ exclude: normalizeText(source.exclude),
20
+ alwaysAvailable: source.alwaysAvailable === true,
21
+ output: Object.hasOwn(source, "output") ? source.output : null,
22
+ transformResult: source.transformResult ?? null
23
+ });
24
+ }
25
+
26
+ function normalizeAssistantActionExtension(action = {}) {
27
+ const source = action && typeof action === "object" && !Array.isArray(action) ? action : {};
28
+ const actionId = normalizeText(source.id);
29
+
30
+ if (Object.prototype.hasOwnProperty.call(source, "assistantTool")) {
31
+ throw new Error(
32
+ `Action definition \"${actionId || "<unknown>"}\" assistantTool is not supported. Use extensions.assistant instead.`
33
+ );
34
+ }
35
+
36
+ const extensions = source.extensions && typeof source.extensions === "object" && !Array.isArray(source.extensions)
37
+ ? source.extensions
38
+ : {};
39
+ return normalizeAssistantExtension(extensions.assistant);
40
+ }
41
+
42
+ function normalizeBarredEntry(value) {
43
+ return normalizeText(value).toLowerCase();
44
+ }
45
+
46
+ function normalizeBarredActionSet(value) {
47
+ const source = Array.isArray(value) ? value : [value];
48
+ const exact = new Set();
49
+ const prefixes = [];
50
+
51
+ for (const entry of source) {
52
+ const normalized = normalizeBarredEntry(entry);
53
+ if (!normalized) {
54
+ continue;
55
+ }
56
+
57
+ if (normalized.endsWith(".*")) {
58
+ const prefix = normalized.slice(0, -1);
59
+ if (prefix) {
60
+ prefixes.push(prefix);
61
+ }
62
+ continue;
63
+ }
64
+
65
+ exact.add(normalized);
66
+ }
67
+
68
+ return Object.freeze({
69
+ exact,
70
+ prefixes: Object.freeze(prefixes)
71
+ });
72
+ }
73
+
74
+ function isActionBarred(barredRules, actionId) {
75
+ const normalizedActionId = normalizeText(actionId).toLowerCase();
76
+ if (!normalizedActionId) {
77
+ return true;
78
+ }
79
+
80
+ if (barredRules.exact.has(normalizedActionId)) {
81
+ return true;
82
+ }
83
+
84
+ return barredRules.prefixes.some((prefix) => normalizedActionId.startsWith(prefix));
85
+ }
86
+
87
+ function sanitizeToolName(value) {
88
+ const normalized = String(value || "")
89
+ .trim()
90
+ .toLowerCase()
91
+ .replace(/[^a-z0-9_]+/g, "_")
92
+ .replace(/_+/g, "_")
93
+ .replace(/^_+|_+$/g, "");
94
+
95
+ if (!normalized) {
96
+ return "tool";
97
+ }
98
+
99
+ return normalized;
100
+ }
101
+
102
+ function resolveUniqueToolName(baseName, used) {
103
+ const normalizedBase = sanitizeToolName(baseName) || "tool";
104
+ let candidate = normalizedBase.slice(0, 64);
105
+ let suffix = 1;
106
+
107
+ while (used.has(candidate)) {
108
+ const suffixText = `_${suffix}`;
109
+ const baseBudget = Math.max(1, 64 - suffixText.length);
110
+ candidate = `${normalizedBase.slice(0, baseBudget)}${suffixText}`;
111
+ suffix += 1;
112
+ }
113
+
114
+ used.add(candidate);
115
+ return candidate;
116
+ }
117
+
118
+ function normalizePermissionSpec(permission) {
119
+ const source = permission && typeof permission === "object" && !Array.isArray(permission)
120
+ ? permission
121
+ : {};
122
+ const requireMode = normalizeText(source.require || "none").toLowerCase();
123
+ const permissions = Array.isArray(source.permissions) ? source.permissions : [];
124
+
125
+ return Object.freeze({
126
+ require: requireMode || "none",
127
+ permissions,
128
+ message: normalizeText(source.message),
129
+ code: normalizeText(source.code)
130
+ });
131
+ }
132
+
133
+ function hasAutomationChannel(action = {}) {
134
+ const channels = Array.isArray(action.channels) ? action.channels : [];
135
+ return channels.some((channel) => normalizeText(channel).toLowerCase() === AUTOMATION_CHANNEL);
136
+ }
137
+
138
+ function normalizeSurfaceList(value) {
139
+ const source = Array.isArray(value) ? value : [value];
140
+ const resolved = [];
141
+
142
+ for (const entry of source) {
143
+ const normalized = normalizeSurfaceId(entry);
144
+ if (normalized) {
145
+ resolved.push(normalized);
146
+ }
147
+ }
148
+
149
+ return Object.freeze(resolved);
150
+ }
151
+
152
+ /**
153
+ * The automation actions that can become tools, by action id.
154
+ * @param {{ listDefinitions(): object[] }} actions The action runtime the tool catalogue has validated.
155
+ */
156
+ function resolveActionBackedToolEntries(actions) {
157
+ const entriesByActionId = new Map();
158
+ for (const action of actions.listDefinitions()) {
159
+ if (!action || typeof action !== "object") {
160
+ continue;
161
+ }
162
+ if (!hasAutomationChannel(action)) {
163
+ continue;
164
+ }
165
+
166
+ const actionId = normalizeText(action.id);
167
+ if (!actionId) {
168
+ continue;
169
+ }
170
+
171
+ let assistantExtension = null;
172
+ try {
173
+ assistantExtension = normalizeAssistantActionExtension(action);
174
+ } catch {
175
+ continue;
176
+ }
177
+
178
+ if (assistantExtension.exclude) {
179
+ continue;
180
+ }
181
+
182
+ const outputDefinition = assistantExtension.output || action.output;
183
+ const inputSchema = resolveSchemaTransportSchemaDefinition(action.input, {
184
+ context: `Action definition "${actionId}" input`,
185
+ defaultMode: "patch"
186
+ }) || null;
187
+ const outputSchema = resolveSchemaTransportSchemaDefinition(outputDefinition, {
188
+ context: `Action definition "${actionId}" assistant output`,
189
+ defaultMode: "replace"
190
+ }) || null;
191
+ if (!inputSchema || !outputSchema) {
192
+ continue;
193
+ }
194
+
195
+ const actionVersion = Number(action.version) || 1;
196
+ const actionKey = actionId.toLowerCase();
197
+ const nextEntry = Object.freeze({
198
+ actionId,
199
+ actionVersion,
200
+ kind: normalizeText(action.kind).toLowerCase() || "command",
201
+ toolBaseName: actionId,
202
+ description: assistantExtension.description || `Run ${actionId}.`,
203
+ alwaysAvailable: assistantExtension.alwaysAvailable,
204
+ inputSchema,
205
+ outputDefinition,
206
+ outputSchema,
207
+ transformResult: assistantExtension.transformResult,
208
+ permission: normalizePermissionSpec(action.permission),
209
+ surfaces: normalizeSurfaceList(action.surfaces)
210
+ });
211
+ const existing = entriesByActionId.get(actionKey);
212
+ if (!existing || actionVersion >= Number(existing.actionVersion || 0)) {
213
+ entriesByActionId.set(actionKey, nextEntry);
214
+ }
215
+ }
216
+
217
+ return entriesByActionId;
218
+ }
219
+
220
+ function resolveActionToolEntries(
221
+ actions,
222
+ { barredActionIds = [], skipActionPrefixes = [] } = {}
223
+ ) {
224
+ const actionBackedEntries = resolveActionBackedToolEntries(actions);
225
+ const barredRules = normalizeBarredActionSet(barredActionIds);
226
+ const usedToolNames = new Set();
227
+ const entries = [];
228
+
229
+ for (const actionEntry of actionBackedEntries.values()) {
230
+ const actionId = normalizeText(actionEntry?.actionId);
231
+ if (!actionId) {
232
+ continue;
233
+ }
234
+
235
+ const normalizedActionId = actionId.toLowerCase();
236
+ const skipByPrefix = skipActionPrefixes.some((prefix) => normalizedActionId.startsWith(prefix));
237
+ if (skipByPrefix) {
238
+ continue;
239
+ }
240
+
241
+ if (isActionBarred(barredRules, actionId)) {
242
+ continue;
243
+ }
244
+
245
+ const toolName = resolveUniqueToolName(actionEntry.toolBaseName, usedToolNames);
246
+ entries.push(
247
+ Object.freeze({
248
+ descriptor: Object.freeze({
249
+ name: toolName,
250
+ actionId,
251
+ actionVersion: Number(actionEntry.actionVersion) || null,
252
+ description: normalizeText(actionEntry.description) || `Run ${actionId}.`,
253
+ alwaysAvailable: actionEntry.alwaysAvailable === true,
254
+ parameters: actionEntry.inputSchema,
255
+ outputSchema: actionEntry.outputSchema
256
+ }),
257
+ kind: actionEntry.kind,
258
+ acceptsWorkspaceSlug: Object.hasOwn(actionEntry.inputSchema.properties || {}, "workspaceSlug"),
259
+ outputDefinition: actionEntry.outputDefinition,
260
+ transformResult: actionEntry.transformResult,
261
+ permission: actionEntry.permission,
262
+ surfaces: actionEntry.surfaces
263
+ })
264
+ );
265
+ }
266
+
267
+ return Object.freeze(entries.sort((left, right) => left.descriptor.name.localeCompare(right.descriptor.name)));
268
+ }
269
+
270
+ export { AUTOMATION_CHANNEL, normalizePermissionSpec, resolveActionToolEntries };
@@ -0,0 +1,78 @@
1
+ // The types of assistant-core's server facilities, for the code that receives them, and the
2
+ // contracts of what an application passes the conversation runtime.
3
+
4
+ /** @typedef {ReturnType<typeof import("./conversation/runtime.js").createConversationRuntime>} ConversationRuntime */
5
+ /** @typedef {ReturnType<typeof import("./conversation/transcript.js").createConversationTranscript>} ConversationTranscript */
6
+ /** @typedef {ReturnType<typeof import("./conversation/streams.js").createConversationStreams>} ConversationStreams */
7
+ /** @typedef {ReturnType<typeof import("./conversation/storage.js").createReentrantConversationStorage>} ReentrantConversationStorage */
8
+ /** @typedef {ReturnType<typeof import("./lib/serviceToolCatalog.js").createServiceToolCatalog>} ServiceToolCatalog */
9
+ /** @typedef {ReturnType<typeof import("./lib/aiClient.js").createAiClient>} AiClient */
10
+ /** @typedef {ReturnType<typeof import("./lib/aiConnectionClient.js").createAiConnectionClient>} AiConnectionClient */
11
+
12
+ /**
13
+ * The storage an application gives the conversation runtime. Each operation runs in one transaction
14
+ * of the conversation scope; createConversationStorage, createFileConversationStorage and
15
+ * createMemoryConversationStorage implement it, and the storage contract checks prove another.
16
+ * @typedef {object} ConversationStorage
17
+ * @property {<T>(scope: string, operation: (transaction: object) => Promise<T>) => Promise<T>} read
18
+ * @property {<T>(scope: string, operation: (transaction: object) => Promise<T>) => Promise<T>} write
19
+ * @property {(scope: string) => Promise<void>} [deleteConversation]
20
+ */
21
+
22
+ /**
23
+ * Whether the caller of one operation may use one conversation.
24
+ * @typedef {(request: { context: object, conversationId: string, operation: string }) => boolean | Promise<boolean>} ConversationAuthorize
25
+ */
26
+
27
+ /**
28
+ * The application's own conversation host: it may supply each conversation's native facilities.
29
+ * @typedef {object} ConversationHost
30
+ * @property {(request: { id?: string, context: object, input?: unknown, options?: unknown, operation?: string }) => unknown} [conversation]
31
+ * @property {boolean} [nativeTools] Whether native engines may use their own coding tools.
32
+ * @property {object} [commandWrapper]
33
+ */
34
+
35
+ /**
36
+ * The application's files offered as message attachments.
37
+ * @typedef {object} ConversationAttachments
38
+ * @property {(request: { attachmentIds: string[], context: object, conversationId: string, signal: AbortSignal }) =>
39
+ * Promise<{ attachments: object[], content?: object[], localFiles?: object[] }>} resolve
40
+ */
41
+
42
+ /**
43
+ * The authorized AI connections an API conversation resolves its integrationId through.
44
+ * @typedef {object} ConversationConnections
45
+ * @property {(request: { context: object, integrationId: string }) => Promise<object>} resolve
46
+ */
47
+
48
+ /**
49
+ * An application's own API model client, instead of its AI connections.
50
+ * @typedef {object} ConversationApiClientFactory
51
+ * @property {(request: { context: object, configuration: object }) =>
52
+ * Promise<{ enabled: boolean, model: object, defaultModel: string }>} resolveClient
53
+ */
54
+
55
+ /**
56
+ * The API history an application selects for one request, instead of the whole transcript.
57
+ * @typedef {(request: { context: object }) => unknown | Promise<unknown>} ConversationApiHistory
58
+ */
59
+
60
+ /**
61
+ * Receives each stage of one message's delivery; its result is never awaited.
62
+ * @typedef {(event: { engine: string, conversationId: string, messageId?: string, stage: string, previousStage: string,
63
+ * durationMs: number, elapsedMs: number, timings: Record<string, number> }) => unknown} ConversationDeliveryTimingObserver
64
+ */
65
+
66
+ /**
67
+ * The runtime's numeric limits; each absent one takes its default.
68
+ * @typedef {object} ConversationLimits
69
+ * @property {number} [maxToolCalls]
70
+ * @property {number} [maxInputCharacters]
71
+ * @property {number} [maxFinalReplyCharacters]
72
+ * @property {number} [maxOutputCharacters]
73
+ * @property {number} [maxContinuityCharacters]
74
+ * @property {number} [maxAttachmentBytes]
75
+ * @property {number} [timeoutMs]
76
+ */
77
+
78
+ export {};
@@ -7,7 +7,6 @@ import {
7
7
  import { ASSISTANT_ATTACHMENT_ID_MAX_LENGTH, ASSISTANT_ATTACHMENT_MAX_ITEMS } from "./conversation/attachments.js";
8
8
 
9
9
  const MAX_INPUT_CHARS = 8000;
10
- const MAX_HISTORY_MESSAGES = 20;
11
10
  const MAX_PAGE_SIZE = 200;
12
11
  const MAX_MESSAGE_PAGE_SIZE = 500;
13
12
 
@@ -15,21 +14,6 @@ const attachmentIdsField = {
15
14
  type: "array", required: false, maxItems: ASSISTANT_ATTACHMENT_MAX_ITEMS,
16
15
  items: { type: "string", minLength: 1, maxLength: ASSISTANT_ATTACHMENT_ID_MAX_LENGTH }
17
16
  };
18
- const historyMessageSchema = createSchema({
19
- attachmentIds: attachmentIdsField,
20
- role: {
21
- type: "string",
22
- required: true,
23
- enum: ["user", "assistant"]
24
- },
25
- content: {
26
- type: "string",
27
- required: true,
28
- minLength: 1,
29
- maxLength: MAX_INPUT_CHARS
30
- }
31
- });
32
-
33
17
  const clientContextSchema = createSchema({
34
18
  locale: {
35
19
  type: "string",
@@ -64,12 +48,6 @@ const chatStreamBodySchema = createSchema({
64
48
  minLength: 1,
65
49
  maxLength: MAX_INPUT_CHARS
66
50
  },
67
- history: {
68
- type: "array",
69
- required: false,
70
- maxItems: MAX_HISTORY_MESSAGES,
71
- items: historyMessageSchema
72
- },
73
51
  clientContext: {
74
52
  type: "object",
75
53
  required: false,
@@ -312,7 +290,6 @@ const assistantResource = defineResource({
312
290
 
313
291
  export {
314
292
  MAX_INPUT_CHARS,
315
- MAX_HISTORY_MESSAGES,
316
293
  MAX_MESSAGE_PAGE_SIZE,
317
294
  attachmentIdsField,
318
295
  assistantResource,
@@ -1,3 +1,5 @@
1
+ import { answerChoiceHeadingLine, isSingleTextareaMessageField, optionalValueMatches } from "./messageInputSugar.js";
2
+
1
3
  const UI_ANSWER_CHOICE_FIELD = "__ui_answer_choice";
2
4
  const ANSWER_CHOICE_MIN_COUNT = 2;
3
5
  const ANSWER_CHOICE_MAX_COUNT = 6;
@@ -14,16 +16,6 @@ function normalizedChoiceText(value = "") {
14
16
  return String(value || "").trim().replace(/\s+/gu, " ");
15
17
  }
16
18
 
17
- function isSingleTextareaMessageField(fields = [], fieldName = "response") {
18
- return fields.length === 1 &&
19
- fields[0]?.name === fieldName &&
20
- fields[0]?.kind === "textarea";
21
- }
22
-
23
- function optionalValueMatches(actual = "", expected = "") {
24
- return !expected || String(actual || "") === String(expected || "");
25
- }
26
-
27
19
  function canRenderAnswerChoiceSugar({
28
20
  fields = [],
29
21
  fieldName = "response",
@@ -39,10 +31,6 @@ function canRenderAnswerChoiceSugar({
39
31
  );
40
32
  }
41
33
 
42
- function answerChoiceHeadingLine(line = "") {
43
- return /^(possible answers|choices):$/iu.test(String(line || "").trim());
44
- }
45
-
46
34
  function answerChoiceLineText(line = "") {
47
35
  const match = String(line || "").trim().match(/^[-*]\s+(.+)$/u);
48
36
  return match ? normalizedChoiceText(match[1]) : "";
@@ -0,0 +1,18 @@
1
+ // What the answer-choice and numbered-question sugars share: whether a message input can
2
+ // show sugar, and the heading line that introduces answer choices.
3
+
4
+ function isSingleTextareaMessageField(fields = [], fieldName = "response") {
5
+ return fields.length === 1 &&
6
+ fields[0]?.name === fieldName &&
7
+ fields[0]?.kind === "textarea";
8
+ }
9
+
10
+ function optionalValueMatches(actual = "", expected = "") {
11
+ return !expected || String(actual || "") === String(expected || "");
12
+ }
13
+
14
+ function answerChoiceHeadingLine(line = "") {
15
+ return /^(possible answers|choices):$/iu.test(String(line || "").trim());
16
+ }
17
+
18
+ export { answerChoiceHeadingLine, isSingleTextareaMessageField, optionalValueMatches };
@@ -1,4 +1,5 @@
1
1
  import { answerChoiceFromLine } from "./answerChoiceSugar.js";
2
+ import { answerChoiceHeadingLine, isSingleTextareaMessageField, optionalValueMatches } from "./messageInputSugar.js";
2
3
 
3
4
  const UI_QUESTION_FIELD_PREFIX = "__ui_question_";
4
5
 
@@ -10,16 +11,6 @@ function inactiveNumberedQuestionSugar() {
10
11
  };
11
12
  }
12
13
 
13
- function isSingleTextareaMessageField(fields = [], fieldName = "response") {
14
- return fields.length === 1 &&
15
- fields[0]?.name === fieldName &&
16
- fields[0]?.kind === "textarea";
17
- }
18
-
19
- function optionalValueMatches(actual = "", expected = "") {
20
- return !expected || String(actual || "") === String(expected || "");
21
- }
22
-
23
14
  function canRenderNumberedQuestionSugar({
24
15
  fields = [],
25
16
  fieldName = "response",
@@ -39,10 +30,6 @@ function numberedQuestionMarkerMatch(line = "") {
39
30
  return String(line || "").match(/^\[(?:Q)?(\d+)\]\s+(.+)$/iu);
40
31
  }
41
32
 
42
- function trailingAnswerChoiceHeadingLine(line = "") {
43
- return /^(possible answers|choices):$/iu.test(String(line || "").trim());
44
- }
45
-
46
33
  function trailingAnswerChoiceLine(line = "") {
47
34
  return /^[-*]\s+.+/u.test(String(line || "").trim());
48
35
  }
@@ -72,7 +59,7 @@ function numberedQuestionChoice(line = "") {
72
59
  }
73
60
 
74
61
  function trailingAnswerChoiceBlock(lines = [], startIndex = 0) {
75
- if (!trailingAnswerChoiceHeadingLine(lines[startIndex])) {
62
+ if (!answerChoiceHeadingLine(lines[startIndex])) {
76
63
  return null;
77
64
  }
78
65
  const choices = [];
@@ -152,7 +139,7 @@ function parseLineNumberedQuestionPrompt(value = "") {
152
139
  }
153
140
  const trailingLines = lines.slice(index);
154
141
  if (
155
- trailingLines.some((trailingLine) => trailingAnswerChoiceHeadingLine(trailingLine)) ||
142
+ trailingLines.some((trailingLine) => answerChoiceHeadingLine(trailingLine)) ||
156
143
  trailingLines.some((trailingLine) => numberedQuestionMarkerMatch(trailingLine))
157
144
  ) {
158
145
  return inactiveNumberedQuestionSugar();
@@ -238,6 +225,7 @@ function numberedQuestionSugarForInput(interaction = {}, fields = []) {
238
225
  return numberedQuestionSugarForMessageInput({
239
226
  fields,
240
227
  fieldName: "response",
228
+ // readable-check: allow no-optional-after-default - data: the interaction is null until a caller knows it
241
229
  message: interaction?.prompt
242
230
  });
243
231
  }
@@ -269,7 +257,9 @@ function numberedQuestionSugarForMessageInput({
269
257
  const ANSWER_REQUIRED = Object.freeze({ messageCode: "assistant.questions.answerRequired" });
270
258
 
271
259
  function numberedQuestionInputFields(questions = [], options = {}) {
260
+ // readable-check: allow no-optional-after-default - data: null field options mean none
272
261
  const autocomplete = String(options?.autocomplete || "").trim();
262
+ // readable-check: allow no-optional-after-default - data: null field options mean none
273
263
  const density = String(options?.density || "").trim();
274
264
  return questions.map((question) => ({
275
265
  ...(autocomplete ? { autocomplete } : {}),
@@ -1,7 +1,7 @@
1
1
  import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
2
2
  import { conversationTurnMessages } from "./turns.js";
3
3
 
4
- // Original paged transcript representation, shared by all browser bindings.
4
+ // The paged transcript representation, shared by all browser bindings.
5
5
  const CONVERSATION_LOG_PAGE_LIMIT = 10;
6
6
 
7
7
  const messageRoles = ["system", "user", "assistant"];
@@ -3,6 +3,7 @@ function messageText(value = "") {
3
3
  }
4
4
 
5
5
  function conversationTurns(conversationLog = {}) {
6
+ // readable-check: allow no-optional-after-default - data: the conversation is null until a caller knows it
6
7
  if (Array.isArray(conversationLog?.turns)) {
7
8
  return conversationLog.turns;
8
9
  }
@@ -3,6 +3,7 @@ import { conversationTurnMessages } from "./turns.js";
3
3
  /** Overlay live provider output without changing the saved transcript. */
4
4
  export function mergeConversationStream(turns = [], snapshot = {}) {
5
5
  const savedIds = new Set(turns.flatMap(conversationTurnMessages).map((message) => message.messageId));
6
+ // readable-check: allow no-optional-after-default - data: the live snapshot is null until a caller knows it
6
7
  const messages = (snapshot?.messages || []).filter((message) => message.text && !savedIds.has(message.messageId));
7
8
  if (!messages.length) return turns;
8
9
  const result = [...turns];
@@ -27,7 +27,6 @@ export {
27
27
 
28
28
  export {
29
29
  MAX_INPUT_CHARS,
30
- MAX_HISTORY_MESSAGES,
31
30
  MAX_MESSAGE_PAGE_SIZE,
32
31
  attachmentIdsField,
33
32
  assistantResource,
@@ -60,6 +60,26 @@ export async function verifyConversationStorageContract(storage, { runtime = fal
60
60
  metadata: await transaction.readMetadata(), turn: await transaction.readTurn(first.turnId), ids: await transaction.listTurnIds()
61
61
  }));
62
62
  assert.deepEqual(after, before, "Failed runtime writes must preserve transcript and metadata together.");
63
+ const ids = before.ids;
64
+ const later = await storage.read(scope, async transaction => ({
65
+ all: await transaction.readTurnsAfter(""), after: await transaction.readTurnsAfter(first.turnId),
66
+ each: await Promise.all(ids.map(id => transaction.readTurn(id))), summary: await transaction.readSummary()
67
+ }));
68
+ assert.deepEqual(later.all, later.each, "readTurnsAfter(\"\") reads every turn in order.");
69
+ assert.deepEqual(later.after, later.each.slice(1), "readTurnsAfter reads only the turns after the given one.");
70
+ assert.equal(later.summary, null, "A conversation starts without a summary.");
71
+ const summary = { text: "The person asked a first question.", throughTurnId: first.turnId };
72
+ await storage.write(scope, transaction => transaction.writeSummary(summary));
73
+ assert.deepEqual(await storage.read(scope, transaction => transaction.readSummary()), summary);
74
+ assert.equal(await storage.read(other, transaction => transaction.readSummary()), null, "Summaries must remain scoped.");
75
+ const detachedSummary = await storage.read(scope, transaction => transaction.readSummary());
76
+ detachedSummary.text = "client edit";
77
+ await assert.rejects(storage.write(scope, async transaction => {
78
+ await transaction.writeSummary({ text: "Uncommitted", throughTurnId: ids.at(-1) });
79
+ throw new Error("contract-rollback");
80
+ }), /contract-rollback/);
81
+ assert.deepEqual(await storage.read(scope, transaction => transaction.readSummary()), summary,
82
+ "Failed runtime writes must preserve the summary.");
63
83
  }
64
84
  return { scope, other, transcript };
65
85
  }
@@ -1,4 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
2
3
  import test from "node:test";
3
4
  import { createAiConnectionResolver } from "../../connectors-catalog/src/server/ai.js";
4
5
  import { createAiConnectionClient } from "../src/server/lib/aiConnectionClient.js";
@@ -13,6 +14,11 @@ function publicConnection(model = "opencode/mimo-v2.5-free") {
13
14
  }).resolve({ context: {}, integrationId: "assistant" });
14
15
  }
15
16
 
17
+ // Node 26.5's web streams lose a rejection when a piped read is cancelled mid-stream; Node 22, 24
18
+ // and 26.13 (the runtime packs' version) do not.
19
+ const NODE_LOSES_CANCELLED_PIPE_REJECTIONS = process.versions.node.startsWith("26.") &&
20
+ Number(process.versions.node.split(".")[1]) < 13 && "this Node 26 loses a web-stream rejection; 26.13 does not";
21
+
16
22
  function eventsResponse(events) {
17
23
  return new Response(events.map(data => `data: ${JSON.stringify(data)}\n\n`).join(""), {
18
24
  headers: { "content-type": "text/event-stream" }
@@ -248,7 +254,9 @@ test("SDK streaming rejects the first invalid tool input without yielding later
248
254
  assert.equal(requests, 1);
249
255
  });
250
256
 
251
- test("SDK cancellation after its first text retains the original reason and suppresses later output", { timeout: 5000 }, async t => {
257
+ test("SDK cancellation after its first text retains the original reason and suppresses later output", {
258
+ timeout: 5000, skip: NODE_LOSES_CANCELLED_PIPE_REJECTIONS
259
+ }, async t => {
252
260
  const source = new TransformStream();
253
261
  const writer = source.writable.getWriter();
254
262
  t.after(() => writer.abort().catch(() => {}));
@@ -425,3 +433,36 @@ for (const modelId of ["deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash
425
433
  assert.deepEqual(connection.modelLimits, { context: 1048576, output: 393216 });
426
434
  });
427
435
  }
436
+
437
+ test("a stream cancelled mid-way leaves no unhandled rejection under strict rejection handling", {
438
+ timeout: 20_000, skip: NODE_LOSES_CANCELLED_PIPE_REJECTIONS
439
+ }, () => {
440
+ const client = new URL("../src/server/lib/aiConnectionClient.js", import.meta.url).href;
441
+ const script = `
442
+ const { createAiConnectionClient } = await import(${JSON.stringify(client)});
443
+ const source = new TransformStream();
444
+ const writer = source.writable.getWriter();
445
+ const send = data => writer.write(new TextEncoder().encode("data: " + JSON.stringify(data) + "\\n\\n"));
446
+ const abort = new AbortController();
447
+ const connection = { providerId: "test", model: "test-model", sdkPackage: "@ai-sdk/openai-compatible", apiKey: "test",
448
+ baseURL: "http://test.invalid/v1" };
449
+ const client = createAiConnectionClient(connection, {
450
+ fetch: async () => new Response(source.readable, { headers: { "content-type": "text/event-stream" } })
451
+ });
452
+ const iterator = client.createChatCompletionStream({ messages: [{ role: "user", content: "Hello" }], signal: abort.signal });
453
+ const first = iterator.next();
454
+ await send({ choices: [{ index: 0, delta: { content: "First text" } }] });
455
+ await first;
456
+ const ending = iterator.next().then(() => null, error => error);
457
+ abort.abort(new Error("Stopped"));
458
+ await send({ choices: [{ index: 0, delta: { content: "Late text" } }] }).catch(() => {});
459
+ await writer.close().catch(() => {});
460
+ if ((await ending)?.message !== "Stopped") throw new Error("The stream did not end with the abort reason.");
461
+ await new Promise(resolve => setTimeout(resolve, 200));
462
+ process.stdout.write("clean");
463
+ `;
464
+ const child = spawnSync(process.execPath, ["--unhandled-rejections=strict", "--input-type=module", "-e", script],
465
+ { encoding: "utf8", timeout: 15_000 });
466
+ assert.equal(child.status, 0, child.stderr);
467
+ assert.equal(child.stdout, "clean");
468
+ });