arcane-os 0.29.1 → 0.30.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 +12 -0
- package/README.md +1 -1
- package/browser-runtime/ai/twin-cloud.mjs +230 -0
- package/docs/reference/ai/twin-cloud.md +120 -3
- package/docs/reference/availability-and-normalization.md +9 -0
- package/docs/reference/inventory/package-api.json +99 -1
- package/docs/reference/sdk-api.md +268 -1
- package/package.json +3 -2
- package/runtime/arcane/modules/AI.js +14 -154
- package/src/import-map.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.0
|
|
4
|
+
|
|
5
|
+
- Add the public `arcane-os/ai/twin-cloud` subpath with stateless `fetchRequest`
|
|
6
|
+
for Node and browser callers. Supply the TWiN key, model, messages, optional
|
|
7
|
+
structured-output schema and cancellation signal explicitly. The result is
|
|
8
|
+
the complete parsed provider completion, with no browser startup, saved
|
|
9
|
+
conversation, hidden model default or output cap.
|
|
10
|
+
- Share the existing TWiN HTTP, structured JSON, overload retry and cancellation
|
|
11
|
+
implementation with the browser AI owner while preserving its public methods
|
|
12
|
+
and lifecycle. Only overload responses with HTTP 429 use the existing
|
|
13
|
+
three-second retry; other failures retain their complete provider response.
|
|
14
|
+
|
|
3
15
|
## 0.29.1
|
|
4
16
|
|
|
5
17
|
- Preserve complete long and non-ASCII filenames in application release bundles
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
|
|
20
20
|
event, cancellation, and browser run contracts.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.
|
|
22
|
+
This checkout defines the `0.30.0` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {arcaneLogging} from '../logging.mjs';
|
|
3
|
+
|
|
4
|
+
const is = new Is(false);
|
|
5
|
+
const twinChatURL = 'https://inference.do-ai.run/v1/chat/completions';
|
|
6
|
+
|
|
7
|
+
/** A stateless TWiN request; browser AI shares the HTTP and format owners below. */
|
|
8
|
+
export async function fetchRequest({
|
|
9
|
+
twinKey,
|
|
10
|
+
model,
|
|
11
|
+
messages = [],
|
|
12
|
+
structuredOutput = false,
|
|
13
|
+
tools = [],
|
|
14
|
+
toolChoice = 'auto',
|
|
15
|
+
parallelToolCalls,
|
|
16
|
+
reasoningEffort,
|
|
17
|
+
signal = null,
|
|
18
|
+
id = Date.now(),
|
|
19
|
+
onRequest = function observeTWiNRequest(){},
|
|
20
|
+
onResponse = function observeTWiNResponse(){}
|
|
21
|
+
} = {}){
|
|
22
|
+
if(signal?.aborted){
|
|
23
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
24
|
+
}
|
|
25
|
+
if(!is.string(twinKey) || !twinKey){
|
|
26
|
+
const error = new Error('AI provider is not configured.');
|
|
27
|
+
error.code = 'AI_PROVIDER_NOT_CONFIGURED';
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
if(!is.string(model) || !model){
|
|
31
|
+
throw new TypeError('TWiN fetchRequest requires an explicit model.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const request = {model, messages, stream:false};
|
|
35
|
+
const format = structuredOutputFormat(structuredOutput);
|
|
36
|
+
if(format){
|
|
37
|
+
request.response_format = openAIResponseFormat(format);
|
|
38
|
+
}
|
|
39
|
+
if(tools.length){
|
|
40
|
+
request.tools = tools;
|
|
41
|
+
request.tool_choice = toolChoice;
|
|
42
|
+
if(parallelToolCalls !== undefined){
|
|
43
|
+
request.parallel_tool_calls = parallelToolCalls;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if(reasoningEffort){
|
|
47
|
+
request.reasoning_effort = reasoningEffort;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try{
|
|
51
|
+
await onRequest(
|
|
52
|
+
request,
|
|
53
|
+
id,
|
|
54
|
+
{operation:'fetch', transport:'http', destination:twinChatURL}
|
|
55
|
+
);
|
|
56
|
+
if(signal?.aborted){
|
|
57
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
58
|
+
}
|
|
59
|
+
const response = await fetchJSONResponse(
|
|
60
|
+
twinChatURL,
|
|
61
|
+
{
|
|
62
|
+
method:'POST',
|
|
63
|
+
credentials:'omit',
|
|
64
|
+
headers:{
|
|
65
|
+
'Content-Type':'application/json',
|
|
66
|
+
Authorization:`Bearer ${twinKey}`
|
|
67
|
+
},
|
|
68
|
+
body:JSON.stringify(request),
|
|
69
|
+
...(signal ? {signal} : {})
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
if(signal?.aborted){
|
|
73
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
74
|
+
}
|
|
75
|
+
await onResponse(response, id, false);
|
|
76
|
+
if(signal?.aborted){
|
|
77
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
78
|
+
}
|
|
79
|
+
return response;
|
|
80
|
+
}catch(error){
|
|
81
|
+
if(isAIRequestAbort(error, signal)){
|
|
82
|
+
throw normalizeAIRequestAbort(error);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isAIRequestAbort(error, signal){
|
|
89
|
+
return signal?.aborted
|
|
90
|
+
|| error?.name === 'AbortError'
|
|
91
|
+
|| error?.code === 'ARCANE_REQUEST_ABORTED'
|
|
92
|
+
|| error?.code === 'ARCANE_AI_REQUEST_ABORTED'
|
|
93
|
+
|| error?.code === 'AI_REQUEST_ABORTED';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function normalizeAIRequestAbort(error){
|
|
97
|
+
if(error?.code === 'ARCANE_AI_REQUEST_ABORTED'){
|
|
98
|
+
return error;
|
|
99
|
+
}
|
|
100
|
+
const normalized = new Error('The AI request was cancelled.', {cause:error});
|
|
101
|
+
normalized.name = 'AbortError';
|
|
102
|
+
normalized.code = 'ARCANE_AI_REQUEST_ABORTED';
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function structuredOutputFormat(value = false){
|
|
107
|
+
if(value === false || value === null || value === undefined){
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if(value === true || value === 'json'){
|
|
111
|
+
return 'json';
|
|
112
|
+
}
|
|
113
|
+
if(
|
|
114
|
+
is.object(value)
|
|
115
|
+
&& !is.array(value)
|
|
116
|
+
&& (
|
|
117
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
118
|
+
|| Object.getPrototypeOf(value) === null
|
|
119
|
+
)
|
|
120
|
+
){
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
const error = new TypeError(
|
|
124
|
+
'AI structured output must be enabled with true, json, or a JSON Schema object.'
|
|
125
|
+
);
|
|
126
|
+
error.code = 'AI_STRUCTURED_OUTPUT_INVALID';
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function openAIResponseFormat(format){
|
|
131
|
+
if(format === 'json'){
|
|
132
|
+
return {type:'json_object'};
|
|
133
|
+
}
|
|
134
|
+
if(format){
|
|
135
|
+
return {
|
|
136
|
+
type:'json_schema',
|
|
137
|
+
json_schema:{name:'structured_response', strict:true, schema:format}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Shared with browser streaming; consume no successful body at this boundary. */
|
|
144
|
+
export async function fetchHTTPResponse(url, options){
|
|
145
|
+
const {signal} = options;
|
|
146
|
+
const retryDelayMs = 3000;
|
|
147
|
+
try{
|
|
148
|
+
while(true){
|
|
149
|
+
if(signal?.aborted){
|
|
150
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
151
|
+
}
|
|
152
|
+
const response = await fetch(url, options);
|
|
153
|
+
if(signal?.aborted){
|
|
154
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
155
|
+
}
|
|
156
|
+
if(response.ok){
|
|
157
|
+
return response;
|
|
158
|
+
}
|
|
159
|
+
const contentType = response.headers.get('content-type') || '';
|
|
160
|
+
const error = contentType.includes('application/json')
|
|
161
|
+
? await response.json()
|
|
162
|
+
: await response.text();
|
|
163
|
+
if(signal?.aborted){
|
|
164
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
165
|
+
}
|
|
166
|
+
const message = is.string(error)
|
|
167
|
+
? error
|
|
168
|
+
: error?.error?.message ?? error?.message;
|
|
169
|
+
if(
|
|
170
|
+
response.status !== 429
|
|
171
|
+
|| !is.string(message)
|
|
172
|
+
|| !message.toLowerCase().includes('overload')
|
|
173
|
+
){
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
arcaneLogging.warn(
|
|
177
|
+
`${message}\nRetrying in ${retryDelayMs / 1000} seconds`,
|
|
178
|
+
error
|
|
179
|
+
);
|
|
180
|
+
await new Promise(function waitForOverloadRetry(resolve, reject){
|
|
181
|
+
function finishRetryDelay(){
|
|
182
|
+
signal?.removeEventListener('abort', cancelRetryDelay);
|
|
183
|
+
resolve();
|
|
184
|
+
}
|
|
185
|
+
function cancelRetryDelay(){
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
signal.removeEventListener('abort', cancelRetryDelay);
|
|
188
|
+
reject(normalizeAIRequestAbort(signal.reason));
|
|
189
|
+
}
|
|
190
|
+
const timer = setTimeout(finishRetryDelay, retryDelayMs);
|
|
191
|
+
signal?.addEventListener('abort', cancelRetryDelay, {once:true});
|
|
192
|
+
if(signal?.aborted){
|
|
193
|
+
cancelRetryDelay();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}catch(error){
|
|
198
|
+
if(isAIRequestAbort(error, signal)){
|
|
199
|
+
throw normalizeAIRequestAbort(error);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Return the entire parsed completion without selecting or rewriting choices. */
|
|
206
|
+
export async function fetchJSONResponse(url, options){
|
|
207
|
+
const {signal} = options;
|
|
208
|
+
try{
|
|
209
|
+
const response = await fetchHTTPResponse(url, options);
|
|
210
|
+
if(signal?.aborted){
|
|
211
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
212
|
+
}
|
|
213
|
+
const contentType = response.headers.get('content-type') || '';
|
|
214
|
+
if(!contentType.includes('application/json')){
|
|
215
|
+
throw new TypeError(
|
|
216
|
+
`AI request returned ${contentType || 'an unknown content type'} instead of JSON.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const completion = await response.json();
|
|
220
|
+
if(signal?.aborted){
|
|
221
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
222
|
+
}
|
|
223
|
+
return completion;
|
|
224
|
+
}catch(error){
|
|
225
|
+
if(isAIRequestAbort(error, signal)){
|
|
226
|
+
throw normalizeAIRequestAbort(error);
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -1,8 +1,125 @@
|
|
|
1
1
|
# TWiN Cloud: one request
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `fetchRequest` from `arcane-os/ai/twin-cloud` for a complete TWiN Cloud
|
|
4
|
+
request in Node or a browser with an explicit key and model. It imports no
|
|
5
|
+
browser profile, DOM, user singleton, or storage, and starts no work on import.
|
|
6
|
+
The existing browser `AI.js` interface remains available for applications that
|
|
7
|
+
already use its provider selection, lifecycle, and speech. TWiN Cloud is that
|
|
8
|
+
interface's default remote LLM service, named `TWIN`; speech stays on device
|
|
9
|
+
and does not use the TWiN access key.
|
|
10
|
+
|
|
11
|
+
## Node: explicit key, model, and structured result
|
|
12
|
+
|
|
13
|
+
Install the published `arcane-os` package in the Node project. Keep the key in
|
|
14
|
+
the application's existing server configuration, outside source control and
|
|
15
|
+
diagnostics. In this example, `server-config.json` is that caller-owned local
|
|
16
|
+
configuration file with a `twinKey` property; add its exact path to `.gitignore`
|
|
17
|
+
before creating it. The SDK does not discover or write this file.
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import serverConfig from './server-config.json' with {type: 'json'};
|
|
21
|
+
import {fetchRequest} from 'arcane-os/ai/twin-cloud';
|
|
22
|
+
|
|
23
|
+
const response = await fetchRequest({
|
|
24
|
+
twinKey: serverConfig.twinKey,
|
|
25
|
+
model: 'openai-gpt-oss-20b',
|
|
26
|
+
messages: [{
|
|
27
|
+
role: 'user',
|
|
28
|
+
content: 'Explain why the moon-powered toaster keeps burning breakfast. Return HTML and plain text.'
|
|
29
|
+
}],
|
|
30
|
+
structuredOutput: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
html: {type: 'string'},
|
|
34
|
+
text: {type: 'string'}
|
|
35
|
+
},
|
|
36
|
+
required: ['html', 'text'],
|
|
37
|
+
additionalProperties: false
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(response);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`model` is required and remains exactly the supplied identifier. This function
|
|
45
|
+
does not select the browser profile's default model. The example's prompt and
|
|
46
|
+
`html`/`text` schema are caller-owned data, not SDK business logic. Changing
|
|
47
|
+
those fields changes the requested result without changing the SDK.
|
|
48
|
+
|
|
49
|
+
The resolved value is the complete parsed provider JSON, including every
|
|
50
|
+
choice and provider field. The SDK does not extract only the first message,
|
|
51
|
+
parse its content into a second object, or replace the response with an
|
|
52
|
+
application-specific record. The supplied schema becomes
|
|
53
|
+
`response_format: {type:'json_schema', json_schema:{name:'structured_response',
|
|
54
|
+
strict:true, schema:...}}`. `structuredOutput:true` or `'json'` instead selects
|
|
55
|
+
`response_format:{type:'json_object'}`; omission leaves structured output off.
|
|
56
|
+
|
|
57
|
+
## Shared request behavior
|
|
58
|
+
|
|
59
|
+
The focused API accepts complete `messages`, optional `tools`, `toolChoice`,
|
|
60
|
+
`parallelToolCalls`, and `reasoningEffort` in addition to the explicit
|
|
61
|
+
`twinKey` and `model`. Tool options use the existing chat-completion wire fields
|
|
62
|
+
`tools`, `tool_choice`, and `parallel_tool_calls` when `tools` is nonempty.
|
|
63
|
+
A supplied nonempty `reasoningEffort` uses the provider's `reasoning_effort`
|
|
64
|
+
field; omission preserves its default. No output limit is added by this API.
|
|
65
|
+
The function neither executes tools nor adds provider-response envelope
|
|
66
|
+
validation.
|
|
67
|
+
|
|
68
|
+
Optional `id`, `onRequest(request,id,metadata)`, and
|
|
69
|
+
`onResponse(response,id,false)` follow the complete-response `AI.fetchRequest`
|
|
70
|
+
callback shape. The request callback runs before dispatch; the response
|
|
71
|
+
callback receives the complete parsed result before it is returned. Omitted
|
|
72
|
+
`id` uses `Date.now()`; request metadata is
|
|
73
|
+
`{operation:'fetch',transport:'http',destination:'https://inference.do-ai.run/v1/chat/completions'}`.
|
|
74
|
+
The key is
|
|
75
|
+
transport authentication, not part of either callback's message payload. Keep
|
|
76
|
+
credentials out of application logging as well.
|
|
77
|
+
|
|
78
|
+
Only HTTP `429` with a message containing `overload` (case-insensitive) repeats automatically,
|
|
79
|
+
after `3000` milliseconds. Another overload repeats the same complete request;
|
|
80
|
+
other HTTP failures do not become an automatic retry loop. Pass a fresh
|
|
81
|
+
`AbortController`'s `signal` and call `abort()` to cancel. Cancellation during
|
|
82
|
+
the request, response-body read, retry wait, or callback settlement prevents
|
|
83
|
+
successful result delivery and rejects with `ARCANE_AI_REQUEST_ABORTED`.
|
|
84
|
+
Other HTTP failures throw the complete parsed JSON error body or text body.
|
|
85
|
+
A missing key uses `AI_PROVIDER_NOT_CONFIGURED`, a missing explicit model
|
|
86
|
+
throws `TypeError`, and an unsupported `structuredOutput` input uses
|
|
87
|
+
`AI_STRUCTURED_OUTPUT_INVALID`.
|
|
88
|
+
|
|
89
|
+
The SDK retains no request or response history between calls and uses no
|
|
90
|
+
DBOPFS, chat entity, or memory extraction. Each call's `messages` are its
|
|
91
|
+
complete caller-supplied context. The caller decides whether and how to keep
|
|
92
|
+
the result; this stateless transport adds no saved conversation or migration.
|
|
93
|
+
|
|
94
|
+
Browser applications can use this same focused import through their generated
|
|
95
|
+
managed import map. Browser Fetch and CORS behavior still apply. Importing it
|
|
96
|
+
does not instantiate `AI`, read saved preferences, configure speech, or change
|
|
97
|
+
the existing `arcane-os/ai` browser entry.
|
|
98
|
+
|
|
99
|
+
### Shared low-level integration helpers
|
|
100
|
+
|
|
101
|
+
The same module also exports the helpers used by browser `AI.js`. Ordinary
|
|
102
|
+
callers use `fetchRequest`; these exports let SDK transport integration share
|
|
103
|
+
the existing implementation rather than maintain another retry or body reader.
|
|
104
|
+
|
|
105
|
+
| Export | Contract |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `fetchHTTPResponse(url,options)` | Uses the caller's Fetch options, overload retry and cancellation; returns a successful `Response` with its body unconsumed. |
|
|
108
|
+
| `fetchJSONResponse(url,options)` | Uses that HTTP owner, requires `application/json`, and returns the complete parsed body without selecting choices. |
|
|
109
|
+
| `structuredOutputFormat(value=false)` | Maps false/null/undefined to null, true/`'json'` to `'json'`, and preserves a supplied plain JSON Schema object. Other inputs use `AI_STRUCTURED_OUTPUT_INVALID`. |
|
|
110
|
+
| `openAIResponseFormat(format)` | Maps the normalized value to `json_object`, strict `json_schema` named `structured_response`, or null. |
|
|
111
|
+
| `isAIRequestAbort(error,signal)` | Recognizes an aborted signal, `AbortError`, or the existing Arcane AI/request cancellation codes. |
|
|
112
|
+
| `normalizeAIRequestAbort(error)` | Preserves an existing `ARCANE_AI_REQUEST_ABORTED` error or creates that `AbortError` with the original value as its cause. |
|
|
113
|
+
|
|
114
|
+
These helpers start no work on import. HTTP helpers require explicit URL and
|
|
115
|
+
options; they do not add a key, model, browser state, or retained conversation.
|
|
116
|
+
Overload warnings use the shared console logger and preserve the complete
|
|
117
|
+
provider error.
|
|
118
|
+
|
|
119
|
+
## Existing browser AI interface
|
|
120
|
+
|
|
121
|
+
The following browser example uses the same managed imports as the
|
|
122
|
+
[browser speech quick start](browser-speech.md).
|
|
6
123
|
|
|
7
124
|
## Install and import
|
|
8
125
|
|
|
@@ -78,6 +78,7 @@ version; WebKitGTK availability must not be generalized to macOS.
|
|
|
78
78
|
| Read host identity, capabilities, storage, preferences, appearance, or platform state | `globalThis.Arcane` | **Cross-host** where the method is implemented and admitted | Promise behavior and `Arcane.Error` are normalized. Result fields are normalized unless the method explicitly documents a platform-dependent snapshot. |
|
|
79
79
|
| Use local AI without coupling app code to Ollama HTTP | `Arcane.localAI`, `Arcane.ai`, or `/arcane/modules/Ollama.js` | Primarily **Native**; Android exposes a narrower admitted inference projection | Admission, errors, and managed-operation events are normalized. Direct Ollama response envelopes remain **Provider-native**. |
|
|
80
80
|
| Use TWiN Cloud from the renderer profile | `/arcane/modules/AI.js` | **Cloud** from an allowed browser/native renderer | High-level chat behavior is normalized by the module. The TWiN access key authenticates remote LLM chat; raw provider diagnostics remain provider-specific. No automatic cloud fallback is inferred from local failure. |
|
|
81
|
+
| Send one TWiN Cloud request with an explicit key and model | `fetchRequest` from `arcane-os/ai/twin-cloud` | **Node** and **Browser**, using standard Fetch and a remote HTTPS provider | Keeps complete messages and returns the full parsed provider JSON. Shared structured-output mapping, overload-only HTTP 429 retry after 3000 ms, and cancellation match browser TWiN transport. No browser profile, AI/user singleton, DBOPFS, or retained request history is created. |
|
|
81
82
|
| Use speech through one application helper | `/arcane/modules/AI.js` and `Arcane.speech` | **Browser** or **Native** | The helper keeps audio on device: Whisper owns STT and Kokoro owns TTS. It automatically cleans only the outbound speech-input copy and normalizes application-facing audio/text behavior while browser and native request/response plumbing differs below that boundary. |
|
|
82
83
|
| Inspect or manage raw Ollama models | `Arcane.ollama` or `/arcane/modules/Ollama.js` | **Native** desktop Core for management; narrower Android inference only | Wrapper method names, errors, streaming correlation, and admission are Arcane-controlled. Direct Ollama success envelopes are intentionally provider-native. |
|
|
83
84
|
| Use native terminal, installation, user, provisioning, or machine controls | matching `Arcane.*` namespace | **Native** and app/capability restricted | Calls and errors use the common bridge contract. Platform results can be host-specific and are marked in the method guide. |
|
|
@@ -240,6 +241,14 @@ snapshot.
|
|
|
240
241
|
|
|
241
242
|
### Provider-native within an Arcane boundary
|
|
242
243
|
|
|
244
|
+
[`arcane-os/ai/twin-cloud`](ai/twin-cloud.md) accepts an explicit `twinKey` and
|
|
245
|
+
`model`, with the same named `fetchRequest` import in Node and managed browsers.
|
|
246
|
+
It preserves complete provider response JSON while mapping the supplied
|
|
247
|
+
structured-output, tool, and reasoning options to the TWiN wire contract.
|
|
248
|
+
The SDK adds no output cap or persisted context. Cancellation applies during
|
|
249
|
+
request, body read, overload retry wait, and callback settlement. The existing
|
|
250
|
+
profile-backed browser `AI.js` entry and its lifecycle remain separate.
|
|
251
|
+
|
|
243
252
|
Direct `Arcane.ollama.chat()`, `generate()`, `show()`, `embed()`, and lifecycle
|
|
244
253
|
methods return complete Ollama-compatible envelopes. Arcane still owns error
|
|
245
254
|
normalization, chunk correlation, and host transport, but it does
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"minimumVersion": "22.23.2 for Node entrypoints",
|
|
7
7
|
"moduleSystem": "ESM"
|
|
8
8
|
},
|
|
9
|
-
"memberCount":
|
|
9
|
+
"memberCount": 217,
|
|
10
10
|
"runtimeSubpathPatterns": {
|
|
11
11
|
"arcane-os/modules/*": "./runtime/arcane/modules/*",
|
|
12
12
|
"arcane-os/entities/*": "./runtime/arcane/entities/*"
|
|
@@ -3395,6 +3395,104 @@
|
|
|
3395
3395
|
"availability": "Node and browser",
|
|
3396
3396
|
"protocol": "Shared user.developer preference",
|
|
3397
3397
|
"normalization": "Returns null until target.user.ready is true, then whether target.user.developer is exactly true; returns false if preference access throws"
|
|
3398
|
+
},
|
|
3399
|
+
{
|
|
3400
|
+
"id": "twin-cloud:fetchRequest",
|
|
3401
|
+
"name": "fetchRequest",
|
|
3402
|
+
"displayName": "fetchRequest()",
|
|
3403
|
+
"kind": "function",
|
|
3404
|
+
"signature": "async fetchRequest(options={})",
|
|
3405
|
+
"entrypoints": ["arcane-os/ai/twin-cloud"],
|
|
3406
|
+
"primaryImport": "arcane-os/ai/twin-cloud",
|
|
3407
|
+
"group": "TWiN Cloud requests",
|
|
3408
|
+
"summary": "Sends one complete TWiN Cloud request with an explicit caller-owned key and model.",
|
|
3409
|
+
"availability": "Node and Browser; remote HTTPS provider",
|
|
3410
|
+
"protocol": "TWiN Cloud complete-response chat",
|
|
3411
|
+
"normalization": "Preserves complete messages and parsed provider JSON; maps explicit structured-output, tool, and reasoning options; shares overload-only HTTP 429 retry after 3000 ms and cancellation; retains no browser profile, storage, or request history"
|
|
3412
|
+
},
|
|
3413
|
+
{
|
|
3414
|
+
"id": "twin-cloud:fetchHTTPResponse",
|
|
3415
|
+
"name": "fetchHTTPResponse",
|
|
3416
|
+
"displayName": "fetchHTTPResponse()",
|
|
3417
|
+
"kind": "function",
|
|
3418
|
+
"signature": "async fetchHTTPResponse(url,options)",
|
|
3419
|
+
"entrypoints": ["arcane-os/ai/twin-cloud"],
|
|
3420
|
+
"primaryImport": "arcane-os/ai/twin-cloud",
|
|
3421
|
+
"group": "Shared AI transport integration",
|
|
3422
|
+
"summary": "Returns a successful unconsumed Fetch Response through the shared AI HTTP owner.",
|
|
3423
|
+
"availability": "Node and Browser",
|
|
3424
|
+
"protocol": "Shared AI HTTP transport",
|
|
3425
|
+
"normalization": "Preserves caller Fetch options and complete error bodies; repeats only overload HTTP 429 after 3000 ms; signal cancellation uses ARCANE_AI_REQUEST_ABORTED"
|
|
3426
|
+
},
|
|
3427
|
+
{
|
|
3428
|
+
"id": "twin-cloud:fetchJSONResponse",
|
|
3429
|
+
"name": "fetchJSONResponse",
|
|
3430
|
+
"displayName": "fetchJSONResponse()",
|
|
3431
|
+
"kind": "function",
|
|
3432
|
+
"signature": "async fetchJSONResponse(url,options)",
|
|
3433
|
+
"entrypoints": ["arcane-os/ai/twin-cloud"],
|
|
3434
|
+
"primaryImport": "arcane-os/ai/twin-cloud",
|
|
3435
|
+
"group": "Shared AI transport integration",
|
|
3436
|
+
"summary": "Reads the complete successful provider JSON through the shared HTTP owner.",
|
|
3437
|
+
"availability": "Node and Browser",
|
|
3438
|
+
"protocol": "Shared AI HTTP transport",
|
|
3439
|
+
"normalization": "Requires application/json, retains every parsed field without envelope validation, and checks cancellation before delivery"
|
|
3440
|
+
},
|
|
3441
|
+
{
|
|
3442
|
+
"id": "twin-cloud:structuredOutputFormat",
|
|
3443
|
+
"name": "structuredOutputFormat",
|
|
3444
|
+
"displayName": "structuredOutputFormat()",
|
|
3445
|
+
"kind": "function",
|
|
3446
|
+
"signature": "structuredOutputFormat(value=false)",
|
|
3447
|
+
"entrypoints": ["arcane-os/ai/twin-cloud"],
|
|
3448
|
+
"primaryImport": "arcane-os/ai/twin-cloud",
|
|
3449
|
+
"group": "Shared AI transport integration",
|
|
3450
|
+
"summary": "Normalizes the existing structured-output option while preserving a supplied schema.",
|
|
3451
|
+
"availability": "Node and Browser",
|
|
3452
|
+
"protocol": "Shared AI structured-output options",
|
|
3453
|
+
"normalization": "False/null/undefined return null, true/json return json, a plain schema object is returned unchanged, and other inputs use AI_STRUCTURED_OUTPUT_INVALID"
|
|
3454
|
+
},
|
|
3455
|
+
{
|
|
3456
|
+
"id": "twin-cloud:openAIResponseFormat",
|
|
3457
|
+
"name": "openAIResponseFormat",
|
|
3458
|
+
"displayName": "openAIResponseFormat()",
|
|
3459
|
+
"kind": "function",
|
|
3460
|
+
"signature": "openAIResponseFormat(format)",
|
|
3461
|
+
"entrypoints": ["arcane-os/ai/twin-cloud"],
|
|
3462
|
+
"primaryImport": "arcane-os/ai/twin-cloud",
|
|
3463
|
+
"group": "Shared AI transport integration",
|
|
3464
|
+
"summary": "Maps normalized structured-output settings to chat-completion response_format.",
|
|
3465
|
+
"availability": "Node and Browser",
|
|
3466
|
+
"protocol": "Shared AI structured-output options",
|
|
3467
|
+
"normalization": "Returns json_object, strict json_schema named structured_response with the original schema, or null; does not validate schema contents"
|
|
3468
|
+
},
|
|
3469
|
+
{
|
|
3470
|
+
"id": "twin-cloud:isAIRequestAbort",
|
|
3471
|
+
"name": "isAIRequestAbort",
|
|
3472
|
+
"displayName": "isAIRequestAbort()",
|
|
3473
|
+
"kind": "function",
|
|
3474
|
+
"signature": "isAIRequestAbort(error,signal)",
|
|
3475
|
+
"entrypoints": ["arcane-os/ai/twin-cloud"],
|
|
3476
|
+
"primaryImport": "arcane-os/ai/twin-cloud",
|
|
3477
|
+
"group": "Shared AI transport integration",
|
|
3478
|
+
"summary": "Recognizes an aborted signal or an existing AI request cancellation error.",
|
|
3479
|
+
"availability": "Node and Browser",
|
|
3480
|
+
"protocol": "Shared AI cancellation",
|
|
3481
|
+
"normalization": "Returns a boolean for AbortError or ARCANE_REQUEST_ABORTED, ARCANE_AI_REQUEST_ABORTED, and AI_REQUEST_ABORTED; changes no operation"
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
"id": "twin-cloud:normalizeAIRequestAbort",
|
|
3485
|
+
"name": "normalizeAIRequestAbort",
|
|
3486
|
+
"displayName": "normalizeAIRequestAbort()",
|
|
3487
|
+
"kind": "function",
|
|
3488
|
+
"signature": "normalizeAIRequestAbort(error)",
|
|
3489
|
+
"entrypoints": ["arcane-os/ai/twin-cloud"],
|
|
3490
|
+
"primaryImport": "arcane-os/ai/twin-cloud",
|
|
3491
|
+
"group": "Shared AI transport integration",
|
|
3492
|
+
"summary": "Returns the common AI AbortError while preserving the original cause.",
|
|
3493
|
+
"availability": "Node and Browser",
|
|
3494
|
+
"protocol": "Shared AI cancellation",
|
|
3495
|
+
"normalization": "Preserves an existing ARCANE_AI_REQUEST_ABORTED value or returns an AbortError with that code and the supplied cause"
|
|
3398
3496
|
}
|
|
3399
3497
|
]
|
|
3400
3498
|
}
|
|
@@ -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`, `arcane-os/ai/tool-text-stream`, and `arcane-os/browser-device` entrypoints, and the browser-only
|
|
6
|
+
`arcane-os/speech-text`, `arcane-os/ai/tool-text-stream`, `arcane-os/ai/twin-cloud`, 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
|
The `arcane-os/modules/<filename>` and `arcane-os/entities/<filename>` paths
|
|
9
9
|
resolve directly to the existing runtime files, with their actual extension.
|
|
@@ -66,6 +66,7 @@ runtime layouts.
|
|
|
66
66
|
| `arcane-os/pwa` | Nonblocking PWA registration, worker updates, native installation state and a dismissible installation component. |
|
|
67
67
|
| `arcane-os/ai/browser-wasm` | Caller-selected browser-local Wllama inference, complete DBOPFS model storage, streaming, cancellation, and structural tool-call results. |
|
|
68
68
|
| `arcane-os/ai/tool-text-stream` | Shared selected tool-argument text observer for provider integration. |
|
|
69
|
+
| `arcane-os/ai/twin-cloud` | Complete TWiN Cloud requests from Node or a browser with an explicit key/model and shared retry/cancellation behavior. |
|
|
69
70
|
| `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. |
|
|
70
71
|
| `arcane-os/mail` | Portable Mail runtime, durable outbox, complete transport responses, and provider-neutral acceptance contracts. |
|
|
71
72
|
|
|
@@ -121,6 +122,13 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
|
|
|
121
122
|
|
|
122
123
|
| Member | Kind | Import | Group | Availability |
|
|
123
124
|
| --- | --- | --- | --- | --- |
|
|
125
|
+
| `fetchRequest()` | function | `arcane-os/ai/twin-cloud` | TWiN Cloud requests | Node and Browser; remote HTTPS provider |
|
|
126
|
+
| `fetchHTTPResponse()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
|
|
127
|
+
| `fetchJSONResponse()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
|
|
128
|
+
| `structuredOutputFormat()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
|
|
129
|
+
| `openAIResponseFormat()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
|
|
130
|
+
| `isAIRequestAbort()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
|
|
131
|
+
| `normalizeAIRequestAbort()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
|
|
124
132
|
| `APP_BUNDLE_DESCRIPTOR_NAME` | constant | `arcane-os` | Packaging and release bundles | Node |
|
|
125
133
|
| `APP_BUNDLE_EXTENSION` | constant | `arcane-os` | Packaging and release bundles | Node |
|
|
126
134
|
| `APP_BUNDLE_FORMAT` | constant | `arcane-os` | Packaging and release bundles | Node |
|
|
@@ -7636,6 +7644,265 @@ import {readArcaneDeveloperMode} from 'arcane-os/logging';
|
|
|
7636
7644
|
const developerMode=readArcaneDeveloperMode();
|
|
7637
7645
|
```
|
|
7638
7646
|
|
|
7647
|
+
## fetchRequest()
|
|
7648
|
+
|
|
7649
|
+
### Overview
|
|
7650
|
+
|
|
7651
|
+
Makes one complete TWiN Cloud request with explicit caller-owned credentials
|
|
7652
|
+
and model selection. This named function is independent of the browser
|
|
7653
|
+
`AI` instance method with the same name.
|
|
7654
|
+
|
|
7655
|
+
### Signature and result
|
|
7656
|
+
|
|
7657
|
+
```text
|
|
7658
|
+
async fetchRequest(options={})
|
|
7659
|
+
```
|
|
7660
|
+
|
|
7661
|
+
Import the named function from `arcane-os/ai/twin-cloud`. Supply `twinKey`,
|
|
7662
|
+
`model`, and complete `messages`. The function requires the explicit model;
|
|
7663
|
+
it reads no browser preference or default-model selection. Optional
|
|
7664
|
+
`structuredOutput:true` or `'json'` sends `response_format:{type:'json_object'}`.
|
|
7665
|
+
A supplied JSON Schema sends `response_format:{type:'json_schema',
|
|
7666
|
+
json_schema:{name:'structured_response',strict:true,schema:...}}`. Omission
|
|
7667
|
+
leaves structured output off. Optional `tools`, `toolChoice`,
|
|
7668
|
+
`parallelToolCalls`, and `reasoningEffort` map to their existing TWiN wire
|
|
7669
|
+
fields. Tool fields are sent when `tools` is nonempty; a nonempty
|
|
7670
|
+
`reasoningEffort` is forwarded without selecting a default. No default output
|
|
7671
|
+
cap, tool execution, or provider-response envelope validation is added.
|
|
7672
|
+
|
|
7673
|
+
The return value is the entire parsed provider JSON, not only one choice or
|
|
7674
|
+
message. Optional callbacks are `onRequest(request,id,metadata)` before
|
|
7675
|
+
dispatch and `onResponse(response,id,false)` before successful return;
|
|
7676
|
+
`id` defaults to `Date.now()` and may be supplied by the caller. Request metadata
|
|
7677
|
+
is `{operation:'fetch',transport:'http',destination:'https://inference.do-ai.run/v1/chat/completions'}`.
|
|
7678
|
+
Both callbacks are awaited, and callback
|
|
7679
|
+
failures propagate. Credentials are supplied to transport rather than added to
|
|
7680
|
+
the message or response callback payload.
|
|
7681
|
+
|
|
7682
|
+
### Availability and normalization
|
|
7683
|
+
|
|
7684
|
+
**Node and Browser; Cloud transport.** Uses standard Fetch and cancellation,
|
|
7685
|
+
without DOM, browser profiles, user singletons, or storage initialization.
|
|
7686
|
+
Import starts no request. HTTP `429` whose message contains `overload`
|
|
7687
|
+
(case-insensitive) waits
|
|
7688
|
+
`3000` milliseconds and retries the complete request; other HTTP failures
|
|
7689
|
+
throw their complete parsed JSON or text bodies. A missing key throws
|
|
7690
|
+
`AI_PROVIDER_NOT_CONFIGURED`; a missing model throws `TypeError`. `signal`
|
|
7691
|
+
cancellation during transport, body reading, retry waiting,
|
|
7692
|
+
or callback settlement prevents successful return and uses
|
|
7693
|
+
`ARCANE_AI_REQUEST_ABORTED`. No request history, DBOPFS write, or recurring
|
|
7694
|
+
model context is retained. The caller owns persistence and key configuration.
|
|
7695
|
+
See [TWiN Cloud](ai/twin-cloud.md) for a complete Node JSON-schema example and
|
|
7696
|
+
the unchanged browser AI interface.
|
|
7697
|
+
|
|
7698
|
+
### Example
|
|
7699
|
+
|
|
7700
|
+
```javascript
|
|
7701
|
+
import serverConfig from './server-config.json' with {type: 'json'};
|
|
7702
|
+
import {fetchRequest} from 'arcane-os/ai/twin-cloud';
|
|
7703
|
+
|
|
7704
|
+
const response = await fetchRequest({
|
|
7705
|
+
twinKey: serverConfig.twinKey,
|
|
7706
|
+
model: 'openai-gpt-oss-20b',
|
|
7707
|
+
messages: [{role: 'user', content: 'Describe a moon-powered toaster.'}]
|
|
7708
|
+
});
|
|
7709
|
+
|
|
7710
|
+
console.log(response);
|
|
7711
|
+
```
|
|
7712
|
+
|
|
7713
|
+
`server-config.json` is an application-owned, ignored configuration file;
|
|
7714
|
+
never commit its key or print it in diagnostics. The SDK does not read it.
|
|
7715
|
+
|
|
7716
|
+
## fetchHTTPResponse()
|
|
7717
|
+
|
|
7718
|
+
### Overview
|
|
7719
|
+
|
|
7720
|
+
Shared low-level HTTP owner used by browser AI and the focused TWiN API.
|
|
7721
|
+
Ordinary TWiN callers use `fetchRequest()` instead.
|
|
7722
|
+
|
|
7723
|
+
### Signature and result
|
|
7724
|
+
|
|
7725
|
+
```text
|
|
7726
|
+
async fetchHTTPResponse(url,options)
|
|
7727
|
+
```
|
|
7728
|
+
|
|
7729
|
+
Returns the successful Fetch `Response` without consuming its body. Both URL
|
|
7730
|
+
and Fetch options are caller-supplied; this helper adds no key, model, or
|
|
7731
|
+
request envelope. Non-success responses are read completely as JSON when the
|
|
7732
|
+
content type contains `application/json`, otherwise as text. Only status 429
|
|
7733
|
+
with an overload message repeats after 3000 ms; other error bodies are thrown.
|
|
7734
|
+
|
|
7735
|
+
### Availability and normalization
|
|
7736
|
+
|
|
7737
|
+
**Node and Browser.** Exported from `arcane-os/ai/twin-cloud` for shared SDK
|
|
7738
|
+
integration. `options.signal` cancels Fetch and the overload wait and is checked
|
|
7739
|
+
after response/error-body reads. Cancellation uses
|
|
7740
|
+
`ARCANE_AI_REQUEST_ABORTED`. Overload warnings use the existing shared logger.
|
|
7741
|
+
|
|
7742
|
+
### Example
|
|
7743
|
+
|
|
7744
|
+
```javascript
|
|
7745
|
+
import {fetchHTTPResponse} from 'arcane-os/ai/twin-cloud';
|
|
7746
|
+
|
|
7747
|
+
// The integration supplies its selected endpoint and complete Fetch options.
|
|
7748
|
+
const response = await fetchHTTPResponse(endpoint, requestOptions);
|
|
7749
|
+
```
|
|
7750
|
+
|
|
7751
|
+
## fetchJSONResponse()
|
|
7752
|
+
|
|
7753
|
+
### Overview
|
|
7754
|
+
|
|
7755
|
+
Shared complete JSON-body reader built on `fetchHTTPResponse()`.
|
|
7756
|
+
|
|
7757
|
+
### Signature and result
|
|
7758
|
+
|
|
7759
|
+
```text
|
|
7760
|
+
async fetchJSONResponse(url,options)
|
|
7761
|
+
```
|
|
7762
|
+
|
|
7763
|
+
Returns the full parsed JSON value. A successful response whose content type
|
|
7764
|
+
does not contain `application/json` throws `TypeError`; JSON parser failures
|
|
7765
|
+
propagate. The helper does not select choices or validate a provider envelope.
|
|
7766
|
+
|
|
7767
|
+
### Availability and normalization
|
|
7768
|
+
|
|
7769
|
+
**Node and Browser.** Exported from `arcane-os/ai/twin-cloud` for shared SDK
|
|
7770
|
+
integration; it retains the HTTP owner's retry/cancellation behavior and checks
|
|
7771
|
+
cancellation again after parsing. Ordinary callers use `fetchRequest()`.
|
|
7772
|
+
|
|
7773
|
+
### Example
|
|
7774
|
+
|
|
7775
|
+
```javascript
|
|
7776
|
+
import {fetchJSONResponse} from 'arcane-os/ai/twin-cloud';
|
|
7777
|
+
|
|
7778
|
+
// The integration supplies its selected endpoint and complete Fetch options.
|
|
7779
|
+
const completion = await fetchJSONResponse(endpoint, requestOptions);
|
|
7780
|
+
```
|
|
7781
|
+
|
|
7782
|
+
## structuredOutputFormat()
|
|
7783
|
+
|
|
7784
|
+
### Overview
|
|
7785
|
+
|
|
7786
|
+
Normalizes the existing AI structured-output option without rewriting a schema.
|
|
7787
|
+
|
|
7788
|
+
### Signature and result
|
|
7789
|
+
|
|
7790
|
+
```text
|
|
7791
|
+
structuredOutputFormat(value=false)
|
|
7792
|
+
```
|
|
7793
|
+
|
|
7794
|
+
False, null, and undefined return null. True and `'json'` return `'json'`.
|
|
7795
|
+
A plain object with `Object.prototype` or a null prototype is returned
|
|
7796
|
+
unchanged. Other values throw `AI_STRUCTURED_OUTPUT_INVALID`.
|
|
7797
|
+
|
|
7798
|
+
### Availability and normalization
|
|
7799
|
+
|
|
7800
|
+
**Node and Browser.** Synchronous helper exported from
|
|
7801
|
+
`arcane-os/ai/twin-cloud`; performs no network or storage operation.
|
|
7802
|
+
|
|
7803
|
+
### Example
|
|
7804
|
+
|
|
7805
|
+
```javascript
|
|
7806
|
+
import {structuredOutputFormat} from 'arcane-os/ai/twin-cloud';
|
|
7807
|
+
|
|
7808
|
+
const format = structuredOutputFormat({
|
|
7809
|
+
type: 'object',
|
|
7810
|
+
properties: {text: {type: 'string'}}
|
|
7811
|
+
});
|
|
7812
|
+
```
|
|
7813
|
+
|
|
7814
|
+
## openAIResponseFormat()
|
|
7815
|
+
|
|
7816
|
+
### Overview
|
|
7817
|
+
|
|
7818
|
+
Maps a normalized structured-output choice to chat-completion wire fields.
|
|
7819
|
+
|
|
7820
|
+
### Signature and result
|
|
7821
|
+
|
|
7822
|
+
```text
|
|
7823
|
+
openAIResponseFormat(format)
|
|
7824
|
+
```
|
|
7825
|
+
|
|
7826
|
+
`'json'` becomes `{type:'json_object'}`; a supplied schema becomes
|
|
7827
|
+
`{type:'json_schema',json_schema:{name:'structured_response',strict:true,schema:format}}`.
|
|
7828
|
+
A disabled format returns null. Use `structuredOutputFormat()` first to
|
|
7829
|
+
normalize the public option; this mapper does not validate schema contents.
|
|
7830
|
+
|
|
7831
|
+
### Availability and normalization
|
|
7832
|
+
|
|
7833
|
+
**Node and Browser.** Synchronous helper exported from
|
|
7834
|
+
`arcane-os/ai/twin-cloud`, shared with browser AI and performing no I/O.
|
|
7835
|
+
|
|
7836
|
+
### Example
|
|
7837
|
+
|
|
7838
|
+
```javascript
|
|
7839
|
+
import {structuredOutputFormat, openAIResponseFormat} from 'arcane-os/ai/twin-cloud';
|
|
7840
|
+
|
|
7841
|
+
const responseFormat = openAIResponseFormat(structuredOutputFormat(true));
|
|
7842
|
+
// {type: 'json_object'}
|
|
7843
|
+
```
|
|
7844
|
+
|
|
7845
|
+
## isAIRequestAbort()
|
|
7846
|
+
|
|
7847
|
+
### Overview
|
|
7848
|
+
|
|
7849
|
+
Recognizes the existing AI request cancellation forms.
|
|
7850
|
+
|
|
7851
|
+
### Signature and result
|
|
7852
|
+
|
|
7853
|
+
```text
|
|
7854
|
+
isAIRequestAbort(error,signal)
|
|
7855
|
+
```
|
|
7856
|
+
|
|
7857
|
+
Returns a boolean: true when the signal is aborted, the error name is
|
|
7858
|
+
`AbortError`, or its code is `ARCANE_REQUEST_ABORTED`,
|
|
7859
|
+
`ARCANE_AI_REQUEST_ABORTED`, or `AI_REQUEST_ABORTED`.
|
|
7860
|
+
|
|
7861
|
+
### Availability and normalization
|
|
7862
|
+
|
|
7863
|
+
**Node and Browser.** Synchronous helper exported from
|
|
7864
|
+
`arcane-os/ai/twin-cloud`; it neither changes nor aborts the supplied operation.
|
|
7865
|
+
|
|
7866
|
+
### Example
|
|
7867
|
+
|
|
7868
|
+
```javascript
|
|
7869
|
+
import {isAIRequestAbort} from 'arcane-os/ai/twin-cloud';
|
|
7870
|
+
|
|
7871
|
+
const controller = new AbortController();
|
|
7872
|
+
controller.abort();
|
|
7873
|
+
const cancelled = isAIRequestAbort(undefined, controller.signal);
|
|
7874
|
+
```
|
|
7875
|
+
|
|
7876
|
+
## normalizeAIRequestAbort()
|
|
7877
|
+
|
|
7878
|
+
### Overview
|
|
7879
|
+
|
|
7880
|
+
Keeps cancellation under the shared AI error code while preserving its cause.
|
|
7881
|
+
|
|
7882
|
+
### Signature and result
|
|
7883
|
+
|
|
7884
|
+
```text
|
|
7885
|
+
normalizeAIRequestAbort(error)
|
|
7886
|
+
```
|
|
7887
|
+
|
|
7888
|
+
An existing `ARCANE_AI_REQUEST_ABORTED` error is returned unchanged. Otherwise
|
|
7889
|
+
the result is an Error named `AbortError`, with code
|
|
7890
|
+
`ARCANE_AI_REQUEST_ABORTED`, message `The AI request was cancelled.`, and the
|
|
7891
|
+
supplied value as its cause. The helper returns the error; it does not throw it.
|
|
7892
|
+
|
|
7893
|
+
### Availability and normalization
|
|
7894
|
+
|
|
7895
|
+
**Node and Browser.** Synchronous helper exported from
|
|
7896
|
+
`arcane-os/ai/twin-cloud`; creates no request, storage, or user state.
|
|
7897
|
+
|
|
7898
|
+
### Example
|
|
7899
|
+
|
|
7900
|
+
```javascript
|
|
7901
|
+
import {normalizeAIRequestAbort} from 'arcane-os/ai/twin-cloud';
|
|
7902
|
+
|
|
7903
|
+
const cancelled = normalizeAIRequestAbort(new DOMException('Cancelled', 'AbortError'));
|
|
7904
|
+
```
|
|
7905
|
+
|
|
7639
7906
|
## Data export subpaths
|
|
7640
7907
|
|
|
7641
7908
|
The package also exposes eight JSON Schemas (including `arcane-os/schemas/event-stack.json`) and its package manifest. These are data contracts, not callable JavaScript members. See [schema contracts](../architecture.md) and the files under `schemas/`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"./speech-text": "./browser-runtime/speech-text.mjs",
|
|
39
39
|
"./ai/browser-wasm": "./browser-runtime/ai/browser-wasm.mjs",
|
|
40
40
|
"./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
|
|
41
|
+
"./ai/twin-cloud": "./browser-runtime/ai/twin-cloud.mjs",
|
|
41
42
|
"./ai/browser-speech": "./browser-runtime/ai/browser-speech.mjs",
|
|
42
43
|
"./mail": "./src/mail-api.mjs",
|
|
43
44
|
"./testing": "./src/testing.mjs",
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
86
87
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
87
88
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
88
|
-
"test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
|
|
89
|
+
"test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs",
|
|
89
90
|
"test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
90
91
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
91
92
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -16,6 +16,14 @@ import {
|
|
|
16
16
|
} from './AIProviderRuntime.js';
|
|
17
17
|
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
18
18
|
import {arcaneLogging} from 'arcane-os/logging';
|
|
19
|
+
import {
|
|
20
|
+
fetchHTTPResponse,
|
|
21
|
+
fetchJSONResponse,
|
|
22
|
+
isAIRequestAbort,
|
|
23
|
+
normalizeAIRequestAbort,
|
|
24
|
+
structuredOutputFormat as normalizeStructuredOutput,
|
|
25
|
+
openAIResponseFormat
|
|
26
|
+
} from 'arcane-os/ai/twin-cloud';
|
|
19
27
|
import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
|
|
20
28
|
import {prepareSpeech} from './PreparedSpeech.js';
|
|
21
29
|
import {createToolTextObserver} from 'arcane-os/ai/tool-text-stream';
|
|
@@ -121,24 +129,6 @@ function aiInitializationError(code,reason,message){
|
|
|
121
129
|
return error;
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
function isAIRequestAbort(error,signal){
|
|
125
|
-
return signal?.aborted
|
|
126
|
-
||error?.name==='AbortError'
|
|
127
|
-
||error?.code==='ARCANE_REQUEST_ABORTED'
|
|
128
|
-
||error?.code==='ARCANE_AI_REQUEST_ABORTED'
|
|
129
|
-
||error?.code==='AI_REQUEST_ABORTED';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function normalizeAIRequestAbort(error){
|
|
133
|
-
if(error?.code==='ARCANE_AI_REQUEST_ABORTED'){
|
|
134
|
-
return error;
|
|
135
|
-
}
|
|
136
|
-
const normalized=new Error('The AI request was cancelled.',{cause:error});
|
|
137
|
-
normalized.name='AbortError';
|
|
138
|
-
normalized.code='ARCANE_AI_REQUEST_ABORTED';
|
|
139
|
-
return normalized;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
132
|
function normalizeAIReasoningEffort(value){
|
|
143
133
|
if(value===undefined||value===null||value===''){
|
|
144
134
|
return '';
|
|
@@ -3776,69 +3766,6 @@ class AI {
|
|
|
3776
3766
|
return true;
|
|
3777
3767
|
}
|
|
3778
3768
|
|
|
3779
|
-
async #fetchHTTPResponse(url,options){
|
|
3780
|
-
const {signal}=options;
|
|
3781
|
-
const retryDelayMs=3000;
|
|
3782
|
-
try{
|
|
3783
|
-
while(true){
|
|
3784
|
-
if(signal?.aborted){
|
|
3785
|
-
throw normalizeAIRequestAbort(signal.reason);
|
|
3786
|
-
}
|
|
3787
|
-
const response=await fetch(url,options);
|
|
3788
|
-
if(signal?.aborted){
|
|
3789
|
-
throw normalizeAIRequestAbort(signal.reason);
|
|
3790
|
-
}
|
|
3791
|
-
if(response.ok){
|
|
3792
|
-
return response;
|
|
3793
|
-
}
|
|
3794
|
-
|
|
3795
|
-
const contentType=response.headers.get('content-type')||'';
|
|
3796
|
-
const error=contentType.includes('application/json')
|
|
3797
|
-
?await response.json()
|
|
3798
|
-
:await response.text();
|
|
3799
|
-
if(signal?.aborted){
|
|
3800
|
-
throw normalizeAIRequestAbort(signal.reason);
|
|
3801
|
-
}
|
|
3802
|
-
const message=is.string(error)
|
|
3803
|
-
?error
|
|
3804
|
-
:error?.error?.message??error?.message;
|
|
3805
|
-
if(
|
|
3806
|
-
response.status!==429
|
|
3807
|
-
||!is.string(message)
|
|
3808
|
-
||!message.toLowerCase().includes('overload')
|
|
3809
|
-
){
|
|
3810
|
-
throw error;
|
|
3811
|
-
}
|
|
3812
|
-
|
|
3813
|
-
arcaneLogging.warn(
|
|
3814
|
-
`${message}\nRetrying in ${retryDelayMs / 1000} seconds`,
|
|
3815
|
-
error
|
|
3816
|
-
);
|
|
3817
|
-
await new Promise(function waitForOverloadRetry(resolve,reject){
|
|
3818
|
-
function finishRetryDelay(){
|
|
3819
|
-
signal?.removeEventListener('abort',cancelRetryDelay);
|
|
3820
|
-
resolve();
|
|
3821
|
-
}
|
|
3822
|
-
function cancelRetryDelay(){
|
|
3823
|
-
clearTimeout(timer);
|
|
3824
|
-
signal.removeEventListener('abort',cancelRetryDelay);
|
|
3825
|
-
reject(normalizeAIRequestAbort(signal.reason));
|
|
3826
|
-
}
|
|
3827
|
-
const timer=setTimeout(finishRetryDelay,retryDelayMs);
|
|
3828
|
-
signal?.addEventListener('abort',cancelRetryDelay,{once:true});
|
|
3829
|
-
if(signal?.aborted){
|
|
3830
|
-
cancelRetryDelay();
|
|
3831
|
-
}
|
|
3832
|
-
});
|
|
3833
|
-
}
|
|
3834
|
-
}catch(error){
|
|
3835
|
-
if(isAIRequestAbort(error,signal)){
|
|
3836
|
-
throw normalizeAIRequestAbort(error);
|
|
3837
|
-
}
|
|
3838
|
-
throw error;
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
3841
|
-
|
|
3842
3769
|
#nativeOllama(){
|
|
3843
3770
|
const client=globalThis.Arcane?.ollama;
|
|
3844
3771
|
|
|
@@ -4171,48 +4098,6 @@ class AI {
|
|
|
4171
4098
|
];
|
|
4172
4099
|
}
|
|
4173
4100
|
|
|
4174
|
-
#structuredOutputFormat(value=false){
|
|
4175
|
-
if(value===false||value===null||value===undefined){
|
|
4176
|
-
return null;
|
|
4177
|
-
}
|
|
4178
|
-
if(value===true||value==='json'){
|
|
4179
|
-
return 'json';
|
|
4180
|
-
}
|
|
4181
|
-
if(
|
|
4182
|
-
is.object(value)
|
|
4183
|
-
&&!is.array(value)
|
|
4184
|
-
&&(
|
|
4185
|
-
Object.getPrototypeOf(value)===Object.prototype
|
|
4186
|
-
||Object.getPrototypeOf(value)===null
|
|
4187
|
-
)
|
|
4188
|
-
){
|
|
4189
|
-
return value;
|
|
4190
|
-
}
|
|
4191
|
-
|
|
4192
|
-
const error=new TypeError(
|
|
4193
|
-
'AI structured output must be enabled with true, json, or a JSON Schema object.'
|
|
4194
|
-
);
|
|
4195
|
-
error.code='AI_STRUCTURED_OUTPUT_INVALID';
|
|
4196
|
-
throw error;
|
|
4197
|
-
}
|
|
4198
|
-
|
|
4199
|
-
#openAIResponseFormat(structuredOutputFormat){
|
|
4200
|
-
if(structuredOutputFormat==='json'){
|
|
4201
|
-
return {type:'json_object'};
|
|
4202
|
-
}
|
|
4203
|
-
if(structuredOutputFormat){
|
|
4204
|
-
return {
|
|
4205
|
-
type:'json_schema',
|
|
4206
|
-
json_schema:{
|
|
4207
|
-
name:'structured_response',
|
|
4208
|
-
strict:true,
|
|
4209
|
-
schema:structuredOutputFormat
|
|
4210
|
-
}
|
|
4211
|
-
};
|
|
4212
|
-
}
|
|
4213
|
-
return null;
|
|
4214
|
-
}
|
|
4215
|
-
|
|
4216
4101
|
async #reportRequest(requestHandler,request,id,metadata){
|
|
4217
4102
|
if(!is.function(requestHandler)){
|
|
4218
4103
|
throw new TypeError('AI onRequest callback must be a function.');
|
|
@@ -4762,7 +4647,7 @@ class AI {
|
|
|
4762
4647
|
if(signal?.aborted){
|
|
4763
4648
|
throw normalizeAIRequestAbort();
|
|
4764
4649
|
}
|
|
4765
|
-
const structuredOutputFormat=
|
|
4650
|
+
const structuredOutputFormat=normalizeStructuredOutput(
|
|
4766
4651
|
structuredOutput
|
|
4767
4652
|
);
|
|
4768
4653
|
|
|
@@ -4776,7 +4661,7 @@ class AI {
|
|
|
4776
4661
|
}
|
|
4777
4662
|
|
|
4778
4663
|
if(structuredOutputFormat){
|
|
4779
|
-
request.response_format=
|
|
4664
|
+
request.response_format=openAIResponseFormat(
|
|
4780
4665
|
structuredOutputFormat
|
|
4781
4666
|
);
|
|
4782
4667
|
}
|
|
@@ -4970,7 +4855,7 @@ class AI {
|
|
|
4970
4855
|
destination:this.url
|
|
4971
4856
|
});
|
|
4972
4857
|
const body = JSON.stringify(request);
|
|
4973
|
-
const response=await
|
|
4858
|
+
const response=await fetchHTTPResponse(
|
|
4974
4859
|
this.url,
|
|
4975
4860
|
{
|
|
4976
4861
|
method:'POST',
|
|
@@ -5622,7 +5507,7 @@ class AI {
|
|
|
5622
5507
|
if(signal?.aborted){
|
|
5623
5508
|
throw normalizeAIRequestAbort();
|
|
5624
5509
|
}
|
|
5625
|
-
const structuredOutputFormat=
|
|
5510
|
+
const structuredOutputFormat=normalizeStructuredOutput(structuredOutput);
|
|
5626
5511
|
|
|
5627
5512
|
const normalizedReasoningEffort=normalizeAIReasoningEffort(
|
|
5628
5513
|
reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
|
|
@@ -5634,7 +5519,7 @@ class AI {
|
|
|
5634
5519
|
}
|
|
5635
5520
|
|
|
5636
5521
|
if(structuredOutputFormat){
|
|
5637
|
-
request.response_format=
|
|
5522
|
+
request.response_format=openAIResponseFormat(
|
|
5638
5523
|
structuredOutputFormat
|
|
5639
5524
|
);
|
|
5640
5525
|
}
|
|
@@ -5718,7 +5603,7 @@ class AI {
|
|
|
5718
5603
|
destination:this.url
|
|
5719
5604
|
});
|
|
5720
5605
|
const body = JSON.stringify(request);
|
|
5721
|
-
const
|
|
5606
|
+
const responseJSON=await fetchJSONResponse(
|
|
5722
5607
|
this.url,
|
|
5723
5608
|
{
|
|
5724
5609
|
method:'POST',
|
|
@@ -5729,36 +5614,11 @@ class AI {
|
|
|
5729
5614
|
}
|
|
5730
5615
|
);
|
|
5731
5616
|
|
|
5732
|
-
const contentType=response.headers.get('content-type')||'';
|
|
5733
|
-
|
|
5734
|
-
if(!contentType.includes('application/json')){
|
|
5735
|
-
throw new TypeError(
|
|
5736
|
-
`AI request returned ${contentType||'an unknown content type'} instead of JSON.`
|
|
5737
|
-
);
|
|
5738
|
-
}
|
|
5739
|
-
|
|
5740
|
-
let responseJSON;
|
|
5741
|
-
try{
|
|
5742
|
-
responseJSON=await response.json();
|
|
5743
|
-
}catch(error){
|
|
5744
|
-
if(isAIRequestAbort(error,signal)){
|
|
5745
|
-
throw normalizeAIRequestAbort(error);
|
|
5746
|
-
}
|
|
5747
|
-
throw error;
|
|
5748
|
-
}
|
|
5749
5617
|
if(signal?.aborted){
|
|
5750
5618
|
throw normalizeAIRequestAbort();
|
|
5751
5619
|
}
|
|
5752
|
-
|
|
5753
|
-
if(!response.id){
|
|
5754
|
-
response.id=id;
|
|
5755
|
-
}
|
|
5756
|
-
|
|
5757
|
-
//console.log(responseJSON);
|
|
5758
|
-
//async
|
|
5759
5620
|
normalizeAICompletionToolCalls(responseJSON);
|
|
5760
5621
|
await responseHandler(responseJSON,id,false);
|
|
5761
|
-
//sync
|
|
5762
5622
|
return responseJSON;
|
|
5763
5623
|
}
|
|
5764
5624
|
|
package/src/import-map.mjs
CHANGED
|
@@ -47,6 +47,7 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
|
|
|
47
47
|
['arcane-os/speech-text','sdk/speech-text.mjs'],
|
|
48
48
|
['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
|
|
49
49
|
['arcane-os/ai/tool-text-stream','sdk/ai/tool-text-stream.mjs'],
|
|
50
|
+
['arcane-os/ai/twin-cloud','sdk/ai/twin-cloud.mjs'],
|
|
50
51
|
['arcane-os/ai/browser-speech',SDK_BROWSER_SPEECH_ENTRY]
|
|
51
52
|
]);
|
|
52
53
|
function fail(message,code='ARCANE_IMPORT_MAP_INVALID'){
|