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
|
@@ -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
|
|
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
|
-
|
|
446
|
-
validates.
|
|
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
|
-
|
|
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()`
|
|
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 `&`, `<`, and `>` 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
|
|
1657
|
+
import {
|
|
1658
|
+
formatConversationClosingReportText
|
|
1659
|
+
} from '/arcane/modules/ConversationClosingReport.js';
|
|
1632
1660
|
|
|
1633
|
-
console.log(
|
|
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
|
|
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
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
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
|
-
|
|
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
|
|
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`,
|
|
3517
|
-
`networkUrls` identify
|
|
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
|
|
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
|
|
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
|
|
3537
|
-
event-callback failure closes
|
|
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()`
|
|
3934
|
-
`httpPort`. `port` selects HTTPS and `httpPort` selects the HTTP `308`
|
|
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
|
|
3939
|
-
|
|
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`
|
|
5909
|
-
terminal record independently of that
|
|
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.
|
|
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(
|
|
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
|
|
77
|
+
export function formatConversationClosingReportText(value){
|
|
78
78
|
return value
|
|
79
79
|
.replaceAll('&','&')
|
|
80
80
|
.replaceAll('<','<')
|
|
@@ -240,7 +240,7 @@ export function formatConversationClosingReport(value){
|
|
|
240
240
|
remembered_actions:value.rememberedActions,
|
|
241
241
|
}
|
|
242
242
|
:value;
|
|
243
|
-
return
|
|
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
|
|
92
|
-
--
|
|
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);
|