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 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
- Every Arcane app uses HTTPS for development and packaged browser previews.
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
- const publicChunk = projectPublicStreamChunk(chunk);
2291
- if (publicChunk === OMITTED_PUBLIC_STREAM_DATA) return;
2292
- const waiter = waiters.shift();
2293
- if (waiter) waiter.resolve({ value: publicChunk, done: false });
2294
- else chunks.push(publicChunk);
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 request = structuralRequest({ ...options, id });
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) {