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.
@@ -432,7 +432,7 @@ declined, cancelled, or not-executed result.
432
432
  That tool-result content must be a nonblank string and is preserved exactly.
433
433
 
434
434
  `streamRequest()` owns the complete terminal callback sequence. `onDataChunk`
435
- receives each complete provider chunk before ordinary projection, while
435
+ receives each provider chunk after private structural fields are removed, while
436
436
  `onChunk` receives every nonstructural content or reasoning value from every
437
437
  choice in provider order. After the stream settles, `onDataResult` receives the
438
438
  complete terminal completion, `onResponse` receives that same unprojected
@@ -442,8 +442,29 @@ output is the ordered structural-call array when the selected result contains
442
442
  tools, the complete completion object when it contains multiple choices, or
443
443
  the ordinary single-result text/completion otherwise; later choices are never
444
444
  discarded.
445
- Partial structural deltas remain private until the matching terminal envelope
446
- validates. Request observers receive
445
+ Raw structural deltas remain private until the matching terminal envelope
446
+ validates. To display one tool argument's text as it arrives, supply
447
+ `toolText:{name,field}` and `onToolText(text,call,displayId)`. `name` is the exact
448
+ tool name and `field` is one root-level string argument. The callback receives
449
+ each newly decoded text fragment in order, including whitespace and JSON string
450
+ escapes decoded to their original characters. Its `call` record contains
451
+ `{id,name,field,index,choiceIndex}`; `id` is the actual normalized tool-call ID,
452
+ `index` identifies the call within its choice, and `choiceIndex` identifies the
453
+ response choice. Delivery starts once the matching tool name and call ID are
454
+ known. `displayId` is the same `M-${id}` request display ID used by `onChunk`.
455
+
456
+ `onToolText` is separate from ordinary `onChunk`, so callers can append to the
457
+ tool's existing display without duplicating assistant prose or saved history.
458
+ It observes text only: it does not execute a tool, alter arguments, persist a
459
+ turn, or change the terminal callbacks. A provider that supplies arguments only
460
+ at completion emits text only when that complete response arrives. Repeated
461
+ terminal snapshots do not replay text already emitted. Omitting `toolText`
462
+ preserves the existing callback path. Invalid selection or callback input throws
463
+ `TypeError`; malformed selected argument text reports
464
+ `ARCANE_AI_TOOL_TEXT_INVALID`. Callback errors reach the request owner and
465
+ cancellation prevents later delivery.
466
+
467
+ Request observers receive
447
468
  `onRequest(request,id,metadata)` and any transport metadata supplied by the
448
469
  selected route is forwarded unchanged. Every async native, HTTP, provider, and
449
470
  built-in callback is observed before the next callback or terminal settlement.
@@ -1609,7 +1630,7 @@ Defines the closing-report tool, instruction, result normalizer, call classifier
1609
1630
 
1610
1631
  ### Public surface
1611
1632
 
1612
- Six constants/helpers for closing reports.
1633
+ Seven constants/helpers for closing reports.
1613
1634
 
1614
1635
  The generated sole-call schema requires both `message` and `final_message`.
1615
1636
  `message` is brief user-facing progress shown while the application accepts and
@@ -1617,9 +1638,14 @@ renders the call. `final_message` remains the complete terminal closeout and is
1617
1638
  never replaced by or duplicated into `message`; `remembered_actions` remains
1618
1639
  optional. `normalizeConversationClosingReport()` returns
1619
1640
  `{message,finalMessage,rememberedActions}`, while
1620
- `formatConversationClosingReport()` escapes and renders only `finalMessage`.
1641
+ `formatConversationClosingReport()` normalizes the complete report and formats
1642
+ only `finalMessage`. Its shared `formatConversationClosingReportText(value)`
1643
+ operation accepts a string, including an empty or whitespace-only chunk, and
1644
+ replaces `&`, `<`, and `>` with `&amp;`, `&lt;`, and `&gt;` respectively. It
1645
+ preserves all other text and does not trim, validate a report, render, or persist
1646
+ anything. Use the same operation for live text chunks and complete final text.
1621
1647
 
1622
- Exact exports: `CONVERSATION_CLOSING_REPORT_TOOL_NAME`, `classifyConversationClosingReportCalls`, `conversationClosingReportInstruction`, `createConversationClosingReportTool`, `formatConversationClosingReport`, `normalizeConversationClosingReport`.
1648
+ Exact exports: `CONVERSATION_CLOSING_REPORT_TOOL_NAME`, `classifyConversationClosingReportCalls`, `conversationClosingReportInstruction`, `createConversationClosingReportTool`, `formatConversationClosingReport`, `formatConversationClosingReportText`, `normalizeConversationClosingReport`.
1623
1649
 
1624
1650
  ### Availability and normalization
1625
1651
 
