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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
- Add explicit `arcane dev --http` and source API `http:true` for HTTP
|
|
6
|
+
development, including LAN device use. Reuse `node-http-server` and the same
|
|
7
|
+
application, PWA, source-mapping and conditional-response routes without
|
|
8
|
+
certificate setup. Report the actual HTTP endpoint and own one listener's
|
|
9
|
+
readiness, cancellation, errors and shutdown.
|
|
10
|
+
- Preserve HTTPS and its HTTP `308` redirect as the default; packaged browser
|
|
11
|
+
previews continue using HTTPS. Browser settings, certificate validation,
|
|
12
|
+
application content and caching remain unchanged.
|
|
13
|
+
|
|
14
|
+
## 0.14.0
|
|
15
|
+
|
|
16
|
+
- Add opt-in `toolText: {name, field}` and `onToolText(text, call, displayId)`
|
|
17
|
+
to model streaming requests. Decode the selected root string field as tool
|
|
18
|
+
arguments arrive, separately from ordinary text and final tool execution.
|
|
19
|
+
Preserve actual call identity, whitespace, ordered delivery, and cancellation
|
|
20
|
+
across HTTP, native, and browser provider routes.
|
|
21
|
+
- Export `formatConversationClosingReportText(value)` so streamed closing text
|
|
22
|
+
uses the same existing formatting as the complete report.
|
|
23
|
+
|
|
3
24
|
## 0.13.3
|
|
4
25
|
|
|
5
26
|
- Skip automatic TTS finalization calls when a model stream completes while
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ each device. Keep these local files ignored by Git. The same certificate pair
|
|
|
73
73
|
works for any selected app in that workspace. The command reports missing TLS
|
|
74
74
|
files instead of starting an HTTP listener.
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
HTTPS is the default for development and required for packaged browser previews.
|
|
77
77
|
Plain `npm run dev` binds to localhost with the same workspace certificate pair.
|
|
78
78
|
An explicit `--host` overrides the bind address, and `--port` selects the HTTPS port.
|
|
79
79
|
The paired HTTP listener returns 308 redirects and uses an OS-assigned port
|
|
@@ -84,6 +84,17 @@ for public mode. Network reachability depends on the machine's firewall and
|
|
|
84
84
|
network. See the [development HTTPS setup](docs/reference/cli.md#development-https-setup)
|
|
85
85
|
for device trust and DBOPFS's secure-context requirement.
|
|
86
86
|
|
|
87
|
+
For explicit HTTP source development, including LAN device testing, use:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm run dev -- --app hello-speech --public --http --port 8000
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This selects one HTTP content listener and prints HTTP network URLs. It reads
|
|
94
|
+
no certificate pair and serves the same application and generated PWA routes.
|
|
95
|
+
PWA and storage availability still depend on the browser's secure-context
|
|
96
|
+
rules; see [HTTP development](docs/reference/cli.md#explicit-http-development).
|
|
97
|
+
|
|
87
98
|
Open the URL printed by the server. The generated page owns its import map and
|
|
88
99
|
Arcane theme; its application module is `apps/hello-speech/modules/App.js`.
|
|
89
100
|
`arcane/AI` is a managed **browser import**, not an npm-exported Node inference
|
|
@@ -518,7 +529,7 @@ arcane new <id> [--path <directory>] [--display-name <name>] [--target <target>]
|
|
|
518
529
|
arcane init [id] [--workspace <directory>] [--display-name <name>] [--target <target>]
|
|
519
530
|
arcane doctor [--workspace <directory>] [--arcane-root <directory>]
|
|
520
531
|
arcane import-map [--workspace <directory>] [--app <id>]
|
|
521
|
-
arcane dev [--app <id>] [--public] [--https] [--cert <file> --key <file>] [--host <address>] [--port 8000]
|
|
532
|
+
arcane dev [--app <id>] [--public] [--http | --https] [--cert <file> --key <file>] [--host <address>] [--port 8000]
|
|
522
533
|
arcane test [--app <id>] [--scope app]
|
|
523
534
|
arcane test --scope shared --test-file <repo-relative.test.mjs>
|
|
524
535
|
arcane check [--app <id>] [--scope app] [--skip-tests]
|
|
@@ -2271,13 +2271,24 @@ function createCompletionAccumulator(modelId, requestId) {
|
|
|
2271
2271
|
return completeValue({ push, result, hasToolCalls, correlateToolCalls });
|
|
2272
2272
|
}
|
|
2273
2273
|
|
|
2274
|
-
function callbackStreamHandle({ runtime, request, signal, onSettled }) {
|
|
2274
|
+
function callbackStreamHandle({ runtime, request, signal, onSettled, observeToolText = null }) {
|
|
2275
2275
|
const linked = linkAbortSignal(signal);
|
|
2276
2276
|
const accumulator = createCompletionAccumulator(request.model ?? null, request.id);
|
|
2277
2277
|
const chunks = [];
|
|
2278
2278
|
const waiters = [];
|
|
2279
2279
|
let ended = false;
|
|
2280
2280
|
let terminalError = null;
|
|
2281
|
+
let observation = Promise.resolve();
|
|
2282
|
+
let observationError = null;
|
|
2283
|
+
|
|
2284
|
+
function publishChunk(chunk) {
|
|
2285
|
+
if (ended || linked.controller.signal.aborted) return;
|
|
2286
|
+
const publicChunk = projectPublicStreamChunk(chunk);
|
|
2287
|
+
if (publicChunk === OMITTED_PUBLIC_STREAM_DATA) return;
|
|
2288
|
+
const waiter = waiters.shift();
|
|
2289
|
+
if (waiter) waiter.resolve({ value: publicChunk, done: false });
|
|
2290
|
+
else chunks.push(publicChunk);
|
|
2291
|
+
}
|
|
2281
2292
|
|
|
2282
2293
|
function deliver(value) {
|
|
2283
2294
|
// This gate prevents delivery after public cancellation. It is not proof
|
|
@@ -2287,11 +2298,20 @@ function callbackStreamHandle({ runtime, request, signal, onSettled }) {
|
|
|
2287
2298
|
? value
|
|
2288
2299
|
: { ...value, id: request.id };
|
|
2289
2300
|
accumulator.push(chunk);
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2301
|
+
if (!observeToolText) {
|
|
2302
|
+
publishChunk(chunk);
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
observation = observation.then(async function observeBrowserWasmChunk() {
|
|
2306
|
+
if (ended) return;
|
|
2307
|
+
throwIfAborted(linked.controller.signal);
|
|
2308
|
+
await observeToolText(chunk);
|
|
2309
|
+
publishChunk(chunk);
|
|
2310
|
+
});
|
|
2311
|
+
observation.catch(function cancelFailedBrowserWasmObservation(error) {
|
|
2312
|
+
observationError ??= error;
|
|
2313
|
+
linked.controller.abort(error);
|
|
2314
|
+
});
|
|
2295
2315
|
}
|
|
2296
2316
|
|
|
2297
2317
|
function finish(error = null) {
|
|
@@ -2313,17 +2333,27 @@ function callbackStreamHandle({ runtime, request, signal, onSettled }) {
|
|
|
2313
2333
|
);
|
|
2314
2334
|
const result = (async () => {
|
|
2315
2335
|
try {
|
|
2316
|
-
await terminal;
|
|
2336
|
+
const terminalValue = await terminal;
|
|
2337
|
+
if (observeToolText) await observation;
|
|
2338
|
+
throwIfAborted(linked.controller.signal);
|
|
2339
|
+
if (observeToolText) await observeToolText(terminalValue);
|
|
2317
2340
|
throwIfAborted(linked.controller.signal);
|
|
2318
2341
|
const value = accumulator.result();
|
|
2342
|
+
if (observeToolText) await observeToolText(value);
|
|
2343
|
+
throwIfAborted(linked.controller.signal);
|
|
2319
2344
|
finish();
|
|
2320
2345
|
return value;
|
|
2321
2346
|
} catch (error) {
|
|
2322
|
-
const normalized = normalizeArcaneAIError(error, {
|
|
2347
|
+
const normalized = normalizeArcaneAIError(observationError ?? error, {
|
|
2323
2348
|
kind: "llm",
|
|
2324
2349
|
operation: "request",
|
|
2325
|
-
signal: normalizationSignal(error, linked.controller.signal),
|
|
2350
|
+
signal: observationError ? null : normalizationSignal(error, linked.controller.signal),
|
|
2326
2351
|
});
|
|
2352
|
+
if (observeToolText) {
|
|
2353
|
+
await observation.catch(function retainFirstBrowserWasmStreamFailure() {
|
|
2354
|
+
// The failure captured above remains the terminal outcome.
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2327
2357
|
finish(normalized);
|
|
2328
2358
|
throw normalized;
|
|
2329
2359
|
}
|
|
@@ -2380,7 +2410,7 @@ function callbackStreamHandle({ runtime, request, signal, onSettled }) {
|
|
|
2380
2410
|
return completeValue(handle);
|
|
2381
2411
|
}
|
|
2382
2412
|
|
|
2383
|
-
function validatedV1StreamHandle(opened, request) {
|
|
2413
|
+
function validatedV1StreamHandle(opened, request, observeToolText = null, signal = null) {
|
|
2384
2414
|
if (
|
|
2385
2415
|
!opened
|
|
2386
2416
|
|| !is.object(opened)
|
|
@@ -2429,6 +2459,16 @@ function validatedV1StreamHandle(opened, request) {
|
|
|
2429
2459
|
let publicStreamSettled = false;
|
|
2430
2460
|
let publicStreamError = null;
|
|
2431
2461
|
|
|
2462
|
+
function stopV1ToolTextObservation(reason) {
|
|
2463
|
+
if (!observeToolText) return;
|
|
2464
|
+
publicStreamError ??= reason instanceof Error ? reason : fail(
|
|
2465
|
+
'ARCANE_AI_REQUEST_ABORTED',
|
|
2466
|
+
'The browser-WASM request was cancelled.',
|
|
2467
|
+
reason,
|
|
2468
|
+
);
|
|
2469
|
+
settlePublicStream(publicStreamError);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2432
2472
|
function publishPublicChunk(value) {
|
|
2433
2473
|
if (publicStreamSettled) return;
|
|
2434
2474
|
const waiter = publicChunkWaiters.shift();
|
|
@@ -2459,10 +2499,24 @@ function validatedV1StreamHandle(opened, request) {
|
|
|
2459
2499
|
return true;
|
|
2460
2500
|
}
|
|
2461
2501
|
accumulator.push(next.value);
|
|
2502
|
+
if (observeToolText) {
|
|
2503
|
+
if (publicStreamError !== null) throw publicStreamError;
|
|
2504
|
+
throwIfAborted(signal);
|
|
2505
|
+
await observeToolText(next.value);
|
|
2506
|
+
if (publicStreamError !== null) throw publicStreamError;
|
|
2507
|
+
throwIfAborted(signal);
|
|
2508
|
+
}
|
|
2462
2509
|
const projected = projectPublicStreamChunk(next.value);
|
|
2463
2510
|
if (projected !== OMITTED_PUBLIC_STREAM_DATA) publishPublicChunk(projected);
|
|
2464
2511
|
}
|
|
2465
2512
|
} catch (error) {
|
|
2513
|
+
if (observeToolText) {
|
|
2514
|
+
Promise.resolve().then(function cancelFailedV1ToolTextStream() {
|
|
2515
|
+
return opened.cancel(error);
|
|
2516
|
+
}).catch(function reportFailedV1ToolTextCancellation(cleanupError) {
|
|
2517
|
+
arcaneLogging.error('Arcane v1 tool-text stream cancellation failed.', cleanupError);
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2466
2520
|
settlePublicStream(error);
|
|
2467
2521
|
throw error;
|
|
2468
2522
|
}
|
|
@@ -2476,8 +2530,15 @@ function validatedV1StreamHandle(opened, request) {
|
|
|
2476
2530
|
);
|
|
2477
2531
|
terminalResult.catch(function retainV1StreamTerminalRejection() {});
|
|
2478
2532
|
const result = Promise.all([terminalResult, privateStreamPump]).then(
|
|
2479
|
-
function correlateV1StreamTerminal([terminal]) {
|
|
2533
|
+
async function correlateV1StreamTerminal([terminal]) {
|
|
2534
|
+
if (observeToolText && publicStreamError !== null) throw publicStreamError;
|
|
2480
2535
|
accumulator.correlateToolCalls(terminal);
|
|
2536
|
+
if (observeToolText) {
|
|
2537
|
+
throwIfAborted(signal);
|
|
2538
|
+
await observeToolText(terminal);
|
|
2539
|
+
if (publicStreamError !== null) throw publicStreamError;
|
|
2540
|
+
throwIfAborted(signal);
|
|
2541
|
+
}
|
|
2481
2542
|
return terminal;
|
|
2482
2543
|
},
|
|
2483
2544
|
);
|
|
@@ -2485,6 +2546,7 @@ function validatedV1StreamHandle(opened, request) {
|
|
|
2485
2546
|
const handle = {
|
|
2486
2547
|
result,
|
|
2487
2548
|
cancel: function cancelValidatedV1Stream(reason) {
|
|
2549
|
+
stopV1ToolTextObservation(reason);
|
|
2488
2550
|
return opened.cancel(reason);
|
|
2489
2551
|
},
|
|
2490
2552
|
async next() {
|
|
@@ -2496,6 +2558,7 @@ function validatedV1StreamHandle(opened, request) {
|
|
|
2496
2558
|
});
|
|
2497
2559
|
},
|
|
2498
2560
|
async return(value) {
|
|
2561
|
+
stopV1ToolTextObservation('The stream consumer stopped before completion.');
|
|
2499
2562
|
if (is.function(iterator.return)) {
|
|
2500
2563
|
Promise.resolve().then(function returnUnderlyingV1Stream() {
|
|
2501
2564
|
return iterator.return(value);
|
|
@@ -2511,6 +2574,7 @@ function validatedV1StreamHandle(opened, request) {
|
|
|
2511
2574
|
return { value, done: true };
|
|
2512
2575
|
},
|
|
2513
2576
|
async throw(error) {
|
|
2577
|
+
stopV1ToolTextObservation(error);
|
|
2514
2578
|
await opened.cancel(error);
|
|
2515
2579
|
throw error;
|
|
2516
2580
|
},
|
|
@@ -3171,6 +3235,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
3171
3235
|
runtime,
|
|
3172
3236
|
request,
|
|
3173
3237
|
signal: externalSignal,
|
|
3238
|
+
observeToolText: context.observeToolText,
|
|
3174
3239
|
onSettled(error) {
|
|
3175
3240
|
if (settled) return;
|
|
3176
3241
|
settled = true;
|
|
@@ -3452,7 +3517,7 @@ export function adaptV1LlmProvider(provider) {
|
|
|
3452
3517
|
});
|
|
3453
3518
|
return status();
|
|
3454
3519
|
},
|
|
3455
|
-
request({ role = "llm", selection, operation, payload, signal = null } = {}) {
|
|
3520
|
+
request({ role = "llm", selection, operation, payload, signal = null } = {}, controls = {}) {
|
|
3456
3521
|
assertSelection(selection, role);
|
|
3457
3522
|
assertActiveSelection(selection);
|
|
3458
3523
|
throwIfAborted(signal);
|
|
@@ -3466,9 +3531,9 @@ export function adaptV1LlmProvider(provider) {
|
|
|
3466
3531
|
}
|
|
3467
3532
|
if (operation === "stream") {
|
|
3468
3533
|
validateStructuralRequest(payload);
|
|
3469
|
-
return Promise.resolve(methods.stream(payload, { signal })).then(
|
|
3534
|
+
return Promise.resolve(methods.stream(payload, { signal, observeToolText: controls.observeToolText })).then(
|
|
3470
3535
|
function wrapV1StreamResult(opened) {
|
|
3471
|
-
return validatedV1StreamHandle(opened, payload);
|
|
3536
|
+
return validatedV1StreamHandle(opened, payload, controls.observeToolText, signal);
|
|
3472
3537
|
},
|
|
3473
3538
|
);
|
|
3474
3539
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Is from "../dependencies/strong-type/index.js";
|
|
2
2
|
import { arcaneLogging } from '../logging.mjs';
|
|
3
3
|
import { createArcaneEventSource } from "arcane-os/event-manager";
|
|
4
|
+
import { createToolTextObserver } from './tool-text-stream.mjs';
|
|
4
5
|
|
|
5
6
|
const is = new Is(false);
|
|
6
7
|
|
|
@@ -1311,13 +1312,22 @@ export class ModelController {
|
|
|
1311
1312
|
}
|
|
1312
1313
|
}
|
|
1313
1314
|
|
|
1314
|
-
stream(request = {}) {
|
|
1315
|
+
stream(request = {}, context = {}) {
|
|
1315
1316
|
this.#assertOperational();
|
|
1316
1317
|
request=structuralRequest(request);
|
|
1317
1318
|
localRequirement(request, this.#provider);
|
|
1318
1319
|
const controller = this;
|
|
1319
1320
|
const externalSignal = request.signal ?? null;
|
|
1320
1321
|
const linked = linkedAbortSignal(externalSignal);
|
|
1322
|
+
const observeToolText = context.observeToolText ? async function observeActiveModelToolText(chunk) {
|
|
1323
|
+
if (linked.controller.signal.aborted) {
|
|
1324
|
+
throw normalizeArcaneAIError(null, { operation: 'request', signal: linked.controller.signal });
|
|
1325
|
+
}
|
|
1326
|
+
await context.observeToolText(chunk);
|
|
1327
|
+
if (linked.controller.signal.aborted) {
|
|
1328
|
+
throw normalizeArcaneAIError(null, { operation: 'request', signal: linked.controller.signal });
|
|
1329
|
+
}
|
|
1330
|
+
} : null;
|
|
1321
1331
|
let opened = null;
|
|
1322
1332
|
let openedIterator = null;
|
|
1323
1333
|
let openError = null;
|
|
@@ -1363,6 +1373,7 @@ export class ModelController {
|
|
|
1363
1373
|
kind: "llm",
|
|
1364
1374
|
operation: "stream",
|
|
1365
1375
|
signal: linked.controller.signal,
|
|
1376
|
+
observeToolText,
|
|
1366
1377
|
},
|
|
1367
1378
|
);
|
|
1368
1379
|
if (
|
|
@@ -1427,10 +1438,18 @@ export class ModelController {
|
|
|
1427
1438
|
return true;
|
|
1428
1439
|
}
|
|
1429
1440
|
streamedToolCalls.observe(next.value);
|
|
1441
|
+
if (observeToolText) await observeToolText(next.value);
|
|
1430
1442
|
const projected=projectPublicStreamChunk(next.value);
|
|
1431
1443
|
if(projected!==OMITTED_PUBLIC_STREAM_DATA)publishPublicChunk(projected);
|
|
1432
1444
|
}
|
|
1433
1445
|
}catch(error){
|
|
1446
|
+
if (observeToolText) {
|
|
1447
|
+
Promise.resolve().then(function cancelFailedModelToolTextStream() {
|
|
1448
|
+
return opened.cancel(error);
|
|
1449
|
+
}).catch(function reportFailedModelToolTextCancellation(cleanupError) {
|
|
1450
|
+
arcaneLogging.error('Arcane model tool-text stream cancellation failed.', cleanupError);
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1434
1453
|
settlePublicStream(error);
|
|
1435
1454
|
throw error;
|
|
1436
1455
|
}
|
|
@@ -1446,8 +1465,9 @@ export class ModelController {
|
|
|
1446
1465
|
});
|
|
1447
1466
|
terminalResult.catch(()=>undefined);
|
|
1448
1467
|
|
|
1449
|
-
const result = Promise.all([terminalResult,privateStreamPump]).then(([terminal])
|
|
1468
|
+
const result = Promise.all([terminalResult,privateStreamPump]).then(async function completeModelStream([terminal]) {
|
|
1450
1469
|
streamedToolCalls.correlate(terminal);
|
|
1470
|
+
if (observeToolText) await observeToolText(terminal);
|
|
1451
1471
|
settlePublicStream();
|
|
1452
1472
|
return terminal;
|
|
1453
1473
|
}).catch((error)=>{
|
|
@@ -1533,10 +1553,18 @@ export class ModelController {
|
|
|
1533
1553
|
this.#assertOperational();
|
|
1534
1554
|
localRequirement(options, this.#provider);
|
|
1535
1555
|
const id = requestIdentity(options.id);
|
|
1536
|
-
const
|
|
1556
|
+
const { toolText, onToolText, ...requestOptions } = options;
|
|
1557
|
+
const request = structuralRequest({ ...requestOptions, id });
|
|
1537
1558
|
const displayId = displayRequestId(id);
|
|
1559
|
+
const observeToolText = createToolTextObserver(
|
|
1560
|
+
toolText,
|
|
1561
|
+
is.function(onToolText) ? function deliverSelectedToolText(text, call) {
|
|
1562
|
+
return onToolText(text, call, displayId);
|
|
1563
|
+
} : onToolText,
|
|
1564
|
+
{ signal: options.signal },
|
|
1565
|
+
);
|
|
1538
1566
|
fireAndForget(options.onRequest, request, id);
|
|
1539
|
-
const handle = this.stream(request);
|
|
1567
|
+
const handle = this.stream(request, { observeToolText });
|
|
1540
1568
|
|
|
1541
1569
|
try {
|
|
1542
1570
|
for await (const chunk of handle) {
|