arcane-os 0.13.3 → 0.15.0

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.
@@ -0,0 +1,364 @@
1
+ import Is from '../dependencies/strong-type/index.js';
2
+
3
+ const is = new Is(false);
4
+ const STRING_ESCAPES = {
5
+ '"': '"',
6
+ '\\': '\\',
7
+ '/': '/',
8
+ b: '\b',
9
+ f: '\f',
10
+ n: '\n',
11
+ r: '\r',
12
+ t: '\t'
13
+ };
14
+
15
+ function projectionError(message, cause) {
16
+ const error = new SyntaxError(
17
+ `Tool text projection ${message}`,
18
+ cause === undefined ? undefined : {cause}
19
+ );
20
+ error.code = 'ARCANE_AI_TOOL_TEXT_INVALID';
21
+ return error;
22
+ }
23
+
24
+ function isWhitespace(character) {
25
+ return character === ' ' || character === '\t'
26
+ || character === '\n' || character === '\r';
27
+ }
28
+
29
+ function createArgumentScanner(field, appendText, beginText, endText) {
30
+ const stack = [];
31
+ let started = false;
32
+ let finished = false;
33
+ let string = null;
34
+ let scalar = null;
35
+
36
+ function finishValue() {
37
+ stack[stack.length - 1].state = 'commaOrEnd';
38
+ }
39
+
40
+ function closeContainer() {
41
+ stack.pop();
42
+ if (stack.length) {
43
+ finishValue();
44
+ } else {
45
+ finished = true;
46
+ }
47
+ }
48
+
49
+ function acceptStringCharacter(character) {
50
+ if (string.key) {
51
+ if (stack.length === 1) string.value += character;
52
+ } else if (string.selected) {
53
+ appendText(character, string.offset);
54
+ string.offset += character.length;
55
+ }
56
+ }
57
+
58
+ function readStringCharacter(character) {
59
+ if (string.unicode !== null) {
60
+ if (!/[0-9a-fA-F]/.test(character)) {
61
+ throw projectionError('encountered an invalid Unicode escape.');
62
+ }
63
+ string.unicode += character;
64
+ if (string.unicode.length === 4) {
65
+ acceptStringCharacter(
66
+ String.fromCharCode(Number.parseInt(string.unicode, 16))
67
+ );
68
+ string.unicode = null;
69
+ }
70
+ return;
71
+ }
72
+ if (string.escaped) {
73
+ string.escaped = false;
74
+ if (character === 'u') {
75
+ string.unicode = '';
76
+ } else if (Object.hasOwn(STRING_ESCAPES, character)) {
77
+ acceptStringCharacter(STRING_ESCAPES[character]);
78
+ } else {
79
+ throw projectionError('encountered an invalid string escape.');
80
+ }
81
+ return;
82
+ }
83
+ if (character === '\\') {
84
+ string.escaped = true;
85
+ } else if (character === '"') {
86
+ const frame = stack[stack.length - 1];
87
+ if (string.key) {
88
+ frame.key = string.value;
89
+ frame.state = 'colon';
90
+ } else {
91
+ if (string.selected) endText(string.offset);
92
+ finishValue();
93
+ }
94
+ string = null;
95
+ } else {
96
+ if (character.charCodeAt(0) < 32) {
97
+ throw projectionError('encountered an unescaped control character.');
98
+ }
99
+ acceptStringCharacter(character);
100
+ }
101
+ }
102
+
103
+ function startString(key, selected = false) {
104
+ string = {key, selected, value: '', offset: 0, escaped: false, unicode: null};
105
+ if (selected) beginText();
106
+ }
107
+
108
+ function startValue(character, frame) {
109
+ const selected = stack.length === 1 && frame.kind === 'object'
110
+ && frame.key === field;
111
+ if (selected && character !== '"') {
112
+ beginText();
113
+ endText(0);
114
+ }
115
+ if (character === '"') {
116
+ startString(false, selected);
117
+ } else if (character === '{') {
118
+ stack.push(
119
+ {kind: 'object', state: 'keyOrEnd', key: ''}
120
+ );
121
+ } else if (character === '[') {
122
+ stack.push(
123
+ {kind: 'array', state: 'valueOrEnd'}
124
+ );
125
+ } else if (character === '-' || character >= '0' && character <= '9'
126
+ || character === 't' || character === 'f' || character === 'n') {
127
+ scalar = character;
128
+ } else {
129
+ throw projectionError('encountered an invalid argument value.');
130
+ }
131
+ }
132
+
133
+ function appendArguments(fragment) {
134
+ for (let position = 0; position < fragment.length; position += 1) {
135
+ const character = fragment[position];
136
+ if (string) {
137
+ readStringCharacter(character);
138
+ continue;
139
+ }
140
+ if (scalar !== null) {
141
+ if (!isWhitespace(character) && character !== ','
142
+ && character !== '}' && character !== ']') {
143
+ scalar += character;
144
+ continue;
145
+ }
146
+ try {
147
+ JSON.parse(scalar);
148
+ } catch (cause) {
149
+ throw projectionError('encountered an invalid argument value.', cause);
150
+ }
151
+ scalar = null;
152
+ finishValue();
153
+ }
154
+ if (isWhitespace(character)) continue;
155
+ if (finished) {
156
+ throw projectionError('encountered content after the argument object.');
157
+ }
158
+ if (!started) {
159
+ if (character !== '{') {
160
+ throw projectionError('requires a root argument object.');
161
+ }
162
+ started = true;
163
+ stack.push(
164
+ {kind: 'object', state: 'keyOrEnd', key: ''}
165
+ );
166
+ continue;
167
+ }
168
+ const frame = stack[stack.length - 1];
169
+ if (frame.state === 'keyOrEnd' || frame.state === 'key') {
170
+ if (character === '"') {
171
+ startString(true);
172
+ } else if (character === '}' && frame.state === 'keyOrEnd') {
173
+ closeContainer();
174
+ } else {
175
+ throw projectionError('encountered an invalid object key.');
176
+ }
177
+ } else if (frame.state === 'colon') {
178
+ if (character !== ':') {
179
+ throw projectionError('expected a colon after an object key.');
180
+ }
181
+ frame.state = 'value';
182
+ } else if (frame.state === 'value' || frame.state === 'valueOrEnd') {
183
+ if (character === ']' && frame.state === 'valueOrEnd') {
184
+ closeContainer();
185
+ } else {
186
+ startValue(character, frame);
187
+ }
188
+ } else if (character === ',') {
189
+ frame.state = frame.kind === 'object' ? 'key' : 'value';
190
+ } else if (character === (frame.kind === 'object' ? '}' : ']')) {
191
+ closeContainer();
192
+ } else {
193
+ throw projectionError('expected a comma or the end of an argument container.');
194
+ }
195
+ }
196
+ }
197
+
198
+ return appendArguments;
199
+ }
200
+
201
+ export function createToolTextObserver(selection, onText, {signal} = {}) {
202
+ if (selection === undefined || selection === null || selection === false) return null;
203
+ if (!is.object(selection) || is.array(selection)
204
+ || !is.string(selection.name) || !selection.name.trim()
205
+ || !is.string(selection.field) || !selection.field.trim()) {
206
+ throw new TypeError('AI toolText must name a tool and a root string field.');
207
+ }
208
+ if (!is.function(onText)) {
209
+ throw new TypeError('AI onToolText must be a function when toolText is selected.');
210
+ }
211
+ const name = selection.name;
212
+ const field = selection.field;
213
+ const choices = new Map();
214
+
215
+ function recordFor(choiceIndex, index) {
216
+ let calls = choices.get(choiceIndex);
217
+ if (!calls) {
218
+ calls = new Map();
219
+ choices.set(choiceIndex, calls);
220
+ }
221
+ let record = calls.get(index);
222
+ if (!record) {
223
+ record = {
224
+ id: '', name: '', index, choiceIndex, pending: [], scanner: null,
225
+ text: '', emitted: 0, textOpen: false
226
+ };
227
+ calls.set(index, record);
228
+ }
229
+ return record;
230
+ }
231
+
232
+ function prepareScanner(record) {
233
+ if (!record.scanner) {
234
+ record.scanner = createArgumentScanner(
235
+ field,
236
+ function appendSelectedToolText(text, offset) {
237
+ for (let index = 0; index < text.length; index += 1) {
238
+ const position = offset + index;
239
+ if (position < record.text.length) {
240
+ if (record.text[position] !== text[index]) {
241
+ throw projectionError('received conflicting selected argument text.');
242
+ }
243
+ } else {
244
+ record.text += text[index];
245
+ }
246
+ }
247
+ },
248
+ function beginSelectedToolText() {
249
+ if (record.emitted === 0) record.text = '';
250
+ record.textOpen = true;
251
+ },
252
+ function finishSelectedToolText(length) {
253
+ if (length < record.text.length) {
254
+ throw projectionError('received a shorter replacement for selected argument text.');
255
+ }
256
+ record.textOpen = false;
257
+ }
258
+ );
259
+ }
260
+ for (const fragment of record.pending) record.scanner(fragment);
261
+ record.pending = [];
262
+ }
263
+
264
+ function observeCompleteArguments(record, argumentsValue) {
265
+ let argumentsObject = argumentsValue;
266
+ if (is.string(argumentsObject)) {
267
+ try {
268
+ argumentsObject = JSON.parse(argumentsObject);
269
+ } catch (cause) {
270
+ throw projectionError('received invalid complete argument JSON.', cause);
271
+ }
272
+ }
273
+ if (!argumentsObject || !is.object(argumentsObject) || is.array(argumentsObject)) {
274
+ throw projectionError('requires a complete argument object.');
275
+ }
276
+ if (!Object.hasOwn(argumentsObject, field)) {
277
+ if (record.emitted > 0) {
278
+ throw projectionError('lost the selected argument field in a complete response.');
279
+ }
280
+ record.text = '';
281
+ record.textOpen = false;
282
+ return;
283
+ }
284
+ const text = argumentsObject[field];
285
+ if (!is.string(text)) {
286
+ throw projectionError('requires the selected argument field to be a string.');
287
+ }
288
+ // A terminal snapshot may repeat a complete value already streamed.
289
+ if (record.emitted > 0 && !text.startsWith(record.text)) {
290
+ throw projectionError('received conflicting complete argument text.');
291
+ }
292
+ record.text = text;
293
+ record.textOpen = false;
294
+ }
295
+
296
+ async function emitSelectedText(record) {
297
+ if (signal?.aborted || record.name !== name || !record.id) return;
298
+ let end = record.text.length;
299
+ if (record.textOpen && end > record.emitted) {
300
+ // Keep a split surrogate pair together without changing its code units.
301
+ const last = record.text.charCodeAt(end - 1);
302
+ if (last >= 0xD800 && last <= 0xDBFF) end -= 1;
303
+ }
304
+ if (end <= record.emitted) return;
305
+ const text = record.text.slice(record.emitted, end);
306
+ record.emitted = end;
307
+ await onText(
308
+ text,
309
+ {id: record.id, name: record.name, field, index: record.index, choiceIndex: record.choiceIndex}
310
+ );
311
+ }
312
+
313
+ async function observeCalls(calls, choiceIndex, complete) {
314
+ if (!is.array(calls)) return;
315
+ for (let position = 0; position < calls.length; position += 1) {
316
+ if (signal?.aborted) return;
317
+ const call = calls[position];
318
+ if (!call || !is.object(call)) continue;
319
+ const record = recordFor(choiceIndex, call.index ?? position);
320
+ if (is.string(call.id) && call.id) {
321
+ if (record.emitted && record.id !== call.id) {
322
+ throw projectionError('changed the identity of a displayed tool call.');
323
+ }
324
+ record.id = call.id;
325
+ }
326
+ const functionValue = call.function && is.object(call.function)
327
+ ? call.function
328
+ : {};
329
+ if (is.string(functionValue.name)) {
330
+ const nextName = complete ? functionValue.name : record.name + functionValue.name;
331
+ if (record.emitted && nextName !== record.name) {
332
+ throw projectionError('changed the name of a displayed tool call.');
333
+ }
334
+ record.name = nextName;
335
+ }
336
+ if (!name.startsWith(record.name)) {
337
+ record.pending = [];
338
+ continue;
339
+ }
340
+ if (!complete && is.string(functionValue.arguments)) {
341
+ record.pending.push(functionValue.arguments);
342
+ }
343
+ if (record.name !== name) continue;
344
+ prepareScanner(record);
345
+ if (complete) observeCompleteArguments(record, functionValue.arguments);
346
+ await emitSelectedText(record);
347
+ }
348
+ }
349
+
350
+ return async function observeToolText(chunk) {
351
+ if (signal?.aborted || !chunk || !is.object(chunk)) return;
352
+ if (is.array(chunk.choices)) {
353
+ for (let position = 0; position < chunk.choices.length; position += 1) {
354
+ if (signal?.aborted) return;
355
+ const choice = chunk.choices[position];
356
+ if (!choice || !is.object(choice)) continue;
357
+ const choiceIndex = choice.index ?? position;
358
+ await observeCalls(choice.delta?.tool_calls, choiceIndex, false);
359
+ await observeCalls(choice.message?.tool_calls, choiceIndex, true);
360
+ }
361
+ }
362
+ if (!signal?.aborted) await observeCalls(chunk.message?.tool_calls, 0, true);
363
+ };
364
+ }
@@ -136,8 +136,8 @@ selected source route and the last successful check when evaluating freshness.
136
136
 