@@ -1628,9 +1654,11 @@ Exact exports: `CONVERSATION_CLOSING_REPORT_TOOL_NAME`, `classifyConversationClo
1628
1654
  ### Example
1629
1655
 
1630
1656
  ```javascript
1631
- import * as module from '/arcane/modules/ConversationClosingReport.js';
1657
+ import {
1658
+ formatConversationClosingReportText
1659
+ } from '/arcane/modules/ConversationClosingReport.js';
1632
1660
 
1633
- console.log(Object.keys(module));
1661
+ console.log(formatConversationClosingReportText('Complete <draft> & next step.'));
1634
1662
  ```
1635
1663
 
1636
1664
  ## ConversationTimebox.js
@@ -3,7 +3,7 @@
3
3
  The npm package exposes a Node.js ESM control plane, the portable
4
4
  `arcane-os/event-manager`, `arcane-os/logging`, `arcane-os/mail`,
5
5
  `arcane-os/preference-store`, `arcane-os/speech-playback`,
6
- `arcane-os/speech-text`, and `arcane-os/browser-device` entrypoints, and the browser-only
6
+ `arcane-os/speech-text`, `arcane-os/ai/tool-text-stream`, and `arcane-os/browser-device` entrypoints, and the browser-only
7
7
  `arcane-os/pwa`, `arcane-os/ai/browser-wasm` and `arcane-os/ai/browser-speech` entrypoints.
8
8
  Those package subpaths are distinct from application-facing projection modules
9
9
  in the managed browser map, such as `arcane/AIProviderRuntime`,
@@ -41,6 +41,7 @@ for the installed-inventory-derived physical-runtime contract in SDK `0.5.18`.
41
41
  | `arcane-os/browser-device` | Synchronous mobile or desktop identity hints for application-owned settings. |
42
42
  | `arcane-os/pwa` | Nonblocking PWA registration, worker updates, native installation state and a dismissible installation component. |
43
43
  | `arcane-os/ai/browser-wasm` | Caller-selected browser-local Wllama inference, complete DBOPFS model storage, streaming, cancellation, and structural tool-call results. |
44
+ | `arcane-os/ai/tool-text-stream` | Shared selected tool-argument text observer for provider integration. |
44
45
  | `arcane-os/ai/browser-speech` | Caller-selected browser-local Whisper STT and Kokoro TTS provider mechanisms, ordinary upstream assets, materialized/native routing, Workers, and cancellation. |
45
46
  | `arcane-os/mail` | Portable Mail runtime, durable outbox, complete transport responses, and provider-neutral acceptance contracts. |
46
47
 
@@ -138,6 +139,7 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
138
139
  | `createNativeTargetAdapter()` | function | `arcane-os` | Targets, native plans, and providers | Node; selected browser/native target or provider as documented |
139
140
  | `createReporter()` | function | `arcane-os` | Events, processes, and testing | Node |
140
141
  | `createToolchain()` | function | `arcane-os` | Headless toolchain operations | Node; selected operation may produce browser or native output |
142
+ | `createToolTextObserver()` | function | `arcane-os/ai/tool-text-stream` | AI provider integration | Compatible JavaScript module host; no browser capability required |
141
143
  | `createWorkspace()` | function | `arcane-os` | Workspace, doctor, repository, and server | Node |
142
144
  | `default()` | function | `arcane-os/testing` | Events, processes, and testing | Node |
143
145
  | `DEFAULT_TEST_TIMEOUT_MS` | constant | `arcane-os` | Events, processes, and testing | Node |
@@ -3462,7 +3464,7 @@ async function useselectApp(...arguments_) {
3462
3464
 
3463
3465
  Starts one owned browser development server with exact runtime/app route mappings and a caller-selected bind address.
3464
3466
 
3465
- HTTPS serving uses the published `node-http-server` module. The SDK
3467
+ HTTP and HTTPS serving use the published `node-http-server` module. The SDK
3466
3468
  selects source routes and supplies generated representations; the module owns
3467
3469
  static-file conditional GET/HEAD handling and response delivery. The SDK
3468
3470
  retains modification dates for its generated representations. Unchanged
@@ -3478,7 +3480,7 @@ async startDevServer(options={})
3478
3480
  ```
3479
3481
 
3480
3482
  Import it from `arcane-os`. Source mode accepts
