arcane-os 0.33.4 → 0.33.5
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 +14 -0
- package/README.md +2 -2
- package/browser-runtime/ai/twin-cloud.mjs +99 -41
- package/docs/reference/ai/twin-cloud.md +24 -7
- package/docs/reference/availability-and-normalization.md +1 -1
- package/docs/reference/runtime-components.md +23 -6
- package/docs/reference/runtime-modules.md +32 -5
- package/package.json +2 -2
- package/runtime/arcane/components/chat.html +105 -18
- package/runtime/arcane/components/modal.html +30 -5
- package/runtime/arcane/modules/AI.js +27 -14
- package/runtime/arcane/modules/AIProviderRuntime.js +14 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.33.5
|
|
4
|
+
|
|
5
|
+
- Deliver the first visible streamed chat chunk to speech exactly once, without
|
|
6
|
+
waiting for speech playback or changing response order.
|
|
7
|
+
- Keep a following transcript at its actual bottom through message and composer
|
|
8
|
+
layout changes while preserving a reader's deliberate scroll-away choice.
|
|
9
|
+
- Retry pre-response Fetch failures and HTTP 529 up to three times after a
|
|
10
|
+
three-second delay. Preserve existing HTTP 429 overload retries, cancellation,
|
|
11
|
+
complete request content, and terminal errors without replaying streams or tools.
|
|
12
|
+
- Add observational `onRetry` waiting/requesting notifications to portable TWiN
|
|
13
|
+
and shared AI request APIs, including queued built-in provider requests.
|
|
14
|
+
- Let modal task jobs report complete transient progress to their own row through
|
|
15
|
+
`task(reportProgress)`, preserving parallel work and settled results.
|
|
16
|
+
|
|
3
17
|
## 0.33.4
|
|
4
18
|
|
|
5
19
|
- Treat a repeated reader `AbortError` during intentional HTTP tool-text stream
|
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 share the theme, packaging, event, cancellation,
|
|
20
20
|
and browser run contracts while retaining their selected application layout.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.33.
|
|
22
|
+
This checkout defines the `0.33.5` 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
|
|
|
@@ -85,7 +85,7 @@ Create one browser application, install its pinned SDK, and start its source
|
|
|
85
85
|
server:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
npx arcane-os@0.33.
|
|
88
|
+
npx arcane-os@0.33.5 new hello-speech --path ./hello-speech --target browser
|
|
89
89
|
cd hello-speech
|
|
90
90
|
npm install
|
|
91
91
|
```
|
|
@@ -17,7 +17,8 @@ export async function fetchRequest({
|
|
|
17
17
|
signal = null,
|
|
18
18
|
id = Date.now(),
|
|
19
19
|
onRequest = function observeTWiNRequest(){},
|
|
20
|
-
onResponse = function observeTWiNResponse(){}
|
|
20
|
+
onResponse = function observeTWiNResponse(){},
|
|
21
|
+
onRetry = null
|
|
21
22
|
} = {}){
|
|
22
23
|
if(signal?.aborted){
|
|
23
24
|
throw normalizeAIRequestAbort(signal.reason);
|
|
@@ -67,7 +68,8 @@ export async function fetchRequest({
|
|
|
67
68
|
},
|
|
68
69
|
body:JSON.stringify(request),
|
|
69
70
|
...(signal ? {signal} : {})
|
|
70
|
-
}
|
|
71
|
+
},
|
|
72
|
+
{onRetry}
|
|
71
73
|
);
|
|
72
74
|
if(signal?.aborted){
|
|
73
75
|
throw normalizeAIRequestAbort(signal.reason);
|
|
@@ -141,72 +143,128 @@ export function openAIResponseFormat(format){
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
/** Shared with browser streaming; consume no successful body at this boundary. */
|
|
144
|
-
export async function fetchHTTPResponse(url, options){
|
|
146
|
+
export async function fetchHTTPResponse(url, options, {onRetry = null} = {}) {
|
|
145
147
|
const {signal} = options;
|
|
146
148
|
const retryDelayMs = 3000;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
const maxRecoveryRetries = 3;
|
|
150
|
+
let recoveryRetries = 0;
|
|
151
|
+
let attempt = 0;
|
|
152
|
+
try {
|
|
153
|
+
while (true) {
|
|
154
|
+
if (signal?.aborted) {
|
|
150
155
|
throw normalizeAIRequestAbort(signal.reason);
|
|
151
156
|
}
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
let response;
|
|
158
|
+
let error;
|
|
159
|
+
try {
|
|
160
|
+
response = await fetch(url, options);
|
|
161
|
+
} catch (fetchError) {
|
|
162
|
+
if (isAIRequestAbort(fetchError, signal)) {
|
|
163
|
+
throw normalizeAIRequestAbort(fetchError);
|
|
164
|
+
}
|
|
165
|
+
error = fetchError;
|
|
166
|
+
}
|
|
167
|
+
if (signal?.aborted) {
|
|
154
168
|
throw normalizeAIRequestAbort(signal.reason);
|
|
155
169
|
}
|
|
156
|
-
if(response
|
|
170
|
+
if (response?.ok) {
|
|
157
171
|
return response;
|
|
158
172
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
if (response) {
|
|
174
|
+
const contentType = response.headers.get('content-type') || '';
|
|
175
|
+
error = contentType.includes('application/json')
|
|
176
|
+
? await response.json()
|
|
177
|
+
: await response.text();
|
|
178
|
+
if (signal?.aborted) {
|
|
179
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
180
|
+
}
|
|
165
181
|
}
|
|
182
|
+
const status = response?.status ?? null;
|
|
166
183
|
const message = is.string(error)
|
|
167
184
|
? error
|
|
168
185
|
: error?.error?.message ?? error?.message;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
186
|
+
const overload = status === 429
|
|
187
|
+
&& is.string(message)
|
|
188
|
+
&& message.toLowerCase().includes('overload');
|
|
189
|
+
if (!overload) {
|
|
190
|
+
if ((status !== null && status !== 529)
|
|
191
|
+
|| recoveryRetries >= maxRecoveryRetries) {
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
recoveryRetries += 1;
|
|
175
195
|
}
|
|
196
|
+
attempt += 1;
|
|
176
197
|
arcaneLogging.warn(
|
|
177
|
-
`${message}\nRetrying in ${retryDelayMs / 1000} seconds`,
|
|
198
|
+
`${message ?? 'AI request transport failed.'}\nRetrying in ${retryDelayMs / 1000} seconds`,
|
|
178
199
|
error
|
|
179
200
|
);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
observeRequestRetry(
|
|
202
|
+
onRetry,
|
|
203
|
+
{phase: 'waiting', attempt, delayMs: retryDelayMs, status, error}
|
|
204
|
+
);
|
|
205
|
+
await new Promise(
|
|
206
|
+
function waitForRequestRetry(resolve, reject) {
|
|
207
|
+
function finishRetryDelay() {
|
|
208
|
+
signal?.removeEventListener('abort', cancelRetryDelay);
|
|
209
|
+
resolve();
|
|
210
|
+
}
|
|
211
|
+
function cancelRetryDelay() {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
signal.removeEventListener('abort', cancelRetryDelay);
|
|
214
|
+
reject(normalizeAIRequestAbort(signal.reason));
|
|
215
|
+
}
|
|
216
|
+
const timer = setTimeout(finishRetryDelay, retryDelayMs);
|
|
217
|
+
signal?.addEventListener(
|
|
218
|
+
'abort',
|
|
219
|
+
cancelRetryDelay,
|
|
220
|
+
{once: true}
|
|
221
|
+
);
|
|
222
|
+
if (signal?.aborted) {
|
|
223
|
+
cancelRetryDelay();
|
|
224
|
+
}
|
|
194
225
|
}
|
|
195
|
-
|
|
226
|
+
);
|
|
227
|
+
if (signal?.aborted) {
|
|
228
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
229
|
+
}
|
|
230
|
+
observeRequestRetry(
|
|
231
|
+
onRetry,
|
|
232
|
+
{phase: 'requesting', attempt, delayMs: retryDelayMs, status, error}
|
|
233
|
+
);
|
|
196
234
|
}
|
|
197
|
-
}catch(error){
|
|
198
|
-
if(isAIRequestAbort(error, signal)){
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (isAIRequestAbort(error, signal)) {
|
|
199
237
|
throw normalizeAIRequestAbort(error);
|
|
200
238
|
}
|
|
201
239
|
throw error;
|
|
202
240
|
}
|
|
203
241
|
}
|
|
204
242
|
|
|
243
|
+
function observeRequestRetry(onRetry, state) {
|
|
244
|
+
if (!is.function(onRetry)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
function reportRetryObserverFailure(error) {
|
|
248
|
+
arcaneLogging.error('Arcane AI retry observer failed.', error);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
Promise.resolve(
|
|
252
|
+
onRetry(state)
|
|
253
|
+
).catch(reportRetryObserverFailure);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
reportRetryObserverFailure(error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
205
259
|
/** Return the entire parsed completion without selecting or rewriting choices. */
|
|
206
|
-
export async function fetchJSONResponse(url, options){
|
|
260
|
+
export async function fetchJSONResponse(url, options, {onRetry = null} = {}) {
|
|
207
261
|
const {signal} = options;
|
|
208
262
|
try{
|
|
209
|
-
const response = await fetchHTTPResponse(
|
|
263
|
+
const response = await fetchHTTPResponse(
|
|
264
|
+
url,
|
|
265
|
+
options,
|
|
266
|
+
{onRetry}
|
|
267
|
+
);
|
|
210
268
|
if(signal?.aborted){
|
|
211
269
|
throw normalizeAIRequestAbort(signal.reason);
|
|
212
270
|
}
|
|
@@ -75,9 +75,15 @@ The key is
|
|
|
75
75
|
transport authentication, not part of either callback's message payload. Keep
|
|
76
76
|
credentials out of application logging as well.
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
Before a successful response is consumed, a rejected Fetch or HTTP `529` can
|
|
79
|
+
retry up to three times, waiting `3000` milliseconds before each retry. These
|
|
80
|
+
two failure types share that three-retry budget. HTTP `429` with a message
|
|
81
|
+
containing `overload` (case-insensitive) retains its unlimited three-second
|
|
82
|
+
retry behavior and does not consume that budget. Each attempt reuses the exact
|
|
83
|
+
destination, Fetch options, serialized request, and signal. Other HTTP errors,
|
|
84
|
+
response decoding failures, and application callback failures do not retry.
|
|
85
|
+
The last complete failure is thrown unchanged when recovery is exhausted.
|
|
86
|
+
Pass a fresh
|
|
81
87
|
`AbortController`'s `signal` and call `abort()` to cancel. Cancellation during
|
|
82
88
|
the request, response-body read, retry wait, or callback settlement prevents
|
|
83
89
|
successful result delivery and rejects with `ARCANE_AI_REQUEST_ABORTED`.
|
|
@@ -86,6 +92,17 @@ A missing key uses `AI_PROVIDER_NOT_CONFIGURED`, a missing explicit model
|
|
|
86
92
|
throws `TypeError`, and an unsupported `structuredOutput` input uses
|
|
87
93
|
`AI_STRUCTURED_OUTPUT_INVALID`.
|
|
88
94
|
|
|
95
|
+
Optional `onRetry({phase,attempt,delayMs,status,error})` observes each retry.
|
|
96
|
+
`phase:'waiting'` arrives before the abortable delay; `phase:'requesting'`
|
|
97
|
+
arrives immediately before the next Fetch. `attempt` is the one-based upcoming
|
|
98
|
+
retry number across both retry policies, `delayMs` is `3000`, `status` is the
|
|
99
|
+
HTTP status or `null` for a rejected Fetch, and `error` is the complete original
|
|
100
|
+
failure value. This observer runs synchronously without awaiting its returned
|
|
101
|
+
promise. Synchronous throws and rejected promises are reported through the
|
|
102
|
+
shared console logger and cannot change the request outcome or trigger another
|
|
103
|
+
retry. `onRequest` still runs once per logical request. Retry observations stay
|
|
104
|
+
outside the model payload and retained history.
|
|
105
|
+
|
|
89
106
|
The SDK retains no request or response history between calls and uses no
|
|
90
107
|
DBOPFS, chat entity, or memory extraction. Each call's `messages` are its
|
|
91
108
|
complete caller-supplied context. The caller decides whether and how to keep
|
|
@@ -104,8 +121,8 @@ the existing implementation rather than maintain another retry or body reader.
|
|
|
104
121
|
|
|
105
122
|
| Export | Contract |
|
|
106
123
|
| --- | --- |
|
|
107
|
-
| `fetchHTTPResponse(url,options)` | Uses the caller's Fetch options
|
|
108
|
-
| `fetchJSONResponse(url,options)` | Uses that HTTP owner, requires `application/json`, and returns the complete parsed body without selecting choices. |
|
|
124
|
+
| `fetchHTTPResponse(url,options,{onRetry=null}={})` | Uses the caller's exact Fetch options and shared retry/cancellation behavior; returns a successful `Response` with its body unconsumed. The separate optional control observes retries without entering Fetch options. |
|
|
125
|
+
| `fetchJSONResponse(url,options,{onRetry=null}={})` | Uses that HTTP owner and retry observer, requires `application/json`, and returns the complete parsed body without selecting choices. |
|
|
109
126
|
| `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
127
|
| `openAIResponseFormat(format)` | Maps the normalized value to `json_object`, strict `json_schema` named `structured_response`, or null. |
|
|
111
128
|
| `isAIRequestAbort(error,signal)` | Recognizes an aborted signal, `AbortError`, or the existing Arcane AI/request cancellation codes. |
|
|
@@ -113,8 +130,8 @@ the existing implementation rather than maintain another retry or body reader.
|
|
|
113
130
|
|
|
114
131
|
These helpers start no work on import. HTTP helpers require explicit URL and
|
|
115
132
|
options; they do not add a key, model, browser state, or retained conversation.
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
Retry warnings and observer failures use the shared console logger and preserve
|
|
134
|
+
the complete original error.
|
|
118
135
|
|
|
119
136
|
## Existing browser AI interface
|
|
120
137
|
|
|
@@ -78,7 +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.
|
|
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. Rejected Fetch and HTTP 529 share three retries after 3000 ms; HTTP 429 overload retains unlimited three-second retries. Optional `onRetry` reports waiting/requesting phases without blocking recovery. Shared structured-output mapping and cancellation match browser TWiN transport. No browser profile, AI/user singleton, DBOPFS, or retained request history is created. |
|
|
82
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. |
|
|
83
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. |
|
|
84
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. |
|
|
@@ -339,8 +339,12 @@ suppressed.
|
|
|
339
339
|
module or an existing compatible session, restores the UI transcript when
|
|
340
340
|
available, and uses provider-safe history as its restoration fallback. The
|
|
341
341
|
transcript is a masked vertical scroll viewport; status and composer remain
|
|
342
|
-
outside it. Initial restoration
|
|
343
|
-
progress,
|
|
342
|
+
outside it. Initial restoration follows the true bottom. User, assistant, tool,
|
|
343
|
+
streaming, progress, and failure updates continue following it, including later
|
|
344
|
+
message-card, status, composer, or viewport resizing, until the reader scrolls
|
|
345
|
+
toward earlier content. Scrolling down to the bottom resumes following. Layout
|
|
346
|
+
clamping, hidden views, and BFCache restoration preserve that choice;
|
|
347
|
+
destruction releases scroll observation. Each
|
|
344
348
|
restored or new message uses a separate semantic `<time>` element at the card's
|
|
345
349
|
lower-right with an ISO `datetime`, full local title, and local 24-hour `HH:MM`
|
|
346
350
|
text.
|
|
@@ -475,10 +479,13 @@ terminal result of the segments submitted by one invocation; Chat continues
|
|
|
475
479
|
to feed chunks without waiting for playback. Runtime mute, cancellation,
|
|
476
480
|
permission waiting, and stale generations remain non-errors.
|
|
477
481
|
|
|
478
|
-
Each visible model chunk
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
+
Each visible model chunk, including the first chunk that creates its assistant
|
|
483
|
+
card, renders once in arrival order. When speech is ready and unmuted, each
|
|
484
|
+
chunk also reaches `AI.streamTTS(text)` once. Card creation is synchronous, so
|
|
485
|
+
consecutive chunks retain their original order without waiting for speech
|
|
486
|
+
preparation or playback. Thinking content remains outside speech forwarding.
|
|
487
|
+
The AI owner automatically removes repeated formatting marks from an outbound
|
|
488
|
+
speech-input copy before the configured segmentation, admits completed segments to bounded synthesis
|
|
482
489
|
immediately, and schedules the contiguous ready audio prefix on the audio
|
|
483
490
|
clock. Chat retains the original Markdown for display and storage and owns no
|
|
484
491
|
second speech queue. No app option selects or disables this cleanup. Single
|
|
@@ -988,6 +995,16 @@ to `true`; `destroy()` releases the component. Product-specific conditions for
|
|
|
988
995
|
closing a modal belong to the application. Content, actions and task results
|
|
989
996
|
are preserved, and `runTasks()` continues to execute independent jobs concurrently.
|
|
990
997
|
|
|
998
|
+
`runTasks(title, jobs)` calls each `job.task(reportProgress)` concurrently.
|
|
999
|
+
The optional callback accepts complete plain-text status for that pending job
|
|
1000
|
+
only and returns `true` when displayed. For example, a request can report its
|
|
1001
|
+
retry delay without replacing another job's progress. Reports after that job
|
|
1002
|
+
settles or the modal is destroyed return `false` and change no UI. A display
|
|
1003
|
+
failure is logged without rejecting the task. Existing zero-argument task
|
|
1004
|
+
functions remain supported, results retain `Promise.allSettled` order, and
|
|
1005
|
+
destruction does not cancel the application-owned work. Progress is transient
|
|
1006
|
+
display content, never a task result or conversation record.
|
|
1007
|
+
|
|
991
1008
|
Events: `modal-ready`, `modal-opened`, `modal-closed`, `modal-action`.
|
|
992
1009
|
|
|
993
1010
|
Slots: `header`, `body`, `footer`.
|
|
@@ -166,7 +166,8 @@ provider implementations; neither form is a retired compatibility API.
|
|
|
166
166
|
Built-in cloud chat decodes an HTTP error body once as JSON or text and rejects
|
|
167
167
|
with that complete value unchanged. It does not reconstruct an Error, replace
|
|
168
168
|
the message, or add `providerMessage`, `status`, or an SDK failure code to the
|
|
169
|
-
provider body. Network
|
|
169
|
+
provider body. Network errors pass through after the retry policy below;
|
|
170
|
+
decoding errors pass through immediately. Cancellation
|
|
170
171
|
retains the existing `ARCANE_AI_REQUEST_ABORTED` contract.
|
|
171
172
|
|
|
172
173
|
When the HTTP status is `429` and the existing `error.message`, `message`, or
|
|
@@ -178,8 +179,26 @@ the same destination, headers, complete serialized body, and cancellation
|
|
|
178
179
|
signal; `onRequest` runs once for the logical request. Cancellation stops the
|
|
179
180
|
delay and prevents another attempt. Retrying happens before a successful
|
|
180
181
|
response is consumed, so partial streams and tool callbacks are never replayed.
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
Rejected Fetch calls and HTTP `529` share a separate budget of three retries,
|
|
183
|
+
each after the same `3000` millisecond delay. HTTP `429` overload retries do not
|
|
184
|
+
consume that budget. The final complete network error or provider body passes
|
|
185
|
+
through unchanged when recovery is exhausted. Aborts, other HTTP errors,
|
|
186
|
+
successful-response body reads, stream decoding, and application callbacks
|
|
187
|
+
never start another attempt.
|
|
188
|
+
|
|
189
|
+
`fetchRequest()` and `streamRequest()` accept
|
|
190
|
+
`onRetry({phase,attempt,delayMs,status,error})`. The observer receives
|
|
191
|
+
`phase:'waiting'` before the abortable delay and `phase:'requesting'` immediately
|
|
192
|
+
before the next Fetch. `attempt` is the one-based upcoming retry number across
|
|
193
|
+
both retry policies; `delayMs` is `3000`; `status` is the HTTP status or `null`
|
|
194
|
+
for rejected Fetch; `error` is the complete original failure value. The SDK
|
|
195
|
+
invokes this observational callback synchronously and observes a returned
|
|
196
|
+
promise without awaiting it. Callback throws and rejections are logged through
|
|
197
|
+
the shared console owner without changing request success or causing retries.
|
|
198
|
+
The callback remains outside model payloads and is forwarded through built-in
|
|
199
|
+
provider controls, including queued requests, with the request owner's existing
|
|
200
|
+
cancellation lifetime. Native Ollama and externally supplied provider adapters
|
|
201
|
+
retain their own transport behavior.
|
|
183
202
|
|
|
184
203
|
Initialization uses the canonical realm user's actual readiness state. If
|
|
185
204
|
`window.user?.ready` is already true, AI initializes immediately. Otherwise one
|
|
@@ -472,8 +491,10 @@ cleanup errors remain visible in developer-console diagnostics.
|
|
|
472
491
|
|
|
473
492
|
Request observers receive
|
|
474
493
|
`onRequest(request,id,metadata)` and any transport metadata supplied by the
|
|
475
|
-
selected route is forwarded unchanged.
|
|
476
|
-
|
|
494
|
+
selected route is forwarded unchanged. Async request, response, content, and
|
|
495
|
+
tool callbacks are observed before the next callback or terminal settlement.
|
|
496
|
+
The observational `onRetry` callback has the separate nonblocking behavior
|
|
497
|
+
described above.
|
|
477
498
|
|
|
478
499
|
Native Ollama responses are adapted before the shared structural validator:
|
|
479
500
|
provider-native calls may omit `id` and `type` or provide object arguments, so
|
|
@@ -914,6 +935,12 @@ caller's payload and request records remain unchanged. The optional
|
|
|
914
935
|
boundary has already cleaned the copy. Applications omit that argument. LLM
|
|
915
936
|
and STT payloads are unaffected.
|
|
916
937
|
|
|
938
|
+
SDK-owned LLM delegation also carries `onRetry` in the separate `preparation`
|
|
939
|
+
control record, alongside `observeToolText` for streams. The runtime preserves
|
|
940
|
+
these controls across its request queue and passes them to the selected
|
|
941
|
+
provider without inserting callbacks in the provider payload. Applications use
|
|
942
|
+
the `AI.fetchRequest()` and `AI.streamRequest()` options described above.
|
|
943
|
+
|
|
917
944
|
`register()` returns the provider's single unregister closure; caller-
|
|
918
945
|
registered providers remain caller-owned. The high-level
|
|
919
946
|
`AI.configureBrowserSpeech()` boundary is different: AI constructs, registers,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.5",
|
|
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",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
87
87
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
88
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",
|
|
89
|
-
"test:functional": "node ./bin/arcane-test.mjs test/ai-stream-cancellation.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
|
+
"test:functional": "node ./bin/arcane-test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.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",
|
|
90
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",
|
|
91
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",
|
|
92
92
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -1083,8 +1083,89 @@
|
|
|
1083
1083
|
&&is.function(session.send);
|
|
1084
1084
|
}
|
|
1085
1085
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1086
|
+
let transcriptFollowing = true;
|
|
1087
|
+
let transcriptScrollTop = chatOutput.scrollTop;
|
|
1088
|
+
let transcriptWasHidden = chatOutput.clientHeight === 0;
|
|
1089
|
+
const transcriptResizeObserver = is.function(globalThis.ResizeObserver)
|
|
1090
|
+
? new ResizeObserver(scrollTranscriptToBottom)
|
|
1091
|
+
: null;
|
|
1092
|
+
const transcriptMutationObserver = is.function(globalThis.MutationObserver)
|
|
1093
|
+
? new MutationObserver(handleTranscriptChildren)
|
|
1094
|
+
: null;
|
|
1095
|
+
|
|
1096
|
+
chatOutput.addEventListener(
|
|
1097
|
+
'scroll',
|
|
1098
|
+
trackTranscriptScrollPosition,
|
|
1099
|
+
{
|
|
1100
|
+
passive: true,
|
|
1101
|
+
signal: aiRuntimeStateAbortController.signal
|
|
1102
|
+
}
|
|
1103
|
+
);
|
|
1104
|
+
transcriptResizeObserver?.observe(
|
|
1105
|
+
chatOutput,
|
|
1106
|
+
{box: 'border-box'}
|
|
1107
|
+
);
|
|
1108
|
+
for(const item of chatOutput.children) {
|
|
1109
|
+
if(item.localName === 'li') {
|
|
1110
|
+
transcriptResizeObserver?.observe(
|
|
1111
|
+
item,
|
|
1112
|
+
{box: 'border-box'}
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
transcriptMutationObserver?.observe(
|
|
1117
|
+
chatOutput,
|
|
1118
|
+
{childList: true}
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
function trackTranscriptScrollPosition() {
|
|
1122
|
+
if(destroyed) return false;
|
|
1123
|
+
if(chatOutput.clientHeight === 0) {
|
|
1124
|
+
transcriptWasHidden = true;
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
const scrollTop = chatOutput.scrollTop;
|
|
1128
|
+
if(transcriptWasHidden) {
|
|
1129
|
+
transcriptWasHidden = false;
|
|
1130
|
+
transcriptScrollTop = scrollTop;
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
const atBottom = chatOutput.scrollHeight - chatOutput.clientHeight - scrollTop <= 1;
|
|
1134
|
+
// A layout clamp reaching the bottom does not resume a reader's following.
|
|
1135
|
+
if(scrollTop < transcriptScrollTop && !atBottom) {
|
|
1136
|
+
transcriptFollowing = false;
|
|
1137
|
+
} else if(scrollTop > transcriptScrollTop && atBottom) {
|
|
1138
|
+
transcriptFollowing = true;
|
|
1139
|
+
}
|
|
1140
|
+
transcriptScrollTop = scrollTop;
|
|
1141
|
+
return true;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function handleTranscriptChildren(records) {
|
|
1145
|
+
if(destroyed) return;
|
|
1146
|
+
for(const record of records) {
|
|
1147
|
+
for(const item of record.removedNodes) {
|
|
1148
|
+
if(item.localName === 'li' && item.parentElement !== chatOutput) {
|
|
1149
|
+
transcriptResizeObserver?.unobserve(item);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
for(const item of record.addedNodes) {
|
|
1153
|
+
if(item.localName === 'li' && item.parentElement === chatOutput) {
|
|
1154
|
+
transcriptResizeObserver?.observe(
|
|
1155
|
+
item,
|
|
1156
|
+
{box: 'border-box'}
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
scrollTranscriptToBottom();
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function scrollTranscriptToBottom() {
|
|
1165
|
+
// Read movement before writing: a reader's scroll event may still be queued.
|
|
1166
|
+
if(!trackTranscriptScrollPosition() || !transcriptFollowing) return false;
|
|
1167
|
+
chatOutput.scrollTop = chatOutput.scrollHeight;
|
|
1168
|
+
transcriptScrollTop = chatOutput.scrollTop;
|
|
1088
1169
|
return true;
|
|
1089
1170
|
}
|
|
1090
1171
|
|
|
@@ -1513,6 +1594,8 @@
|
|
|
1513
1594
|
actionable?[...restoredPendingToolCalls.values()]:[]
|
|
1514
1595
|
);
|
|
1515
1596
|
chatOutput.replaceChildren(fragment);
|
|
1597
|
+
transcriptFollowing = true;
|
|
1598
|
+
transcriptScrollTop = chatOutput.scrollTop;
|
|
1516
1599
|
scrollTranscriptToBottom();
|
|
1517
1600
|
return rendered;
|
|
1518
1601
|
}
|
|
@@ -2014,6 +2097,10 @@
|
|
|
2014
2097
|
? selectedRole.localOnly === false
|
|
2015
2098
|
|| (selectedRole.state === 'ready' && selectedRole.loaded)
|
|
2016
2099
|
: availability.llm;
|
|
2100
|
+
if(!conversationVisible && !host.conversationComplete) {
|
|
2101
|
+
trackTranscriptScrollPosition();
|
|
2102
|
+
transcriptWasHidden = true;
|
|
2103
|
+
}
|
|
2017
2104
|
chatArea.dataset.modelStartRequired = String(!conversationVisible);
|
|
2018
2105
|
send.disabled=host.conversationComplete
|
|
2019
2106
|
||!availability.llm
|
|
@@ -2835,21 +2922,15 @@
|
|
|
2835
2922
|
return false;
|
|
2836
2923
|
}
|
|
2837
2924
|
|
|
2838
|
-
let message=[...chatOutput.children].find(
|
|
2839
|
-
candidate
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
if(!message){
|
|
2843
|
-
await receivedMessage(host.aiName,isThinking?'':text,id);
|
|
2844
|
-
if(!isThinking){
|
|
2845
|
-
return false;
|
|
2846
|
-
}
|
|
2847
|
-
message=[...chatOutput.children].find(
|
|
2848
|
-
candidate=>candidate.id===`message-${id}`
|
|
2849
|
-
)||null;
|
|
2850
|
-
if(!message){
|
|
2851
|
-
return false;
|
|
2925
|
+
let message = [...chatOutput.children].find(
|
|
2926
|
+
function findStreamedMessage(candidate) {
|
|
2927
|
+
return candidate.id === `message-${id}`;
|
|
2852
2928
|
}
|
|
2929
|
+
) || null;
|
|
2930
|
+
|
|
2931
|
+
if(!message) {
|
|
2932
|
+
// The first chunk uses the same display and speech path as later chunks.
|
|
2933
|
+
message = receivedMessage(host.aiName, '', id);
|
|
2853
2934
|
}
|
|
2854
2935
|
|
|
2855
2936
|
let target=message.querySelector('.markdown');
|
|
@@ -3060,8 +3141,12 @@
|
|
|
3060
3141
|
}
|
|
3061
3142
|
|
|
3062
3143
|
// Reset height to shrink to content size
|
|
3144
|
+
trackTranscriptScrollPosition();
|
|
3063
3145
|
textArea.style.height = 'auto';
|
|
3064
3146
|
textArea.style.height = `${textArea.scrollHeight}px`;
|
|
3147
|
+
// The intermediate auto height can clamp scrollTop before final layout.
|
|
3148
|
+
transcriptScrollTop = chatOutput.scrollTop;
|
|
3149
|
+
scrollTranscriptToBottom();
|
|
3065
3150
|
}
|
|
3066
3151
|
|
|
3067
3152
|
function renderSessionMessageFailure(messageId,error){
|
|
@@ -3164,7 +3249,7 @@
|
|
|
3164
3249
|
applyAIAvailability(host.aiAvailability);
|
|
3165
3250
|
setSessionStatus('sending','AI is responding…');
|
|
3166
3251
|
try{
|
|
3167
|
-
|
|
3252
|
+
receivedMessage(host.aiName, '', messageId);
|
|
3168
3253
|
await streamMessage('',messageId,true);
|
|
3169
3254
|
setMessageProgress(
|
|
3170
3255
|
messageId,
|
|
@@ -3775,7 +3860,7 @@
|
|
|
3775
3860
|
}
|
|
3776
3861
|
}
|
|
3777
3862
|
|
|
3778
|
-
|
|
3863
|
+
function receivedMessage(name = 'Assistant', text = '', id = '') {
|
|
3779
3864
|
const message=appendTranscriptMessage('assistant',text,name);
|
|
3780
3865
|
if(id){
|
|
3781
3866
|
message.id=`message-${id}`;
|
|
@@ -3854,6 +3939,8 @@
|
|
|
3854
3939
|
sessionBindingGeneration+=1;
|
|
3855
3940
|
setSessionStatus('destroyed','Chat closed.');
|
|
3856
3941
|
destroyed=true;
|
|
3942
|
+
transcriptResizeObserver?.disconnect();
|
|
3943
|
+
transcriptMutationObserver?.disconnect();
|
|
3857
3944
|
const destroyReason=createChatSubmissionAbort(
|
|
3858
3945
|
chatReasons.componentDestroyed,
|
|
3859
3946
|
'The chat component was destroyed before message submission settled.'
|
|
@@ -421,16 +421,38 @@
|
|
|
421
421
|
await open();
|
|
422
422
|
|
|
423
423
|
activeRun=Promise.allSettled(tasks.map(async function runProgressTask(job,index){
|
|
424
|
+
let settled = false;
|
|
425
|
+
function displayTaskProgress(message, state) {
|
|
426
|
+
if (disposed) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
if (state) {
|
|
431
|
+
rows[index].row.dataset.status = state;
|
|
432
|
+
}
|
|
433
|
+
rows[index].status.innerText = message;
|
|
434
|
+
return true;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
arcaneLogging.error(`Unable to display progress for ${job.name}:`, error);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function reportProgress(message) {
|
|
441
|
+
if (settled || !is.string(message)) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return displayTaskProgress(message);
|
|
445
|
+
}
|
|
424
446
|
try{
|
|
425
|
-
const result=await job.task();
|
|
426
|
-
|
|
427
|
-
rows[index].status.innerText='Complete';
|
|
447
|
+
const result=await job.task(reportProgress);
|
|
448
|
+
displayTaskProgress('Complete', 'complete');
|
|
428
449
|
return result;
|
|
429
450
|
}catch(error){
|
|
430
|
-
|
|
431
|
-
rows[index].status.innerText='Unable to complete';
|
|
451
|
+
displayTaskProgress('Unable to complete', 'failed');
|
|
432
452
|
arcaneLogging.error(`Unable to complete ${job.name}:`,error);
|
|
433
453
|
throw error;
|
|
454
|
+
} finally {
|
|
455
|
+
settled = true;
|
|
434
456
|
}
|
|
435
457
|
}));
|
|
436
458
|
|
|
@@ -439,6 +461,9 @@
|
|
|
439
461
|
return result.status==='rejected';
|
|
440
462
|
});
|
|
441
463
|
running=false;
|
|
464
|
+
if (disposed) {
|
|
465
|
+
return results;
|
|
466
|
+
}
|
|
442
467
|
updateCloseButton();
|
|
443
468
|
|
|
444
469
|
if(failed){
|
|
@@ -1484,7 +1484,8 @@ class AI {
|
|
|
1484
1484
|
return Promise.resolve(
|
|
1485
1485
|
runtime.#requestBuiltInLLMChat(
|
|
1486
1486
|
context.payload,
|
|
1487
|
-
context.signal
|
|
1487
|
+
context.signal,
|
|
1488
|
+
controls.onRetry
|
|
1488
1489
|
)
|
|
1489
1490
|
).finally(releaseBuiltInLLMRequest);
|
|
1490
1491
|
}
|
|
@@ -1494,7 +1495,8 @@ class AI {
|
|
|
1494
1495
|
return runtime.#requestBuiltInLLMStream(
|
|
1495
1496
|
context.payload,
|
|
1496
1497
|
bridge,
|
|
1497
|
-
controls.observeToolText
|
|
1498
|
+
controls.observeToolText,
|
|
1499
|
+
controls.onRetry
|
|
1498
1500
|
);
|
|
1499
1501
|
},
|
|
1500
1502
|
context.signal
|
|
@@ -4154,7 +4156,7 @@ class AI {
|
|
|
4154
4156
|
return is.string(content)?content:completion;
|
|
4155
4157
|
}
|
|
4156
4158
|
|
|
4157
|
-
#requestBuiltInLLMChat(payload={},signal=null){
|
|
4159
|
+
#requestBuiltInLLMChat(payload = {}, signal = null, onRetry = null) {
|
|
4158
4160
|
const parallelToolCalls=payload.parallelToolCalls!==undefined
|
|
4159
4161
|
?payload.parallelToolCalls
|
|
4160
4162
|
:payload.parallel_tool_calls;
|
|
@@ -4168,11 +4170,12 @@ class AI {
|
|
|
4168
4170
|
payload.id??Date.now(),
|
|
4169
4171
|
function ignoreBuiltInLLMProviderRequest(){},
|
|
4170
4172
|
signal,
|
|
4171
|
-
payload.reasoningEffort
|
|
4173
|
+
payload.reasoningEffort,
|
|
4174
|
+
onRetry
|
|
4172
4175
|
);
|
|
4173
4176
|
}
|
|
4174
4177
|
|
|
4175
|
-
#requestBuiltInLLMStream(payload={},bridge,observeToolText=null){
|
|
4178
|
+
#requestBuiltInLLMStream(payload = {}, bridge, observeToolText = null, onRetry = null) {
|
|
4176
4179
|
const parallelToolCalls=payload.parallelToolCalls!==undefined
|
|
4177
4180
|
?payload.parallelToolCalls
|
|
4178
4181
|
:payload.parallel_tool_calls;
|
|
@@ -4198,7 +4201,8 @@ class AI {
|
|
|
4198
4201
|
emitBuiltInLLMStreamData,
|
|
4199
4202
|
function ignoreBuiltInLLMStreamResult(){},
|
|
4200
4203
|
payload.reasoningEffort,
|
|
4201
|
-
observeToolText
|
|
4204
|
+
observeToolText,
|
|
4205
|
+
onRetry
|
|
4202
4206
|
);
|
|
4203
4207
|
}
|
|
4204
4208
|
|
|
@@ -4357,6 +4361,7 @@ class AI {
|
|
|
4357
4361
|
toolChoice='auto',
|
|
4358
4362
|
onToolCall=function ignoreEarlyFunction(){},
|
|
4359
4363
|
onRequest=function ignoreStreamRequest(){},
|
|
4364
|
+
onRetry = null,
|
|
4360
4365
|
parallelToolCalls,
|
|
4361
4366
|
id=Date.now(),
|
|
4362
4367
|
seeThinking=false,
|
|
@@ -4438,7 +4443,7 @@ class AI {
|
|
|
4438
4443
|
localOnly,
|
|
4439
4444
|
signal
|
|
4440
4445
|
},
|
|
4441
|
-
{observeToolText}
|
|
4446
|
+
{observeToolText, onRetry}
|
|
4442
4447
|
);
|
|
4443
4448
|
for await(const chunk of handle){
|
|
4444
4449
|
if(signal?.aborted){
|
|
@@ -4523,7 +4528,8 @@ class AI {
|
|
|
4523
4528
|
onDataChunk,
|
|
4524
4529
|
onDataResult,
|
|
4525
4530
|
normalizedReasoningEffort,
|
|
4526
|
-
observeToolText
|
|
4531
|
+
observeToolText,
|
|
4532
|
+
onRetry
|
|
4527
4533
|
);
|
|
4528
4534
|
const structuralToolCalls=normalizeAICompletionToolCalls(
|
|
4529
4535
|
completion
|
|
@@ -4631,7 +4637,8 @@ class AI {
|
|
|
4631
4637
|
dataChunkHandler=function ignoreBuiltInStreamDataChunk(){},
|
|
4632
4638
|
dataResultHandler=function ignoreBuiltInStreamDataResult(){},
|
|
4633
4639
|
reasoningEffort,
|
|
4634
|
-
observeToolText=null
|
|
4640
|
+
observeToolText=null,
|
|
4641
|
+
onRetry = null
|
|
4635
4642
|
){
|
|
4636
4643
|
let speechTurnCompleted=false;
|
|
4637
4644
|
|
|
@@ -4863,7 +4870,8 @@ class AI {
|
|
|
4863
4870
|
headers:this.#serviceHeaders[this.llmService],
|
|
4864
4871
|
body,
|
|
4865
4872
|
...(signal?{signal}:{})
|
|
4866
|
-
}
|
|
4873
|
+
},
|
|
4874
|
+
{onRetry}
|
|
4867
4875
|
);
|
|
4868
4876
|
|
|
4869
4877
|
let sseBuffer='';
|
|
@@ -5357,6 +5365,7 @@ class AI {
|
|
|
5357
5365
|
signal=null,
|
|
5358
5366
|
onRequest=function ignoreFetchRequest(){},
|
|
5359
5367
|
onResponse=function ignoreFetchResponse(){},
|
|
5368
|
+
onRetry = null,
|
|
5360
5369
|
maxOutputTokens,
|
|
5361
5370
|
maxTokens,
|
|
5362
5371
|
temperature,
|
|
@@ -5420,7 +5429,8 @@ class AI {
|
|
|
5420
5429
|
payload:request,
|
|
5421
5430
|
localOnly,
|
|
5422
5431
|
signal
|
|
5423
|
-
}
|
|
5432
|
+
},
|
|
5433
|
+
{onRetry}
|
|
5424
5434
|
);
|
|
5425
5435
|
if(signal?.aborted){
|
|
5426
5436
|
throw normalizeAIRequestAbort();
|
|
@@ -5443,7 +5453,8 @@ class AI {
|
|
|
5443
5453
|
id,
|
|
5444
5454
|
onRequest,
|
|
5445
5455
|
signal,
|
|
5446
|
-
normalizedReasoningEffort
|
|
5456
|
+
normalizedReasoningEffort,
|
|
5457
|
+
onRetry
|
|
5447
5458
|
);
|
|
5448
5459
|
}
|
|
5449
5460
|
|
|
@@ -5496,7 +5507,8 @@ class AI {
|
|
|
5496
5507
|
id=Date.now(),
|
|
5497
5508
|
requestHandler=function ignoreFetchRequest(){},
|
|
5498
5509
|
signal=null,
|
|
5499
|
-
reasoningEffort
|
|
5510
|
+
reasoningEffort,
|
|
5511
|
+
onRetry = null
|
|
5500
5512
|
){
|
|
5501
5513
|
validateAIStructuralRequest(messages,tools,parallel_tool_calls);
|
|
5502
5514
|
this.#assertServiceConfigured(this.llmService);
|
|
@@ -5613,7 +5625,8 @@ class AI {
|
|
|
5613
5625
|
headers:this.#serviceHeaders[this.llmService],
|
|
5614
5626
|
body,
|
|
5615
5627
|
...(signal?{signal}:{})
|
|
5616
|
-
}
|
|
5628
|
+
},
|
|
5629
|
+
{onRetry}
|
|
5617
5630
|
);
|
|
5618
5631
|
|
|
5619
5632
|
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, observeToolText = null} = {}) {
|
|
3350
|
+
request(role, options = {}, {speechInputPrepared = false, observeToolText = null, onRetry = 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, observeToolText);
|
|
3354
|
+
return this.#requestRole(role, options, false, speechInputPrepared, observeToolText, onRetry);
|
|
3355
3355
|
}
|
|
3356
3356
|
|
|
3357
|
-
#requestRole(role, options, queued, speechInputPrepared = false, observeToolText = null) {
|
|
3357
|
+
#requestRole(role, options, queued, speechInputPrepared = false, observeToolText = null, onRetry = 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, observeToolText);
|
|
3489
|
+
return this.#enqueueRoleRequest(slot, options, observeToolText, onRetry);
|
|
3490
3490
|
}
|
|
3491
3491
|
if (!slot.ready
|
|
3492
3492
|
|| providerStatus.state !== 'ready'
|
|
@@ -3808,7 +3808,7 @@ export class AIProviderRuntime {
|
|
|
3808
3808
|
payload: options.payload,
|
|
3809
3809
|
signal: controller.signal
|
|
3810
3810
|
},
|
|
3811
|
-
{observeToolText: observeCurrentToolText}
|
|
3811
|
+
{observeToolText: observeCurrentToolText, onRetry}
|
|
3812
3812
|
);
|
|
3813
3813
|
}
|
|
3814
3814
|
);
|
|
@@ -4039,7 +4039,12 @@ export class AIProviderRuntime {
|
|
|
4039
4039
|
}
|
|
4040
4040
|
const result = role === 'tts'
|
|
4041
4041
|
? await provider.request(providerRequest, {speechInputPrepared: true})
|
|
4042
|
-
:
|
|
4042
|
+
: role === 'llm'
|
|
4043
|
+
? await provider.request(
|
|
4044
|
+
providerRequest,
|
|
4045
|
+
{onRetry}
|
|
4046
|
+
)
|
|
4047
|
+
: await provider.request(providerRequest);
|
|
4043
4048
|
if (SPEECH_ROLES.includes(role)) {
|
|
4044
4049
|
arcaneLogging.debug('[Arcane speech runtime] provider.request.result', {role, generation, requestSequence, operationId}, result);
|
|
4045
4050
|
}
|
|
@@ -4222,7 +4227,7 @@ export class AIProviderRuntime {
|
|
|
4222
4227
|
);
|
|
4223
4228
|
}
|
|
4224
4229
|
|
|
4225
|
-
#enqueueRoleRequest(slot, options, observeToolText = null) {
|
|
4230
|
+
#enqueueRoleRequest(slot, options, observeToolText = null, onRetry = null) {
|
|
4226
4231
|
if (SPEECH_ROLES.includes(slot.role)) {
|
|
4227
4232
|
arcaneLogging.debug('[Arcane speech runtime] queue.enqueue', {
|
|
4228
4233
|
role: slot.role,
|
|
@@ -4237,6 +4242,7 @@ export class AIProviderRuntime {
|
|
|
4237
4242
|
const entry = {
|
|
4238
4243
|
options,
|
|
4239
4244
|
observeToolText,
|
|
4245
|
+
onRetry,
|
|
4240
4246
|
resolve,
|
|
4241
4247
|
reject,
|
|
4242
4248
|
detachSignal: null
|
|
@@ -4309,7 +4315,7 @@ export class AIProviderRuntime {
|
|
|
4309
4315
|
let operation;
|
|
4310
4316
|
try {
|
|
4311
4317
|
operation = runtime.#requestRole(
|
|
4312
|
-
slot.role, entry.options, true, false, entry.observeToolText
|
|
4318
|
+
slot.role, entry.options, true, false, entry.observeToolText, entry.onRetry
|
|
4313
4319
|
);
|
|
4314
4320
|
} catch (error) {
|
|
4315
4321
|
if (SPEECH_ROLES.includes(slot.role)) {
|