137
137
  The shared dev server uses RIAEvangelist's `node-http-server` public interface
138
138
  for HTTPS and conditional responses on those selected routes. The SDK
139
- owns source selection and generated representations. Every Arcane development
140
- server and packaged browser preview serves content on HTTPS and redirects its
139
+ owns source selection and generated representations. By default, source development
140
+ and every packaged browser preview serve content on HTTPS and redirect their
141
141
  paired HTTP listener with status 308 through the public request hook.
142
142
  `arcane dev --public` selects the IPv4 wildcard address;
143
143
  explicit `--host` controls the bind address. CLI startup reads one workspace-local
@@ -151,6 +151,14 @@ redirect listener, defaulting to an OS-assigned port. Existing raw `tls` inputs
151
151
  retain their native HTTPS transport under the SDK, as described by the module's
152
152
  advanced TLS extension guidance; all content uses its public serving methods.
153
153
 
154
+ Explicit `arcane dev --http` or source API `http:true` selects the module's
155
+ single HTTP listener on `port`. It skips certificate resolution and serves the
156
+ same selected source mappings, generated PWA routes, and conditional responses.
157
+ No HTTPS listener or redirect is started in this mode. Listener readiness,
158
+ failure, cancellation and shutdown retain the same operation owner; reported
159
+ URLs use the actual HTTP protocol and bound port. Browser secure-context
160
+ requirements remain browser-owned. Packaged previews retain HTTPS.
161
+
154
162
  Development is an intentionally fast feedback loop. Keep each increment small
