@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.
- package/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -0,0 +1,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 (!
|
|
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) =>
|
|
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
|
-
//
|
|
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];
|
package/src/shared/index.js
CHANGED
|
@@ -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", {
|
|
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
|
+
});
|