arcane-os 0.3.0 → 0.3.2
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/CHANGELOG.md +27 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
- package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -146
- package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1050 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +40 -62
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +112 -779
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -187
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1252
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -677
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2960
- package/docs/reference/sdk-api.md +0 -6694
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
|
@@ -7,13 +7,15 @@ import {
|
|
|
7
7
|
subscribeAIRuntimeIntents
|
|
8
8
|
} from './AIRuntimeState.js';
|
|
9
9
|
|
|
10
|
+
const completeValue=(value)=>value;
|
|
11
|
+
|
|
10
12
|
export const AI_PROVIDER_PROTOCOL = 'arcane-ai-provider/2';
|
|
11
13
|
export const AI_PROVIDER_RUNTIME_PROTOCOL = 'arcane-ai-runtime/2';
|
|
12
14
|
export const AI_MODEL_AUTHORITY_PROTOCOL = 'arcane-ai-model-authority/1';
|
|
13
15
|
|
|
14
16
|
const ROLE_SET = new Set(AI_RUNTIME_ROLES);
|
|
15
|
-
const SPEECH_ROLES =
|
|
16
|
-
const PROVIDER_METHODS =
|
|
17
|
+
const SPEECH_ROLES = completeValue(['stt', 'tts']);
|
|
18
|
+
const PROVIDER_METHODS = completeValue([
|
|
17
19
|
'catalog',
|
|
18
20
|
'inspect',
|
|
19
21
|
'status',
|
|
@@ -22,25 +24,22 @@ const PROVIDER_METHODS = Object.freeze([
|
|
|
22
24
|
'unload',
|
|
23
25
|
'dispose'
|
|
24
26
|
]);
|
|
25
|
-
const ROLE_OPERATIONS =
|
|
27
|
+
const ROLE_OPERATIONS = completeValue(
|
|
26
28
|
{
|
|
27
|
-
llm:
|
|
28
|
-
stt:
|
|
29
|
-
tts:
|
|
29
|
+
llm: completeValue(['chat', 'stream']),
|
|
30
|
+
stt: completeValue(['transcribe']),
|
|
31
|
+
tts: completeValue(['synthesize'])
|
|
30
32
|
}
|
|
31
33
|
);
|
|
32
|
-
const ROLE_OPERATION_SETS =
|
|
34
|
+
const ROLE_OPERATION_SETS = completeValue(
|
|
33
35
|
{
|
|
34
36
|
llm: new Set(ROLE_OPERATIONS.llm),
|
|
35
37
|
stt: new Set(ROLE_OPERATIONS.stt),
|
|
36
38
|
tts: new Set(ROLE_OPERATIONS.tts)
|
|
37
39
|
}
|
|
38
40
|
);
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const STREAM_CLEANUP_TIMEOUT_MS = 2_000;
|
|
42
|
-
const ROUTE_KEYS = Object.freeze(['default', 'localOnly']);
|
|
43
|
-
const RUNTIME_CONSTRUCTION_AUTHORITY = Object.freeze({});
|
|
41
|
+
const ROUTE_KEYS = completeValue(['default', 'localOnly']);
|
|
42
|
+
const RUNTIME_CONSTRUCTION_AUTHORITY = completeValue({});
|
|
44
43
|
const PROVIDER_RECORDS = new WeakMap();
|
|
45
44
|
|
|
46
45
|
function fail(message, code = 'ARCANE_AI_PROVIDER_RUNTIME_INVALID') {
|
|
@@ -80,30 +79,9 @@ function isAbort(error, signal) {
|
|
|
80
79
|
|| error?.code === 'ARCANE_REQUEST_ABORTED';
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
async function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
timer = setTimeout(
|
|
87
|
-
function settleAIStreamCleanupTimeout() {
|
|
88
|
-
resolve({completed: false, results: null});
|
|
89
|
-
},
|
|
90
|
-
STREAM_CLEANUP_TIMEOUT_MS
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
try {
|
|
94
|
-
return await Promise.race([
|
|
95
|
-
Promise.resolve(operation).then(
|
|
96
|
-
function settleAIStreamCleanup(results) {
|
|
97
|
-
return {completed: true, results};
|
|
98
|
-
}
|
|
99
|
-
),
|
|
100
|
-
timeout
|
|
101
|
-
]);
|
|
102
|
-
} finally {
|
|
103
|
-
if (timer !== null) {
|
|
104
|
-
clearTimeout(timer);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
82
|
+
async function awaitStreamCleanup(operation) {
|
|
83
|
+
const results = await Promise.resolve(operation);
|
|
84
|
+
return {completed: true, results};
|
|
107
85
|
}
|
|
108
86
|
|
|
109
87
|
function assertStreamCleanupComplete(outcome) {
|
|
@@ -114,7 +92,7 @@ function assertStreamCleanupComplete(outcome) {
|
|
|
114
92
|
});
|
|
115
93
|
if (!outcome.completed || rejected) {
|
|
116
94
|
throw operationError(
|
|
117
|
-
'The AI provider stream did not confirm
|
|
95
|
+
'The AI provider stream did not confirm cleanup.',
|
|
118
96
|
'ARCANE_AI_STREAM_CLEANUP_INCOMPLETE'
|
|
119
97
|
);
|
|
120
98
|
}
|
|
@@ -129,10 +107,19 @@ function assertRole(role) {
|
|
|
129
107
|
function assertIdentifier(value, label) {
|
|
130
108
|
if (typeof value !== 'string'
|
|
131
109
|
|| value.length < 1
|
|
132
|
-
|| value.length > IDENTIFIER_LIMIT
|
|
133
110
|
|| value.trim() !== value) {
|
|
134
|
-
fail(`${label} must be a trimmed
|
|
111
|
+
fail(`${label} must be a nonempty trimmed string.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function nextSequence(value) {
|
|
116
|
+
if (typeof value === 'bigint') {
|
|
117
|
+
return value + 1n;
|
|
118
|
+
}
|
|
119
|
+
if (value === Number.MAX_SAFE_INTEGER) {
|
|
120
|
+
return BigInt(value) + 1n;
|
|
135
121
|
}
|
|
122
|
+
return value + 1;
|
|
136
123
|
}
|
|
137
124
|
|
|
138
125
|
function assertAbortSignal(signal) {
|
|
@@ -159,7 +146,366 @@ function assertPlainObject(value, label) {
|
|
|
159
146
|
}
|
|
160
147
|
}
|
|
161
148
|
|
|
162
|
-
function
|
|
149
|
+
function isPlainRecord(value) {
|
|
150
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const prototype = Object.getPrototypeOf(value);
|
|
154
|
+
return prototype === Object.prototype || prototype === null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validateLLMToolDeclarations(value) {
|
|
158
|
+
if (value === undefined) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!Array.isArray(value)) {
|
|
162
|
+
fail(
|
|
163
|
+
'AI LLM tools must be an array.',
|
|
164
|
+
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
168
|
+
const tool = value[index];
|
|
169
|
+
const parameters = tool?.function?.parameters;
|
|
170
|
+
const messageSchema = parameters?.properties?.message;
|
|
171
|
+
if (!isPlainRecord(tool)
|
|
172
|
+
|| tool.type !== 'function'
|
|
173
|
+
|| !isPlainRecord(tool.function)
|
|
174
|
+
|| !isPlainRecord(parameters)
|
|
175
|
+
|| parameters.type !== 'object'
|
|
176
|
+
|| !isPlainRecord(parameters.properties)
|
|
177
|
+
|| !isPlainRecord(messageSchema)
|
|
178
|
+
|| messageSchema.type !== 'string'
|
|
179
|
+
|| !Number.isInteger(messageSchema.minLength)
|
|
180
|
+
|| messageSchema.minLength < 1
|
|
181
|
+
|| !Array.isArray(parameters.required)
|
|
182
|
+
|| !parameters.required.includes('message')) {
|
|
183
|
+
fail(
|
|
184
|
+
`AI LLM tools[${index}] must require a nonempty string parameters.properties.message.`,
|
|
185
|
+
'ARCANE_AI_TOOL_MESSAGE_REQUIRED'
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function validateLLMToolCall(call, label) {
|
|
192
|
+
if (!isPlainRecord(call)
|
|
193
|
+
|| typeof call.id !== 'string'
|
|
194
|
+
|| !call.id.trim()
|
|
195
|
+
|| call.type !== 'function'
|
|
196
|
+
|| !isPlainRecord(call.function)
|
|
197
|
+
|| typeof call.function.name !== 'string'
|
|
198
|
+
|| !call.function.name.trim()
|
|
199
|
+
|| typeof call.function.arguments !== 'string') {
|
|
200
|
+
fail(
|
|
201
|
+
`${label} must be one complete structural function call.`,
|
|
202
|
+
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
let argumentsRecord;
|
|
206
|
+
try {
|
|
207
|
+
argumentsRecord = JSON.parse(call.function.arguments);
|
|
208
|
+
} catch (cause) {
|
|
209
|
+
throw operationError(
|
|
210
|
+
`${label} arguments must encode a JSON object.`,
|
|
211
|
+
'ARCANE_AI_TOOL_CALL_INVALID',
|
|
212
|
+
cause
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (!isPlainRecord(argumentsRecord)) {
|
|
216
|
+
fail(
|
|
217
|
+
`${label} arguments must encode a JSON object.`,
|
|
218
|
+
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (typeof argumentsRecord.message !== 'string'
|
|
222
|
+
|| !argumentsRecord.message.trim()) {
|
|
223
|
+
fail(
|
|
224
|
+
`${label} arguments must include a nonempty user-facing message.`,
|
|
225
|
+
'ARCANE_AI_TOOL_MESSAGE_REQUIRED'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return call.id;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function validateLLMMessageToolCalls(message, label) {
|
|
232
|
+
if (!isPlainRecord(message)) {
|
|
233
|
+
fail(
|
|
234
|
+
`${label} must be a plain message object.`,
|
|
235
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (Object.hasOwn(message, 'toolCalls')
|
|
239
|
+
|| Object.hasOwn(message, 'function_call')
|
|
240
|
+
|| Object.hasOwn(message, 'functionCall')) {
|
|
241
|
+
fail(
|
|
242
|
+
`${label} contains a noncanonical structural tool-call field.`,
|
|
243
|
+
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (!Object.hasOwn(message, 'tool_calls')) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
const descriptor = Object.getOwnPropertyDescriptor(message, 'tool_calls');
|
|
250
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')
|
|
251
|
+
|| !Array.isArray(descriptor.value)) {
|
|
252
|
+
fail(
|
|
253
|
+
`${label}.tool_calls must be an array data property.`,
|
|
254
|
+
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (descriptor.value.length > 1) {
|
|
258
|
+
fail(
|
|
259
|
+
'The Arcane AI runtime accepts one structural tool call at a time.',
|
|
260
|
+
'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
for (let index = 0; index < descriptor.value.length; index += 1) {
|
|
264
|
+
validateLLMToolCall(
|
|
265
|
+
descriptor.value[index],
|
|
266
|
+
`${label}.tool_calls[${index}]`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return descriptor.value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function validateLLMRequestPayload(payload) {
|
|
273
|
+
assertPlainObject(payload, 'AI LLM request payload');
|
|
274
|
+
if (!Array.isArray(payload.messages)) {
|
|
275
|
+
fail('AI LLM request payload.messages must be an array.');
|
|
276
|
+
}
|
|
277
|
+
let pendingToolCallId = null;
|
|
278
|
+
for (let index = 0; index < payload.messages.length; index += 1) {
|
|
279
|
+
const message = payload.messages[index];
|
|
280
|
+
if (!isPlainRecord(message)) {
|
|
281
|
+
fail(`AI LLM request payload.messages[${index}] must be a plain object.`);
|
|
282
|
+
}
|
|
283
|
+
if (Object.hasOwn(message, 'tool_calls')
|
|
284
|
+
&& message.role !== 'assistant') {
|
|
285
|
+
fail(
|
|
286
|
+
`AI LLM request payload.messages[${index}].tool_calls is valid only for an assistant message.`,
|
|
287
|
+
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const calls = validateLLMMessageToolCalls(
|
|
291
|
+
message,
|
|
292
|
+
`AI LLM request payload.messages[${index}]`
|
|
293
|
+
);
|
|
294
|
+
let openedToolCall = false;
|
|
295
|
+
if (calls.length) {
|
|
296
|
+
if (pendingToolCallId !== null) {
|
|
297
|
+
fail(
|
|
298
|
+
'The Arcane AI runtime accepts one structural tool call at a time.',
|
|
299
|
+
'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
pendingToolCallId = calls[0].id;
|
|
303
|
+
openedToolCall = true;
|
|
304
|
+
}
|
|
305
|
+
if (message.role === 'tool') {
|
|
306
|
+
if (typeof message.content !== 'string'
|
|
307
|
+
|| !message.content.trim()) {
|
|
308
|
+
fail(
|
|
309
|
+
`AI LLM request payload.messages[${index}] must contain a nonblank user-facing tool result.`,
|
|
310
|
+
'ARCANE_AI_INVALID_TOOL_MESSAGE'
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (pendingToolCallId === null
|
|
314
|
+
|| typeof message.tool_call_id !== 'string'
|
|
315
|
+
|| message.tool_call_id !== pendingToolCallId) {
|
|
316
|
+
fail(
|
|
317
|
+
`AI LLM request payload.messages[${index}] does not settle the pending structural tool call.`,
|
|
318
|
+
'ARCANE_AI_INVALID_TOOL_MESSAGE'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
pendingToolCallId = null;
|
|
322
|
+
} else {
|
|
323
|
+
if (Object.hasOwn(message, 'tool_call_id')) {
|
|
324
|
+
fail(
|
|
325
|
+
`AI LLM request payload.messages[${index}].tool_call_id is valid only for a tool result.`,
|
|
326
|
+
'ARCANE_AI_INVALID_TOOL_MESSAGE'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (pendingToolCallId !== null && !openedToolCall) {
|
|
330
|
+
fail(
|
|
331
|
+
`AI LLM request payload.messages[${index}] precedes the pending structural tool result.`,
|
|
332
|
+
'ARCANE_AI_TOOL_RESULT_REQUIRED'
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (pendingToolCallId !== null) {
|
|
338
|
+
fail(
|
|
339
|
+
'The pending structural tool call must be settled before requesting another response.',
|
|
340
|
+
'ARCANE_AI_TOOL_RESULT_REQUIRED'
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
validateLLMToolDeclarations(payload.tools);
|
|
344
|
+
const parallelValues = [
|
|
345
|
+
payload.parallelToolCalls,
|
|
346
|
+
payload.parallel_tool_calls
|
|
347
|
+
];
|
|
348
|
+
if (parallelValues.some(function enablesParallelLLMTools(value) {
|
|
349
|
+
return value !== undefined && value !== false;
|
|
350
|
+
})) {
|
|
351
|
+
fail(
|
|
352
|
+
'The Arcane AI runtime accepts one structural tool call at a time.',
|
|
353
|
+
'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function validateLLMTerminalMessage(message, label) {
|
|
359
|
+
if (!isPlainRecord(message) || message.role !== 'assistant') {
|
|
360
|
+
fail(
|
|
361
|
+
`${label} must contain an assistant message.`,
|
|
362
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return validateLLMMessageToolCalls(message, label);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function validateLLMTerminalResult(value, label) {
|
|
369
|
+
if (typeof value === 'string') {
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
if (!isPlainRecord(value)) {
|
|
373
|
+
fail(
|
|
374
|
+
`${label} must be text or a chat completion object.`,
|
|
375
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const hasMessage = Object.hasOwn(value, 'message');
|
|
379
|
+
const hasChoices = Object.hasOwn(value, 'choices');
|
|
380
|
+
if (hasMessage === hasChoices) {
|
|
381
|
+
fail(
|
|
382
|
+
`${label} must contain exactly one message or choices envelope.`,
|
|
383
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (hasMessage) {
|
|
387
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, 'message');
|
|
388
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
389
|
+
fail(
|
|
390
|
+
`${label}.message must be a data property.`,
|
|
391
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
validateLLMTerminalMessage(descriptor.value, `${label}.message`);
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
397
|
+
const choicesDescriptor = Object.getOwnPropertyDescriptor(value, 'choices');
|
|
398
|
+
if (!choicesDescriptor || !Object.hasOwn(choicesDescriptor, 'value')
|
|
399
|
+
|| !Array.isArray(choicesDescriptor.value)
|
|
400
|
+
|| choicesDescriptor.value.length === 0) {
|
|
401
|
+
fail(
|
|
402
|
+
`${label}.choices must be a nonempty array data property.`,
|
|
403
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const indexes = new Set();
|
|
407
|
+
let totalToolCalls = 0;
|
|
408
|
+
for (let position = 0; position < choicesDescriptor.value.length; position += 1) {
|
|
409
|
+
const choice = choicesDescriptor.value[position];
|
|
410
|
+
if (!isPlainRecord(choice)
|
|
411
|
+
|| !Number.isSafeInteger(choice.index)
|
|
412
|
+
|| choice.index < 0
|
|
413
|
+
|| indexes.has(choice.index)) {
|
|
414
|
+
fail(
|
|
415
|
+
`${label}.choices[${position}] must have a unique nonnegative safe-integer index.`,
|
|
416
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
indexes.add(choice.index);
|
|
420
|
+
const messageDescriptor = Object.getOwnPropertyDescriptor(choice, 'message');
|
|
421
|
+
if (!messageDescriptor || !Object.hasOwn(messageDescriptor, 'value')) {
|
|
422
|
+
fail(
|
|
423
|
+
`${label}.choices[${position}].message must be a data property.`,
|
|
424
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const calls = validateLLMTerminalMessage(
|
|
428
|
+
messageDescriptor.value,
|
|
429
|
+
`${label}.choices[${position}].message`
|
|
430
|
+
);
|
|
431
|
+
if (position > 0 && calls.length) {
|
|
432
|
+
fail(
|
|
433
|
+
`${label} placed a structural tool call outside the selected first choice.`,
|
|
434
|
+
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
totalToolCalls += calls.length;
|
|
438
|
+
if (totalToolCalls > 1) {
|
|
439
|
+
fail(
|
|
440
|
+
'The Arcane AI runtime accepts one structural tool call at a time.',
|
|
441
|
+
'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return value;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function isLLMStreamContentKey(key) {
|
|
449
|
+
return key === 'content'
|
|
450
|
+
|| key === 'text'
|
|
451
|
+
|| key === 'thinking'
|
|
452
|
+
|| key === 'reasoning'
|
|
453
|
+
|| key === 'reasoning_content';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function projectLLMStreamContent(value, seen = new WeakSet()) {
|
|
457
|
+
if (!value || typeof value !== 'object' || seen.has(value)) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
seen.add(value);
|
|
461
|
+
if (Array.isArray(value)) {
|
|
462
|
+
const result = [];
|
|
463
|
+
for (const item of value) {
|
|
464
|
+
const projected = projectLLMStreamContent(item, seen);
|
|
465
|
+
if (projected !== null) {
|
|
466
|
+
result.push(projected);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
seen.delete(value);
|
|
470
|
+
return result.length ? result : null;
|
|
471
|
+
}
|
|
472
|
+
if (!isPlainRecord(value)) {
|
|
473
|
+
seen.delete(value);
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const result = {};
|
|
477
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
478
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
479
|
+
if (typeof key === 'symbol') {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const descriptor = descriptors[key];
|
|
483
|
+
if (!Object.hasOwn(descriptor, 'value')) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (isLLMStreamContentKey(key)
|
|
487
|
+
&& descriptor.value !== null
|
|
488
|
+
&& descriptor.value !== undefined) {
|
|
489
|
+
result[key] = descriptor.value;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const projected = projectLLMStreamContent(descriptor.value, seen);
|
|
493
|
+
if (projected !== null) {
|
|
494
|
+
result[key] = projected;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
seen.delete(value);
|
|
498
|
+
return Object.keys(result).length ? result : null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function projectLLMStreamChunk(value) {
|
|
502
|
+
if (typeof value === 'string') {
|
|
503
|
+
return value;
|
|
504
|
+
}
|
|
505
|
+
return projectLLMStreamContent(value);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function assertCallbackFreeProviderValue(value, seen = new WeakSet()) {
|
|
163
509
|
if (typeof value === 'function') {
|
|
164
510
|
fail(
|
|
165
511
|
'AI providers receive data-only request payloads.',
|
|
@@ -169,9 +515,6 @@ function assertCallbackFreeProviderValue(value, seen = new WeakSet(), depth = 0)
|
|
|
169
515
|
if (!value || typeof value !== 'object') {
|
|
170
516
|
return;
|
|
171
517
|
}
|
|
172
|
-
if (depth > 32) {
|
|
173
|
-
fail('AI provider request payload nesting exceeds the supported limit.');
|
|
174
|
-
}
|
|
175
518
|
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
176
519
|
return;
|
|
177
520
|
}
|
|
@@ -188,7 +531,7 @@ function assertCallbackFreeProviderValue(value, seen = new WeakSet(), depth = 0)
|
|
|
188
531
|
if (typeof key === 'symbol' || !Object.hasOwn(descriptor, 'value')) {
|
|
189
532
|
fail('AI provider request payloads must contain data properties only.');
|
|
190
533
|
}
|
|
191
|
-
assertCallbackFreeProviderValue(descriptor.value, seen
|
|
534
|
+
assertCallbackFreeProviderValue(descriptor.value, seen);
|
|
192
535
|
}
|
|
193
536
|
seen.delete(value);
|
|
194
537
|
}
|
|
@@ -231,7 +574,7 @@ function immutableSelection(role, value) {
|
|
|
231
574
|
fail(`${role} provider selection.localOnly must be null or a boolean.`);
|
|
232
575
|
}
|
|
233
576
|
|
|
234
|
-
return
|
|
577
|
+
return completeValue(
|
|
235
578
|
{
|
|
236
579
|
providerId: value.providerId,
|
|
237
580
|
modelId: value.modelId,
|
|
@@ -247,7 +590,7 @@ function immutableRoleRoutes(role, value) {
|
|
|
247
590
|
if (localSelection && localSelection.localOnly !== true) {
|
|
248
591
|
fail(`${role} localOnly route must identify a local-only selection.`);
|
|
249
592
|
}
|
|
250
|
-
return
|
|
593
|
+
return completeValue(
|
|
251
594
|
{
|
|
252
595
|
default: defaultSelection,
|
|
253
596
|
localOnly: localSelection
|
|
@@ -257,7 +600,7 @@ function immutableRoleRoutes(role, value) {
|
|
|
257
600
|
|
|
258
601
|
function immutableSelections(value) {
|
|
259
602
|
assertClosedRecord(value, AI_RUNTIME_ROLES, 'AI provider selections');
|
|
260
|
-
return
|
|
603
|
+
return completeValue(
|
|
261
604
|
{
|
|
262
605
|
llm: immutableRoleRoutes('llm', value.llm),
|
|
263
606
|
stt: immutableRoleRoutes('stt', value.stt),
|
|
@@ -269,7 +612,7 @@ function immutableSelections(value) {
|
|
|
269
612
|
function immutableSpeechSelections(value) {
|
|
270
613
|
try {
|
|
271
614
|
assertClosedRecord(value, SPEECH_ROLES, 'AI speech provider selections');
|
|
272
|
-
return
|
|
615
|
+
return completeValue(
|
|
273
616
|
{
|
|
274
617
|
stt: immutableRoleRoutes('stt', value.stt),
|
|
275
618
|
tts: immutableRoleRoutes('tts', value.tts)
|
|
@@ -286,7 +629,8 @@ function immutableSpeechSelections(value) {
|
|
|
286
629
|
|
|
287
630
|
function immutableStartupOptions(options) {
|
|
288
631
|
if (options === undefined) {
|
|
289
|
-
return
|
|
632
|
+
return completeValue({
|
|
633
|
+
startLanguageModel: true,
|
|
290
634
|
startMuted: true,
|
|
291
635
|
startTranscription: false,
|
|
292
636
|
signal: null
|
|
@@ -297,7 +641,8 @@ function immutableStartupOptions(options) {
|
|
|
297
641
|
const descriptors = Object.getOwnPropertyDescriptors(options);
|
|
298
642
|
for (const key of Reflect.ownKeys(descriptors)) {
|
|
299
643
|
if (typeof key === 'symbol'
|
|
300
|
-
|| (key !== '
|
|
644
|
+
|| (key !== 'startLanguageModel'
|
|
645
|
+
&& key !== 'startMuted'
|
|
301
646
|
&& key !== 'startTranscription'
|
|
302
647
|
&& key !== 'signal')) {
|
|
303
648
|
fail('AI provider startup options contain an unknown option.');
|
|
@@ -306,6 +651,12 @@ function immutableStartupOptions(options) {
|
|
|
306
651
|
fail(`AI provider startup options.${key} must be a data property.`);
|
|
307
652
|
}
|
|
308
653
|
}
|
|
654
|
+
const startLanguageModel = Object.hasOwn(
|
|
655
|
+
descriptors,
|
|
656
|
+
'startLanguageModel'
|
|
657
|
+
)
|
|
658
|
+
? descriptors.startLanguageModel.value
|
|
659
|
+
: true;
|
|
309
660
|
const startMuted = Object.hasOwn(descriptors, 'startMuted')
|
|
310
661
|
? descriptors.startMuted.value
|
|
311
662
|
: true;
|
|
@@ -315,6 +666,9 @@ function immutableStartupOptions(options) {
|
|
|
315
666
|
const signal = Object.hasOwn(descriptors, 'signal')
|
|
316
667
|
? descriptors.signal.value
|
|
317
668
|
: null;
|
|
669
|
+
if (typeof startLanguageModel !== 'boolean') {
|
|
670
|
+
fail('AI startup startLanguageModel must be a boolean.');
|
|
671
|
+
}
|
|
318
672
|
if (typeof startMuted !== 'boolean') {
|
|
319
673
|
fail('AI startup startMuted must be a boolean.');
|
|
320
674
|
}
|
|
@@ -322,7 +676,12 @@ function immutableStartupOptions(options) {
|
|
|
322
676
|
fail('AI startup startTranscription must be a boolean.');
|
|
323
677
|
}
|
|
324
678
|
assertAbortSignal(signal);
|
|
325
|
-
return
|
|
679
|
+
return completeValue({
|
|
680
|
+
startLanguageModel,
|
|
681
|
+
startMuted,
|
|
682
|
+
startTranscription,
|
|
683
|
+
signal
|
|
684
|
+
});
|
|
326
685
|
}
|
|
327
686
|
|
|
328
687
|
function immutableInspectionOptions(options) {
|
|
@@ -347,12 +706,12 @@ function immutableInspectionOptions(options) {
|
|
|
347
706
|
fail('AI provider inspection localOnly must be a boolean.');
|
|
348
707
|
}
|
|
349
708
|
assertAbortSignal(signal);
|
|
350
|
-
return
|
|
709
|
+
return completeValue({localOnly, signal});
|
|
351
710
|
}
|
|
352
711
|
|
|
353
712
|
function nullableTupleIdentifier(value) {
|
|
354
713
|
return typeof value === 'string' && value.trim()
|
|
355
|
-
? value
|
|
714
|
+
? value
|
|
356
715
|
: null;
|
|
357
716
|
}
|
|
358
717
|
|
|
@@ -362,7 +721,7 @@ function immutableProgress(value) {
|
|
|
362
721
|
['phase', 'completed', 'total', 'unit', 'heartbeat'],
|
|
363
722
|
'AI provider progress'
|
|
364
723
|
);
|
|
365
|
-
return
|
|
724
|
+
return completeValue(
|
|
366
725
|
{
|
|
367
726
|
phase: value.phase,
|
|
368
727
|
completed: value.completed,
|
|
@@ -375,21 +734,24 @@ function immutableProgress(value) {
|
|
|
375
734
|
|
|
376
735
|
function stateError(error, fallbackCode) {
|
|
377
736
|
const code = typeof error?.code === 'string'
|
|
378
|
-
&& /^[A-Z][A-Z0-9_]
|
|
379
|
-
? error.code
|
|
737
|
+
&& /^[A-Z][A-Z0-9_]*$/.test(error.code)
|
|
738
|
+
? error.code
|
|
380
739
|
: fallbackCode;
|
|
381
|
-
const
|
|
740
|
+
const fallbackMessage = code === 'ARCANE_AI_REQUEST_ABORTED'
|
|
382
741
|
|| code === 'AI_REQUEST_ABORTED'
|
|
383
742
|
? 'The AI operation was cancelled.'
|
|
384
743
|
: code.includes('AUTHORITY') || code.includes('PROVENANCE')
|
|
385
|
-
? 'The selected AI model
|
|
744
|
+
? 'The selected AI model could not be used.'
|
|
386
745
|
: code.includes('UNAVAILABLE') || code.includes('NOT_REGISTERED')
|
|
387
746
|
? 'The selected AI provider is unavailable.'
|
|
388
747
|
: 'The selected AI provider operation failed.';
|
|
389
|
-
|
|
748
|
+
const message = typeof error?.message === 'string' && error.message
|
|
749
|
+
? error.message
|
|
750
|
+
: fallbackMessage;
|
|
751
|
+
return completeValue(
|
|
390
752
|
{
|
|
391
753
|
code,
|
|
392
|
-
message
|
|
754
|
+
message
|
|
393
755
|
}
|
|
394
756
|
);
|
|
395
757
|
}
|
|
@@ -442,7 +804,7 @@ function validateProvider(provider) {
|
|
|
442
804
|
for (const method of PROVIDER_METHODS) {
|
|
443
805
|
record[method] = provider[method].bind(provider);
|
|
444
806
|
}
|
|
445
|
-
const admitted =
|
|
807
|
+
const admitted = completeValue(record);
|
|
446
808
|
PROVIDER_RECORDS.set(provider, admitted);
|
|
447
809
|
return admitted;
|
|
448
810
|
}
|
|
@@ -595,11 +957,11 @@ function immutableSpeechProviderReplacement(value) {
|
|
|
595
957
|
providers[role] = provider;
|
|
596
958
|
}
|
|
597
959
|
|
|
598
|
-
return
|
|
960
|
+
return completeValue(
|
|
599
961
|
{
|
|
600
|
-
providers:
|
|
962
|
+
providers: completeValue(providers),
|
|
601
963
|
routes,
|
|
602
|
-
expectedProviders:
|
|
964
|
+
expectedProviders: completeValue(expectedProviders),
|
|
603
965
|
removal
|
|
604
966
|
}
|
|
605
967
|
);
|
|
@@ -690,7 +1052,7 @@ function immutableSpeechProviderRoleReplacement(role, value) {
|
|
|
690
1052
|
);
|
|
691
1053
|
}
|
|
692
1054
|
}
|
|
693
|
-
return
|
|
1055
|
+
return completeValue({role, provider, routes, expectedProvider, removal});
|
|
694
1056
|
}
|
|
695
1057
|
|
|
696
1058
|
function validateProviderStatus(status) {
|
|
@@ -709,21 +1071,20 @@ function validateInspection(inspection, selection) {
|
|
|
709
1071
|
fail('AI provider inspection.available must be a boolean.');
|
|
710
1072
|
}
|
|
711
1073
|
if (!inspection.available) {
|
|
712
|
-
const code = typeof inspection.code === 'string' && inspection.code.
|
|
713
|
-
? inspection.code
|
|
1074
|
+
const code = typeof inspection.code === 'string' && inspection.code.length > 0
|
|
1075
|
+
? inspection.code
|
|
714
1076
|
: 'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED';
|
|
715
|
-
const message = typeof inspection.message === 'string' && inspection.message.
|
|
716
|
-
? inspection.message
|
|
717
|
-
: 'The selected AI provider is
|
|
1077
|
+
const message = typeof inspection.message === 'string' && inspection.message.length > 0
|
|
1078
|
+
? inspection.message
|
|
1079
|
+
: 'The selected AI provider is unavailable.';
|
|
718
1080
|
throw operationError(message, code);
|
|
719
1081
|
}
|
|
720
1082
|
assertPlainObject(inspection.authority, 'AI provider inspection.authority');
|
|
721
1083
|
if (inspection.authority.protocol !== AI_MODEL_AUTHORITY_PROTOCOL
|
|
722
1084
|
|| inspection.authority.providerId !== selection.providerId
|
|
723
|
-
|| inspection.authority.modelId !== selection.modelId
|
|
724
|
-
|| inspection.authority.admitted !== true) {
|
|
1085
|
+
|| inspection.authority.modelId !== selection.modelId) {
|
|
725
1086
|
throw operationError(
|
|
726
|
-
'The selected AI provider
|
|
1087
|
+
'The selected AI provider returned a different model authority.',
|
|
727
1088
|
'ARCANE_AI_MODEL_AUTHORITY_REQUIRED'
|
|
728
1089
|
);
|
|
729
1090
|
}
|
|
@@ -733,7 +1094,7 @@ function validateInspection(inspection, selection) {
|
|
|
733
1094
|
function createRoleSlot(role) {
|
|
734
1095
|
return {
|
|
735
1096
|
role,
|
|
736
|
-
routes:
|
|
1097
|
+
routes: completeValue({default: null, localOnly: null}),
|
|
737
1098
|
selection: null,
|
|
738
1099
|
generation: 0,
|
|
739
1100
|
operationSequence: 0,
|
|
@@ -742,7 +1103,7 @@ function createRoleSlot(role) {
|
|
|
742
1103
|
loadPromise: null,
|
|
743
1104
|
unloadPromise: null,
|
|
744
1105
|
disposePromise: null,
|
|
745
|
-
|
|
1106
|
+
requestQueue: [],
|
|
746
1107
|
request: null,
|
|
747
1108
|
disposed: false,
|
|
748
1109
|
ready: false
|
|
@@ -751,7 +1112,7 @@ function createRoleSlot(role) {
|
|
|
751
1112
|
|
|
752
1113
|
export class AIProviderRuntime {
|
|
753
1114
|
#providers = new Map();
|
|
754
|
-
#slots =
|
|
1115
|
+
#slots = completeValue(
|
|
755
1116
|
{
|
|
756
1117
|
llm: createRoleSlot('llm'),
|
|
757
1118
|
stt: createRoleSlot('stt'),
|
|
@@ -828,7 +1189,7 @@ export class AIProviderRuntime {
|
|
|
828
1189
|
);
|
|
829
1190
|
}
|
|
830
1191
|
if (selection.localOnly === null) {
|
|
831
|
-
nextRoutes[routeName] =
|
|
1192
|
+
nextRoutes[routeName] = completeValue(
|
|
832
1193
|
{
|
|
833
1194
|
providerId: selection.providerId,
|
|
834
1195
|
modelId: selection.modelId,
|
|
@@ -847,7 +1208,7 @@ export class AIProviderRuntime {
|
|
|
847
1208
|
this.#providers.set(key, admitted);
|
|
848
1209
|
if (reconciled) {
|
|
849
1210
|
const previousSelection = slot.selection;
|
|
850
|
-
slot.routes =
|
|
1211
|
+
slot.routes = completeValue(nextRoutes);
|
|
851
1212
|
if (previousSelection?.providerId === admitted.id
|
|
852
1213
|
&& previousSelection.localOnly === null) {
|
|
853
1214
|
slot.generation += 1;
|
|
@@ -938,7 +1299,7 @@ export class AIProviderRuntime {
|
|
|
938
1299
|
if (!provider) {
|
|
939
1300
|
return null;
|
|
940
1301
|
}
|
|
941
|
-
return
|
|
1302
|
+
return completeValue(
|
|
942
1303
|
{
|
|
943
1304
|
protocol: provider.protocol,
|
|
944
1305
|
role: provider.role,
|
|
@@ -1124,6 +1485,30 @@ export class AIProviderRuntime {
|
|
|
1124
1485
|
}
|
|
1125
1486
|
}
|
|
1126
1487
|
|
|
1488
|
+
#pendingSpeechProviderHydrationMatches(role, slot, provider, routes) {
|
|
1489
|
+
if (!provider || !slot.selection || !routes.default) {
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
const pending = slot.selection;
|
|
1493
|
+
if (this.#providers.has(providerKey(role, pending.providerId))
|
|
1494
|
+
|| provider.id !== pending.providerId
|
|
1495
|
+
|| routes.default.providerId !== pending.providerId
|
|
1496
|
+
|| routes.default.modelId !== pending.modelId) {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
const currentSelections = [
|
|
1500
|
+
slot.selection,
|
|
1501
|
+
slot.routes.default,
|
|
1502
|
+
slot.routes.localOnly
|
|
1503
|
+
].filter(Boolean);
|
|
1504
|
+
return currentSelections.length > 0
|
|
1505
|
+
&& currentSelections.every(selection =>
|
|
1506
|
+
selection.providerId === pending.providerId
|
|
1507
|
+
&& selection.modelId === pending.modelId
|
|
1508
|
+
&& selection.localOnly === null
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1127
1512
|
#commitSpeechProviderRoleReplacement(role, value) {
|
|
1128
1513
|
const replacement = immutableSpeechProviderRoleReplacement(role, value);
|
|
1129
1514
|
const currentState = getAIRuntimeState();
|
|
@@ -1168,9 +1553,15 @@ export class AIProviderRuntime {
|
|
|
1168
1553
|
`AI role ${role} selected routes no longer belong to the expected provider identity.`
|
|
1169
1554
|
);
|
|
1170
1555
|
}
|
|
1171
|
-
} else if (slot.routes.default
|
|
1556
|
+
} else if ((slot.routes.default
|
|
1172
1557
|
|| slot.routes.localOnly
|
|
1173
|
-
|| slot.selection)
|
|
1558
|
+
|| slot.selection)
|
|
1559
|
+
&& !this.#pendingSpeechProviderHydrationMatches(
|
|
1560
|
+
role,
|
|
1561
|
+
slot,
|
|
1562
|
+
replacement.provider,
|
|
1563
|
+
replacement.routes
|
|
1564
|
+
)) {
|
|
1174
1565
|
throw speechProviderReplacementError(
|
|
1175
1566
|
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_REQUIRED',
|
|
1176
1567
|
'speech-provider-replacement-expected-provider-required',
|
|
@@ -1218,14 +1609,6 @@ export class AIProviderRuntime {
|
|
|
1218
1609
|
);
|
|
1219
1610
|
}
|
|
1220
1611
|
}
|
|
1221
|
-
if (currentState.revision === Number.MAX_SAFE_INTEGER) {
|
|
1222
|
-
throw speechProviderReplacementError(
|
|
1223
|
-
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
|
|
1224
|
-
'speech-provider-replacement-state-revision-exhausted',
|
|
1225
|
-
'AI speech provider replacement cannot publish another runtime state revision.'
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
1612
|
if (expected) {
|
|
1230
1613
|
this.#providers.delete(providerKey(role, expected.id));
|
|
1231
1614
|
this.#disposeAllCompletedProviders.delete(expected);
|
|
@@ -1242,7 +1625,7 @@ export class AIProviderRuntime {
|
|
|
1242
1625
|
this.#configured = true;
|
|
1243
1626
|
publishAIRuntimeRoleState(role, roleRecord(role, slot.selection));
|
|
1244
1627
|
|
|
1245
|
-
return
|
|
1628
|
+
return completeValue(
|
|
1246
1629
|
{
|
|
1247
1630
|
role,
|
|
1248
1631
|
routes: replacement.routes,
|
|
@@ -1315,9 +1698,15 @@ export class AIProviderRuntime {
|
|
|
1315
1698
|
`AI role ${role} selected routes no longer belong to the expected provider identity.`
|
|
1316
1699
|
);
|
|
1317
1700
|
}
|
|
1318
|
-
} else if (slot.routes.default
|
|
1701
|
+
} else if ((slot.routes.default
|
|
1319
1702
|
|| slot.routes.localOnly
|
|
1320
|
-
|| slot.selection)
|
|
1703
|
+
|| slot.selection)
|
|
1704
|
+
&& !this.#pendingSpeechProviderHydrationMatches(
|
|
1705
|
+
role,
|
|
1706
|
+
slot,
|
|
1707
|
+
replacement.providers[role],
|
|
1708
|
+
replacement.routes[role]
|
|
1709
|
+
)) {
|
|
1321
1710
|
throw speechProviderReplacementError(
|
|
1322
1711
|
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_REQUIRED',
|
|
1323
1712
|
'speech-provider-replacement-expected-provider-required',
|
|
@@ -1373,14 +1762,6 @@ export class AIProviderRuntime {
|
|
|
1373
1762
|
}
|
|
1374
1763
|
}
|
|
1375
1764
|
}
|
|
1376
|
-
if (currentState.revision === Number.MAX_SAFE_INTEGER) {
|
|
1377
|
-
throw speechProviderReplacementError(
|
|
1378
|
-
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
|
|
1379
|
-
'speech-provider-replacement-state-revision-exhausted',
|
|
1380
|
-
'AI speech provider replacement cannot publish another runtime state revision.'
|
|
1381
|
-
);
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
1765
|
for (const role of SPEECH_ROLES) {
|
|
1385
1766
|
const expected = replacement.expectedProviders[role];
|
|
1386
1767
|
const provider = replacement.providers[role];
|
|
@@ -1411,10 +1792,10 @@ export class AIProviderRuntime {
|
|
|
1411
1792
|
}
|
|
1412
1793
|
);
|
|
1413
1794
|
|
|
1414
|
-
return
|
|
1795
|
+
return completeValue(
|
|
1415
1796
|
{
|
|
1416
1797
|
routes: replacement.routes,
|
|
1417
|
-
unregisters:
|
|
1798
|
+
unregisters: completeValue(
|
|
1418
1799
|
{
|
|
1419
1800
|
stt: replacement.removal
|
|
1420
1801
|
? null
|
|
@@ -1502,16 +1883,16 @@ export class AIProviderRuntime {
|
|
|
1502
1883
|
fail('AI provider.catalog() must synchronously return an array.');
|
|
1503
1884
|
}
|
|
1504
1885
|
entries.push(
|
|
1505
|
-
|
|
1886
|
+
completeValue(
|
|
1506
1887
|
{
|
|
1507
1888
|
providerId: provider.id,
|
|
1508
1889
|
localOnly: provider.localOnly,
|
|
1509
|
-
models:
|
|
1890
|
+
models: completeValue([...providerCatalog])
|
|
1510
1891
|
}
|
|
1511
1892
|
)
|
|
1512
1893
|
);
|
|
1513
1894
|
}
|
|
1514
|
-
return
|
|
1895
|
+
return completeValue(entries);
|
|
1515
1896
|
}
|
|
1516
1897
|
|
|
1517
1898
|
async inspect(role, options = {}) {
|
|
@@ -1528,7 +1909,7 @@ export class AIProviderRuntime {
|
|
|
1528
1909
|
const generation = slot.generation;
|
|
1529
1910
|
const selection = this.selection(role, {localOnly});
|
|
1530
1911
|
if (!selection) {
|
|
1531
|
-
return
|
|
1912
|
+
return completeValue(
|
|
1532
1913
|
{
|
|
1533
1914
|
available: false,
|
|
1534
1915
|
code: 'ARCANE_AI_ROLE_NOT_SELECTED',
|
|
@@ -1540,7 +1921,7 @@ export class AIProviderRuntime {
|
|
|
1540
1921
|
providerKey(role, selection.providerId)
|
|
1541
1922
|
) ?? null;
|
|
1542
1923
|
if (!provider) {
|
|
1543
|
-
return
|
|
1924
|
+
return completeValue(
|
|
1544
1925
|
{
|
|
1545
1926
|
available: false,
|
|
1546
1927
|
code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
|
|
@@ -1572,7 +1953,7 @@ export class AIProviderRuntime {
|
|
|
1572
1953
|
error,
|
|
1573
1954
|
'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED'
|
|
1574
1955
|
);
|
|
1575
|
-
return
|
|
1956
|
+
return completeValue(
|
|
1576
1957
|
{
|
|
1577
1958
|
available: false,
|
|
1578
1959
|
code: unavailable.code,
|
|
@@ -1591,7 +1972,7 @@ export class AIProviderRuntime {
|
|
|
1591
1972
|
error,
|
|
1592
1973
|
'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED'
|
|
1593
1974
|
);
|
|
1594
|
-
return
|
|
1975
|
+
return completeValue(
|
|
1595
1976
|
{
|
|
1596
1977
|
available: false,
|
|
1597
1978
|
code: unavailable.code,
|
|
@@ -1669,7 +2050,7 @@ export class AIProviderRuntime {
|
|
|
1669
2050
|
)
|
|
1670
2051
|
);
|
|
1671
2052
|
}
|
|
1672
|
-
if (slot.request || slot.
|
|
2053
|
+
if (slot.request || slot.requestQueue.length) {
|
|
1673
2054
|
return Promise.reject(
|
|
1674
2055
|
operationError(
|
|
1675
2056
|
`AI role ${role} cannot load during active request ownership.`,
|
|
@@ -1867,7 +2248,7 @@ export class AIProviderRuntime {
|
|
|
1867
2248
|
state: 'loading',
|
|
1868
2249
|
operationId,
|
|
1869
2250
|
progress: {
|
|
1870
|
-
phase: '
|
|
2251
|
+
phase: 'loading',
|
|
1871
2252
|
completed: 0,
|
|
1872
2253
|
total: null,
|
|
1873
2254
|
unit: 'items',
|
|
@@ -1912,8 +2293,8 @@ export class AIProviderRuntime {
|
|
|
1912
2293
|
|| providerStatus?.loaded === true
|
|
1913
2294
|
|| providerStatus?.busy === true;
|
|
1914
2295
|
if (!slot.loadPromise
|
|
1915
|
-
&& !slot.requestAdmission
|
|
1916
2296
|
&& !slot.request
|
|
2297
|
+
&& slot.requestQueue.length === 0
|
|
1917
2298
|
&& !providerOwned
|
|
1918
2299
|
&& (!provider || providerStatus)) {
|
|
1919
2300
|
slot.ready = false;
|
|
@@ -1931,8 +2312,12 @@ export class AIProviderRuntime {
|
|
|
1931
2312
|
this.#speechMuted = true;
|
|
1932
2313
|
}
|
|
1933
2314
|
slot.loadController?.abort();
|
|
1934
|
-
|
|
1935
|
-
|
|
2315
|
+
const requestCancellation = operationError(
|
|
2316
|
+
`AI role ${role} is unloading.`,
|
|
2317
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
2318
|
+
);
|
|
2319
|
+
this.#rejectQueuedRoleRequests(slot, requestCancellation);
|
|
2320
|
+
slot.request?.controller.abort(requestCancellation);
|
|
1936
2321
|
const capturedLoad = slot.loadPromise;
|
|
1937
2322
|
const capturedRequestRecord = slot.request;
|
|
1938
2323
|
const capturedRequest = capturedRequestRecord?.promise ?? null;
|
|
@@ -1947,12 +2332,7 @@ export class AIProviderRuntime {
|
|
|
1947
2332
|
}
|
|
1948
2333
|
if (capturedRequestRecord?.cancel) {
|
|
1949
2334
|
try {
|
|
1950
|
-
await capturedRequestRecord.cancel(
|
|
1951
|
-
operationError(
|
|
1952
|
-
`AI role ${role} is unloading.`,
|
|
1953
|
-
'ARCANE_AI_REQUEST_ABORTED'
|
|
1954
|
-
)
|
|
1955
|
-
);
|
|
2335
|
+
await capturedRequestRecord.cancel(requestCancellation);
|
|
1956
2336
|
} catch {
|
|
1957
2337
|
// Provider unload below remains the authoritative cleanup.
|
|
1958
2338
|
}
|
|
@@ -2177,13 +2557,13 @@ export class AIProviderRuntime {
|
|
|
2177
2557
|
for (const role of AI_RUNTIME_ROLES) {
|
|
2178
2558
|
const slot = runtime.#slots[role];
|
|
2179
2559
|
slot.generation += 1;
|
|
2180
|
-
slot.routes =
|
|
2560
|
+
slot.routes = completeValue({default: null, localOnly: null});
|
|
2181
2561
|
slot.selection = null;
|
|
2182
2562
|
slot.loadController = null;
|
|
2183
2563
|
slot.loadPromise = null;
|
|
2184
2564
|
slot.unloadPromise = null;
|
|
2185
2565
|
slot.disposePromise = null;
|
|
2186
|
-
slot.
|
|
2566
|
+
slot.requestQueue = [];
|
|
2187
2567
|
slot.request = null;
|
|
2188
2568
|
slot.ready = false;
|
|
2189
2569
|
slot.disposed = true;
|
|
@@ -2234,6 +2614,9 @@ export class AIProviderRuntime {
|
|
|
2234
2614
|
assertAbortSignal(options.signal);
|
|
2235
2615
|
try {
|
|
2236
2616
|
assertCallbackFreeProviderValue(options.payload);
|
|
2617
|
+
if (role === 'llm') {
|
|
2618
|
+
validateLLMRequestPayload(options.payload);
|
|
2619
|
+
}
|
|
2237
2620
|
} catch (error) {
|
|
2238
2621
|
return Promise.reject(error);
|
|
2239
2622
|
}
|
|
@@ -2310,8 +2693,8 @@ export class AIProviderRuntime {
|
|
|
2310
2693
|
)
|
|
2311
2694
|
);
|
|
2312
2695
|
}
|
|
2313
|
-
if (slot.request
|
|
2314
|
-
return this.#
|
|
2696
|
+
if (slot.request) {
|
|
2697
|
+
return this.#enqueueRoleRequest(slot, options);
|
|
2315
2698
|
}
|
|
2316
2699
|
if (!slot.ready
|
|
2317
2700
|
|| providerStatus.state !== 'ready'
|
|
@@ -2356,7 +2739,6 @@ export class AIProviderRuntime {
|
|
|
2356
2739
|
function restoreAIProviderRoleAfterRequest(error) {
|
|
2357
2740
|
if (slot.generation !== generation
|
|
2358
2741
|
|| slot.requestSequence !== requestSequence
|
|
2359
|
-
|| slot.requestAdmission
|
|
2360
2742
|
|| slot.unloadPromise) {
|
|
2361
2743
|
return;
|
|
2362
2744
|
}
|
|
@@ -2415,6 +2797,7 @@ export class AIProviderRuntime {
|
|
|
2415
2797
|
if (slot.request === requestRecord) {
|
|
2416
2798
|
slot.request = null;
|
|
2417
2799
|
}
|
|
2800
|
+
runtime.#drainRoleRequestQueue(slot);
|
|
2418
2801
|
if (error) {
|
|
2419
2802
|
restoreAIProviderRoleAfterRequest(error);
|
|
2420
2803
|
rejectTerminal(error);
|
|
@@ -2444,7 +2827,7 @@ export class AIProviderRuntime {
|
|
|
2444
2827
|
}
|
|
2445
2828
|
|
|
2446
2829
|
async function cleanupAIProviderStreamHandle(opened, activeIterator, reason) {
|
|
2447
|
-
return
|
|
2830
|
+
return awaitStreamCleanup(
|
|
2448
2831
|
beginAIProviderStreamHandleCleanup(
|
|
2449
2832
|
opened,
|
|
2450
2833
|
activeIterator,
|
|
@@ -2486,7 +2869,7 @@ export class AIProviderRuntime {
|
|
|
2486
2869
|
// A rejected open confirms that no provider handle was returned.
|
|
2487
2870
|
}
|
|
2488
2871
|
);
|
|
2489
|
-
return
|
|
2872
|
+
return awaitStreamCleanup(
|
|
2490
2873
|
Promise.allSettled([lateCleanup])
|
|
2491
2874
|
);
|
|
2492
2875
|
}
|
|
@@ -2503,7 +2886,7 @@ export class AIProviderRuntime {
|
|
|
2503
2886
|
rejectCleanup = reject;
|
|
2504
2887
|
}
|
|
2505
2888
|
);
|
|
2506
|
-
controller.abort();
|
|
2889
|
+
controller.abort(reason);
|
|
2507
2890
|
(async function closeAIProviderStream() {
|
|
2508
2891
|
const terminalOutcome = terminalError ?? normalizedAbort(
|
|
2509
2892
|
reason instanceof Error
|
|
@@ -2592,6 +2975,12 @@ export class AIProviderRuntime {
|
|
|
2592
2975
|
}
|
|
2593
2976
|
providerHandle = opened;
|
|
2594
2977
|
iterator = opened[Symbol.asyncIterator]();
|
|
2978
|
+
if (!iterator || typeof iterator.next !== 'function') {
|
|
2979
|
+
throw operationError(
|
|
2980
|
+
'The AI provider stream iterator has no next() method.',
|
|
2981
|
+
'ARCANE_AI_PROVIDER_STREAM_INVALID'
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2595
2984
|
runtime.#assertCurrentRequest(
|
|
2596
2985
|
slot,
|
|
2597
2986
|
generation,
|
|
@@ -2607,7 +2996,13 @@ export class AIProviderRuntime {
|
|
|
2607
2996
|
requestSequence,
|
|
2608
2997
|
controller.signal
|
|
2609
2998
|
);
|
|
2610
|
-
|
|
2999
|
+
const terminalValue = role === 'llm'
|
|
3000
|
+
? validateLLMTerminalResult(
|
|
3001
|
+
value,
|
|
3002
|
+
'AI provider stream result'
|
|
3003
|
+
)
|
|
3004
|
+
: value;
|
|
3005
|
+
settleAIProviderStream(null, terminalValue);
|
|
2611
3006
|
} catch (error) {
|
|
2612
3007
|
const normalized = isAbort(error, controller.signal)
|
|
2613
3008
|
? normalizedAbort(error)
|
|
@@ -2632,14 +3027,30 @@ export class AIProviderRuntime {
|
|
|
2632
3027
|
cancel: cancelAIProviderStream,
|
|
2633
3028
|
async next(value) {
|
|
2634
3029
|
try {
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
3030
|
+
let nextValue = value;
|
|
3031
|
+
while (true) {
|
|
3032
|
+
const result = await iterator.next(nextValue);
|
|
3033
|
+
nextValue = undefined;
|
|
3034
|
+
runtime.#assertCurrentRequest(
|
|
3035
|
+
slot,
|
|
3036
|
+
generation,
|
|
3037
|
+
requestSequence,
|
|
3038
|
+
controller.signal
|
|
3039
|
+
);
|
|
3040
|
+
if (role !== 'llm') {
|
|
3041
|
+
return result;
|
|
3042
|
+
}
|
|
3043
|
+
if (result.done) {
|
|
3044
|
+
return {value: undefined, done: true};
|
|
3045
|
+
}
|
|
3046
|
+
const projected = projectLLMStreamChunk(result.value);
|
|
3047
|
+
if (projected !== null) {
|
|
3048
|
+
return {
|
|
3049
|
+
value: projected,
|
|
3050
|
+
done: false
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
2643
3054
|
} catch (error) {
|
|
2644
3055
|
await cancelAIProviderStream(error);
|
|
2645
3056
|
throw isAbort(error, controller.signal)
|
|
@@ -2648,11 +3059,22 @@ export class AIProviderRuntime {
|
|
|
2648
3059
|
}
|
|
2649
3060
|
},
|
|
2650
3061
|
async return(value) {
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3062
|
+
Promise.resolve().then(
|
|
3063
|
+
function beginReturnedAIProviderStreamCancellation() {
|
|
3064
|
+
return cancelAIProviderStream(
|
|
3065
|
+
operationError(
|
|
3066
|
+
'The AI stream consumer stopped before completion.',
|
|
3067
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
3068
|
+
)
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
).catch(
|
|
3072
|
+
function reportReturnedAIProviderStreamCancellationFailure(error) {
|
|
3073
|
+
console.error(
|
|
3074
|
+
'Arcane AI provider stream early-return cancellation failed.',
|
|
3075
|
+
error
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
2656
3078
|
);
|
|
2657
3079
|
return {value, done: true};
|
|
2658
3080
|
},
|
|
@@ -2664,7 +3086,7 @@ export class AIProviderRuntime {
|
|
|
2664
3086
|
return this;
|
|
2665
3087
|
}
|
|
2666
3088
|
};
|
|
2667
|
-
return
|
|
3089
|
+
return completeValue(handle);
|
|
2668
3090
|
} catch (error) {
|
|
2669
3091
|
const normalized = isAbort(error, controller.signal)
|
|
2670
3092
|
? normalizedAbort(error)
|
|
@@ -2701,8 +3123,8 @@ export class AIProviderRuntime {
|
|
|
2701
3123
|
return openPromise;
|
|
2702
3124
|
}
|
|
2703
3125
|
|
|
2704
|
-
requestRecord.cancel = async function cancelAIProviderRequest() {
|
|
2705
|
-
controller.abort();
|
|
3126
|
+
requestRecord.cancel = async function cancelAIProviderRequest(reason) {
|
|
3127
|
+
controller.abort(reason);
|
|
2706
3128
|
await requestRecord.promise?.catch(
|
|
2707
3129
|
function retainCancelledAIProviderRequest() {}
|
|
2708
3130
|
);
|
|
@@ -2728,7 +3150,12 @@ export class AIProviderRuntime {
|
|
|
2728
3150
|
requestSequence,
|
|
2729
3151
|
controller.signal
|
|
2730
3152
|
);
|
|
2731
|
-
return
|
|
3153
|
+
return role === 'llm'
|
|
3154
|
+
? validateLLMTerminalResult(
|
|
3155
|
+
result,
|
|
3156
|
+
'AI provider chat result'
|
|
3157
|
+
)
|
|
3158
|
+
: result;
|
|
2732
3159
|
} catch (error) {
|
|
2733
3160
|
const normalized = isAbort(error, controller.signal)
|
|
2734
3161
|
? normalizedAbort(error)
|
|
@@ -2741,6 +3168,7 @@ export class AIProviderRuntime {
|
|
|
2741
3168
|
if (slot.request === requestRecord) {
|
|
2742
3169
|
slot.request = null;
|
|
2743
3170
|
}
|
|
3171
|
+
runtime.#drainRoleRequestQueue(slot);
|
|
2744
3172
|
if (!requestError
|
|
2745
3173
|
&& slot.generation === generation
|
|
2746
3174
|
&& !slot.unloadPromise) {
|
|
@@ -2770,18 +3198,16 @@ export class AIProviderRuntime {
|
|
|
2770
3198
|
cancel(role) {
|
|
2771
3199
|
assertRole(role);
|
|
2772
3200
|
const slot = this.#slots[role];
|
|
2773
|
-
if (!slot.request
|
|
3201
|
+
if (!slot.request) {
|
|
2774
3202
|
return false;
|
|
2775
3203
|
}
|
|
2776
|
-
slot.requestAdmission?.controller.abort();
|
|
2777
3204
|
const requestRecord = slot.request;
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
`AI role ${role} request was cancelled.`,
|
|
2782
|
-
'ARCANE_AI_REQUEST_ABORTED'
|
|
2783
|
-
)
|
|
3205
|
+
const reason = operationError(
|
|
3206
|
+
`AI role ${role} request was cancelled.`,
|
|
3207
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
2784
3208
|
);
|
|
3209
|
+
requestRecord?.controller.abort(reason);
|
|
3210
|
+
const cancellation = requestRecord?.cancel?.(reason);
|
|
2785
3211
|
cancellation?.catch(function retainAIProviderCancelFailureInState() {});
|
|
2786
3212
|
return true;
|
|
2787
3213
|
}
|
|
@@ -2871,126 +3297,83 @@ export class AIProviderRuntime {
|
|
|
2871
3297
|
);
|
|
2872
3298
|
}
|
|
2873
3299
|
|
|
2874
|
-
#
|
|
2875
|
-
const generation = slot.generation;
|
|
2876
|
-
const requestSequence = this.#nextRequestSequence(slot);
|
|
2877
|
-
const priorAdmission = slot.requestAdmission;
|
|
2878
|
-
const activeRequest = priorAdmission?.activeRequest ?? slot.request;
|
|
2879
|
-
const controller = new AbortController();
|
|
2880
|
-
const detachSignal = this.#forwardAbort(options.signal, controller);
|
|
2881
|
-
let settlement = priorAdmission?.settlement ?? null;
|
|
2882
|
-
if (!settlement && activeRequest) {
|
|
2883
|
-
activeRequest.controller.abort();
|
|
2884
|
-
settlement = Promise.allSettled(
|
|
2885
|
-
[
|
|
2886
|
-
Promise.resolve().then(
|
|
2887
|
-
function cancelSupersededAIProviderRequest() {
|
|
2888
|
-
return activeRequest.cancel(
|
|
2889
|
-
operationError(
|
|
2890
|
-
`AI role ${slot.role} request was superseded.`,
|
|
2891
|
-
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
2892
|
-
)
|
|
2893
|
-
);
|
|
2894
|
-
}
|
|
2895
|
-
),
|
|
2896
|
-
activeRequest.promise
|
|
2897
|
-
]
|
|
2898
|
-
);
|
|
2899
|
-
}
|
|
2900
|
-
if (!settlement) {
|
|
2901
|
-
settlement = Promise.resolve([]);
|
|
2902
|
-
}
|
|
2903
|
-
const admission = {
|
|
2904
|
-
activeRequest,
|
|
2905
|
-
controller,
|
|
2906
|
-
generation,
|
|
2907
|
-
requestSequence,
|
|
2908
|
-
settlement
|
|
2909
|
-
};
|
|
2910
|
-
slot.requestAdmission = admission;
|
|
3300
|
+
#enqueueRoleRequest(slot, options) {
|
|
2911
3301
|
const runtime = this;
|
|
2912
|
-
return (
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3302
|
+
return new Promise(function queueAIProviderRoleRequest(resolve, reject) {
|
|
3303
|
+
const entry = {
|
|
3304
|
+
options,
|
|
3305
|
+
resolve,
|
|
3306
|
+
reject,
|
|
3307
|
+
detachSignal: null
|
|
3308
|
+
};
|
|
3309
|
+
function cancelQueuedAIProviderRoleRequest() {
|
|
3310
|
+
const index = slot.requestQueue.indexOf(entry);
|
|
3311
|
+
if (index === -1) {
|
|
3312
|
+
return;
|
|
2918
3313
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
);
|
|
3314
|
+
slot.requestQueue.splice(index, 1);
|
|
3315
|
+
entry.detachSignal?.();
|
|
3316
|
+
reject(normalizedAbort(options.signal?.reason));
|
|
3317
|
+
}
|
|
3318
|
+
if (options.signal) {
|
|
3319
|
+
if (options.signal.aborted) {
|
|
3320
|
+
reject(normalizedAbort(options.signal.reason));
|
|
3321
|
+
return;
|
|
2928
3322
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3323
|
+
options.signal.addEventListener(
|
|
3324
|
+
'abort',
|
|
3325
|
+
cancelQueuedAIProviderRoleRequest,
|
|
3326
|
+
{once: true}
|
|
3327
|
+
);
|
|
3328
|
+
entry.detachSignal = function detachQueuedAIProviderRoleRequestSignal() {
|
|
3329
|
+
options.signal.removeEventListener(
|
|
3330
|
+
'abort',
|
|
3331
|
+
cancelQueuedAIProviderRoleRequest
|
|
2938
3332
|
);
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
return await runtime.request(slot.role, options);
|
|
2973
|
-
} catch (error) {
|
|
2974
|
-
const normalized = isAbort(error, controller.signal)
|
|
2975
|
-
? normalizedAbort(error)
|
|
2976
|
-
: error;
|
|
2977
|
-
if (slot.requestAdmission === admission) {
|
|
2978
|
-
slot.requestAdmission = null;
|
|
2979
|
-
if (slot.generation === generation
|
|
2980
|
-
&& !slot.unloadPromise
|
|
2981
|
-
&& !slot.disposePromise) {
|
|
2982
|
-
runtime.#publishRequestAdmissionFailure(
|
|
2983
|
-
slot,
|
|
2984
|
-
provider,
|
|
2985
|
-
normalized
|
|
2986
|
-
);
|
|
2987
|
-
}
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
slot.requestQueue.push(entry);
|
|
3336
|
+
runtime.#drainRoleRequestQueue(slot);
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
#drainRoleRequestQueue(slot) {
|
|
3341
|
+
if (slot.request
|
|
3342
|
+
|| slot.unloadPromise
|
|
3343
|
+
|| slot.disposePromise
|
|
3344
|
+
|| slot.disposed) {
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
const entry = slot.requestQueue.shift();
|
|
3348
|
+
if (!entry) {
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
entry.detachSignal?.();
|
|
3352
|
+
if (entry.options.signal?.aborted) {
|
|
3353
|
+
entry.reject(normalizedAbort(entry.options.signal.reason));
|
|
3354
|
+
this.#drainRoleRequestQueue(slot);
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
const runtime = this;
|
|
3358
|
+
Promise.resolve().then(
|
|
3359
|
+
function runQueuedAIProviderRoleRequest() {
|
|
3360
|
+
return runtime.request(slot.role, entry.options);
|
|
3361
|
+
}
|
|
3362
|
+
).then(entry.resolve, entry.reject).finally(
|
|
3363
|
+
function continueAIProviderRoleRequestQueue() {
|
|
3364
|
+
if (!slot.request) {
|
|
3365
|
+
runtime.#drainRoleRequestQueue(slot);
|
|
2988
3366
|
}
|
|
2989
|
-
throw normalized;
|
|
2990
|
-
} finally {
|
|
2991
|
-
detachSignal();
|
|
2992
3367
|
}
|
|
2993
|
-
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
#rejectQueuedRoleRequests(slot, error) {
|
|
3372
|
+
const queued = slot.requestQueue.splice(0);
|
|
3373
|
+
for (const entry of queued) {
|
|
3374
|
+
entry.detachSignal?.();
|
|
3375
|
+
entry.reject(error);
|
|
3376
|
+
}
|
|
2994
3377
|
}
|
|
2995
3378
|
|
|
2996
3379
|
#sameSelection(left, right) {
|
|
@@ -3031,8 +3414,8 @@ export class AIProviderRuntime {
|
|
|
3031
3414
|
slot.loadPromise
|
|
3032
3415
|
|| slot.unloadPromise
|
|
3033
3416
|
|| slot.disposePromise
|
|
3034
|
-
|| slot.requestAdmission
|
|
3035
3417
|
|| slot.request
|
|
3418
|
+
|| slot.requestQueue.length > 0
|
|
3036
3419
|
|| slot.ready
|
|
3037
3420
|
);
|
|
3038
3421
|
}
|
|
@@ -3057,24 +3440,12 @@ export class AIProviderRuntime {
|
|
|
3057
3440
|
}
|
|
3058
3441
|
|
|
3059
3442
|
#nextRequestSequence(slot) {
|
|
3060
|
-
|
|
3061
|
-
throw operationError(
|
|
3062
|
-
`AI role ${slot.role} exhausted its request sequence.`,
|
|
3063
|
-
'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
|
|
3064
|
-
);
|
|
3065
|
-
}
|
|
3066
|
-
slot.requestSequence += 1;
|
|
3443
|
+
slot.requestSequence = nextSequence(slot.requestSequence);
|
|
3067
3444
|
return slot.requestSequence;
|
|
3068
3445
|
}
|
|
3069
3446
|
|
|
3070
3447
|
#nextOperationId(slot, operation) {
|
|
3071
|
-
|
|
3072
|
-
throw operationError(
|
|
3073
|
-
`AI role ${slot.role} exhausted its operation sequence.`,
|
|
3074
|
-
'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
|
|
3075
|
-
);
|
|
3076
|
-
}
|
|
3077
|
-
slot.operationSequence += 1;
|
|
3448
|
+
slot.operationSequence = nextSequence(slot.operationSequence);
|
|
3078
3449
|
return `${slot.role}-${operation}-${slot.operationSequence}`;
|
|
3079
3450
|
}
|
|
3080
3451
|
|
|
@@ -3094,22 +3465,8 @@ export class AIProviderRuntime {
|
|
|
3094
3465
|
this.#assertCurrentOperation(slot, generation, signal);
|
|
3095
3466
|
if (slot.requestSequence !== requestSequence) {
|
|
3096
3467
|
throw operationError(
|
|
3097
|
-
`AI role ${slot.role}
|
|
3098
|
-
'
|
|
3099
|
-
);
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
#assertCurrentRequestAdmission(slot, admission) {
|
|
3104
|
-
if (admission.controller.signal.aborted) {
|
|
3105
|
-
throw normalizedAbort();
|
|
3106
|
-
}
|
|
3107
|
-
if (slot.generation !== admission.generation
|
|
3108
|
-
|| slot.requestSequence !== admission.requestSequence
|
|
3109
|
-
|| slot.requestAdmission !== admission) {
|
|
3110
|
-
throw operationError(
|
|
3111
|
-
`AI role ${slot.role} request admission was superseded.`,
|
|
3112
|
-
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
3468
|
+
`AI role ${slot.role} no longer owns the active provider request.`,
|
|
3469
|
+
'ARCANE_AI_REQUEST_STALE'
|
|
3113
3470
|
);
|
|
3114
3471
|
}
|
|
3115
3472
|
}
|
|
@@ -3119,10 +3476,10 @@ export class AIProviderRuntime {
|
|
|
3119
3476
|
return function detachAbsentAIProviderAbort() {};
|
|
3120
3477
|
}
|
|
3121
3478
|
function forwardAIProviderAbort() {
|
|
3122
|
-
controller.abort();
|
|
3479
|
+
controller.abort(signal.reason);
|
|
3123
3480
|
}
|
|
3124
3481
|
if (signal.aborted) {
|
|
3125
|
-
controller.abort();
|
|
3482
|
+
controller.abort(signal.reason);
|
|
3126
3483
|
return function detachAlreadyAbortedAIProviderSignal() {};
|
|
3127
3484
|
}
|
|
3128
3485
|
signal.addEventListener('abort', forwardAIProviderAbort, {once: true});
|
|
@@ -3150,68 +3507,6 @@ export class AIProviderRuntime {
|
|
|
3150
3507
|
return true;
|
|
3151
3508
|
}
|
|
3152
3509
|
|
|
3153
|
-
#publishRequestAdmissionFailure(slot, provider, error) {
|
|
3154
|
-
let providerStatus = null;
|
|
3155
|
-
try {
|
|
3156
|
-
providerStatus = validateProviderStatus(provider.status());
|
|
3157
|
-
} catch {
|
|
3158
|
-
// The admission error remains authoritative when status is malformed.
|
|
3159
|
-
}
|
|
3160
|
-
if (isAbort(error, null)
|
|
3161
|
-
&& providerStatus?.state === 'ready'
|
|
3162
|
-
&& providerStatus.loaded
|
|
3163
|
-
&& !providerStatus.busy) {
|
|
3164
|
-
slot.ready = true;
|
|
3165
|
-
publishAIRuntimeRoleState(
|
|
3166
|
-
slot.role,
|
|
3167
|
-
roleRecord(
|
|
3168
|
-
slot.role,
|
|
3169
|
-
slot.selection,
|
|
3170
|
-
{
|
|
3171
|
-
state: 'ready',
|
|
3172
|
-
loaded: true
|
|
3173
|
-
}
|
|
3174
|
-
)
|
|
3175
|
-
);
|
|
3176
|
-
return;
|
|
3177
|
-
}
|
|
3178
|
-
if (isAbort(error, null)
|
|
3179
|
-
&& providerStatus?.state === 'unloaded'
|
|
3180
|
-
&& !providerStatus.loaded
|
|
3181
|
-
&& !providerStatus.busy) {
|
|
3182
|
-
slot.ready = false;
|
|
3183
|
-
publishAIRuntimeRoleState(
|
|
3184
|
-
slot.role,
|
|
3185
|
-
roleRecord(slot.role, slot.selection)
|
|
3186
|
-
);
|
|
3187
|
-
return;
|
|
3188
|
-
}
|
|
3189
|
-
if (providerStatus?.state === 'ready'
|
|
3190
|
-
&& providerStatus.loaded
|
|
3191
|
-
&& providerStatus.busy) {
|
|
3192
|
-
slot.ready = true;
|
|
3193
|
-
publishAIRuntimeRoleState(
|
|
3194
|
-
slot.role,
|
|
3195
|
-
roleRecord(
|
|
3196
|
-
slot.role,
|
|
3197
|
-
slot.selection,
|
|
3198
|
-
{
|
|
3199
|
-
state: 'ready',
|
|
3200
|
-
loaded: true,
|
|
3201
|
-
busy: true
|
|
3202
|
-
}
|
|
3203
|
-
)
|
|
3204
|
-
);
|
|
3205
|
-
return;
|
|
3206
|
-
}
|
|
3207
|
-
slot.ready = false;
|
|
3208
|
-
this.#publishRoleError(
|
|
3209
|
-
slot,
|
|
3210
|
-
error,
|
|
3211
|
-
providerStatus?.loaded === true
|
|
3212
|
-
);
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
3510
|
#publishRoleError(slot, error, loaded) {
|
|
3216
3511
|
publishAIRuntimeRoleState(
|
|
3217
3512
|
slot.role,
|