3481
- `{workspaceRoot=process.cwd(), appId, mode='source', host='127.0.0.1', port=0, httpPort=0,
3483
+ `{workspaceRoot=process.cwd(), appId, mode='source', host='127.0.0.1', port=0, httpPort=0, http=false,
3482
3484
  certPath, keyPath, tls, signal, onEvent}` and serves one validated
3483
3485
  workspace application plus its complete SDK or integrated runtime. Packaged mode uses
3484
3486
  `{mode:'packaged', releaseRoot, workspaceRoot, host, port, httpPort, certPath, keyPath, tls,
@@ -3492,9 +3494,9 @@ The SDK request hook returns `308` for HTTP with a `Location` pointing to the
3492
3494
  actual HTTPS port while preserving the original request path and query. Both listeners
3493
3495
  use the selected host.
3494
3496
 
3495
- Every source server and packaged browser preview enforces HTTPS, including
3496
- localhost. The legacy `https` option is accepted but cannot disable it.
3497
- Startup reads `.arcane/dev/server-cert.pem` and
3497
+ Source servers default to HTTPS, including localhost; packaged browser previews
3498
+ require HTTPS. The legacy `https` option is accepted but `https:false` and
3499
+ `tls:false` alone do not disable HTTPS. HTTPS startup reads `.arcane/dev/server-cert.pem` and
3498
3500
  `.arcane/dev/server-key.pem` relative to `workspaceRoot` unless explicit
3499
3501
  `certPath` and `keyPath` are supplied together; relative paths resolve from the
3500
3502
  workspace. A direct `tls` object instead supplies Node HTTPS server options, including
@@ -3505,16 +3507,27 @@ handshake; browser trust and address matching are evaluated when a client
3505
3507
  connects. The CLI's `--public` selects the wildcard bind;
3506
3508
  the API's `host` option alone changes only the bind address.
3507
3509
 
3508
- The promise settles after both listeners are ready and resolves to
3510
+ Explicit `http:true` selects one HTTP content listener in source mode and skips
3511
+ certificate loading. `port` selects that listener's port, including `0` for an
3512
+ available port. The same application, runtime and generated PWA routes are
3513
+ served. `http` must be boolean; `http:true` rejects packaged mode, `https:true`,
3514
+ explicit certificate/key paths, a `tls` value other than `undefined`, `null` or
3515
+ `false`, and a nonzero `httpPort`. Browser secure-context and installation
3516
+ requirements remain browser-owned; see [explicit HTTP development](cli.md#explicit-http-development).
3517
+
3518
+ The promise settles after all selected listeners are ready and resolves to
3509
3519
  `{server, protocol, mode, workspaceRoot, appId, host, port, origin, cleanUrl, url,
3510
- networkUrls, httpPort, httpOrigin, httpUrl, close, closed, lifecycle}`. `server` is the raw Node HTTPS
3511
- server; `protocol` is `'https:'`.
3520
+ networkUrls, httpPort, httpOrigin, httpUrl, close, closed, lifecycle}`. `server` is the raw Node
3521
+ server for application content; `protocol` is `'https:'` by default or `'http:'`
3522
+ with explicit HTTP source mode.
3512
3523
  `url` and `cleanUrl` are the same application URL. Wildcard listeners use
3513
3524
  `localhost` in that local URL; `host` retains the actual bound address.
3514
3525
  `httpPort` is the actual HTTP listener port, `httpOrigin` is its HTTP origin,
3515
3526
  and `httpUrl` combines that origin with the application start path. These
3516
- fields identify the redirect endpoint; `origin`, `url`, `cleanUrl`, and
3517
- `networkUrls` identify HTTPS application endpoints.
3527
+ fields identify the redirect endpoint in HTTPS mode; `origin`, `url`, `cleanUrl`,
3528
+ and `networkUrls` identify application endpoints. In HTTP mode, `httpPort`,
3529
+ `httpOrigin`, and `httpUrl` equal `port`, `origin`, and `url`; they identify the
3530
+ single content listener, with no redirect listener.
3518
3531
  `networkUrls` lists application URLs for applicable non-loopback interface
3519
3532
  addresses discovered once at startup. These URLs are connection candidates,
3520
3533
  not evidence of reachability from another device. The server adds no session
@@ -3526,15 +3539,15 @@ certificates or modify trust stores. Each client must trust the issuing CA and o
3526
3539
  server certificate. Lifecycle events and CLI summaries exclude TLS options and
3527
3540
  private key contents. See [development HTTPS setup](cli.md#development-https-setup).
3528
3541
 
3529
- Starting the server opens both selected listeners and emits awaited,
3542
+ Starting the server opens the selected listener or listeners and emits awaited,
3530
3543
  backpressured `server.starting` and `server.started` events. `server.started`
3531
- includes `httpPort`, `httpOrigin`, and `httpUrl` alongside the HTTPS endpoint.
3544
+ includes the selected `protocol`, `httpPort`, `httpOrigin`, and `httpUrl` alongside the application endpoint.
3532
3545
  Request failures emit `server.request.failed`; shutdown emits `server.stopped` after owned
3533
3546
  requests and event delivery drain. Call `await result.close()` in a `finally`
3534
3547
  block, or abort `signal`; `close()` is idempotent and returns the
3535
3548
  same settlement represented by both `closed` and `lifecycle`. Closing the
3536
- operation closes both listeners. An error from either listener or an
3537
- event-callback failure closes both and rejects the lifecycle. Invalid
3549
+ operation closes every selected listener once. A listener error or an
3550
+ event-callback failure closes the operation and rejects the lifecycle. Invalid
3538
3551
  mode/host/port/httpPort, malformed workspace or release content, an occupied port,
3539
3552
  or an already-aborted signal rejects startup.
3540
3553
 
@@ -3929,14 +3942,15 @@ async function usedescribeTargets(...arguments_) {
3929
3942
 
3930
3943
  Starts one owned browser development server for the selected application.
3931
3944
 
3932
- `https`, `certPath`, `keyPath`, and `tls` follow the
3933
- [`startDevServer()` TLS contract](#startdevserver), alongside `host`, `port`, and
3934
- `httpPort`. `port` selects HTTPS and `httpPort` selects the HTTP `308` redirect
3935
- listener; each defaults to an available port.
3945
+ `http`, `https`, `certPath`, `keyPath`, and `tls` follow the
3946
+ [`startDevServer()` transport contract](#startdevserver), alongside `host`, `port`, and
3947
+ `httpPort`. By default, `port` selects HTTPS and `httpPort` selects the HTTP `308`
3948
+ redirect listener; each defaults to an available port. Explicit `http:true`
3949
+ selects one source HTTP content listener at `port`, without certificate loading.
3936
3950
  The operation refreshes the selected authored descriptor's `arcane-package.json`
3937
3951
  projection and managed import maps under one development-refresh lock, then
3938
- releases that lock before opening the HTTPS source listener and its HTTP
3939
- redirect listener. It returns both endpoints and their shared shutdown
3952
+ releases that lock before opening the selected source listener or listeners.
3953
+ It returns their application endpoints and shared shutdown
3940
3954
  lifecycle. Legacy package-only apps remain unchanged. The operation generates
3941
3955
  no packaged output; enabled PWA manifests
3942
3956
  are served directly from the selected source resources.
@@ -5905,8 +5919,20 @@ At least one `llm` or `provider` is required. The mutable API object contains
5905
5919
  delivers every choice's ordinary content/reasoning values in provider order,
5906
5920
  and returns ordinary terminal text for one choice, a structural-call array for
5907
5921
  selected tool output, or complete JSON text for a multi-choice completion.
5908
- `onDataChunk` and `onDataResult` preserve the complete provider chunk and
5909
- terminal record independently of that application-facing projection. Use
5922
+ `onDataChunk` receives provider chunks with private structural fields removed;
5923
+ `onDataResult` preserves the complete terminal record independently of that
5924
+ application-facing projection. Supply `toolText:{name,field}` with
5925
+ `onToolText(text,call,displayId)` to receive one selected root string argument
5926
+ as it arrives. `call` contains `{id,name,field,index,choiceIndex}` with the real
5927
+ normalized call ID, call position, and response-choice index. The SDK decodes
5928
+ JSON string escapes, preserves whitespace and order, and starts delivery once
5929
+ the matching tool name and call ID are known. `displayId` is the same request
5930
+ display ID as `onChunk`. This separate callback does not add ordinary prose,
5931
+ execute tools, or persist history. Complete-only providers emit the selected
5932
+ text only on actual completion; repeated terminal snapshots do not replay
5933
+ emitted text. Callback failures reach the request owner and cancellation
5934
+ prevents later delivery. Omitting `toolText` preserves the existing behavior.
5935
+ See the [tool-text example](ai/browser-wasm.md#stream-selected-tool-text). Use
5910
5936
  `ai.llm.stream()` for the async iterator; structural fragments remain private
5911
5937
  until the complete terminal result validates.
5912
5938
 
@@ -5991,6 +6017,61 @@ async function openLocalReviewAfterUserChoice() {
5991
6017
  }
5992
6018
  ```
5993
6019
 
6020
+ ## createToolTextObserver()
6021
+
6022
+ ### Overview
6023
+
6024
+ Creates the shared tool-text observer used by SDK provider integrations before
6025
+ private structural fields are removed. Application callers normally use
6026
+ `streamRequest({toolText,onToolText})` instead.
6027
+
6028
+ ### Signature and result
6029
+
6030
+ ```text
6031
+ createToolTextObserver(selection, onText, {signal}={})
6032
+ ```
6033
+
6034
+ `selection` is `{name,field}`: the exact tool name and one root string argument.
6035
+ `undefined`, `null`, or `false` selection returns `null`. An active selection
6036
+ requires nonblank string `name` and `field` values and an `onText` function;
6037
+ invalid configuration throws `TypeError`.
6038
+
6039
+ The result is an async `observeToolText(chunk)` function. It accepts raw
6040
+ OpenAI-compatible `choices[].delta.tool_calls` fragments and complete
6041
+ `choices[].message.tool_calls` or `message.tool_calls` snapshots. `onText` is
6042
+ awaited with each newly decoded fragment and
6043
+ `{id,name,field,index,choiceIndex}`. It preserves decoded string content and
6044
+ whitespace, waits for the matching name and real normalized ID, and reports
6045
+ malformed selected argument text with `ARCANE_AI_TOOL_TEXT_INVALID`.
6046
+
6047
+ ### Availability and normalization
6048
+
6049
+ **Compatible JavaScript module host.** One observer owns one request's call
6050
+ state. Consume each raw delta once, before any structural filtering; pass the
6051
+ same observer through the provider integration instead of creating another
6052
+ parser. Complete snapshots may be observed again without replaying emitted
6053
+ text. A complete-only response emits only when received. `signal` stops later
6054
+ observation after cancellation; callback errors propagate to the caller. The
6055
+ observer does not modify the supplied chunk, invoke tools, persist history, or
6056
+ start inference.
6057
+
6058
+ ### Example
6059
+
6060
+ ```javascript
6061
+ import {createToolTextObserver} from 'arcane-os/ai/tool-text-stream';
6062
+
6063
+ function createProviderTextObserver(onText, signal) {
6064
+ return createToolTextObserver(
6065
+ {name:'prepare_conversation_closing_report', field:'final_message'},
6066
+ onText,
6067
+ {signal}
6068
+ );
6069
+ }
6070
+ ```
6071
+
6072
+ The provider owner passes actual raw chunks to the returned observer and awaits
6073
+ it before continuing its existing filtered stream delivery.
6074
+
5994
6075
  ## createBrowserModelSource()
5995
6076
 
5996
6077
  ### Overview
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.13.3",
3
+ "version": "0.15.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -20,6 +20,7 @@
20
20
  "./speech-playback": "./runtime/arcane/modules/SpeechPlayback.js",
21
21
  "./speech-text": "./browser-runtime/speech-text.mjs",
22
22
  "./ai/browser-wasm": "./browser-runtime/ai/browser-wasm.mjs",
23
+ "./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
23
24
  "./ai/browser-speech": "./browser-runtime/ai/browser-speech.mjs",
24
25
  "./mail": "./src/mail-api.mjs",
25
26
  "./testing": "./src/testing.mjs",
@@ -18,6 +18,7 @@ import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
18
18
  import {arcaneLogging} from 'arcane-os/logging';
19
19
  import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
20
20
  import {prepareSpeech} from './PreparedSpeech.js';
21
+ import {createToolTextObserver} from 'arcane-os/ai/tool-text-stream';
21
22
 
22
23
  const completeValue=(value)=>value;
23
24
 
@@ -1470,7 +1471,7 @@ class AI {
1470
1471
  status:statusBuiltInLLMProvider()
1471
1472
  });