155
163
  and independently understandable so its effect has one clear cause and a
156
164
  mistake can be isolated without untangling unrelated work. A development
@@ -320,6 +320,68 @@ Ordinary iteration exposes only text/reasoning chunks; raw structural deltas
320
320
  remain internal until the complete terminal result validates. Explicit
321
321
  `onResponse` or inspection consumers retain the complete raw terminal response.
322
322
 
323
+ ## Stream selected tool text
324
+
325
+ Both `AI.streamRequest()` and this browser controller's `streamRequest()` accept
326
+ `toolText:{name,field}` with `onToolText(text,call,displayId)`. Select the exact
327
+ tool name and one root-level string argument. The callback receives newly
328
+ decoded text in arrival order, preserving whitespace and JSON string escapes as
329
+ their original characters. Its `call` record is
330
+ `{id,name,field,index,choiceIndex}`: the actual normalized tool-call ID, matching
331
+ tool name, selected field, call index within the response choice, and choice
332
+ index. Delivery waits until the matching name and ID are known. `displayId` is
333
+ the request's existing `M-${id}` display ID.
334
+
335
+ This callback is distinct from ordinary `onChunk`; display the text in the
336
+ corresponding tool turn and keep ordinary prose in its own existing turn.
337
+ `onToolCall` still receives only the complete normalized call after terminal
338
+ settlement. The text callback does not execute a tool, rewrite arguments, or
339
+ retain a transcript. The application owns the completed call's execution and
340
+ stores its complete visible result once through its normal history path.
341
+ If a provider supplies arguments only at completion, the callback runs only
342
+ when that actual complete response arrives. Repeated terminal snapshots do not
343
+ replay text already emitted. Callback errors reach the request owner, and a
344
+ cancelled or superseded request stops further text delivery.
345
+
346
+ For example, an application that already supplies a closing-report tool can
347
+ connect its existing display operations without parsing provider protocol:
348
+
349
+ ```javascript
350
+ import {
351
+ formatConversationClosingReportText
352
+ } from '/arcane/modules/ConversationClosingReport.js';
353
+
354
+ async function streamClosingReport(ai, messages, closingTool, view, signal) {
355
+ return ai.streamRequest(
356
+ {
357
+ messages,
358
+ tools:[closingTool],
359
+ signal,
360
+ toolText:{name:closingTool.function.name, field:'final_message'},
361
+ onChunk:function appendOrdinaryText(text, displayId, thinking) {
362
+ return view.appendAssistantText(text, displayId, thinking);
363
+ },
364
+ onToolText:function appendClosingText(text, call, displayId) {
365
+ return view.appendToolText(
366
+ formatConversationClosingReportText(text),
367
+ call.id,
368
+ displayId
369
+ );
370
+ }
371
+ }
372
+ );
373
+ }
374
+ ```
375
+
376
+ The application supplies `view` and the existing `closingTool`; those are not
377
+ SDK exports. The formatter applies the same existing `&`, `<`, and `>` escaping
378
+ used for the complete closing report, including whitespace-only chunks. The
379
+ returned terminal call remains available to the application's normal tool
380
+ execution and persistence path. These options do not change the tool schema,
381
+ tool choice, or authored request content. Omitting `toolText` creates no text
382
+ observer. Invalid selection or callback input throws `TypeError`; malformed
383
+ selected argument text reports `ARCANE_AI_TOOL_TEXT_INVALID`.
384
+
323
385
  ## Errors and unavailable states
