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.
- package/CHANGELOG.md +21 -0
- package/README.md +13 -2
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +79 -14
- package/browser-runtime/ai/model-controller.mjs +32 -4
- package/browser-runtime/ai/tool-text-stream.mjs +364 -0
- package/docs/architecture.md +10 -2
- package/docs/reference/ai/browser-wasm.md +62 -0
- package/docs/reference/cli.md +39 -6
- package/docs/reference/inventory/package-api.json +18 -2
- package/docs/reference/inventory/runtime-modules.json +4 -3
- package/docs/reference/pwa.md +10 -5
- package/docs/reference/runtime-modules.md +36 -8
- package/docs/reference/sdk-api.md +104 -23
- package/package.json +2 -1
- package/runtime/arcane/modules/AI.js +55 -7
- package/runtime/arcane/modules/AIProviderRuntime.js +24 -7
- package/runtime/arcane/modules/ConversationClosingReport.js +2 -2
- package/src/cli/main.mjs +15 -5
- package/src/dev-server.mjs +33 -18
- package/src/import-map.mjs +1 -0
- package/src/toolchain.mjs +1 -0
|
@@ -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
|
+
}
|
package/docs/architecture.md
CHANGED
|
@@ -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.
|
|
140
|
-
|
|
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
|
package/docs/reference/cli.md
CHANGED
|
@@ -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`;
|
|
54
|
-
| `--
|
|
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
|
|
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
|
-
|
|
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":
|
|
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
|
|
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":
|
|
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
|
|
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": "
|
|
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",
|
package/docs/reference/pwa.md
CHANGED
|
@@ -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
|
-
|
|
116
|
-
|
|
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.
|
|
276
|
-
|
|
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).
|