1472
1473
  },
1473
- request:function requestBuiltInLLMProvider(context={}){
1474
+ request:function requestBuiltInLLMProvider(context={},controls={}){
1474
1475
  if(context.signal?.aborted){
1475
1476
  throw normalizeAIRequestAbort(context.signal.reason);
1476
1477
  }
@@ -1502,7 +1503,8 @@ class AI {
1502
1503
  function executeBuiltInLLMProviderStream(bridge){
1503
1504
  return runtime.#requestBuiltInLLMStream(
1504
1505
  context.payload,
1505
- bridge
1506
+ bridge,
1507
+ controls.observeToolText
1506
1508
  );
1507
1509
  },
1508
1510
  context.signal
@@ -4285,7 +4287,7 @@ class AI {
4285
4287
  );
4286
4288
  }
4287
4289
 
4288
- #requestBuiltInLLMStream(payload={},bridge){
4290
+ #requestBuiltInLLMStream(payload={},bridge,observeToolText=null){
4289
4291
  const parallelToolCalls=payload.parallelToolCalls!==undefined
4290
4292
  ?payload.parallelToolCalls
4291
4293
  :payload.parallel_tool_calls;
@@ -4310,7 +4312,8 @@ class AI {
4310
4312
  true,
4311
4313
  emitBuiltInLLMStreamData,
4312
4314
  function ignoreBuiltInLLMStreamResult(){},
4313
- payload.reasoningEffort
4315
+ payload.reasoningEffort,
4316
+ observeToolText
4314
4317
  );
4315
4318
  }
