arcane-os 0.3.1 → 0.3.3
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 +855 -895
- package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
- 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 -148
- package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
- 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 +113 -69
- 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 +1042 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
- 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 +19 -149
- 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 +42 -327
- 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 +48 -66
- 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 +492 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +48 -79
- 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 +55 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +133 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +109 -1558
- 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 -185
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1295
- 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 -719
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2965
- package/docs/reference/sdk-api.md +0 -6698
- 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,
|
|
@@ -1248,14 +1609,6 @@ export class AIProviderRuntime {
|
|
|
1248
1609
|
);
|
|
1249
1610
|
}
|
|
1250
1611
|
}
|
|
1251
|
-
if (currentState.revision === Number.MAX_SAFE_INTEGER) {
|
|
1252
|
-
throw speechProviderReplacementError(
|
|
1253
|
-
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
|
|
1254
|
-
'speech-provider-replacement-state-revision-exhausted',
|
|
1255
|
-
'AI speech provider replacement cannot publish another runtime state revision.'
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
1612
|
if (expected) {
|
|
1260
1613
|
this.#providers.delete(providerKey(role, expected.id));
|
|
1261
1614
|
this.#disposeAllCompletedProviders.delete(expected);
|
|
@@ -1272,7 +1625,7 @@ export class AIProviderRuntime {
|
|
|
1272
1625
|
this.#configured = true;
|
|
1273
1626
|
publishAIRuntimeRoleState(role, roleRecord(role, slot.selection));
|
|
1274
1627
|
|
|
1275
|
-
return
|
|
1628
|
+
return completeValue(
|
|
1276
1629
|
{
|
|
1277
1630
|
role,
|
|
1278
1631
|
routes: replacement.routes,
|
|
@@ -1409,14 +1762,6 @@ export class AIProviderRuntime {
|
|
|
1409
1762
|
}
|
|
1410
1763
|
}
|
|
1411
1764
|
}
|
|
1412
|
-
if (currentState.revision === Number.MAX_SAFE_INTEGER) {
|
|
1413
|
-
throw speechProviderReplacementError(
|
|
1414
|
-
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
|
|
1415
|
-
'speech-provider-replacement-state-revision-exhausted',
|
|
1416
|
-
'AI speech provider replacement cannot publish another runtime state revision.'
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
1765
|
for (const role of SPEECH_ROLES) {
|
|
1421
1766
|
const expected = replacement.expectedProviders[role];
|
|
1422
1767
|
const provider = replacement.providers[role];
|
|
@@ -1447,10 +1792,10 @@ export class AIProviderRuntime {
|
|
|
1447
1792
|
}
|
|
1448
1793
|
);
|
|
1449
1794
|
|
|
1450
|
-
return
|
|
1795
|
+
return completeValue(
|
|
1451
1796
|
{
|
|
1452
1797
|
routes: replacement.routes,
|
|
1453
|
-
unregisters:
|
|
1798
|
+
unregisters: completeValue(
|
|
1454
1799
|
{
|
|
1455
1800
|
stt: replacement.removal
|
|
1456
1801
|
? null
|
|
@@ -1538,16 +1883,16 @@ export class AIProviderRuntime {
|
|
|
1538
1883
|
fail('AI provider.catalog() must synchronously return an array.');
|
|
1539
1884
|
}
|
|
1540
1885
|
entries.push(
|
|
1541
|
-
|
|
1886
|
+
completeValue(
|
|
1542
1887
|
{
|
|
1543
1888
|
providerId: provider.id,
|
|
1544
1889
|
localOnly: provider.localOnly,
|
|
1545
|
-
models:
|
|
1890
|
+
models: completeValue([...providerCatalog])
|
|
1546
1891
|
}
|
|
1547
1892
|
)
|
|
1548
1893
|
);
|
|
1549
1894
|
}
|
|
1550
|
-
return
|
|
1895
|
+
return completeValue(entries);
|
|
1551
1896
|
}
|
|
1552
1897
|
|
|
1553
1898
|
async inspect(role, options = {}) {
|
|
@@ -1564,7 +1909,7 @@ export class AIProviderRuntime {
|
|
|
1564
1909
|
const generation = slot.generation;
|
|
1565
1910
|
const selection = this.selection(role, {localOnly});
|
|
1566
1911
|
if (!selection) {
|
|
1567
|
-
return
|
|
1912
|
+
return completeValue(
|
|
1568
1913
|
{
|
|
1569
1914
|
available: false,
|
|
1570
1915
|
code: 'ARCANE_AI_ROLE_NOT_SELECTED',
|
|
@@ -1576,7 +1921,7 @@ export class AIProviderRuntime {
|
|
|
1576
1921
|
providerKey(role, selection.providerId)
|
|
1577
1922
|
) ?? null;
|
|
1578
1923
|
if (!provider) {
|
|
1579
|
-
return
|
|
1924
|
+
return completeValue(
|
|
1580
1925
|
{
|
|
1581
1926
|
available: false,
|
|
1582
1927
|
code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
|
|
@@ -1608,7 +1953,7 @@ export class AIProviderRuntime {
|
|
|
1608
1953
|
error,
|
|
1609
1954
|
'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED'
|
|
1610
1955
|
);
|
|
1611
|
-
return
|
|
1956
|
+
return completeValue(
|
|
1612
1957
|
{
|
|
1613
1958
|
available: false,
|
|
1614
1959
|
code: unavailable.code,
|
|
@@ -1627,7 +1972,7 @@ export class AIProviderRuntime {
|
|
|
1627
1972
|
error,
|
|
1628
1973
|
'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED'
|
|
1629
1974
|
);
|
|
1630
|
-
return
|
|
1975
|
+
return completeValue(
|
|
1631
1976
|
{
|
|
1632
1977
|
available: false,
|
|
1633
1978
|
code: unavailable.code,
|
|
@@ -1705,7 +2050,7 @@ export class AIProviderRuntime {
|
|
|
1705
2050
|
)
|
|
1706
2051
|
);
|
|
1707
2052
|
}
|
|
1708
|
-
if (slot.request || slot.
|
|
2053
|
+
if (slot.request || slot.requestQueue.length) {
|
|
1709
2054
|
return Promise.reject(
|
|
1710
2055
|
operationError(
|
|
1711
2056
|
`AI role ${role} cannot load during active request ownership.`,
|
|
@@ -1903,7 +2248,7 @@ export class AIProviderRuntime {
|
|
|
1903
2248
|
state: 'loading',
|
|
1904
2249
|
operationId,
|
|
1905
2250
|
progress: {
|
|
1906
|
-
phase: '
|
|
2251
|
+
phase: 'loading',
|
|
1907
2252
|
completed: 0,
|
|
1908
2253
|
total: null,
|
|
1909
2254
|
unit: 'items',
|
|
@@ -1948,8 +2293,8 @@ export class AIProviderRuntime {
|
|
|
1948
2293
|
|| providerStatus?.loaded === true
|
|
1949
2294
|
|| providerStatus?.busy === true;
|
|
1950
2295
|
if (!slot.loadPromise
|
|
1951
|
-
&& !slot.requestAdmission
|
|
1952
2296
|
&& !slot.request
|
|
2297
|
+
&& slot.requestQueue.length === 0
|
|
1953
2298
|
&& !providerOwned
|
|
1954
2299
|
&& (!provider || providerStatus)) {
|
|
1955
2300
|
slot.ready = false;
|
|
@@ -1967,8 +2312,12 @@ export class AIProviderRuntime {
|
|
|
1967
2312
|
this.#speechMuted = true;
|
|
1968
2313
|
}
|
|
1969
2314
|
slot.loadController?.abort();
|
|
1970
|
-
|
|
1971
|
-
|
|
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);
|
|
1972
2321
|
const capturedLoad = slot.loadPromise;
|
|
1973
2322
|
const capturedRequestRecord = slot.request;
|
|
1974
2323
|
const capturedRequest = capturedRequestRecord?.promise ?? null;
|
|
@@ -1983,12 +2332,7 @@ export class AIProviderRuntime {
|
|
|
1983
2332
|
}
|
|
1984
2333
|
if (capturedRequestRecord?.cancel) {
|
|
1985
2334
|
try {
|
|
1986
|
-
await capturedRequestRecord.cancel(
|
|
1987
|
-
operationError(
|
|
1988
|
-
`AI role ${role} is unloading.`,
|
|
1989
|
-
'ARCANE_AI_REQUEST_ABORTED'
|
|
1990
|
-
)
|
|
1991
|
-
);
|
|
2335
|
+
await capturedRequestRecord.cancel(requestCancellation);
|
|
1992
2336
|
} catch {
|
|
1993
2337
|
// Provider unload below remains the authoritative cleanup.
|
|
1994
2338
|
}
|
|
@@ -2213,13 +2557,13 @@ export class AIProviderRuntime {
|
|
|
2213
2557
|
for (const role of AI_RUNTIME_ROLES) {
|
|
2214
2558
|
const slot = runtime.#slots[role];
|
|
2215
2559
|
slot.generation += 1;
|
|
2216
|
-
slot.routes =
|
|
2560
|
+
slot.routes = completeValue({default: null, localOnly: null});
|
|
2217
2561
|
slot.selection = null;
|
|
2218
2562
|
slot.loadController = null;
|
|
2219
2563
|
slot.loadPromise = null;
|
|
2220
2564
|
slot.unloadPromise = null;
|
|
2221
2565
|
slot.disposePromise = null;
|
|
2222
|
-
slot.
|
|
2566
|
+
slot.requestQueue = [];
|
|
2223
2567
|
slot.request = null;
|
|
2224
2568
|
slot.ready = false;
|
|
2225
2569
|
slot.disposed = true;
|
|
@@ -2270,6 +2614,9 @@ export class AIProviderRuntime {
|
|
|
2270
2614
|
assertAbortSignal(options.signal);
|
|
2271
2615
|
try {
|
|
2272
2616
|
assertCallbackFreeProviderValue(options.payload);
|
|
2617
|
+
if (role === 'llm') {
|
|
2618
|
+
validateLLMRequestPayload(options.payload);
|
|
2619
|
+
}
|
|
2273
2620
|
} catch (error) {
|
|
2274
2621
|
return Promise.reject(error);
|
|
2275
2622
|
}
|
|
@@ -2346,8 +2693,8 @@ export class AIProviderRuntime {
|
|
|
2346
2693
|
)
|
|
2347
2694
|
);
|
|
2348
2695
|
}
|
|
2349
|
-
if (slot.request
|
|
2350
|
-
return this.#
|
|
2696
|
+
if (slot.request) {
|
|
2697
|
+
return this.#enqueueRoleRequest(slot, options);
|
|
2351
2698
|
}
|
|
2352
2699
|
if (!slot.ready
|
|
2353
2700
|
|| providerStatus.state !== 'ready'
|
|
@@ -2392,7 +2739,6 @@ export class AIProviderRuntime {
|
|
|
2392
2739
|
function restoreAIProviderRoleAfterRequest(error) {
|
|
2393
2740
|
if (slot.generation !== generation
|
|
2394
2741
|
|| slot.requestSequence !== requestSequence
|
|
2395
|
-
|| slot.requestAdmission
|
|
2396
2742
|
|| slot.unloadPromise) {
|
|
2397
2743
|
return;
|
|
2398
2744
|
}
|
|
@@ -2451,6 +2797,7 @@ export class AIProviderRuntime {
|
|
|
2451
2797
|
if (slot.request === requestRecord) {
|
|
2452
2798
|
slot.request = null;
|
|
2453
2799
|
}
|
|
2800
|
+
runtime.#drainRoleRequestQueue(slot);
|
|
2454
2801
|
if (error) {
|
|
2455
2802
|
restoreAIProviderRoleAfterRequest(error);
|
|
2456
2803
|
rejectTerminal(error);
|
|
@@ -2480,7 +2827,7 @@ export class AIProviderRuntime {
|
|
|
2480
2827
|
}
|
|
2481
2828
|
|
|
2482
2829
|
async function cleanupAIProviderStreamHandle(opened, activeIterator, reason) {
|
|
2483
|
-
return
|
|
2830
|
+
return awaitStreamCleanup(
|
|
2484
2831
|
beginAIProviderStreamHandleCleanup(
|
|
2485
2832
|
opened,
|
|
2486
2833
|
activeIterator,
|
|
@@ -2522,7 +2869,7 @@ export class AIProviderRuntime {
|
|
|
2522
2869
|
// A rejected open confirms that no provider handle was returned.
|
|
2523
2870
|
}
|
|
2524
2871
|
);
|
|
2525
|
-
return
|
|
2872
|
+
return awaitStreamCleanup(
|
|
2526
2873
|
Promise.allSettled([lateCleanup])
|
|
2527
2874
|
);
|
|
2528
2875
|
}
|
|
@@ -2539,7 +2886,7 @@ export class AIProviderRuntime {
|
|
|
2539
2886
|
rejectCleanup = reject;
|
|
2540
2887
|
}
|
|
2541
2888
|
);
|
|
2542
|
-
controller.abort();
|
|
2889
|
+
controller.abort(reason);
|
|
2543
2890
|
(async function closeAIProviderStream() {
|
|
2544
2891
|
const terminalOutcome = terminalError ?? normalizedAbort(
|
|
2545
2892
|
reason instanceof Error
|
|
@@ -2628,6 +2975,12 @@ export class AIProviderRuntime {
|
|
|
2628
2975
|
}
|
|
2629
2976
|
providerHandle = opened;
|
|
2630
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
|
+
}
|
|
2631
2984
|
runtime.#assertCurrentRequest(
|
|
2632
2985
|
slot,
|
|
2633
2986
|
generation,
|
|
@@ -2643,7 +2996,13 @@ export class AIProviderRuntime {
|
|
|
2643
2996
|
requestSequence,
|
|
2644
2997
|
controller.signal
|
|
2645
2998
|
);
|
|
2646
|
-
|
|
2999
|
+
const terminalValue = role === 'llm'
|
|
3000
|
+
? validateLLMTerminalResult(
|
|
3001
|
+
value,
|
|
3002
|
+
'AI provider stream result'
|
|
3003
|
+
)
|
|
3004
|
+
: value;
|
|
3005
|
+
settleAIProviderStream(null, terminalValue);
|
|
2647
3006
|
} catch (error) {
|
|
2648
3007
|
const normalized = isAbort(error, controller.signal)
|
|
2649
3008
|
? normalizedAbort(error)
|
|
@@ -2668,14 +3027,30 @@ export class AIProviderRuntime {
|
|
|
2668
3027
|
cancel: cancelAIProviderStream,
|
|
2669
3028
|
async next(value) {
|
|
2670
3029
|
try {
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
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
|
+
}
|
|
2679
3054
|
} catch (error) {
|
|
2680
3055
|
await cancelAIProviderStream(error);
|
|
2681
3056
|
throw isAbort(error, controller.signal)
|
|
@@ -2684,11 +3059,22 @@ export class AIProviderRuntime {
|
|
|
2684
3059
|
}
|
|
2685
3060
|
},
|
|
2686
3061
|
async return(value) {
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
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
|
+
}
|
|
2692
3078
|
);
|
|
2693
3079
|
return {value, done: true};
|
|
2694
3080
|
},
|
|
@@ -2700,7 +3086,7 @@ export class AIProviderRuntime {
|
|
|
2700
3086
|
return this;
|
|
2701
3087
|
}
|
|
2702
3088
|
};
|
|
2703
|
-
return
|
|
3089
|
+
return completeValue(handle);
|
|
2704
3090
|
} catch (error) {
|
|
2705
3091
|
const normalized = isAbort(error, controller.signal)
|
|
2706
3092
|
? normalizedAbort(error)
|
|
@@ -2737,8 +3123,8 @@ export class AIProviderRuntime {
|
|
|
2737
3123
|
return openPromise;
|
|
2738
3124
|
}
|
|
2739
3125
|
|
|
2740
|
-
requestRecord.cancel = async function cancelAIProviderRequest() {
|
|
2741
|
-
controller.abort();
|
|
3126
|
+
requestRecord.cancel = async function cancelAIProviderRequest(reason) {
|
|
3127
|
+
controller.abort(reason);
|
|
2742
3128
|
await requestRecord.promise?.catch(
|
|
2743
3129
|
function retainCancelledAIProviderRequest() {}
|
|
2744
3130
|
);
|
|
@@ -2764,7 +3150,12 @@ export class AIProviderRuntime {
|
|
|
2764
3150
|
requestSequence,
|
|
2765
3151
|
controller.signal
|
|
2766
3152
|
);
|
|
2767
|
-
return
|
|
3153
|
+
return role === 'llm'
|
|
3154
|
+
? validateLLMTerminalResult(
|
|
3155
|
+
result,
|
|
3156
|
+
'AI provider chat result'
|
|
3157
|
+
)
|
|
3158
|
+
: result;
|
|
2768
3159
|
} catch (error) {
|
|
2769
3160
|
const normalized = isAbort(error, controller.signal)
|
|
2770
3161
|
? normalizedAbort(error)
|
|
@@ -2777,6 +3168,7 @@ export class AIProviderRuntime {
|
|
|
2777
3168
|
if (slot.request === requestRecord) {
|
|
2778
3169
|
slot.request = null;
|
|
2779
3170
|
}
|
|
3171
|
+
runtime.#drainRoleRequestQueue(slot);
|
|
2780
3172
|
if (!requestError
|
|
2781
3173
|
&& slot.generation === generation
|
|
2782
3174
|
&& !slot.unloadPromise) {
|
|
@@ -2806,18 +3198,16 @@ export class AIProviderRuntime {
|
|
|
2806
3198
|
cancel(role) {
|
|
2807
3199
|
assertRole(role);
|
|
2808
3200
|
const slot = this.#slots[role];
|
|
2809
|
-
if (!slot.request
|
|
3201
|
+
if (!slot.request) {
|
|
2810
3202
|
return false;
|
|
2811
3203
|
}
|
|
2812
|
-
slot.requestAdmission?.controller.abort();
|
|
2813
3204
|
const requestRecord = slot.request;
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
`AI role ${role} request was cancelled.`,
|
|
2818
|
-
'ARCANE_AI_REQUEST_ABORTED'
|
|
2819
|
-
)
|
|
3205
|
+
const reason = operationError(
|
|
3206
|
+
`AI role ${role} request was cancelled.`,
|
|
3207
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
2820
3208
|
);
|
|
3209
|
+
requestRecord?.controller.abort(reason);
|
|
3210
|
+
const cancellation = requestRecord?.cancel?.(reason);
|
|
2821
3211
|
cancellation?.catch(function retainAIProviderCancelFailureInState() {});
|
|
2822
3212
|
return true;
|
|
2823
3213
|
}
|
|
@@ -2907,126 +3297,83 @@ export class AIProviderRuntime {
|
|
|
2907
3297
|
);
|
|
2908
3298
|
}
|
|
2909
3299
|
|
|
2910
|
-
#
|
|
2911
|
-
const generation = slot.generation;
|
|
2912
|
-
const requestSequence = this.#nextRequestSequence(slot);
|
|
2913
|
-
const priorAdmission = slot.requestAdmission;
|
|
2914
|
-
const activeRequest = priorAdmission?.activeRequest ?? slot.request;
|
|
2915
|
-
const controller = new AbortController();
|
|
2916
|
-
const detachSignal = this.#forwardAbort(options.signal, controller);
|
|
2917
|
-
let settlement = priorAdmission?.settlement ?? null;
|
|
2918
|
-
if (!settlement && activeRequest) {
|
|
2919
|
-
activeRequest.controller.abort();
|
|
2920
|
-
settlement = Promise.allSettled(
|
|
2921
|
-
[
|
|
2922
|
-
Promise.resolve().then(
|
|
2923
|
-
function cancelSupersededAIProviderRequest() {
|
|
2924
|
-
return activeRequest.cancel(
|
|
2925
|
-
operationError(
|
|
2926
|
-
`AI role ${slot.role} request was superseded.`,
|
|
2927
|
-
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
2928
|
-
)
|
|
2929
|
-
);
|
|
2930
|
-
}
|
|
2931
|
-
),
|
|
2932
|
-
activeRequest.promise
|
|
2933
|
-
]
|
|
2934
|
-
);
|
|
2935
|
-
}
|
|
2936
|
-
if (!settlement) {
|
|
2937
|
-
settlement = Promise.resolve([]);
|
|
2938
|
-
}
|
|
2939
|
-
const admission = {
|
|
2940
|
-
activeRequest,
|
|
2941
|
-
controller,
|
|
2942
|
-
generation,
|
|
2943
|
-
requestSequence,
|
|
2944
|
-
settlement
|
|
2945
|
-
};
|
|
2946
|
-
slot.requestAdmission = admission;
|
|
3300
|
+
#enqueueRoleRequest(slot, options) {
|
|
2947
3301
|
const runtime = this;
|
|
2948
|
-
return (
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
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;
|
|
2954
3313
|
}
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
);
|
|
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;
|
|
2964
3322
|
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
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
|
|
2974
3332
|
);
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
return await runtime.request(slot.role, options);
|
|
3009
|
-
} catch (error) {
|
|
3010
|
-
const normalized = isAbort(error, controller.signal)
|
|
3011
|
-
? normalizedAbort(error)
|
|
3012
|
-
: error;
|
|
3013
|
-
if (slot.requestAdmission === admission) {
|
|
3014
|
-
slot.requestAdmission = null;
|
|
3015
|
-
if (slot.generation === generation
|
|
3016
|
-
&& !slot.unloadPromise
|
|
3017
|
-
&& !slot.disposePromise) {
|
|
3018
|
-
runtime.#publishRequestAdmissionFailure(
|
|
3019
|
-
slot,
|
|
3020
|
-
provider,
|
|
3021
|
-
normalized
|
|
3022
|
-
);
|
|
3023
|
-
}
|
|
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);
|
|
3024
3366
|
}
|
|
3025
|
-
throw normalized;
|
|
3026
|
-
} finally {
|
|
3027
|
-
detachSignal();
|
|
3028
3367
|
}
|
|
3029
|
-
|
|
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
|
+
}
|
|
3030
3377
|
}
|
|
3031
3378
|
|
|
3032
3379
|
#sameSelection(left, right) {
|
|
@@ -3067,8 +3414,8 @@ export class AIProviderRuntime {
|
|
|
3067
3414
|
slot.loadPromise
|
|
3068
3415
|
|| slot.unloadPromise
|
|
3069
3416
|
|| slot.disposePromise
|
|
3070
|
-
|| slot.requestAdmission
|
|
3071
3417
|
|| slot.request
|
|
3418
|
+
|| slot.requestQueue.length > 0
|
|
3072
3419
|
|| slot.ready
|
|
3073
3420
|
);
|
|
3074
3421
|
}
|
|
@@ -3093,24 +3440,12 @@ export class AIProviderRuntime {
|
|
|
3093
3440
|
}
|
|
3094
3441
|
|
|
3095
3442
|
#nextRequestSequence(slot) {
|
|
3096
|
-
|
|
3097
|
-
throw operationError(
|
|
3098
|
-
`AI role ${slot.role} exhausted its request sequence.`,
|
|
3099
|
-
'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
|
|
3100
|
-
);
|
|
3101
|
-
}
|
|
3102
|
-
slot.requestSequence += 1;
|
|
3443
|
+
slot.requestSequence = nextSequence(slot.requestSequence);
|
|
3103
3444
|
return slot.requestSequence;
|
|
3104
3445
|
}
|
|
3105
3446
|
|
|
3106
3447
|
#nextOperationId(slot, operation) {
|
|
3107
|
-
|
|
3108
|
-
throw operationError(
|
|
3109
|
-
`AI role ${slot.role} exhausted its operation sequence.`,
|
|
3110
|
-
'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
|
|
3111
|
-
);
|
|
3112
|
-
}
|
|
3113
|
-
slot.operationSequence += 1;
|
|
3448
|
+
slot.operationSequence = nextSequence(slot.operationSequence);
|
|
3114
3449
|
return `${slot.role}-${operation}-${slot.operationSequence}`;
|
|
3115
3450
|
}
|
|
3116
3451
|
|
|
@@ -3130,22 +3465,8 @@ export class AIProviderRuntime {
|
|
|
3130
3465
|
this.#assertCurrentOperation(slot, generation, signal);
|
|
3131
3466
|
if (slot.requestSequence !== requestSequence) {
|
|
3132
3467
|
throw operationError(
|
|
3133
|
-
`AI role ${slot.role}
|
|
3134
|
-
'
|
|
3135
|
-
);
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
#assertCurrentRequestAdmission(slot, admission) {
|
|
3140
|
-
if (admission.controller.signal.aborted) {
|
|
3141
|
-
throw normalizedAbort();
|
|
3142
|
-
}
|
|
3143
|
-
if (slot.generation !== admission.generation
|
|
3144
|
-
|| slot.requestSequence !== admission.requestSequence
|
|
3145
|
-
|| slot.requestAdmission !== admission) {
|
|
3146
|
-
throw operationError(
|
|
3147
|
-
`AI role ${slot.role} request admission was superseded.`,
|
|
3148
|
-
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
3468
|
+
`AI role ${slot.role} no longer owns the active provider request.`,
|
|
3469
|
+
'ARCANE_AI_REQUEST_STALE'
|
|
3149
3470
|
);
|
|
3150
3471
|
}
|
|
3151
3472
|
}
|
|
@@ -3155,10 +3476,10 @@ export class AIProviderRuntime {
|
|
|
3155
3476
|
return function detachAbsentAIProviderAbort() {};
|
|
3156
3477
|
}
|
|
3157
3478
|
function forwardAIProviderAbort() {
|
|
3158
|
-
controller.abort();
|
|
3479
|
+
controller.abort(signal.reason);
|
|
3159
3480
|
}
|
|
3160
3481
|
if (signal.aborted) {
|
|
3161
|
-
controller.abort();
|
|
3482
|
+
controller.abort(signal.reason);
|
|
3162
3483
|
return function detachAlreadyAbortedAIProviderSignal() {};
|
|
3163
3484
|
}
|
|
3164
3485
|
signal.addEventListener('abort', forwardAIProviderAbort, {once: true});
|
|
@@ -3186,68 +3507,6 @@ export class AIProviderRuntime {
|
|
|
3186
3507
|
return true;
|
|
3187
3508
|
}
|
|
3188
3509
|
|
|
3189
|
-
#publishRequestAdmissionFailure(slot, provider, error) {
|
|
3190
|
-
let providerStatus = null;
|
|
3191
|
-
try {
|
|
3192
|
-
providerStatus = validateProviderStatus(provider.status());
|
|
3193
|
-
} catch {
|
|
3194
|
-
// The admission error remains authoritative when status is malformed.
|
|
3195
|
-
}
|
|
3196
|
-
if (isAbort(error, null)
|
|
3197
|
-
&& providerStatus?.state === 'ready'
|
|
3198
|
-
&& providerStatus.loaded
|
|
3199
|
-
&& !providerStatus.busy) {
|
|
3200
|
-
slot.ready = true;
|
|
3201
|
-
publishAIRuntimeRoleState(
|
|
3202
|
-
slot.role,
|
|
3203
|
-
roleRecord(
|
|
3204
|
-
slot.role,
|
|
3205
|
-
slot.selection,
|
|
3206
|
-
{
|
|
3207
|
-
state: 'ready',
|
|
3208
|
-
loaded: true
|
|
3209
|
-
}
|
|
3210
|
-
)
|
|
3211
|
-
);
|
|
3212
|
-
return;
|
|
3213
|
-
}
|
|
3214
|
-
if (isAbort(error, null)
|
|
3215
|
-
&& providerStatus?.state === 'unloaded'
|
|
3216
|
-
&& !providerStatus.loaded
|
|
3217
|
-
&& !providerStatus.busy) {
|
|
3218
|
-
slot.ready = false;
|
|
3219
|
-
publishAIRuntimeRoleState(
|
|
3220
|
-
slot.role,
|
|
3221
|
-
roleRecord(slot.role, slot.selection)
|
|
3222
|
-
);
|
|
3223
|
-
return;
|
|
3224
|
-
}
|
|
3225
|
-
if (providerStatus?.state === 'ready'
|
|
3226
|
-
&& providerStatus.loaded
|
|
3227
|
-
&& providerStatus.busy) {
|
|
3228
|
-
slot.ready = true;
|
|
3229
|
-
publishAIRuntimeRoleState(
|
|
3230
|
-
slot.role,
|
|
3231
|
-
roleRecord(
|
|
3232
|
-
slot.role,
|
|
3233
|
-
slot.selection,
|
|
3234
|
-
{
|
|
3235
|
-
state: 'ready',
|
|
3236
|
-
loaded: true,
|
|
3237
|
-
busy: true
|
|
3238
|
-
}
|
|
3239
|
-
)
|
|
3240
|
-
);
|
|
3241
|
-
return;
|
|
3242
|
-
}
|
|
3243
|
-
slot.ready = false;
|
|
3244
|
-
this.#publishRoleError(
|
|
3245
|
-
slot,
|
|
3246
|
-
error,
|
|
3247
|
-
providerStatus?.loaded === true
|
|
3248
|
-
);
|
|
3249
|
-
}
|
|
3250
|
-
|
|
3251
3510
|
#publishRoleError(slot, error, loaded) {
|
|
3252
3511
|
publishAIRuntimeRoleState(
|
|
3253
3512
|
slot.role,
|