324
386
 
325
387
  Invalid configuration can throw `TypeError` or `RangeError`. Operational
@@ -50,8 +50,9 @@ meaning and cardinality rules:
50
50
  | `--arcane-root` | directory | `doctor`, native `build`/`run`, `native-doctor`, `native-prepare` |
51
51
  | `--host` / `--port` | host / integer 0–65535 | Browser `dev`/`run` default to HTTPS at `127.0.0.1:8000`; `mail serve` defaults to HTTP at `127.0.0.1:8025` and admits numeric loopback only. |
52
52
  | `--http-port` | integer 0–65535 | Browser `dev`/`run` HTTP redirect listener; defaults to `0`, which selects an available port. |
53
- | `--public` | flag | `dev`; serves HTTPS and binds to `0.0.0.0` unless `--host` explicitly selects another address. |
54
- | `--https` | flag | Browser `dev`/`run`; retained explicitly, while HTTPS is always enabled. |
53
+ | `--public` | flag | `dev`; binds to `0.0.0.0` unless `--host` explicitly selects another address. |
54
+ | `--http` | flag | `dev` only; serves source and PWA routes on one HTTP listener selected by `--port`, without TLS. |
55
+ | `--https` | flag | Browser `dev`/`run`; explicitly selects the default HTTPS transport. |
55
56
  | `--cert` / `--key` | PEM file paths | Browser `dev`/`run`; supply both for an explicit certificate chain and private key. Relative paths resolve from the workspace. |