4316
4319
 
@@ -4459,6 +4462,8 @@ class AI {
4459
4462
  structuredOutput=false,
4460
4463
  localOnly=false,
4461
4464
  onChunk=function ignoreStreamChunk(){},
4465
+ toolText,
4466
+ onToolText,
4462
4467
  onComplete=function finishIgnoredStream(){},
4463
4468
  onDataChunk=function ignoreStreamDataChunk(){},
4464
4469
  onDataResult=function ignoreStreamDataResult(){},
@@ -4484,6 +4489,15 @@ class AI {
4484
4489
  reasoningEffort
4485
4490
  }={}){
4486
4491
  validateAIStructuralRequest(messages,tools,parallelToolCalls);
4492
+ const observeToolText=createToolTextObserver(
4493
+ toolText,
4494
+ is.function(onToolText)
4495
+ ?function reportAIStreamToolText(text,call){
4496
+ return onToolText(text,call,`M-${id}`);
4497
+ }
4498
+ :onToolText,
4499
+ {signal}
4500
+ );
4487
4501
  const normalizedReasoningEffort=normalizeAIReasoningEffort(
4488
4502
  reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
4489
4503
  );
@@ -4538,7 +4552,8 @@ class AI {
4538
4552
  payload:request,
4539
4553
  localOnly,
4540
4554
  signal
4541
- }
4555
+ },
4556
+ {observeToolText}
4542
4557
  );
