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.
Files changed (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. 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 SECURITY_KEYS = Object.freeze(["secure", "checks"]);
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
- if (value === undefined) return EMPTY_MODEL_SECURITY;
34
- const descriptors = closedSecurityRecord(value, SECURITY_KEYS, label);
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 (Object.hasOwn(scope, "secure")) secure = scope.secure;
75
- if (Object.hasOwn(scope.checks, "byteLength")) byteLength = scope.checks.byteLength;
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 Object.freeze({
79
- secure,
80
- checks: Object.freeze({
81
- byteLength: byteLength ?? secure,
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 = Object.freeze({
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 Object.freeze({
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, depth = 0) {
150
+ function copyProgressValue(value, state) {
223
151
  if (value === null || typeof value !== "object") return value;
224
- if (depth > MAX_PROGRESS_DEPTH || state.seen.has(value)) {
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
- state.entries += 1;
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, depth + 1),
170
+ value: copyProgressValue(descriptor.value, state),
244
171
  enumerable: descriptor.enumerable,
245
- configurable: false,
246
- writable: false,
172
+ configurable: true,
173
+ writable: true,
247
174
  });
248
175
  }
249
- return Object.freeze(copy);
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(), entries: 0 });
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
- if (!progress || typeof progress !== "object") return null;
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 Object.freeze({
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
- const content = completion?.choices?.[0]?.message?.content;
343
- return typeof content === "string" ? content : "";
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
- const result = {};
348
- let count = 0;
349
- for (const choice of completion?.choices ?? []) {
350
- for (const call of choice?.message?.tool_calls ?? []) {
351
- result[call.function.name] = call.function.arguments;
352
- count += 1;
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
- return count ? result : null;
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 Object.freeze({
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: Object.freeze({
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
- const explicitOperationSecurity = options.security !== undefined;
480
- const securityMustResolve = explicitOperationSecurity
481
- || (
482
- state === "ready"
483
- && !this.#readyPolicyResolved
484
- && hasModelSecurityOverrides(this.#security)
485
- );
486
- if (state === "ready" && !securityMustResolve) return this.status();
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 (!explicitOperationSecurity) return this.#loadPromise;
839
+ if (!loadMustResolve) return this.#loadPromise;
489
840
  try {
490
- await load(options, Object.freeze({
841
+ await load(loadOptions, {
491
842
  protocol: ARCANE_AI_ADAPTER_PROTOCOL,
492
843
  kind: "llm",
493
844
  operation: "load",
494
845
  signal,
495
- security: this.#security,
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(options, Object.freeze({
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 = Object.freeze({
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
- return await chat(request, Object.freeze({
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
- Object.freeze({
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 (!value || typeof value[Symbol.asyncIterator] !== "function") {
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 did not return an async stream handle.",
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) => value).finally(() => {
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
- // result exposes the normalized terminal error.
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
- const streamHandle = opened ?? await openPromise;
757
- return streamHandle.next(value);
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
- await this.cancel("The stream consumer stopped before completion.");
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);