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
|
@@ -2,107 +2,38 @@ import { createArcaneEventSource } from "arcane-os/event-manager";
|
|
|
2
2
|
|
|
3
3
|
export const ARCANE_AI_ADAPTER_PROTOCOL = "arcane-ai-adapter/1";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const SECURITY_CHECK_KEYS = Object.freeze(["byteLength", "sha256"]);
|
|
7
|
-
const EMPTY_SECURITY_CHECKS = Object.freeze({});
|
|
8
|
-
const EMPTY_MODEL_SECURITY = Object.freeze({ checks: EMPTY_SECURITY_CHECKS });
|
|
9
|
-
const MODEL_CONTROLLER_EVENT_TYPES = Object.freeze(["statechange", "progress"]);
|
|
5
|
+
const MODEL_CONTROLLER_EVENT_TYPES = ["statechange", "progress"];
|
|
10
6
|
const MODEL_CONTROLLER_EVENT_TYPE_SET = new Set(MODEL_CONTROLLER_EVENT_TYPES);
|
|
11
7
|
|
|
12
|
-
function closedSecurityRecord(value, keys, label) {
|
|
13
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
-
throw new TypeError(`${label} must be a plain object when provided.`);
|
|
15
|
-
}
|
|
16
|
-
const prototype = Object.getPrototypeOf(value);
|
|
17
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
18
|
-
throw new TypeError(`${label} must be a plain object when provided.`);
|
|
19
|
-
}
|
|
20
|
-
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
21
|
-
for (const key of Reflect.ownKeys(descriptors)) {
|
|
22
|
-
if (typeof key !== "string" || !keys.includes(key)) {
|
|
23
|
-
throw new TypeError(`${label} contains an unknown ${String(key)} field.`);
|
|
24
|
-
}
|
|
25
|
-
if (descriptors[key].get || descriptors[key].set) {
|
|
26
|
-
throw new TypeError(`${label}.${key} must be a data property.`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return descriptors;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
8
|
export function normalizeModelSecurity(value, label = "security") {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const normalized = {};
|
|
36
|
-
if (descriptors.secure?.value !== undefined) {
|
|
37
|
-
if (typeof descriptors.secure.value !== "boolean") {
|
|
38
|
-
throw new TypeError(`${label}.secure must be a boolean when provided.`);
|
|
39
|
-
}
|
|
40
|
-
normalized.secure = descriptors.secure.value;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let checks = EMPTY_SECURITY_CHECKS;
|
|
44
|
-
if (descriptors.checks?.value !== undefined) {
|
|
45
|
-
const checkDescriptors = closedSecurityRecord(
|
|
46
|
-
descriptors.checks.value,
|
|
47
|
-
SECURITY_CHECK_KEYS,
|
|
48
|
-
`${label}.checks`,
|
|
49
|
-
);
|
|
50
|
-
const normalizedChecks = {};
|
|
51
|
-
for (const check of SECURITY_CHECK_KEYS) {
|
|
52
|
-
if (checkDescriptors[check]?.value === undefined) continue;
|
|
53
|
-
if (typeof checkDescriptors[check].value !== "boolean") {
|
|
54
|
-
throw new TypeError(`${label}.checks.${check} must be a boolean when provided.`);
|
|
55
|
-
}
|
|
56
|
-
normalizedChecks[check] = checkDescriptors[check].value;
|
|
57
|
-
}
|
|
58
|
-
checks = Object.freeze(normalizedChecks);
|
|
59
|
-
}
|
|
60
|
-
normalized.checks = checks;
|
|
61
|
-
return Object.freeze(normalized);
|
|
9
|
+
void label;
|
|
10
|
+
return value?.secure === true ? { secure: true } : undefined;
|
|
62
11
|
}
|
|
63
12
|
|
|
64
13
|
export function resolveModelSecurity({ app, binding, load } = {}) {
|
|
65
|
-
const scopes = [
|
|
66
|
-
normalizeModelSecurity(app, "app security"),
|
|
67
|
-
normalizeModelSecurity(binding, "provider security"),
|
|
68
|
-
normalizeModelSecurity(load, "load security"),
|
|
69
|
-
];
|
|
14
|
+
const scopes = [app, binding, load];
|
|
70
15
|
let secure = false;
|
|
71
|
-
let byteLength;
|
|
72
|
-
let sha256;
|
|
73
16
|
for (const scope of scopes) {
|
|
74
|
-
if (
|
|
75
|
-
if (
|
|
76
|
-
if (Object.hasOwn(scope.checks, "sha256")) sha256 = scope.checks.sha256;
|
|
17
|
+
if (scope?.secure === true) secure = true;
|
|
18
|
+
else if (scope?.secure === false) secure = false;
|
|
77
19
|
}
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
sha256: sha256 ?? secure,
|
|
83
|
-
}),
|
|
84
|
-
});
|
|
20
|
+
if (!secure) return undefined;
|
|
21
|
+
// Secure mode currently carries activation intent only. Security checks remain
|
|
22
|
+
// disabled and must be reviewed with the user before any implementation runs.
|
|
23
|
+
return { secure: true };
|
|
85
24
|
}
|
|
86
25
|
|
|
87
26
|
export function sameModelSecurity(left, right) {
|
|
88
|
-
return left?.secure === right?.secure
|
|
89
|
-
&& left?.checks?.byteLength === right?.checks?.byteLength
|
|
90
|
-
&& left?.checks?.sha256 === right?.checks?.sha256;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function hasModelSecurityOverrides(value) {
|
|
94
|
-
return Object.hasOwn(value, "secure")
|
|
95
|
-
|| Object.hasOwn(value.checks, "byteLength")
|
|
96
|
-
|| Object.hasOwn(value.checks, "sha256");
|
|
27
|
+
return (left?.secure === true) === (right?.secure === true);
|
|
97
28
|
}
|
|
98
29
|
|
|
99
|
-
const ERROR_CODES =
|
|
30
|
+
const ERROR_CODES = {
|
|
100
31
|
load: "ARCANE_AI_LOAD_FAILED",
|
|
101
32
|
unload: "ARCANE_AI_UNLOAD_FAILED",
|
|
102
33
|
request: "ARCANE_AI_REQUEST_FAILED",
|
|
103
34
|
dispose: "ARCANE_AI_DISPOSE_FAILED",
|
|
104
35
|
probe: "ARCANE_AI_PROBE_FAILED",
|
|
105
|
-
}
|
|
36
|
+
};
|
|
106
37
|
|
|
107
38
|
function abortLike(error, signal) {
|
|
108
39
|
return signal?.aborted === true
|
|
@@ -174,10 +105,10 @@ function copyError(error) {
|
|
|
174
105
|
throw invalidStatus(copyErrorFailure);
|
|
175
106
|
}
|
|
176
107
|
}
|
|
177
|
-
return
|
|
108
|
+
return {
|
|
178
109
|
code: String(code ?? "ARCANE_AI_REQUEST_FAILED"),
|
|
179
110
|
message: String(message ?? "The Arcane AI operation failed."),
|
|
180
|
-
}
|
|
111
|
+
};
|
|
181
112
|
}
|
|
182
113
|
|
|
183
114
|
function isModelControllerListener(value) {
|
|
@@ -208,9 +139,6 @@ function copyProviderStatus(value) {
|
|
|
208
139
|
}
|
|
209
140
|
}
|
|
210
141
|
|
|
211
|
-
const MAX_PROGRESS_DEPTH = 8;
|
|
212
|
-
const MAX_PROGRESS_ENTRIES = 256;
|
|
213
|
-
|
|
214
142
|
function invalidProgress(cause) {
|
|
215
143
|
return new ArcaneAIError(
|
|
216
144
|
"ARCANE_AI_PROVIDER_PROGRESS_INVALID",
|
|
@@ -219,9 +147,9 @@ function invalidProgress(cause) {
|
|
|
219
147
|
);
|
|
220
148
|
}
|
|
221
149
|
|
|
222
|
-
function copyProgressValue(value, state
|
|
150
|
+
function copyProgressValue(value, state) {
|
|
223
151
|
if (value === null || typeof value !== "object") return value;
|
|
224
|
-
if (
|
|
152
|
+
if (state.seen.has(value)) {
|
|
225
153
|
throw invalidProgress();
|
|
226
154
|
}
|
|
227
155
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -233,20 +161,19 @@ function copyProgressValue(value, state, depth = 0) {
|
|
|
233
161
|
try {
|
|
234
162
|
for (const key of Reflect.ownKeys(value)) {
|
|
235
163
|
if (Array.isArray(value) && key === "length") continue;
|
|
236
|
-
|
|
237
|
-
if (state.entries > MAX_PROGRESS_ENTRIES || typeof key !== "string") {
|
|
164
|
+
if (typeof key !== "string") {
|
|
238
165
|
throw invalidProgress();
|
|
239
166
|
}
|
|
240
167
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
241
168
|
if (!descriptor || !("value" in descriptor)) throw invalidProgress();
|
|
242
169
|
Object.defineProperty(copy, key, {
|
|
243
|
-
value: copyProgressValue(descriptor.value, state
|
|
170
|
+
value: copyProgressValue(descriptor.value, state),
|
|
244
171
|
enumerable: descriptor.enumerable,
|
|
245
|
-
configurable:
|
|
246
|
-
writable:
|
|
172
|
+
configurable: true,
|
|
173
|
+
writable: true,
|
|
247
174
|
});
|
|
248
175
|
}
|
|
249
|
-
return
|
|
176
|
+
return copy;
|
|
250
177
|
} finally {
|
|
251
178
|
state.seen.delete(value);
|
|
252
179
|
}
|
|
@@ -258,7 +185,7 @@ function copyProgress(progress) {
|
|
|
258
185
|
throw invalidProgress();
|
|
259
186
|
}
|
|
260
187
|
try {
|
|
261
|
-
return copyProgressValue(progress, { seen: new WeakSet()
|
|
188
|
+
return copyProgressValue(progress, { seen: new WeakSet() });
|
|
262
189
|
} catch (error) {
|
|
263
190
|
if (error instanceof ArcaneAIError) throw error;
|
|
264
191
|
throw invalidProgress(error);
|
|
@@ -266,31 +193,7 @@ function copyProgress(progress) {
|
|
|
266
193
|
}
|
|
267
194
|
|
|
268
195
|
function publicProgress(progress) {
|
|
269
|
-
|
|
270
|
-
const file = progress.file && typeof progress.file === "object"
|
|
271
|
-
? Object.freeze({
|
|
272
|
-
...(Number.isSafeInteger(progress.file.index) ? { index: progress.file.index } : {}),
|
|
273
|
-
...(Number.isSafeInteger(progress.file.count) ? { count: progress.file.count } : {}),
|
|
274
|
-
...(typeof progress.file.name === "string" ? { name: progress.file.name } : {}),
|
|
275
|
-
...(Number.isSafeInteger(progress.file.loaded) ? { loaded: progress.file.loaded } : {}),
|
|
276
|
-
...(progress.file.total === null || Number.isSafeInteger(progress.file.total)
|
|
277
|
-
? { total: progress.file.total }
|
|
278
|
-
: {}),
|
|
279
|
-
})
|
|
280
|
-
: null;
|
|
281
|
-
return Object.freeze({
|
|
282
|
-
...(typeof progress.modelId === "string" ? { modelId: progress.modelId } : {}),
|
|
283
|
-
...(typeof progress.phase === "string" ? { phase: progress.phase } : {}),
|
|
284
|
-
...(Number.isSafeInteger(progress.loaded) ? { loaded: progress.loaded } : {}),
|
|
285
|
-
...(progress.total === null || Number.isSafeInteger(progress.total)
|
|
286
|
-
? { total: progress.total }
|
|
287
|
-
: {}),
|
|
288
|
-
...(progress.percent === null
|
|
289
|
-
|| (typeof progress.percent === "number" && Number.isFinite(progress.percent))
|
|
290
|
-
? { percent: progress.percent }
|
|
291
|
-
: {}),
|
|
292
|
-
...(file ? { file } : {}),
|
|
293
|
-
});
|
|
196
|
+
return progress && typeof progress === "object" ? copyProgress(progress) : null;
|
|
294
197
|
}
|
|
295
198
|
|
|
296
199
|
function localRequirement(options, provider) {
|
|
@@ -311,10 +214,10 @@ function linkedAbortSignal(externalSignal) {
|
|
|
311
214
|
const forward = () => controller.abort(externalSignal.reason);
|
|
312
215
|
if (externalSignal?.aborted) forward();
|
|
313
216
|
else externalSignal?.addEventListener?.("abort", forward, { once: true });
|
|
314
|
-
return
|
|
217
|
+
return {
|
|
315
218
|
controller,
|
|
316
219
|
release: () => externalSignal?.removeEventListener?.("abort", forward),
|
|
317
|
-
}
|
|
220
|
+
};
|
|
318
221
|
}
|
|
319
222
|
|
|
320
223
|
function fireAndForget(callback, ...args) {
|
|
@@ -338,21 +241,464 @@ function displayRequestId(value) {
|
|
|
338
241
|
return `M-${String(value)}`;
|
|
339
242
|
}
|
|
340
243
|
|
|
244
|
+
function completeTextValue(value, seen, location) {
|
|
245
|
+
if (value === null) return null;
|
|
246
|
+
if (value === undefined) return { $type: "undefined" };
|
|
247
|
+
if (typeof value === "bigint") return { $type: "bigint", value: value.toString() };
|
|
248
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
249
|
+
return { $type: "number", value: String(value) };
|
|
250
|
+
}
|
|
251
|
+
if (typeof value === "symbol") return { $type: "symbol", value: String(value) };
|
|
252
|
+
if (typeof value === "function") {
|
|
253
|
+
return { $type: "function", value: Function.prototype.toString.call(value) };
|
|
254
|
+
}
|
|
255
|
+
if (typeof value !== "object") return value;
|
|
256
|
+
if (seen.has(value)) return { $ref: seen.get(value) };
|
|
257
|
+
seen.set(value, location);
|
|
258
|
+
if (value instanceof Date) return { $type: "date", value: value.toISOString() };
|
|
259
|
+
if (value instanceof RegExp) return { $type: "regexp", value: String(value) };
|
|
260
|
+
if (value instanceof Map) {
|
|
261
|
+
return {
|
|
262
|
+
$type: "map",
|
|
263
|
+
entries: [...value.entries()].map(([key, entry], index) => [
|
|
264
|
+
completeTextValue(key, seen, `${location}.entries[${index}].key`),
|
|
265
|
+
completeTextValue(entry, seen, `${location}.entries[${index}].value`),
|
|
266
|
+
]),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (value instanceof Set) {
|
|
270
|
+
return {
|
|
271
|
+
$type: "set",
|
|
272
|
+
values: [...value].map((entry, index) => completeTextValue(
|
|
273
|
+
entry,
|
|
274
|
+
seen,
|
|
275
|
+
`${location}.values[${index}]`,
|
|
276
|
+
)),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (ArrayBuffer.isView(value)) {
|
|
280
|
+
return {
|
|
281
|
+
$type: value.constructor?.name ?? "ArrayBufferView",
|
|
282
|
+
values: Array.from(
|
|
283
|
+
value instanceof DataView
|
|
284
|
+
? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
285
|
+
: value,
|
|
286
|
+
(entry, index) => completeTextValue(entry, seen, `${location}.values[${index}]`),
|
|
287
|
+
),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (value instanceof ArrayBuffer) {
|
|
291
|
+
return { $type: "ArrayBuffer", values: Array.from(new Uint8Array(value)) };
|
|
292
|
+
}
|
|
293
|
+
const copy = Array.isArray(value) ? [] : {};
|
|
294
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
295
|
+
if (Array.isArray(value) && key === "length") continue;
|
|
296
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
297
|
+
const renderedKey = typeof key === "symbol" ? `[${String(key)}]` : key;
|
|
298
|
+
copy[renderedKey] = descriptor && "value" in descriptor
|
|
299
|
+
? completeTextValue(descriptor.value, seen, `${location}.${renderedKey}`)
|
|
300
|
+
: {
|
|
301
|
+
$type: "accessor",
|
|
302
|
+
get: Boolean(descriptor?.get),
|
|
303
|
+
set: Boolean(descriptor?.set),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return copy;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function completeValueText(value) {
|
|
310
|
+
if (typeof value === "string") return value;
|
|
311
|
+
return JSON.stringify(completeTextValue(value, new WeakMap(), "$"), null, 2);
|
|
312
|
+
}
|
|
313
|
+
|
|
341
314
|
function textFromCompletion(completion) {
|
|
342
|
-
|
|
343
|
-
return
|
|
315
|
+
if (!Array.isArray(completion?.choices)) return completeValueText(completion);
|
|
316
|
+
return completion.choices.map((choice) => {
|
|
317
|
+
const content = choice?.message?.content;
|
|
318
|
+
return content === undefined ? completeValueText(choice) : completeValueText(content);
|
|
319
|
+
}).join("\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function structuralToolCall(call,location){
|
|
323
|
+
if(
|
|
324
|
+
!call
|
|
325
|
+
||typeof call!=="object"
|
|
326
|
+
||Array.isArray(call)
|
|
327
|
+
||call.type!=="function"
|
|
328
|
+
||typeof call.id!=="string"
|
|
329
|
+
||!call.id.trim()
|
|
330
|
+
||!call.function
|
|
331
|
+
||typeof call.function!=="object"
|
|
332
|
+
||Array.isArray(call.function)
|
|
333
|
+
||typeof call.function.name!=="string"
|
|
334
|
+
||!call.function.name.trim()
|
|
335
|
+
||typeof call.function.arguments!=="string"
|
|
336
|
+
){
|
|
337
|
+
throw new ArcaneAIError(
|
|
338
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
339
|
+
`${location} is not a complete structural function call.`,
|
|
340
|
+
{operation:"request"},
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
let argumentsRecord;
|
|
344
|
+
try{
|
|
345
|
+
argumentsRecord=JSON.parse(call.function.arguments);
|
|
346
|
+
}catch(error){
|
|
347
|
+
throw new ArcaneAIError(
|
|
348
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
349
|
+
`${location} arguments must encode a JSON object.`,
|
|
350
|
+
{cause:error,operation:"request"},
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if(
|
|
354
|
+
!argumentsRecord
|
|
355
|
+
||typeof argumentsRecord!=="object"
|
|
356
|
+
||Array.isArray(argumentsRecord)
|
|
357
|
+
){
|
|
358
|
+
throw new ArcaneAIError(
|
|
359
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
360
|
+
`${location} arguments must encode a JSON object.`,
|
|
361
|
+
{operation:"request"},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if(typeof argumentsRecord.message!=="string"||!argumentsRecord.message.trim()){
|
|
365
|
+
throw new ArcaneAIError(
|
|
366
|
+
"ARCANE_AI_TOOL_MESSAGE_REQUIRED",
|
|
367
|
+
`${location} arguments must include a nonempty user-facing message.`,
|
|
368
|
+
{operation:"request"},
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
id:call.id,
|
|
373
|
+
type:"function",
|
|
374
|
+
function:{
|
|
375
|
+
name:call.function.name,
|
|
376
|
+
arguments:call.function.arguments,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function plainStructuralRecord(value){
|
|
382
|
+
if(!value||typeof value!=="object"||Array.isArray(value)) return false;
|
|
383
|
+
const prototype=Object.getPrototypeOf(value);
|
|
384
|
+
return prototype===Object.prototype||prototype===null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function requireToolMessageSchemas(value,location){
|
|
388
|
+
if(value===undefined) return;
|
|
389
|
+
if(!Array.isArray(value)){
|
|
390
|
+
throw new ArcaneAIError(
|
|
391
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
392
|
+
`${location} must be an array.`,
|
|
393
|
+
{operation:"request"},
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
for(const [index,tool] of value.entries()){
|
|
397
|
+
const parameters=tool?.function?.parameters;
|
|
398
|
+
const messageSchema=parameters?.properties?.message;
|
|
399
|
+
if(
|
|
400
|
+
!plainStructuralRecord(tool)
|
|
401
|
+
||tool.type!=="function"
|
|
402
|
+
||!plainStructuralRecord(tool.function)
|
|
403
|
+
||!plainStructuralRecord(parameters)
|
|
404
|
+
||parameters.type!=="object"
|
|
405
|
+
||!plainStructuralRecord(parameters.properties)
|
|
406
|
+
||!plainStructuralRecord(messageSchema)
|
|
407
|
+
||messageSchema.type!=="string"
|
|
408
|
+
||!Number.isInteger(messageSchema.minLength)
|
|
409
|
+
||messageSchema.minLength<1
|
|
410
|
+
||!Array.isArray(parameters.required)
|
|
411
|
+
||!parameters.required.includes("message")
|
|
412
|
+
){
|
|
413
|
+
throw new ArcaneAIError(
|
|
414
|
+
"ARCANE_AI_TOOL_MESSAGE_REQUIRED",
|
|
415
|
+
`${location}[${String(index)}] must require a nonempty string parameters.properties.message.`,
|
|
416
|
+
{operation:"request"},
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function structuralCallsFromMessage(message,location){
|
|
423
|
+
if(!plainStructuralRecord(message)){
|
|
424
|
+
throw new ArcaneAIError(
|
|
425
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
426
|
+
`${location} must be a plain message object.`,
|
|
427
|
+
{operation:"request"},
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if(
|
|
431
|
+
Object.hasOwn(message,"toolCalls")
|
|
432
|
+
||Object.hasOwn(message,"tool_call")
|
|
433
|
+
||Object.hasOwn(message,"toolCall")
|
|
434
|
+
||Object.hasOwn(message,"function_call")
|
|
435
|
+
||Object.hasOwn(message,"functionCall")
|
|
436
|
+
){
|
|
437
|
+
throw new ArcaneAIError(
|
|
438
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
439
|
+
`${location} contains a noncanonical structural tool-call field.`,
|
|
440
|
+
{operation:"request"},
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
if(!Object.hasOwn(message,"tool_calls"))return [];
|
|
444
|
+
const descriptor=Object.getOwnPropertyDescriptor(message,"tool_calls");
|
|
445
|
+
if(
|
|
446
|
+
!descriptor
|
|
447
|
+
||!Object.hasOwn(descriptor,"value")
|
|
448
|
+
||!Array.isArray(descriptor.value)
|
|
449
|
+
){
|
|
450
|
+
throw new ArcaneAIError(
|
|
451
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
452
|
+
`${location}.tool_calls must be an array data property.`,
|
|
453
|
+
{operation:"request"},
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return descriptor.value;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function structuralRequest(value){
|
|
460
|
+
if(!plainStructuralRecord(value)){
|
|
461
|
+
throw new TypeError("AI request options must be a plain object.");
|
|
462
|
+
}
|
|
463
|
+
if(value.messages!==undefined&&!Array.isArray(value.messages)){
|
|
464
|
+
throw new TypeError("AI request messages must be an array.");
|
|
465
|
+
}
|
|
466
|
+
requireToolMessageSchemas(value.tools,"AI request tools");
|
|
467
|
+
if(value.parallelToolCalls===true||value.parallel_tool_calls===true){
|
|
468
|
+
throw new ArcaneAIError(
|
|
469
|
+
"ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED",
|
|
470
|
+
"The Arcane chat session accepts one structural tool call at a time.",
|
|
471
|
+
{operation:"request"},
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let pendingToolCallId=null;
|
|
476
|
+
for(const [messageIndex,message] of (value.messages??[]).entries()){
|
|
477
|
+
const calls=structuralCallsFromMessage(
|
|
478
|
+
message,
|
|
479
|
+
`AI request messages[${String(messageIndex)}]`,
|
|
480
|
+
);
|
|
481
|
+
let openedToolCall=false;
|
|
482
|
+
if(Object.hasOwn(message,"tool_calls")){
|
|
483
|
+
if(message?.role!=="assistant"||!Array.isArray(calls)){
|
|
484
|
+
throw new ArcaneAIError(
|
|
485
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
486
|
+
`AI request messages[${String(messageIndex)}].tool_calls is invalid.`,
|
|
487
|
+
{operation:"request"},
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if(calls.length>1||pendingToolCallId!==null&&calls.length){
|
|
491
|
+
throw new ArcaneAIError(
|
|
492
|
+
"ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED",
|
|
493
|
+
"The Arcane chat session accepts one structural tool call at a time.",
|
|
494
|
+
{operation:"request"},
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if(calls.length){
|
|
498
|
+
pendingToolCallId=structuralToolCall(
|
|
499
|
+
calls[0],
|
|
500
|
+
`AI request messages[${String(messageIndex)}].tool_calls[0]`,
|
|
501
|
+
).id;
|
|
502
|
+
openedToolCall=true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if(message?.role==="tool"){
|
|
506
|
+
if(
|
|
507
|
+
typeof message.content!=="string"
|
|
508
|
+
||!message.content.trim()
|
|
509
|
+
){
|
|
510
|
+
throw new ArcaneAIError(
|
|
511
|
+
"ARCANE_AI_INVALID_TOOL_MESSAGE",
|
|
512
|
+
`AI request messages[${String(messageIndex)}] must contain a nonblank user-facing tool result.`,
|
|
513
|
+
{operation:"request"},
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
if(
|
|
517
|
+
pendingToolCallId===null
|
|
518
|
+
||typeof message.tool_call_id!=="string"
|
|
519
|
+
||message.tool_call_id!==pendingToolCallId
|
|
520
|
+
){
|
|
521
|
+
throw new ArcaneAIError(
|
|
522
|
+
"ARCANE_AI_INVALID_TOOL_MESSAGE",
|
|
523
|
+
`AI request messages[${String(messageIndex)}] does not settle the pending structural tool call.`,
|
|
524
|
+
{operation:"request"},
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
pendingToolCallId=null;
|
|
528
|
+
}else if(pendingToolCallId!==null&&!openedToolCall){
|
|
529
|
+
throw new ArcaneAIError(
|
|
530
|
+
"ARCANE_AI_TOOL_RESULT_REQUIRED",
|
|
531
|
+
`AI request messages[${String(messageIndex)}] precedes the pending structural tool result.`,
|
|
532
|
+
{operation:"request"},
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if(pendingToolCallId!==null){
|
|
537
|
+
throw new ArcaneAIError(
|
|
538
|
+
"ARCANE_AI_TOOL_RESULT_REQUIRED",
|
|
539
|
+
"The pending structural tool call must be settled before requesting another response.",
|
|
540
|
+
{operation:"request"},
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
...value,
|
|
546
|
+
...(value.tools?.length
|
|
547
|
+
&&value.parallelToolCalls===undefined
|
|
548
|
+
&&value.parallel_tool_calls===undefined
|
|
549
|
+
?{parallelToolCalls:false}
|
|
550
|
+
:{}),
|
|
551
|
+
};
|
|
344
552
|
}
|
|
345
553
|
|
|
346
554
|
function toolRecordFromCompletion(completion) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
555
|
+
if(typeof completion==="string")return null;
|
|
556
|
+
if(!plainStructuralRecord(completion)){
|
|
557
|
+
throw new ArcaneAIError(
|
|
558
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
559
|
+
"The model returned neither text nor a structured completion.",
|
|
560
|
+
{operation:"request"},
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
const hasMessage=Object.hasOwn(completion,"message");
|
|
564
|
+
const hasChoices=Object.hasOwn(completion,"choices");
|
|
565
|
+
if(hasMessage===hasChoices){
|
|
566
|
+
throw new ArcaneAIError(
|
|
567
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
568
|
+
"The model completion must contain exactly one message or choices envelope.",
|
|
569
|
+
{operation:"request"},
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const result = [];
|
|
573
|
+
const messages=[];
|
|
574
|
+
if(hasMessage){
|
|
575
|
+
const descriptor=Object.getOwnPropertyDescriptor(completion,"message");
|
|
576
|
+
if(
|
|
577
|
+
!descriptor
|
|
578
|
+
||!Object.hasOwn(descriptor,"value")
|
|
579
|
+
||!plainStructuralRecord(descriptor.value)
|
|
580
|
+
||descriptor.value.role!=="assistant"
|
|
581
|
+
){
|
|
582
|
+
throw new ArcaneAIError(
|
|
583
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
584
|
+
"The model completion message must be an assistant message data property.",
|
|
585
|
+
{operation:"request"},
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
messages.push(descriptor.value);
|
|
589
|
+
}else{
|
|
590
|
+
const descriptor=Object.getOwnPropertyDescriptor(completion,"choices");
|
|
591
|
+
if(
|
|
592
|
+
!descriptor
|
|
593
|
+
||!Object.hasOwn(descriptor,"value")
|
|
594
|
+
||!Array.isArray(descriptor.value)
|
|
595
|
+
||!descriptor.value.length
|
|
596
|
+
){
|
|
597
|
+
throw new ArcaneAIError(
|
|
598
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
599
|
+
"The model completion choices envelope must be a nonempty array data property.",
|
|
600
|
+
{operation:"request"},
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
const indexes=new Set();
|
|
604
|
+
for(const [choiceIndex,choice] of descriptor.value.entries()){
|
|
605
|
+
const messageDescriptor=plainStructuralRecord(choice)
|
|
606
|
+
?Object.getOwnPropertyDescriptor(choice,"message")
|
|
607
|
+
:null;
|
|
608
|
+
if(
|
|
609
|
+
!plainStructuralRecord(choice)
|
|
610
|
+
||!Number.isSafeInteger(choice.index)
|
|
611
|
+
||choice.index<0
|
|
612
|
+
||indexes.has(choice.index)
|
|
613
|
+
||!messageDescriptor
|
|
614
|
+
||!Object.hasOwn(messageDescriptor,"value")
|
|
615
|
+
||!plainStructuralRecord(messageDescriptor.value)
|
|
616
|
+
||messageDescriptor.value.role!=="assistant"
|
|
617
|
+
){
|
|
618
|
+
throw new ArcaneAIError(
|
|
619
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
620
|
+
`The model completion choice ${String(choiceIndex)} is invalid.`,
|
|
621
|
+
{operation:"request"},
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
indexes.add(choice.index);
|
|
625
|
+
messages.push(messageDescriptor.value);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (let messageIndex=0;messageIndex<messages.length;messageIndex+=1) {
|
|
629
|
+
const calls=structuralCallsFromMessage(
|
|
630
|
+
messages[messageIndex],
|
|
631
|
+
`Structural tool call message ${String(messageIndex+1)}`,
|
|
632
|
+
);
|
|
633
|
+
if(messageIndex>0&&calls?.length){
|
|
634
|
+
throw new ArcaneAIError(
|
|
635
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
636
|
+
"The model placed a structural tool call outside the selected first choice.",
|
|
637
|
+
{operation:"request"},
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
for (const [callIndex,call] of (calls ?? []).entries()) {
|
|
641
|
+
result.push(structuralToolCall(
|
|
642
|
+
call,
|
|
643
|
+
`Structural tool call ${String(messageIndex+1)}.${String(callIndex+1)}`,
|
|
644
|
+
));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if(result.length>1){
|
|
648
|
+
throw new ArcaneAIError(
|
|
649
|
+
"ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED",
|
|
650
|
+
"The Arcane chat session accepts one structural tool call at a time.",
|
|
651
|
+
{operation:"request"},
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
return result.length ? result : null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function isPublicStreamContentKey(key){
|
|
658
|
+
return key==="content"
|
|
659
|
+
||key==="text"
|
|
660
|
+
||key==="thinking"
|
|
661
|
+
||key==="reasoning"
|
|
662
|
+
||key==="reasoning_content";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function projectPublicStreamContent(value,seen=new WeakSet()){
|
|
666
|
+
if(!value||typeof value!=="object"||seen.has(value))return null;
|
|
667
|
+
seen.add(value);
|
|
668
|
+
if(Array.isArray(value)){
|
|
669
|
+
const result=[];
|
|
670
|
+
for(const item of value){
|
|
671
|
+
const projected=projectPublicStreamContent(item,seen);
|
|
672
|
+
if(projected!==null)result.push(projected);
|
|
673
|
+
}
|
|
674
|
+
seen.delete(value);
|
|
675
|
+
return result.length?result:null;
|
|
676
|
+
}
|
|
677
|
+
if(!plainStructuralRecord(value)){
|
|
678
|
+
seen.delete(value);
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
const result={};
|
|
682
|
+
for(const [key,descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))){
|
|
683
|
+
if(!Object.hasOwn(descriptor,"value"))continue;
|
|
684
|
+
if(
|
|
685
|
+
isPublicStreamContentKey(key)
|
|
686
|
+
&&descriptor.value!==null
|
|
687
|
+
&&descriptor.value!==undefined
|
|
688
|
+
){
|
|
689
|
+
result[key]=descriptor.value;
|
|
690
|
+
continue;
|
|
353
691
|
}
|
|
692
|
+
const projected=projectPublicStreamContent(descriptor.value,seen);
|
|
693
|
+
if(projected!==null)result[key]=projected;
|
|
354
694
|
}
|
|
355
|
-
|
|
695
|
+
seen.delete(value);
|
|
696
|
+
return Object.keys(result).length?result:null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function projectPublicStreamChunk(value){
|
|
700
|
+
if(typeof value==="string")return value;
|
|
701
|
+
return projectPublicStreamContent(value);
|
|
356
702
|
}
|
|
357
703
|
|
|
358
704
|
export class ModelController {
|
|
@@ -401,18 +747,18 @@ export class ModelController {
|
|
|
401
747
|
|
|
402
748
|
status() {
|
|
403
749
|
const providerStatus = copyProviderStatus(
|
|
404
|
-
providerMethod(this.#provider, "status")?.(
|
|
405
|
-
Object.freeze({ security: this.#security }),
|
|
406
|
-
),
|
|
750
|
+
providerMethod(this.#provider, "status")?.({}),
|
|
407
751
|
);
|
|
752
|
+
delete providerStatus.security;
|
|
408
753
|
const progress = copyProgress(providerStatus.progress ?? this.#progress);
|
|
409
|
-
return
|
|
754
|
+
return {
|
|
410
755
|
...providerStatus,
|
|
411
756
|
kind: "llm",
|
|
412
757
|
state: providerStatus.state ?? this.#fallbackState,
|
|
413
758
|
progress,
|
|
414
759
|
error: copyError(providerStatus.error ?? this.#error),
|
|
415
|
-
|
|
760
|
+
...(this.#security ? { security: { secure: true } } : {}),
|
|
761
|
+
};
|
|
416
762
|
}
|
|
417
763
|
|
|
418
764
|
addEventListener(type, listener, options) {
|
|
@@ -449,11 +795,11 @@ export class ModelController {
|
|
|
449
795
|
const progress = type === "progress" ? publicProgress(status.progress) : null;
|
|
450
796
|
this.#events.dispatch(type, status, {
|
|
451
797
|
operationId,
|
|
452
|
-
publicDetail:
|
|
798
|
+
publicDetail: {
|
|
453
799
|
...(typeof status.state === "string" ? { state: status.state } : {}),
|
|
454
800
|
...(progress ? { progress } : {}),
|
|
455
801
|
...(typeof status.error?.code === "string" ? { code: status.error.code } : {}),
|
|
456
|
-
}
|
|
802
|
+
},
|
|
457
803
|
});
|
|
458
804
|
}
|
|
459
805
|
|
|
@@ -476,24 +822,28 @@ export class ModelController {
|
|
|
476
822
|
const load = providerMethod(this.#provider, "load");
|
|
477
823
|
if (!load) throw new ArcaneAIError("ARCANE_AI_UNAVAILABLE", "The LLM provider cannot load a model.");
|
|
478
824
|
const signal = options.signal ?? null;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
825
|
+
this.#security = resolveModelSecurity({
|
|
826
|
+
app: this.#security,
|
|
827
|
+
load: options.security,
|
|
828
|
+
});
|
|
829
|
+
const loadOptions = { ...options };
|
|
830
|
+
delete loadOptions.security;
|
|
831
|
+
// `secure: true` is an activation-intent seam only. Existing security
|
|
832
|
+
// implementations remain disabled until they are reviewed with the user.
|
|
833
|
+
const explicitLoadConfiguration=Object.keys(options).some(
|
|
834
|
+
key=>!['signal','security'].includes(key)
|
|
835
|
+
);
|
|
836
|
+
const loadMustResolve = explicitLoadConfiguration;
|
|
837
|
+
if (state === "ready" && !loadMustResolve) return this.status();
|
|
487
838
|
if (this.#loadPromise) {
|
|
488
|
-
if (!
|
|
839
|
+
if (!loadMustResolve) return this.#loadPromise;
|
|
489
840
|
try {
|
|
490
|
-
await load(
|
|
841
|
+
await load(loadOptions, {
|
|
491
842
|
protocol: ARCANE_AI_ADAPTER_PROTOCOL,
|
|
492
843
|
kind: "llm",
|
|
493
844
|
operation: "load",
|
|
494
845
|
signal,
|
|
495
|
-
|
|
496
|
-
}));
|
|
846
|
+
});
|
|
497
847
|
return this.status();
|
|
498
848
|
} catch (error) {
|
|
499
849
|
throw normalizeArcaneAIError(error, { operation: "load", signal });
|
|
@@ -527,14 +877,13 @@ export class ModelController {
|
|
|
527
877
|
}
|
|
528
878
|
async function executeModelLoad() {
|
|
529
879
|
try {
|
|
530
|
-
await load(
|
|
880
|
+
await load(loadOptions, {
|
|
531
881
|
protocol: ARCANE_AI_ADAPTER_PROTOCOL,
|
|
532
882
|
kind: "llm",
|
|
533
883
|
operation: "load",
|
|
534
884
|
signal,
|
|
535
|
-
security: controller.#security,
|
|
536
885
|
reportProgress: reportModelLoadProgress,
|
|
537
|
-
})
|
|
886
|
+
});
|
|
538
887
|
if (
|
|
539
888
|
operationGeneration !== controller.#operationGeneration
|
|
540
889
|
|| controller.#disposing
|
|
@@ -604,12 +953,12 @@ export class ModelController {
|
|
|
604
953
|
const inFlightLoad = this.#loadPromise;
|
|
605
954
|
const operationGeneration = ++this.#operationGeneration;
|
|
606
955
|
const operationId = `${this.#events.instanceId}:unload:${operationGeneration.toString(36)}`;
|
|
607
|
-
const context =
|
|
956
|
+
const context = {
|
|
608
957
|
protocol: ARCANE_AI_ADAPTER_PROTOCOL,
|
|
609
958
|
kind: "llm",
|
|
610
959
|
operation: "unload",
|
|
611
960
|
signal,
|
|
612
|
-
}
|
|
961
|
+
};
|
|
613
962
|
let resolveOperation;
|
|
614
963
|
let rejectOperation;
|
|
615
964
|
const operation = new Promise(function createModelUnloadOperation(resolve, reject) {
|
|
@@ -658,6 +1007,7 @@ export class ModelController {
|
|
|
658
1007
|
|
|
659
1008
|
async chat(request = {}) {
|
|
660
1009
|
this.#assertOperational();
|
|
1010
|
+
request=structuralRequest(request);
|
|
661
1011
|
const signal = request.signal ?? null;
|
|
662
1012
|
localRequirement(request, this.#provider);
|
|
663
1013
|
if (abortLike(null, signal)) {
|
|
@@ -667,12 +1017,14 @@ export class ModelController {
|
|
|
667
1017
|
const chat = providerMethod(this.#provider, "chat") ?? providerMethod(this.#provider, "use");
|
|
668
1018
|
if (!chat) throw new ArcaneAIError("ARCANE_AI_UNAVAILABLE", "The LLM provider cannot chat.");
|
|
669
1019
|
try {
|
|
670
|
-
|
|
1020
|
+
const response=await chat(request, {
|
|
671
1021
|
protocol: ARCANE_AI_ADAPTER_PROTOCOL,
|
|
672
1022
|
kind: "llm",
|
|
673
1023
|
operation: "chat",
|
|
674
1024
|
signal,
|
|
675
|
-
})
|
|
1025
|
+
});
|
|
1026
|
+
toolRecordFromCompletion(response);
|
|
1027
|
+
return response;
|
|
676
1028
|
} catch (error) {
|
|
677
1029
|
throw normalizeArcaneAIError(error, { operation: "request", signal });
|
|
678
1030
|
}
|
|
@@ -680,11 +1032,13 @@ export class ModelController {
|
|
|
680
1032
|
|
|
681
1033
|
stream(request = {}) {
|
|
682
1034
|
this.#assertOperational();
|
|
1035
|
+
request=structuralRequest(request);
|
|
683
1036
|
localRequirement(request, this.#provider);
|
|
684
1037
|
const controller = this;
|
|
685
1038
|
const externalSignal = request.signal ?? null;
|
|
686
1039
|
const linked = linkedAbortSignal(externalSignal);
|
|
687
1040
|
let opened = null;
|
|
1041
|
+
let openedIterator = null;
|
|
688
1042
|
let openError = null;
|
|
689
1043
|
let cancelPromise = null;
|
|
690
1044
|
|
|
@@ -701,20 +1055,56 @@ export class ModelController {
|
|
|
701
1055
|
if (!stream) throw new ArcaneAIError("ARCANE_AI_UNAVAILABLE", "The LLM provider cannot stream.");
|
|
702
1056
|
const value = await stream(
|
|
703
1057
|
{ ...request, signal: linked.controller.signal },
|
|
704
|
-
|
|
1058
|
+
{
|
|
705
1059
|
protocol: ARCANE_AI_ADAPTER_PROTOCOL,
|
|
706
1060
|
kind: "llm",
|
|
707
1061
|
operation: "stream",
|
|
708
1062
|
signal: linked.controller.signal,
|
|
709
|
-
}
|
|
1063
|
+
},
|
|
710
1064
|
);
|
|
711
|
-
if (
|
|
1065
|
+
if (
|
|
1066
|
+
!value
|
|
1067
|
+
||typeof value[Symbol.asyncIterator]!=="function"
|
|
1068
|
+
||typeof value.cancel!=="function"
|
|
1069
|
+
||!value.result
|
|
1070
|
+
||typeof value.result.then!=="function"
|
|
1071
|
+
) {
|
|
1072
|
+
if(typeof value?.cancel==="function"){
|
|
1073
|
+
Promise.resolve().then(()=>value.cancel(
|
|
1074
|
+
"The provider returned an invalid stream handle.",
|
|
1075
|
+
)).catch(function reportInvalidModelStreamCleanupFailure(error){
|
|
1076
|
+
console.error("Arcane invalid model stream cleanup failed.",error);
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
throw new ArcaneAIError(
|
|
1080
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
1081
|
+
"The LLM provider did not return an async stream handle with result and cancel().",
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
let iterator;
|
|
1085
|
+
try{
|
|
1086
|
+
iterator=value[Symbol.asyncIterator]();
|
|
1087
|
+
}catch(error){
|
|
1088
|
+
Promise.resolve().then(()=>value.cancel(error)).catch(
|
|
1089
|
+
function reportRejectedModelIteratorCleanupFailure(cleanupError){
|
|
1090
|
+
console.error("Arcane rejected model stream iterator cleanup failed.",cleanupError);
|
|
1091
|
+
},
|
|
1092
|
+
);
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1095
|
+
if(!iterator||typeof iterator.next!=="function"){
|
|
1096
|
+
Promise.resolve().then(()=>value.cancel(
|
|
1097
|
+
"The provider returned an invalid stream iterator.",
|
|
1098
|
+
)).catch(function reportInvalidModelIteratorCleanupFailure(error){
|
|
1099
|
+
console.error("Arcane invalid model stream iterator cleanup failed.",error);
|
|
1100
|
+
});
|
|
712
1101
|
throw new ArcaneAIError(
|
|
713
1102
|
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
714
|
-
"The LLM provider
|
|
1103
|
+
"The LLM provider stream iterator has no next() method.",
|
|
715
1104
|
);
|
|
716
1105
|
}
|
|
717
1106
|
opened = value;
|
|
1107
|
+
openedIterator=iterator;
|
|
718
1108
|
return value;
|
|
719
1109
|
})().catch((error) => {
|
|
720
1110
|
openError = normalizeArcaneAIError(error, {
|
|
@@ -725,7 +1115,10 @@ export class ModelController {
|
|
|
725
1115
|
});
|
|
726
1116
|
openPromise.catch(() => undefined);
|
|
727
1117
|
|
|
728
|
-
const result = openPromise.then((value) => value.result).then((value) =>
|
|
1118
|
+
const result = openPromise.then((value) => value.result).then((value) => {
|
|
1119
|
+
toolRecordFromCompletion(value);
|
|
1120
|
+
return value;
|
|
1121
|
+
}).finally(() => {
|
|
729
1122
|
linked.release();
|
|
730
1123
|
controller.#activeStreams.delete(handle);
|
|
731
1124
|
});
|
|
@@ -739,8 +1132,8 @@ export class ModelController {
|
|
|
739
1132
|
try {
|
|
740
1133
|
const value = opened ?? await openPromise;
|
|
741
1134
|
await value.cancel?.(reason);
|
|
742
|
-
} catch {
|
|
743
|
-
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
console.error("Arcane model provider cancellation failed.",error);
|
|
744
1137
|
}
|
|
745
1138
|
try {
|
|
746
1139
|
await result;
|
|
@@ -753,11 +1146,23 @@ export class ModelController {
|
|
|
753
1146
|
},
|
|
754
1147
|
async next(value) {
|
|
755
1148
|
if (openError) throw openError;
|
|
756
|
-
|
|
757
|
-
|
|
1149
|
+
if(!opened)await openPromise;
|
|
1150
|
+
const streamIterator=openedIterator;
|
|
1151
|
+
let nextValue=value;
|
|
1152
|
+
while(true){
|
|
1153
|
+
const next=await streamIterator.next(nextValue);
|
|
1154
|
+
nextValue=undefined;
|
|
1155
|
+
if(next.done)return {value:undefined,done:true};
|
|
1156
|
+
const projected=projectPublicStreamChunk(next.value);
|
|
1157
|
+
if(projected!==null)return {value:projected,done:false};
|
|
1158
|
+
}
|
|
758
1159
|
},
|
|
759
1160
|
async return(value) {
|
|
760
|
-
|
|
1161
|
+
Promise.resolve().then(()=>this.cancel(
|
|
1162
|
+
"The stream consumer stopped before completion.",
|
|
1163
|
+
)).catch(function reportModelStreamReturnCancellationFailure(error){
|
|
1164
|
+
console.error("Arcane model stream early-return cancellation failed.",error);
|
|
1165
|
+
});
|
|
761
1166
|
return { value, done: true };
|
|
762
1167
|
},
|
|
763
1168
|
async throw(error) {
|
|
@@ -768,7 +1173,6 @@ export class ModelController {
|
|
|
768
1173
|
return this;
|
|
769
1174
|
},
|
|
770
1175
|
};
|
|
771
|
-
Object.freeze(handle);
|
|
772
1176
|
this.#activeStreams.add(handle);
|
|
773
1177
|
return handle;
|
|
774
1178
|
}
|
|
@@ -777,13 +1181,14 @@ export class ModelController {
|
|
|
777
1181
|
this.#assertOperational();
|
|
778
1182
|
localRequirement(options, this.#provider);
|
|
779
1183
|
const id = requestIdentity(options.id);
|
|
780
|
-
const request = { ...options, id };
|
|
1184
|
+
const request = structuralRequest({ ...options, id });
|
|
781
1185
|
// localOnly admission is complete before app callbacks observe a request.
|
|
782
1186
|
fireAndForget(options.onRequest, request, id);
|
|
783
1187
|
const response = await this.chat(request);
|
|
784
1188
|
if (options.signal?.aborted) {
|
|
785
1189
|
throw normalizeArcaneAIError(null, { operation: "request", signal: options.signal });
|
|
786
1190
|
}
|
|
1191
|
+
toolRecordFromCompletion(response);
|
|
787
1192
|
if (typeof options.onResponse === "function") {
|
|
788
1193
|
await options.onResponse(response, id, false);
|
|
789
1194
|
}
|
|
@@ -794,11 +1199,10 @@ export class ModelController {
|
|
|
794
1199
|
this.#assertOperational();
|
|
795
1200
|
localRequirement(options, this.#provider);
|
|
796
1201
|
const id = requestIdentity(options.id);
|
|
797
|
-
const request = { ...options, id };
|
|
1202
|
+
const request = structuralRequest({ ...options, id });
|
|
798
1203
|
const displayId = displayRequestId(id);
|
|
799
1204
|
fireAndForget(options.onRequest, request, id);
|
|
800
1205
|
const handle = this.stream(request);
|
|
801
|
-
const announcedTools = new Set();
|
|
802
1206
|
|
|
803
1207
|
try {
|
|
804
1208
|
for await (const chunk of handle) {
|
|
@@ -806,17 +1210,10 @@ export class ModelController {
|
|
|
806
1210
|
for (const choice of chunk?.choices ?? []) {
|
|
807
1211
|
const delta = choice?.delta ?? {};
|
|
808
1212
|
if (typeof delta.reasoning_content === "string" && options.seeThinking === true) {
|
|
809
|
-
options.onChunk?.(delta.reasoning_content, displayId, true);
|
|
1213
|
+
await options.onChunk?.(delta.reasoning_content, displayId, true);
|
|
810
1214
|
}
|
|
811
1215
|
if (typeof delta.content === "string") {
|
|
812
|
-
options.onChunk?.(delta.content, displayId, false);
|
|
813
|
-
}
|
|
814
|
-
for (const tool of delta.tool_calls ?? []) {
|
|
815
|
-
const name = tool?.function?.name;
|
|
816
|
-
if (typeof name === "string" && name && !announcedTools.has(name)) {
|
|
817
|
-
announcedTools.add(name);
|
|
818
|
-
fireAndForget(options.onToolCall, name);
|
|
819
|
-
}
|
|
1216
|
+
await options.onChunk?.(delta.content, displayId, false);
|
|
820
1217
|
}
|
|
821
1218
|
}
|
|
822
1219
|
}
|
|
@@ -825,6 +1222,12 @@ export class ModelController {
|
|
|
825
1222
|
throw normalizeArcaneAIError(null, { operation: "request", signal: options.signal });
|
|
826
1223
|
}
|
|
827
1224
|
const tools = toolRecordFromCompletion(completion);
|
|
1225
|
+
if (typeof options.onResponse === "function") {
|
|
1226
|
+
await options.onResponse(completion, id, false);
|
|
1227
|
+
}
|
|
1228
|
+
for (const call of tools ?? []) {
|
|
1229
|
+
await options.onToolCall?.(call, displayId);
|
|
1230
|
+
}
|
|
828
1231
|
const output = tools ?? textFromCompletion(completion);
|
|
829
1232
|
if (typeof options.onComplete === "function") {
|
|
830
1233
|
await options.onComplete(output, displayId, false);
|