56
57
  | `--target` | target id | `new`, `init`, native diagnostics, `build`, `run` |
57
58
  | `--format` / `--signing` | target-supported values | Native diagnostics, `build`, `run` |
@@ -313,7 +314,7 @@ npm exec -- arcane upgrade --workspace . --app hello-world
313
314
 
314
315
  Starts one development server for one selected app and maps the exact
315
316
  workspace/runtime routes. It defaults to HTTPS on localhost; `--public` enables access
316
- from other devices on the network over HTTPS.
317
+ from other devices on the network, using HTTPS by default.
317
318
 
318
319
  For an external workspace, the server exposes the selected projected
319
320
  `arcane/` root, including `arcane/sdk` and `arcane/dependencies`, alongside the
@@ -322,7 +323,7 @@ The explicit live-source SDK mapping remains unchanged and does not replace the
322
323
  installed projection.
323
324
 
324
325
  ```text
325
- arcane dev [--app <id>] [--public] [--https] [--cert <file> --key <file>] [--host <address>] [--port 8000] [--http-port 0]
326
+ arcane dev [--app <id>] [--public] [--http | --https] [--cert <file> --key <file>] [--host <address>] [--port 8000] [--http-port 0]
326
327
  ```
327
328
 
328
329
  ### Lifecycle
@@ -344,7 +345,7 @@ device, since `localhost` refers to that device and `0.0.0.0` is a bind address.
344
345
  Network URLs come from one interface snapshot at startup and do not establish
345
346
  remote reachability through the machine's firewall or network.
346
347
 
347
- Every Arcane app uses HTTPS for development and packaged browser previews.
348
+ HTTPS is the default for development and required for packaged browser previews.
348
349
  `--https` remains accepted but is no longer needed to select the transport.
349
350
  `--port` selects the HTTPS application port. A second HTTP listener returns
350
351
  `308` redirects to that HTTPS port, preserving the requested path and query.