4543
4558
  for await(const chunk of handle){
4544
4559
  if(signal?.aborted){
@@ -4622,7 +4637,8 @@ class AI {
4622
4637
  true,
4623
4638
  onDataChunk,
4624
4639
  onDataResult,
4625
- normalizedReasoningEffort
4640
+ normalizedReasoningEffort,
4641
+ observeToolText
4626
4642
  );
4627
4643
  const structuralToolCalls=normalizeAICompletionToolCalls(
4628
4644
  completion
@@ -4729,7 +4745,8 @@ class AI {
4729
4745
  returnCompletion=false,
4730
4746
  dataChunkHandler=function ignoreBuiltInStreamDataChunk(){},
4731
4747
  dataResultHandler=function ignoreBuiltInStreamDataResult(){},
4732
- reasoningEffort
4748
+ reasoningEffort,
4749
+ observeToolText=null
4733
4750
  ){
4734
4751
  let speechTurnCompleted=false;
4735
4752
 
@@ -4845,6 +4862,12 @@ class AI {
4845
4862
  );
4846
4863
  }
4847
4864
  streamedNativeToolCalls=observed;
4865
+ if(observeToolText){
4866
+ await observeToolText({message:{tool_calls:observed}});
4867
+ }
4868
+ }
4869
+ if(signal?.aborted){
4870
+ return;
4848
4871
  }
4849
4872
  const thinking=seeThinking
4850
4873
  ?String(message.thinking||'')
@@ -4918,6 +4941,12 @@ class AI {
4918
4941
  structuralToolCalls,
4919
4942
  tool_choice
4920
4943
  );
4944
+ if(observeToolText){
4945
+ await observeToolText(nativeCompletion);
4946
+ }
4947
+ if(signal?.aborted){
4948
+ throw normalizeAIRequestAbort();
4949
+ }
4921
4950
  await dataResultHandler(nativeCompletion,id);
4922
4951
  for(const call of structuralToolCalls){
4923
4952
  if(signal?.aborted){
@@ -5096,6 +5125,12 @@ class AI {
5096
5125
  'The AI stream returned a non-object event payload.'
5097
5126
  );
5098
5127
  }
5128
+ if(observeToolText){
5129
+ await observeToolText(streamedResponse);
5130
+ }
5131
+ if(signal?.aborted){
5132
+ throw normalizeAIRequestAbort();
5133
+ }
5099
5134
  const dataChunk=projectAIStreamChunk(streamedResponse);
5100
5135
  if(dataChunk!==OMITTED_AI_STREAM_DATA){
5101
5136
  await dataChunkHandler(dataChunk,id);
@@ -5253,6 +5288,13 @@ class AI {
5253
5288
  await drainSseBuffer(true);
5254
5289
  }
5255
5290
  }catch(error){
5291
+ if(observeToolText){
5292
+ await reader.cancel(error).catch(
5293
+ function reportToolTextReaderCleanupFailure(cleanupError){
5294
+ arcaneLogging.error('Arcane tool text reader cleanup failed.',cleanupError);
5295
+ }
5296
+ );
5297
+ }
5256
5298
  if(isAIRequestAbort(error,signal)){
5257
5299
  throw normalizeAIRequestAbort(error);
5258
5300
  }
@@ -5383,6 +5425,12 @@ class AI {
5383
5425
  terminalToolCalls,
5384
5426
  'The built-in HTTP stream'
5385
5427
  );
5428
+ if(observeToolText){
5429
+ await observeToolText(completion);
5430
+ }
5431
+ if(signal?.aborted){
5432
+ throw normalizeAIRequestAbort();
5433
+ }
5386
5434
  await dataResultHandler(completion,id);
5387
5435
  for(const call of terminalToolCalls){
5388
5436
  if(signal?.aborted){
@@ -3347,14 +3347,14 @@ export class AIProviderRuntime {
3347
3347
  return this.#disposeAllPromise;
3348
3348
  }
3349
3349
 
3350
- request(role, options = {}, {speechInputPrepared = false} = {}) {
3350
+ request(role, options = {}, {speechInputPrepared = false, observeToolText = null} = {}) {
3351
3351
  if (SPEECH_ROLES.includes(role)) {
3352
3352
  arcaneLogging.debug('[Arcane speech runtime] request', role, options);
3353
3353
  }
3354
- return this.#requestRole(role, options, false, speechInputPrepared);
3354
+ return this.#requestRole(role, options, false, speechInputPrepared, observeToolText);
3355
3355
  }
3356
3356
 
3357
- #requestRole(role, options, queued, speechInputPrepared = false) {
3357
+ #requestRole(role, options, queued, speechInputPrepared = false, observeToolText = null) {
3358
3358
  this.#assertOpen();
3359
3359
  this.#assertNotConfiguring();
3360
3360
  assertRole(role);
@@ -3486,7 +3486,7 @@ export class AIProviderRuntime {
3486
3486
  }
3487
3487
  if ((!queued && slot.requestQueue.length)
3488
3488
  || slot.activeRequests.size >= maxConcurrentRequests) {
3489
- return this.#enqueueRoleRequest(slot, options);
3489
+ return this.#enqueueRoleRequest(slot, options, observeToolText);
3490
3490
  }
3491
3491
  if (!slot.ready
3492
3492
  || providerStatus.state !== 'ready'
@@ -3556,6 +3556,17 @@ export class AIProviderRuntime {
3556
3556
  }
3557
3557
 
3558
3558
  if (options.operation === 'stream') {
3559
+ const observeCurrentToolText = role === 'llm' && observeToolText
3560
+ ? async function observeCurrentAIProviderToolText(chunk) {
3561
+ runtime.#assertCurrentRequest(
3562
+ slot, generation, requestSequence, requestRecord, controller.signal
3563
+ );
3564
+ await observeToolText(chunk);
3565
+ runtime.#assertCurrentRequest(
3566
+ slot, generation, requestSequence, requestRecord, controller.signal
3567
+ );
3568
+ }
3569
+ : null;
3559
3570
  let providerHandle = null;
3560
3571
  let providerOpenPromise = null;
3561
3572
  let iterator = null;
@@ -3796,7 +3807,8 @@ export class AIProviderRuntime {
3796
3807
  operation: options.operation,
3797
3808
  payload: options.payload,
3798
3809
  signal: controller.signal
3799
- }
3810
+ },
3811
+ {observeToolText: observeCurrentToolText}
3800
3812
  );
3801
3813
  }
3802
3814
  );
@@ -3862,6 +3874,7 @@ export class AIProviderRuntime {
3862
3874
  continue;
3863
3875
  }
3864
3876
  llmToolCallCorrelation.observe(result.value);
3877
+ if (observeCurrentToolText) await observeCurrentToolText(result.value);
3865
3878
  const projected = projectLLMStreamChunk(result.value);
3866
3879
  if (projected !== OMITTED_LLM_STREAM_DATA) {
3867
3880
  publishProjectedAIProviderStreamChunk(projected);
@@ -3902,6 +3915,7 @@ export class AIProviderRuntime {
3902
3915
  )
3903
3916
  : value;
3904
3917
  llmToolCallCorrelation?.assertTerminal(terminalValue);
3918
+ if (observeCurrentToolText) await observeCurrentToolText(terminalValue);
3905
3919
  settleAIProviderStream(null, terminalValue);
3906
3920
  } catch (error) {
3907
3921
  if (cleanupPromise) {
@@ -4208,7 +4222,7 @@ export class AIProviderRuntime {
4208
4222
  );
4209
4223
  }
4210
4224
 
4211
- #enqueueRoleRequest(slot, options) {
4225
+ #enqueueRoleRequest(slot, options, observeToolText = null) {
4212
4226
  if (SPEECH_ROLES.includes(slot.role)) {
4213
4227
  arcaneLogging.debug('[Arcane speech runtime] queue.enqueue', {
4214
4228
  role: slot.role,
@@ -4222,6 +4236,7 @@ export class AIProviderRuntime {
4222
4236
  return new Promise(function queueAIProviderRoleRequest(resolve, reject) {
4223
4237
  const entry = {
4224
4238
  options,
4239
+ observeToolText,
4225
4240
  resolve,
4226
4241
  reject,
4227
4242
  detachSignal: null
@@ -4293,7 +4308,9 @@ export class AIProviderRuntime {
4293
4308
  }
4294
4309
  let operation;
4295
4310
  try {
4296
- operation = runtime.#requestRole(slot.role, entry.options, true);
4311
+ operation = runtime.#requestRole(
4312
+ slot.role, entry.options, true, false, entry.observeToolText
4313
+ );
4297
4314
  } catch (error) {
4298
4315
  if (SPEECH_ROLES.includes(slot.role)) {
4299
4316
  arcaneLogging.debug('[Arcane speech runtime] queue.error', slot.role, entry.options, error);
@@ -74,7 +74,7 @@ function parseArguments(value){
74
74
  return value;
75
75
  }
76
76
 
77
- function escapeRawHTML(value){
77
+ export function formatConversationClosingReportText(value){
78
78
  return value
79
79
  .replaceAll('&','&amp;')
80
80
  .replaceAll('<','&lt;')
@@ -240,7 +240,7 @@ export function formatConversationClosingReport(value){
240
240
  remembered_actions:value.rememberedActions,
241
241
  }
242
242
  :value;
243
- return escapeRawHTML(
243
+ return formatConversationClosingReportText(
244
244
  normalizeConversationClosingReport(normalizedInput).finalMessage
245
245
  );
246
246
  }
package/src/cli/main.mjs CHANGED
@@ -39,6 +39,7 @@ const VALUE_OPTIONS=new Set([
39
39
  const FLAG_OPTIONS=new Set([
40
40
  'git',
41
41
  'public',
42
+ 'http',
42
43
  'https',
43
44
  'skip-tests',
44
45
  'dry-run',
@@ -65,7 +66,7 @@ Usage:
65
66
  ${CLI_NAME} upgrade [--workspace <directory>] [--app <id>]
66
67
  ${CLI_NAME} doctor [--workspace <directory>] [--arcane-root <directory>]
67
68
  ${CLI_NAME} import-map [--workspace <directory>] [--app <id>]
68
- ${CLI_NAME} dev [--app <id>] [--public] [--https] [--cert <pem>] [--key <pem>] [--host <address>] [--port 8000] [--http-port 0] [--sdk-runtime-source <sdk-root>]
69
+ ${CLI_NAME} dev [--app <id>] [--public] [--http | --https] [--cert <pem>] [--key <pem>] [--host <address>] [--port 8000] [--http-port 0] [--sdk-runtime-source <sdk-root>]
69
70
  ${CLI_NAME} test [--app <id>] [--scope app]
70
71
  ${CLI_NAME} test --scope shared --test-file <repo-relative.test.mjs>
71
72
  ${CLI_NAME} check [--app <id>] [--scope app] [--skip-tests]
@@ -88,8 +89,9 @@ Usage:
88
89
  ${CLI_NAME} mail serve --profile <profile> --from <address> --app <id> --origin <origin> [--allow-to <addresses>] [--app-key-stdin] [--host 127.0.0.1] [--port 8025] [--request-timeout <ms>]
89
90
 
90
91
  Development:
91
- --public Serve HTTPS on all IPv4 interfaces (0.0.0.0) and print network URLs.
92
- --https Accepted for compatibility; Arcane browser serving always uses HTTPS.
92
+ --public Bind dev to all IPv4 interfaces (0.0.0.0) and print network URLs.
93
+ --http Dev-only HTTP content on --port; no TLS or HTTPS redirect listener.
94
+ --https Explicitly select the default HTTPS browser transport.
93
95
  --cert <pem> --key <pem> Use an existing certificate pair; paths are relative to the workspace.
94
96
  --host <address> Override the bind address; takes precedence over --public.
95
97
  --http-port <port> Browser dev/run HTTP redirect port; 0 selects an available port (default).
@@ -458,6 +460,14 @@ function operationOptions(command,parsed,cwd){
458
460
  if(flags.has('public')&&command!=='dev'){
459
461
  usage('--public is supported only by dev.');
460
462
  }
463
+ const http = flags.has('http');
464
+ if (http && command !== 'dev') {
465
+ usage('--http is supported only by dev.');
466
+ }
467
+ if (http && (flags.has('https') || values.cert !== undefined
468
+ || values.key !== undefined || values['http-port'] !== undefined)) {
469
+ usage('--http cannot combine --https, --cert, --key, or --http-port; select its listener with --port.');
470
+ }
461
471
  const browserServing = command === 'dev'
462
472
  || (command === 'run' && (values.target ?? 'browser') === 'browser');
463
473
  if ((flags.has('https') || values.cert !== undefined || values.key !== undefined) && !browserServing) {
@@ -470,7 +480,7 @@ function operationOptions(command,parsed,cwd){
470
480
  usage('Arcane HTTPS serving requires --cert and --key together.');
471
481
  }
472
482
  const browserServerOptions = browserServing ? {
473
- https: true,
483
+ ...(http ? {http: true} : {https: true}),
474
484
  httpPort: readPort(values['http-port'], 0),
475
485
  ...(values.cert === undefined ? {} : {
476
486
  certPath: path.resolve(workspaceRoot, values.cert),
@@ -882,7 +892,7 @@ function serverSummary(result){
882
892
  async function waitForServer(result,signal,reporter){
883
893
  const readyMessage=[
884
894
  `Development server ready at ${result.url}`,
885
- ...(result.httpUrl ? [`HTTP redirect: ${result.httpUrl}`] : []),
895
+ ...(result.httpUrl && result.protocol !== 'http:' ? [`HTTP redirect: ${result.httpUrl}`] : []),
886
896
  ...(result.networkUrls??[]).map(function networkAddress(url){return `Network: ${url}`;})
887
897
  ].join('\n');
888
898
  reporter.emit('server.ready',serverSummary(result),readyMessage);