@@ -357,9 +358,41 @@ the redirect endpoint.
357
358
  Supplying both `--cert` and `--key` selects an explicit PEM pair. The command
358
359
  does not configure a firewall, router forwarding, or an internet tunnel.
359
360
 
361
+ ### Explicit HTTP development
362
+
363
+ `--http` selects source development over HTTP. Combine it with `--public` or
364
+ `--host` to use a LAN address, and choose the content listener with `--port`:
365
+
366
+ ```bash
367
+ npm run dev -- --app hello-world --public --http --port 8000
368
+ ```
369
+
370
+ The command starts one HTTP listener through `node-http-server`, skips all TLS
371
+ file reads, and serves the same selected source files, generated manifest,
372
+ service worker and offline inventory. PWA configuration and caching are
373
+ unchanged. Startup prints the actual HTTP local and network URLs. Structured
374
+ results contain `protocol:'http:'`; `httpPort`, `httpOrigin`, and `httpUrl`
375
+ identify that content listener and equal `port`, `origin`, and `url`.
376
+ There is no separate redirect endpoint. Cancellation and listener failure
377
+ close the owned HTTP listener and settle the same lifecycle.
378
+
379
+ `--http` is supported only by `dev`. Combining it with `--https`, `--cert`,
380
+ `--key`, or `--http-port` is a usage error. Omitting it preserves HTTPS;
381
+ certificate errors never select HTTP automatically.
382
+
383
+ A LAN HTTP origin does not receive the browser's localhost secure-context
384
+ exception. For Chrome development, Chromium documents
385
+ `chrome://flags/#unsafely-treat-insecure-origin-as-secure` with the exact HTTP
386
+ origin, such as `http://192.0.2.10:8000`; see
387
+ [Chromium's development guidance](https://www.chromium.org/Home/chromium-security/deprecating-powerful-features-on-insecure-origins/).
388
+ The developer owns that browser setting. The SDK does not change it, alter
389
+ certificate validation, or claim a PWA is installable merely because its server
390
+ started. HTTP and HTTPS are distinct origins with separate browser storage and
391
+ registrations; existing HTTPS data is preserved.
392
+
360
393
  ### Development HTTPS setup
361
394
 
362
- Before starting `arcane dev` or a packaged browser preview, place the server's
395
+ Before starting HTTPS `arcane dev` or a packaged browser preview, place the server's
363
396
  PEM certificate chain at `.arcane/dev/server-cert.pem` and its PEM private key at
364
397
  `.arcane/dev/server-key.pem`, relative to the workspace. Alternatively, pass
365
398
  `--cert <file> --key <file>` together. The certificate must cover localhost or the LAN IP
@@ -6,7 +6,7 @@
6
6
  "minimumVersion": "22.23.2 for Node entrypoints",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 208,
9
+ "memberCount": 209,
10
10
  "members": [
11
11
  {
12
12
  "id": "root:APP_BUNDLE_DESCRIPTOR_NAME",
@@ -2902,7 +2902,23 @@
2902
2902
  "summary": "Creates the provider-neutral browser AI API module and LLM lifecycle controller around one compatible provider or controller.",
2903
2903
  "availability": "Browser; the selected provider observes its own WebAssembly, storage, and model readiness",
2904
2904
  "protocol": "arcane-ai-adapter/1",
2905
- "normalization": "Normalizes lazy or manual load, mutable complete lifecycle status, cancellation, complete all-choice nonstructural stream projection, complete provider data callbacks, single-choice text or multi-choice terminal output, required structural arguments.message, and atomic exact matching nonblank all-ID tool-result sequencing without executing tools; an existing ModelController retains its preconfigured load policy and rejects a new security option"
2905
+ "normalization": "Normalizes lazy or manual load, mutable complete lifecycle status, cancellation, complete all-choice nonstructural stream projection, provider chunks with private structural fields removed, complete terminal data callbacks, optional toolText:{name,field} and distinct onToolText(text,call,displayId) delivery of selected decoded argument text, single-choice text or multi-choice terminal output, required structural arguments.message, and atomic exact matching nonblank all-ID tool-result sequencing without executing tools; an existing ModelController retains its preconfigured load policy and rejects a new security option"
2906
+ },
2907
+ {
2908
+ "id": "tool-text-stream:createToolTextObserver",
2909
+ "name": "createToolTextObserver",
2910
+ "displayName": "createToolTextObserver()",
2911
+ "kind": "function",
2912
+ "signature": "createToolTextObserver(selection, onText, {signal}={})",
2913
+ "entrypoints": [
2914
+ "arcane-os/ai/tool-text-stream"
2915
+ ],
2916
+ "primaryImport": "arcane-os/ai/tool-text-stream",
2917
+ "group": "AI provider integration",
2918
+ "summary": "Observes one selected root string argument from actual tool-call chunks before provider structural filtering.",
2919
+ "availability": "Compatible JavaScript module host; no browser capability required",
2920
+ "protocol": "OpenAI-compatible tool-call deltas and complete message snapshots",
2921
+ "normalization": "Returns null for absent selection or an async chunk observer; preserves decoded text and whitespace, emits actual normalized call identity metadata, observes each raw delta once, avoids replay from complete snapshots, awaits callbacks, and stops later observation after cancellation without altering payloads, executing tools, or persisting history"
2906
2922
  },
2907
2923
  {
2908
2924
  "id": "browser-wasm:createBrowserModelSource",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "artifactCount": 85,
12
12
  "javascriptArtifactCount": 83,
13
- "esmExportCount": 352,
13
+ "esmExportCount": 353,
14
14
  "artifacts": [
15
15
  {
16
16
  "file": "runtime/arcane/modules/AI.js",
@@ -30,7 +30,7 @@
30
30
  "availability": "Browser + native bridge + TWiN Cloud",
31
31
  "protocol": "arcane-ai-browser-speech-configuration/1, AIProviderRuntime arcane-ai-provider/2 routes, globalThis.arcaneEvents, TWiN Cloud HTTPS, Arcane.ollama, Arcane.speech, Android WebView bridge",
32
32
  "normalization": "Complete mutable caller-owned browser speech authority, SDK-owned provider registration/replacement/disposal, explicit STT/TTS activation, normalized Whisper STT and Kokoro TTS execution with default capacities 1 and 4 respectively, explicit provider-neutral execution snapshots through status(role,{execution:true}), sticky readiness, canonical window.user readiness without compatibility-event object-identity admission or polling, exact ordered structural calls with required arguments.message, atomic nonblank all-ID tool-result sequencing, complete all-choice streaming/data validation, native Ollama structural adaptation, complete-response then all validated tool callbacks then completion ordering, observed async callbacks, Blob/File transcription, automatic repeated-formatting-mark removal from cloned outbound TTS input while caller and visible content stays exact, immediate exact-segment synthesis, per-call voice/speed capture, optional final-segment pause on the existing audio clock, optional per-call terminal playback completion, detached preparation with complete semantic DBOPFS audio reuse and same-owner pending request sharing, independent prepared playback without a model load for cached audio, ordered audio-clock scheduling, playable audio, active-generation TTS operation-failure routing, mute, and cancellation are normalized; provider/model/runtime/voice policy remains caller-owned. Both speech roles default execution.device to auto: webnn-npu when navigator.ml.createContext is exposed, then webgpu when navigator.gpu is exposed, then wasm for CPU execution. Failed backend loads clean up the pool before trying fresh Workers with the same prepared model and dtype; explicit devices never fall back. STT capacity only accepts 1; TTS accepts 1 through 4. Execution snapshots expose requestedDevice, selectedDevice, maxConcurrentRequests, and activeRequestCount for both roles; selectedDevice is null while unloaded and names the successful upstream session backend request when loaded. It does not prove every operation used the physical NPU: WebNN unsupported operations may use WASM, and compatibility depends on the browser, driver, hardware, and graph.",
33
- "surface": "Browser-speech protocol/event/error/reason constants; default `AI`; read-only `providerRuntime`, `browserSpeechConfiguration`, and `browserSpeechDescriptor`; explicit `providerRuntime.status(role,{execution:true})` snapshots; `streamTTS(text,end,options={})` with optional voice, speed, pauseAfterMs, and waitForPlayback; automatic speech-input cleanup with SDK-internal preparation metadata; `prepareTTS({parts,storage,identity,signal,onState})` returning ordered segments, state, ready, getAudio(index), and cancel(); `playPreparedTTS(prepared,{signal,onState})` returning state, error, finished, pause(), resume(), and stop() with one playback lane per AI and observational state/error callbacks; configure/dispose speech, route lifecycle, declaration-validated chat/stream requests with exact ordered calls, synthesis/transcription, and playback controls; initializes from current canonical user readiness, installs `window.ai`, and projects `ai-ready` plus active-generation `ai-tts-failure`."
33
+ "surface": "Browser-speech protocol/event/error/reason constants; default `AI`; read-only `providerRuntime`, `browserSpeechConfiguration`, and `browserSpeechDescriptor`; explicit `providerRuntime.status(role,{execution:true})` snapshots; `streamTTS(text,end,options={})` with optional voice, speed, pauseAfterMs, and waitForPlayback; automatic speech-input cleanup with SDK-internal preparation metadata; `prepareTTS({parts,storage,identity,signal,onState})` returning ordered segments, state, ready, getAudio(index), and cancel(); `playPreparedTTS(prepared,{signal,onState})` returning state, error, finished, pause(), resume(), and stop() with one playback lane per AI and observational state/error callbacks; configure/dispose speech, route lifecycle, declaration-validated chat/stream requests with exact ordered calls; optional streamRequest toolText:{name,field} and onToolText(text,call,displayId) for decoded selected argument text with actual call identity, distinct from ordinary prose and terminal tool execution; synthesis/transcription and playback controls; initializes from current canonical user readiness, installs `window.ai`, and projects `ai-ready` plus active-generation `ai-tts-failure`."
34
34
  },
35
35
  {
36
36
  "file": "runtime/arcane/modules/AIPreferenceRuntime.js",
@@ -456,13 +456,14 @@
456
456
  "conversationClosingReportInstruction",
457
457
  "createConversationClosingReportTool",
458
458
  "formatConversationClosingReport",
459
+ "formatConversationClosingReportText",
459
460
  "normalizeConversationClosingReport"
460
461
  ],
461
462
  "summary": "Defines the closing-report tool, instruction, result normalizer, call classifier, and formatter.",
462
463
  "availability": "Cross-host",
463
464
  "protocol": "In-process only",
464
465
  "normalization": "Required user-facing message remains distinct from the complete final_message; normalized results expose message, finalMessage, and rememberedActions.",
465
- "surface": "Six constants/helpers for closing reports; the sole-call schema requires message and final_message."
466
+ "surface": "Seven constants/helpers for closing reports; the sole-call schema requires message and final_message. formatConversationClosingReportText(value) exposes the existing &, <, and > escaping for any string, including whitespace-only chunks, and the complete report formatter reuses it."
466
467
  },
467
468
  {
468
469
  "file": "runtime/arcane/modules/ConversationTimebox.js",
@@ -112,9 +112,11 @@ For an enabled application, `arcane dev` serves the generated PWA files at the
112
112
  origin root and starts at the selected app page under `/apps/<id>/`. Use one
113
113
  selected app per development origin. The SDK uses `node-http-server` for source
114
114
  and packaged-preview serving, including conditional resource responses.
115
- Every Arcane development server and packaged browser preview serves HTTPS,
116
- including localhost. Configure the workspace certificate pair before starting
117
- the ordinary command; see [development HTTPS setup](cli.md#development-https-setup).
115
+ Arcane development servers default to HTTPS, including localhost, and packaged
116
+ browser previews require HTTPS. Configure the workspace certificate pair before
117
+ starting the ordinary command; see [development HTTPS setup](cli.md#development-https-setup).
118
+ Explicit source-only `arcane dev --http` serves the same generated PWA routes
119
+ without loading certificates; see [explicit HTTP development](cli.md#explicit-http-development).
118
120
  `--public` selects the IPv4 wildcard bind address; it does not enable PWA
119
121
  configuration, change manifest metadata, or determine browser installability.
120
122
 
@@ -272,8 +274,11 @@ Keep actual icon dimensions in `sizes`. Browser diagnostics about missing
272
274
  and are separate from a usable installation icon.
273
275
 
274
276
  Browser installation requires HTTPS or the browser's localhost/loopback
275
- exception. A device-facing LAN address is not loopback. Arcane's development
276
- server still follows its own HTTPS serving contract above. Browser engagement,
277
+ exception. A device-facing LAN address is not loopback. Selecting HTTP serving
278
+ does not make that LAN origin a secure context. Chromium documents a separate
279
+ [explicit developer origin setting](https://www.chromium.org/Home/chromium-security/deprecating-powerful-features-on-insecure-origins/);
280
+ the SDK does not configure that setting or claim that starting the server proves
281
+ installation eligibility. Browser engagement,
277
282
  installation state and platform support also affect whether native promotion
278
283
  appears; worker cache readiness is not an installation UI prerequisite. See
279
284
  [browser installation requirements](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable).