arcane-os 0.43.0 → 0.45.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 +35 -0
- package/README.md +1 -1
- package/browser-runtime/ai/twin-cloud.mjs +234 -0
- package/docs/reference/ai/twin-cloud.md +124 -0
- package/docs/reference/runtime-components.md +42 -3
- package/docs/reference/runtime-modules.md +93 -1
- package/package.json +2 -2
- package/runtime/arcane/components/chat.html +27 -6
- package/runtime/arcane/components/file-manager.html +111 -5
- package/runtime/arcane/components/markdown-document.html +31 -4
- package/runtime/arcane/components/markdown-editor.html +113 -4
- package/runtime/arcane/entities/Chat.js +6 -4
- package/runtime/arcane/modules/MarkdownMedia.js +211 -0
- package/runtime/arcane/modules/PersistentAIChatSession.js +22 -8
- package/runtime/arcane/modules/PrintView.js +51 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.45.0
|
|
4
|
+
|
|
5
|
+
- Add public MarkdownMedia save, read, reference parsing and hydration helpers.
|
|
6
|
+
Store complete image records in application-owned DBOPFS tables outside raw
|
|
7
|
+
Markdown, with stable references and application-selected filenames. Preserve
|
|
8
|
+
JSONL/NDJSON singleton records across cold reads and backup restoration.
|
|
9
|
+
- Hydrate local images independently in shared Markdown views while preserving
|
|
10
|
+
raw Markdown, external URLs, source maps and saving. Observe cancellation and
|
|
11
|
+
individual failures without removing successful sibling images. Retain print
|
|
12
|
+
resources through afterprint; explicit PrintView preparation awaits local
|
|
13
|
+
media while native beforeprint remains synchronous.
|
|
14
|
+
- Apply no-retention semantics to entity-wide persistence choices as well as
|
|
15
|
+
request persist:false: complete input and response remain available to the
|
|
16
|
+
active operation, then stay outside retained transcript, recurring model
|
|
17
|
+
context, DBOPFS and memory extraction. Capture the accepted choice before
|
|
18
|
+
rendering waits and preserve original pending calls after temporary tool
|
|
19
|
+
continuations. Existing retained turns and saved history remain untouched;
|
|
20
|
+
structural sanitization applies to new durable writes only, without migration.
|
|
21
|
+
- Keep image model selection, credentials, storage selection and app orchestration
|
|
22
|
+
application-owned. No model/provider/default changes are introduced.
|
|
23
|
+
|
|
24
|
+
## 0.44.0
|
|
25
|
+
|
|
26
|
+
- Add stateless `generateImages` through `arcane-os/ai/twin-cloud` with
|
|
27
|
+
caller-selected FAL Flux Schnell or Stable Diffusion 3.5 Large. Preserve
|
|
28
|
+
complete prompts and provider parameters, and return every image as a Blob
|
|
29
|
+
in provider order with declared media metadata.
|
|
30
|
+
- Expose semantic progress and complete diagnostic callbacks, asynchronous
|
|
31
|
+
job polling, and cancellation across credentials, callbacks, requests,
|
|
32
|
+
response bodies and media downloads. Submit a paid generation request once;
|
|
33
|
+
never automatically replay an ambiguous submission.
|
|
34
|
+
- Keep image model selection, credentials, display and storage with the
|
|
35
|
+
application. No global model default, saved-history migration, durable media
|
|
36
|
+
storage or change to existing chat and speech behavior is introduced.
|
|
37
|
+
|
|
3
38
|
## 0.43.0
|
|
4
39
|
|
|
5
40
|
- Add reusable rendered printing through `createPrintView`, preserving the
|
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.
|
|
22
|
+
This checkout defines the `0.45.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
|
|
|
@@ -105,6 +105,240 @@ export function normalizeAIRequestAbort(error){
|
|
|
105
105
|
return normalized;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** Stateless image generation; model selection and provider parameters belong to the caller. */
|
|
109
|
+
export async function generateImages({
|
|
110
|
+
model,
|
|
111
|
+
prompt,
|
|
112
|
+
parameters = {},
|
|
113
|
+
twinKey,
|
|
114
|
+
getApiKey,
|
|
115
|
+
signal = null,
|
|
116
|
+
id = Date.now(),
|
|
117
|
+
onRequest = function observeImageRequest(){},
|
|
118
|
+
onResponse = function observeImageResponse(){},
|
|
119
|
+
onProgress = function observeImageProgress(){}
|
|
120
|
+
} = {}) {
|
|
121
|
+
if (signal?.aborted) throw normalizeAIRequestAbort(signal.reason);
|
|
122
|
+
const asynchronous = model === 'fal-ai/flux/schnell';
|
|
123
|
+
if (!asynchronous && model !== 'stable-diffusion-3.5-large') {
|
|
124
|
+
const error = new TypeError('TWiN generateImages requires a supported explicit image model.');
|
|
125
|
+
error.code = 'ARCANE_AI_IMAGE_MODEL_UNSUPPORTED';
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
if (!is.string(prompt)) throw new TypeError('TWiN generateImages requires a prompt string.');
|
|
129
|
+
if (!parameters || !is.object(parameters) || is.array(parameters)) {
|
|
130
|
+
throw new TypeError('Image parameters must be an object of additional provider fields.');
|
|
131
|
+
}
|
|
132
|
+
if (Object.hasOwn(parameters, 'prompt') || (!asynchronous && Object.hasOwn(parameters, 'model'))) {
|
|
133
|
+
throw new TypeError('Supply the image prompt and model through their named arguments.');
|
|
134
|
+
}
|
|
135
|
+
const destination = asynchronous
|
|
136
|
+
? 'https://inference.do-ai.run/v1/async-invoke'
|
|
137
|
+
: 'https://inference.do-ai.run/v1/images/generations';
|
|
138
|
+
const request = asynchronous
|
|
139
|
+
? {model_id: model, input: {...parameters, prompt}}
|
|
140
|
+
: {...parameters, model, prompt};
|
|
141
|
+
// Observe the complete request without allowing a later callback to rewrite its payload.
|
|
142
|
+
const body = JSON.stringify(request);
|
|
143
|
+
const outputFormat = parameters.output_format;
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const operationSignal = controller.signal;
|
|
146
|
+
function cancelImageRequest() {controller.abort(signal.reason);}
|
|
147
|
+
signal?.addEventListener('abort', cancelImageRequest, {once: true});
|
|
148
|
+
if (signal?.aborted) cancelImageRequest();
|
|
149
|
+
|
|
150
|
+
function progress(stage, requestId) {
|
|
151
|
+
return settleImageOperation(
|
|
152
|
+
function publishImageProgress() {
|
|
153
|
+
return onProgress({stage, model, id, ...(requestId === undefined ? {} : {requestId})});
|
|
154
|
+
},
|
|
155
|
+
operationSignal
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function inference(url, options) {
|
|
160
|
+
const response = await settleImageOperation(
|
|
161
|
+
function requestImageInference() {return fetch(url, {...options, credentials: 'omit', signal: operationSignal});},
|
|
162
|
+
operationSignal
|
|
163
|
+
);
|
|
164
|
+
const result = await settleImageOperation(
|
|
165
|
+
function readImageInference() {
|
|
166
|
+
if (response.ok || response.headers.get('content-type')?.includes('application/json')) {
|
|
167
|
+
return response.json();
|
|
168
|
+
}
|
|
169
|
+
return response.text();
|
|
170
|
+
},
|
|
171
|
+
operationSignal
|
|
172
|
+
);
|
|
173
|
+
if (!response.ok) throw result;
|
|
174
|
+
const retryAfter = response.headers.get('retry-after');
|
|
175
|
+
const seconds = retryAfter?.trim() ? Number(retryAfter) : NaN;
|
|
176
|
+
const date = retryAfter?.trim() ? Date.parse(retryAfter) : NaN;
|
|
177
|
+
const delay = is.finite(seconds) && seconds >= 0 ? seconds * 1000
|
|
178
|
+
: is.finite(date) ? Math.max(0, date - Date.now()) : 1000;
|
|
179
|
+
return {result, delay};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await progress('credentials');
|
|
184
|
+
const key = twinKey === undefined && is.function(getApiKey)
|
|
185
|
+
? await settleImageOperation(getApiKey, operationSignal) : twinKey;
|
|
186
|
+
if (!is.string(key) || !key) {
|
|
187
|
+
const error = new Error('AI provider is not configured.');
|
|
188
|
+
error.code = 'AI_PROVIDER_NOT_CONFIGURED';
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
const headers = {'Content-Type': 'application/json', Authorization: `Bearer ${key}`};
|
|
192
|
+
await settleImageOperation(
|
|
193
|
+
function observeImageSubmission() {
|
|
194
|
+
return onRequest(request, id, {operation: 'images', transport: 'http', destination});
|
|
195
|
+
},
|
|
196
|
+
operationSignal
|
|
197
|
+
);
|
|
198
|
+
await progress('requesting');
|
|
199
|
+
// A lost POST response can represent an accepted paid job. Never replay it.
|
|
200
|
+
let response = await inference(destination, {method: 'POST', headers, body});
|
|
201
|
+
let result = response.result;
|
|
202
|
+
if (asynchronous) {
|
|
203
|
+
const requestId = result?.request_id;
|
|
204
|
+
while (result?.status === 'QUEUED' || result?.status === 'IN_PROGRESS') {
|
|
205
|
+
if (!is.string(requestId) || !requestId) {
|
|
206
|
+
throw imageResultError('Image generation returned a pending job without a request_id.', result);
|
|
207
|
+
}
|
|
208
|
+
await progress(result.status === 'QUEUED' ? 'queued' : 'generating', requestId);
|
|
209
|
+
await waitForImageStatus(response.delay, operationSignal);
|
|
210
|
+
response = await inference(
|
|
211
|
+
`${destination}/${encodeURIComponent(requestId)}/status`,
|
|
212
|
+
{method: 'GET', headers}
|
|
213
|
+
);
|
|
214
|
+
result = response.result;
|
|
215
|
+
}
|
|
216
|
+
if (result?.status === 'FAILED') {
|
|
217
|
+
throw imageResultError('TWiN Cloud image generation failed.', result);
|
|
218
|
+
}
|
|
219
|
+
if (result?.status !== 'COMPLETED' && result?.status !== 'COMPLETE') {
|
|
220
|
+
throw imageResultError('TWiN Cloud returned an unsupported image job status.', result);
|
|
221
|
+
}
|
|
222
|
+
if (!is.array(result.output?.images)) {
|
|
223
|
+
if (!is.string(requestId) || !requestId) {
|
|
224
|
+
throw imageResultError('Image generation returned no result or request_id.', result);
|
|
225
|
+
}
|
|
226
|
+
response = await inference(`${destination}/${encodeURIComponent(requestId)}`, {method: 'GET', headers});
|
|
227
|
+
result = response.result;
|
|
228
|
+
if (result?.status !== 'COMPLETED' && result?.status !== 'COMPLETE') {
|
|
229
|
+
throw imageResultError('TWiN Cloud image generation did not return a completed result.', result);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const returnedImages = asynchronous ? result?.output?.images : result?.data;
|
|
234
|
+
if (!is.array(returnedImages)) {
|
|
235
|
+
throw imageResultError('TWiN Cloud returned no image collection.', result);
|
|
236
|
+
}
|
|
237
|
+
const images = returnedImages.map(function retainImageDescriptor(image) {return {...image};});
|
|
238
|
+
const format = result.output_format ?? outputFormat;
|
|
239
|
+
const requestId = asynchronous ? result.request_id : undefined;
|
|
240
|
+
await settleImageOperation(
|
|
241
|
+
function observeImageResult() {return onResponse(result, id, false);},
|
|
242
|
+
operationSignal
|
|
243
|
+
);
|
|
244
|
+
await progress('downloading', requestId);
|
|
245
|
+
const completed = await Promise.all(images.map(async function materializeImage(image) {
|
|
246
|
+
let blob;
|
|
247
|
+
const mediaType = image.content_type
|
|
248
|
+
?? ({png: 'image/png', jpeg: 'image/jpeg', jpg: 'image/jpeg', webp: 'image/webp'}[format])
|
|
249
|
+
?? 'application/octet-stream';
|
|
250
|
+
if (is.string(image.b64_json)) {
|
|
251
|
+
blob = await settleImageOperation(function decodeImageBase64() {
|
|
252
|
+
return new Blob([Uint8Array.from(atob(image.b64_json), function imageCode(character) {
|
|
253
|
+
return character.charCodeAt(0);
|
|
254
|
+
})], {type: mediaType});
|
|
255
|
+
}, operationSignal);
|
|
256
|
+
} else if (is.string(image.url) && image.url) {
|
|
257
|
+
// Media URLs have their own delivery authority. Never forward the inference key.
|
|
258
|
+
const download = await settleImageOperation(function downloadGeneratedImage() {
|
|
259
|
+
return fetch(image.url, {credentials: 'omit', signal: operationSignal});
|
|
260
|
+
}, operationSignal);
|
|
261
|
+
if (!download.ok) {
|
|
262
|
+
throw await settleImageOperation(function readImageDownloadError() {
|
|
263
|
+
return download.headers.get('content-type')?.includes('application/json')
|
|
264
|
+
? download.json() : download.text();
|
|
265
|
+
}, operationSignal);
|
|
266
|
+
}
|
|
267
|
+
blob = await settleImageOperation(function readGeneratedImage() {return download.blob();}, operationSignal);
|
|
268
|
+
if (!blob.type) blob = new Blob([blob], {type: mediaType});
|
|
269
|
+
} else {
|
|
270
|
+
throw imageResultError('TWiN Cloud returned an image without data or a URL.', image);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
blob,
|
|
274
|
+
mediaType: blob.type || mediaType,
|
|
275
|
+
...(image.width === undefined ? {} : {width: image.width}),
|
|
276
|
+
...(image.height === undefined ? {} : {height: image.height})
|
|
277
|
+
};
|
|
278
|
+
}));
|
|
279
|
+
await progress('complete', requestId);
|
|
280
|
+
if (operationSignal.aborted) throw normalizeAIRequestAbort(operationSignal.reason);
|
|
281
|
+
return {images: completed};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (isAIRequestAbort(error, operationSignal)) throw normalizeAIRequestAbort(error);
|
|
284
|
+
throw error;
|
|
285
|
+
} finally {
|
|
286
|
+
signal?.removeEventListener('abort', cancelImageRequest);
|
|
287
|
+
controller.abort('Image operation settled.');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function imageResultError(message, result) {
|
|
292
|
+
const error = new Error(message, {cause: result});
|
|
293
|
+
error.code = 'ARCANE_AI_INVALID_PROVIDER_RESULT';
|
|
294
|
+
return error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Abort a wait even when a credential, callback, or host Fetch ignores the signal. */
|
|
298
|
+
function settleImageOperation(operation, signal) {
|
|
299
|
+
return new Promise(function settleCloudImageOperation(resolve, reject) {
|
|
300
|
+
let settled = false;
|
|
301
|
+
function finish(error, value, failed) {
|
|
302
|
+
if (settled) return;
|
|
303
|
+
settled = true;
|
|
304
|
+
signal.removeEventListener('abort', cancelled);
|
|
305
|
+
if (failed) reject(error);
|
|
306
|
+
else resolve(value);
|
|
307
|
+
}
|
|
308
|
+
function cancelled() {finish(normalizeAIRequestAbort(signal.reason), undefined, true);}
|
|
309
|
+
signal.addEventListener('abort', cancelled, {once: true});
|
|
310
|
+
if (signal.aborted) {
|
|
311
|
+
cancelled();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
Promise.resolve(operation()).then(
|
|
316
|
+
function accepted(value) {finish(undefined, value, false);},
|
|
317
|
+
function rejected(error) {finish(error, undefined, true);}
|
|
318
|
+
);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
finish(error, undefined, true);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function waitForImageStatus(delay, signal) {
|
|
326
|
+
let remaining = delay;
|
|
327
|
+
do {
|
|
328
|
+
// Split only at the host timer range; retain the provider's entire delay.
|
|
329
|
+
const milliseconds = Math.min(remaining, 2147483647);
|
|
330
|
+
let timer;
|
|
331
|
+
try {
|
|
332
|
+
await settleImageOperation(function waitForImagePoll() {
|
|
333
|
+
return new Promise(function scheduleImagePoll(finish) {timer = setTimeout(finish, milliseconds);});
|
|
334
|
+
}, signal);
|
|
335
|
+
} finally {
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
}
|
|
338
|
+
remaining -= milliseconds;
|
|
339
|
+
} while (remaining > 0);
|
|
340
|
+
}
|
|
341
|
+
|
|
108
342
|
export function structuredOutputFormat(value = false){
|
|
109
343
|
if(value === false || value === null || value === undefined){
|
|
110
344
|
return null;
|
|
@@ -136,6 +136,130 @@ options; they do not add a key, model, browser state, or retained conversation.
|
|
|
136
136
|
Retry warnings and observer failures use the shared console logger and preserve
|
|
137
137
|
the complete original error.
|
|
138
138
|
|
|
139
|
+
## Generate images with an application-selected model
|
|
140
|
+
|
|
141
|
+
`generateImages` is a separate stateless operation on the same public import.
|
|
142
|
+
The application owns the selected model, labels, prompt, provider parameters,
|
|
143
|
+
credential source, display, and any saved image. It does not instantiate the
|
|
144
|
+
browser AI singleton or change chat, speech, model preferences, or history.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
import {generateImages} from 'arcane-os/ai/twin-cloud';
|
|
148
|
+
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const {images} = await generateImages({
|
|
151
|
+
model: 'stable-diffusion-3.5-large',
|
|
152
|
+
prompt: 'A moon-powered toaster launches croissants over purple craters.',
|
|
153
|
+
parameters: {n: 2, size: '1024x1024', output_format: 'png'},
|
|
154
|
+
getApiKey: applicationRuntime.getTwinKey,
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
onProgress({stage}) {
|
|
157
|
+
console.log(stage);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
for (const image of images) {
|
|
162
|
+
await applicationRuntime.saveGeneratedImage(image.blob, image.mediaType);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`applicationRuntime.getTwinKey` and `saveGeneratedImage` are caller-owned
|
|
167
|
+
functions in this example, not SDK APIs. Pass the credential function with any
|
|
168
|
+
required receiver already bound. Use `twinKey` instead when the application
|
|
169
|
+
already has its key. `getApiKey()` may return a key or a promise and is called
|
|
170
|
+
only when `twinKey` is `undefined`. An explicitly supplied empty key fails with
|
|
171
|
+
`AI_PROVIDER_NOT_CONFIGURED`; it does not select another credential source.
|
|
172
|
+
The example's model and parameters are application choices, not SDK defaults.
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
generateImages({
|
|
176
|
+
model,
|
|
177
|
+
prompt,
|
|
178
|
+
parameters,
|
|
179
|
+
twinKey,
|
|
180
|
+
getApiKey,
|
|
181
|
+
signal,
|
|
182
|
+
id,
|
|
183
|
+
onRequest,
|
|
184
|
+
onResponse,
|
|
185
|
+
onProgress
|
|
186
|
+
})
|
|
187
|
+
// Promise<{images: [{blob, mediaType, width?, height?}]}>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`parameters` defaults to `{}`, `signal` to `null`, and `id` to `Date.now()`.
|
|
191
|
+
All diagnostic and progress callbacks are optional.
|
|
192
|
+
|
|
193
|
+
The SDK owns the route for these exact supported identifiers:
|
|
194
|
+
|
|
195
|
+
| Explicit model | Submission and result |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `fal-ai/flux/schnell` | POST `/v1/async-invoke` with `{model_id:model,input:{...parameters,prompt}}`; pending jobs use GET `/v1/async-invoke/{request_id}/status`, then GET `/v1/async-invoke/{request_id}` when the completed status lacks `output.images`. |
|
|
198
|
+
| `stable-diffusion-3.5-large` | POST `/v1/images/generations` with `{...parameters,model,prompt}`; images come from the completed response's `data` array. |
|
|
199
|
+
|
|
200
|
+
Both routes use `https://inference.do-ai.run`. Missing or unsupported model
|
|
201
|
+
identifiers reject with `ARCANE_AI_IMAGE_MODEL_UNSUPPORTED`; there is no SDK
|
|
202
|
+
image model default. The prompt must be a string and is sent unchanged,
|
|
203
|
+
including whitespace. `parameters` is an object of additional provider fields;
|
|
204
|
+
the SDK neither supplies nor changes image counts, dimensions, quality, seeds,
|
|
205
|
+
or output limits. Supply the prompt only through `prompt`, and the synchronous
|
|
206
|
+
model only through `model`; duplicate reserved fields in `parameters` throw
|
|
207
|
+
`TypeError` instead of silently replacing one input. Provider-supported
|
|
208
|
+
parameter names and values remain that selected model's contract.
|
|
209
|
+
|
|
210
|
+
Every returned image is materialized in provider order. Base64 image data
|
|
211
|
+
becomes a `Blob`; returned URLs are fetched concurrently without an inference
|
|
212
|
+
Authorization header or browser credentials. The output's `mediaType` uses the
|
|
213
|
+
downloaded Blob's declared type, provider `content_type`, or a declared
|
|
214
|
+
`output_format` of `png`, `jpeg`/`jpg`, or `webp`. With no declared type or known
|
|
215
|
+
format, it is `application/octet-stream`; the SDK does not guess dimensions or
|
|
216
|
+
image metadata. `width` and `height` are included only when the provider supplies
|
|
217
|
+
them. The SDK saves nothing and creates no object URLs; the caller owns any
|
|
218
|
+
storage and the lifetime of display URLs it creates.
|
|
219
|
+
|
|
220
|
+
`onRequest(request,id,metadata)` runs once before submission with the complete
|
|
221
|
+
provider request. Metadata is `{operation:'images',transport:'http',destination}`.
|
|
222
|
+
`onResponse(response,id,false)` runs once with the complete final provider JSON
|
|
223
|
+
after successful generation and before image materialization. Intermediate
|
|
224
|
+
poll replies do not call it. These are explicit diagnostic callbacks: keep raw
|
|
225
|
+
protocol outside ordinary user status and durable chat history. Credentials are
|
|
226
|
+
transport-only and are absent from these payloads. The request and image
|
|
227
|
+
descriptors are captured before their diagnostic callbacks so observer edits do
|
|
228
|
+
not rewrite the operation's content.
|
|
229
|
+
|
|
230
|
+
`onProgress({stage,model,id,requestId?})` reports semantic stages: `credentials`,
|
|
231
|
+
`requesting`, `queued`, `generating`, `downloading`, and `complete`. Pending
|
|
232
|
+
stages can repeat; immediately completed jobs skip them. Async job stages
|
|
233
|
+
include `requestId` when present. Progress contains no image data or protocol
|
|
234
|
+
body. All three callbacks may return a promise; their settlement is awaited
|
|
235
|
+
and their failure rejects the operation. `onResponse` means generation returned
|
|
236
|
+
a final result, not that media downloads have finished. The resolved promise
|
|
237
|
+
delivers all materialized images; a failed download rejects instead of returning
|
|
238
|
+
an incomplete collection.
|
|
239
|
+
|
|
240
|
+
The image operation submits once and performs no automatic retry, including
|
|
241
|
+
after a network loss, HTTP `429`, body-read failure, or callback failure. A lost
|
|
242
|
+
POST response may correspond to an accepted paid job. This image-specific rule
|
|
243
|
+
does not alter chat or speech retry behavior. While a FAL job is `QUEUED` or
|
|
244
|
+
`IN_PROGRESS`, polling uses the provider's `Retry-After` delay when supplied,
|
|
245
|
+
otherwise one second. Completed jobs accept `COMPLETED` and the guide's
|
|
246
|
+
`COMPLETE` spelling. Failed or unusable job results reject with
|
|
247
|
+
`ARCANE_AI_INVALID_PROVIDER_RESULT` and the complete result in `error.cause`.
|
|
248
|
+
HTTP failures throw their complete parsed JSON or text body; transport,
|
|
249
|
+
decoding, and callback failures remain their original errors.
|
|
250
|
+
|
|
251
|
+
Calling `controller.abort()` cancels local credential/callback waits, requests,
|
|
252
|
+
body reads, polling timers, and downloads. It rejects with
|
|
253
|
+
`ARCANE_AI_REQUEST_ABORTED` and ignores later settlement from an operation that
|
|
254
|
+
does not honor the signal. A callback already running remains caller-owned;
|
|
255
|
+
the SDK invokes no subsequent callbacks after cancellation. Local cancellation
|
|
256
|
+
does not claim to cancel a remote paid job. Dispose the controller with the
|
|
257
|
+
owning page or operation; no global startup wait is required.
|
|
258
|
+
|
|
259
|
+
The route and response contracts follow DigitalOcean's official
|
|
260
|
+
[FAL inference guide](https://docs.digitalocean.com/products/inference/how-to/use-fal-models/)
|
|
261
|
+
and [multimodal inference guide](https://docs.digitalocean.com/products/inference/how-to/use-multimodal-inference/).
|
|
262
|
+
|
|
139
263
|
## Existing browser AI interface
|
|
140
264
|
|
|
141
265
|
The following browser example uses the same managed imports as the
|
|
@@ -426,7 +426,8 @@ history stores each tool's required user-facing message as an ordinary
|
|
|
426
426
|
call envelope or ID and therefore does not recreate a pending executable call
|
|
427
427
|
after reload. `submitToolResult()` and `submitToolResults()` accept `status` as
|
|
428
428
|
the public result term; `disposition` remains an accepted input spelling.
|
|
429
|
-
When a submitted turn uses `persist:false`,
|
|
429
|
+
When a submitted turn uses `persist:false`, or its session's Chat entity has
|
|
430
|
+
`persist=false`, Chat may show its cards while that
|
|
430
431
|
one operation is active, then removes them when the operation settles; neither
|
|
431
432
|
the input nor response remains in the retained transcript or model context.
|
|
432
433
|
This cleanup also applies to failed, cancelled, detached, or replaced-session
|
|
@@ -437,6 +438,15 @@ operation's nodes so a completion listener can start another turn safely.
|
|
|
437
438
|
Submission code passes those card references directly when it creates them;
|
|
438
439
|
reusing an operation identifier never selects earlier conversation cards for
|
|
439
440
|
cleanup or timestamp updates.
|
|
441
|
+
The session's terminal `retained` boolean reports its actual commit decision;
|
|
442
|
+
Chat uses that decision instead of retroactively interpreting a flag changed
|
|
443
|
+
for a later request. A completion listener cannot erase an earlier settled
|
|
444
|
+
turn by changing the next operation's retention setting.
|
|
445
|
+
Chat carries a submission's no-retention choice into its copied session request
|
|
446
|
+
before any rendering wait, so re-enabling the entity before dispatch cannot
|
|
447
|
+
retain that temporary input. Existing falsy entity settings retain their
|
|
448
|
+
no-retention meaning; a compatible custom session without a Chat entity keeps
|
|
449
|
+
its existing default behavior.
|
|
440
450
|
Persistent-turn recovery and existing saved history remain unchanged.
|
|
441
451
|
|
|
442
452
|
For streaming sessions, every provisional structural card and the terminal
|
|
@@ -926,6 +936,16 @@ Native PDF and inaccessible embedded viewers require their own print command.
|
|
|
926
936
|
was produced. Closed, destroyed, or unavailable previews return `false`;
|
|
927
937
|
preparation failures reject and the visible control reports a concise failure.
|
|
928
938
|
|
|
939
|
+
Markdown previews resolve saved `arcane-media:` images through
|
|
940
|
+
[`MarkdownMedia.js`](runtime-modules.md#markdownmediajs) before connecting IMG
|
|
941
|
+
nodes. Text appears while independent image reads finish; failures leave the
|
|
942
|
+
complete text and successful images visible with a concise recovery status.
|
|
943
|
+
Explicit printing waits for local reads and decoding. Native Print reports
|
|
944
|
+
pending media because it cannot await them. Closing or replacing a preview
|
|
945
|
+
cancels display readiness without waiting for storage; URLs already retained
|
|
946
|
+
by a requested print remain until `afterprint`. Original Markdown and ordinary
|
|
947
|
+
external image URLs remain unchanged.
|
|
948
|
+
|
|
929
949
|
`await close()` cancels pending presentation work and closes delete, file, and
|
|
930
950
|
directory dialogs in that order. A settled preview keeps its rendered content,
|
|
931
951
|
media, and printing available until the file dialog actually closes; refusing
|
|
@@ -1054,7 +1074,16 @@ Events: `markdown-document-ready`, `markdown-document-state`,
|
|
|
1054
1074
|
`markdown-document-empty`, `markdown-document-error`, and
|
|
1055
1075
|
`markdown-document-navigate`.
|
|
1056
1076
|
|
|
1057
|
-
|
|
1077
|
+
Saved local image references use
|
|
1078
|
+
[`MarkdownMedia.js`](runtime-modules.md#markdownmediajs). Text rendering and
|
|
1079
|
+
the existing rendered event do not wait for image reads. The optional
|
|
1080
|
+
`labels.mediaError` supplies concise visible failure text while complete
|
|
1081
|
+
diagnostics remain in the developer console; successful images and document
|
|
1082
|
+
text remain visible. Replacing, clearing, failing, or destroying the document
|
|
1083
|
+
releases its display resources without deleting stored media. Markdown source
|
|
1084
|
+
and ordinary external image URLs remain unchanged.
|
|
1085
|
+
|
|
1086
|
+
Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`MarkdownMedia.js`](runtime-modules.md#markdownmediajs).
|
|
1058
1087
|
|
|
1059
1088
|
### Availability and normalization
|
|
1060
1089
|
|
|
@@ -1104,6 +1133,16 @@ when the browser dialog was requested. Completion of physical printing or PDF
|
|
|
1104
1133
|
saving is not observable through this method. See
|
|
1105
1134
|
[`PrintView.js`](runtime-modules.md#printviewjs).
|
|
1106
1135
|
|
|
1136
|
+
Saved `arcane-media:` image references are hydrated through
|
|
1137
|
+
[`MarkdownMedia.js`](runtime-modules.md#markdownmediajs) in detached rendered
|
|
1138
|
+
nodes. Source text, edit selection, source-map comments, ordinary image URLs,
|
|
1139
|
+
and the existing save flow remain unchanged. Image status has its own
|
|
1140
|
+
idle-hidden `media-status` part, separate from save status. Explicit printing
|
|
1141
|
+
waits for the current image reads/decoding, follows a replacement preview, and
|
|
1142
|
+
rechecks readiness before capture. Native Print reports still-pending media
|
|
1143
|
+
synchronously; it cannot wait or cancel the native dialog. Print-owned URLs
|
|
1144
|
+
outlive a changed or destroyed editor until `afterprint`.
|
|
1145
|
+
|
|
1107
1146
|
`configure({fit:true,followPreview:true})` opts into the two independent
|
|
1108
1147
|
behaviors. Both options default to `false`; omitted options retain their
|
|
1109
1148
|
previous values during reconfiguration, and explicit `false` disables either.
|
|
@@ -1140,7 +1179,7 @@ lists, quotes, and tables follow their block, while invisible source positions
|
|
|
1140
1179
|
use a nearby available rendered block rather than exact glyph geometry.
|
|
1141
1180
|
|
|
1142
1181
|
Public CSS parts are `editor`, `title`, `toolbar`, `input`, `preview`,
|
|
1143
|
-
`actions`, `status`, `print-action`, and `save-action`. Applications can make the title
|
|
1182
|
+
`actions`, `status`, `media-status`, `print-action`, and `save-action`. Applications can make the title
|
|
1144
1183
|
borderless and compact through `::part(title)` while retaining its native
|
|
1145
1184
|
editing behavior and visible keyboard focus. These parts are styling hooks,
|
|
1146
1185
|
not slots or replacement controls.
|
|
@@ -96,6 +96,7 @@ own asynchronous work, cancellation, and backpressure.
|
|
|
96
96
|
| [`Mail.js`](#mailjs) | esm | Builds complete reports and prefers the native mail capability with an explicit HTTP transport fallback. | Browser/native hybrid + cloud | Mail inputs/results normalized; transport failures mixed. |
|
|
97
97
|
| [`MailOutbox.mjs`](#mailoutboxmjs) | esm | Persists complete mail reports before delivery and normalizes idempotent enqueue, retry, reconciliation, and invalid-record maintenance. | Browser/native WebView or compatible injected host | Complete records, full work, cancellation, and lifecycle states normalized; storage, lock, and delivery failures coded. |
|
|
98
98
|
| [`MailTransport.mjs`](#mailtransportmjs) | esm | Sends one complete mail report to a normalized HTTP(S) endpoint. | Browser/server with fetch + cloud | Normalized endpoint and transport errors; remote detail preserved. |
|
|
99
|
+
| [`MarkdownMedia.js`](#markdownmediajs) | esm | Saves local Markdown images separately and resolves their stable references in rendered views. | Browser / supported native WebView | Complete image records, concurrent display reads, cancellation, and print-owned URL lifetime. |
|
|
99
100
|
| [`MarkdownSpeech.js`](#markdownspeechjs) | esm | Removes repeated Markdown formatting marks from streamed narration. | Cross-host | Speech-only filtering; single marks and ordinary punctuation remain literal. |
|
|
100
101
|
| [`Marked.min.js`](#markedminjs) | esm | Vendored Marked 18.0.5 Markdown lexer, parser, renderer, extension, and walk-token API. | Cross-host vendor module | Vendor-native Marked contract. |
|
|
101
102
|
| [`MD.js`](#mdjs) | esm | Renders complete Markdown with Marked and optionally maps source blocks to rendered comment anchors. | Browser / native WebView | Complete raw and rendered Marked values; opt-in original-source offsets; parse errors vendor-native. |
|
|
@@ -2794,6 +2795,73 @@ import * as module from '/arcane/modules/Marked.min.js';
|
|
|
2794
2795
|
console.log(Object.keys(module));
|
|
2795
2796
|
```
|
|
2796
2797
|
|
|
2798
|
+
## MarkdownMedia.js
|
|
2799
|
+
|
|
2800
|
+
### Overview
|
|
2801
|
+
|
|
2802
|
+
Stores complete images in the application's existing DBOPFS database while
|
|
2803
|
+
Markdown contains only a short, stable `arcane-media:` reference. Rendering
|
|
2804
|
+
resolves those local references into temporary display URLs. Importing the
|
|
2805
|
+
module starts no storage, network, provider, or rendering operation.
|
|
2806
|
+
|
|
2807
|
+
### Public surface
|
|
2808
|
+
|
|
2809
|
+
Public package import: `arcane-os/modules/MarkdownMedia.js`.
|
|
2810
|
+
|
|
2811
|
+
- `await saveMarkdownMedia({blob,tableName='markdown-media',fileName})` writes
|
|
2812
|
+
`{mediaType,dataUrl}` as a JSON-compatible record and returns
|
|
2813
|
+
`{reference,tableName,fileName,mediaType}` only after the write completes.
|
|
2814
|
+
The default filename is a new UUID followed by `.json`. There is no abort
|
|
2815
|
+
option: accepted storage writes settle through the storage owner.
|
|
2816
|
+
- `parseMarkdownMediaReference(reference)` returns `{tableName,fileName}` for
|
|
2817
|
+
a local reference, or `null` for an ordinary URL. Malformed local addresses
|
|
2818
|
+
report their actual parsing error.
|
|
2819
|
+
- `await readMarkdownMedia(reference)` returns the complete `Blob`, using
|
|
2820
|
+
only local storage and data-URL decoding; stored strings are never fetched
|
|
2821
|
+
as network addresses.
|
|
2822
|
+
- `hydrateMarkdownMedia(root,{signal}={})` returns `{ready,destroy,retain}`
|
|
2823
|
+
immediately. It includes an IMG root and descendant IMG elements, removes
|
|
2824
|
+
local-reference `src` values synchronously, starts independent reads
|
|
2825
|
+
together, and assigns owned object URLs. `ready` includes image decoding.
|
|
2826
|
+
Use a detached fragment before connecting those images to the document.
|
|
2827
|
+
|
|
2828
|
+
Hydration preserves Markdown source, image descriptions, ordinary URLs, and
|
|
2829
|
+
surrounding content. Failed reads or decodes reject `ready` with an
|
|
2830
|
+
`AggregateError` whose complete `failures` array contains
|
|
2831
|
+
`{image,reference,reason}`; successful sibling images remain visible.
|
|
2832
|
+
|
|
2833
|
+
`destroy()` or abort settles display readiness promptly, prevents late display,
|
|
2834
|
+
and releases owned URLs. An already-started storage read remains observed and
|
|
2835
|
+
settles independently. `retain()` returns an idempotent release callback for
|
|
2836
|
+
printing; retained URLs survive destruction of the original view until every
|
|
2837
|
+
print owner releases them. Destruction never deletes stored images.
|
|
2838
|
+
|
|
2839
|
+
Applications own record names, entry association, cleanup policy, and which
|
|
2840
|
+
records their existing JSON backup/export includes. Reusing the same filename
|
|
2841
|
+
supports an application-selected save-only retry without another generation
|
|
2842
|
+
request. The helper performs no existing-data migration.
|
|
2843
|
+
|
|
2844
|
+
### Availability and normalization
|
|
2845
|
+
|
|
2846
|
+
Browser or native WebView with the existing app-scoped DBOPFS owner, Blob,
|
|
2847
|
+
FileReader, object URLs, and IMG decoding. Encoding, storage, missing-record,
|
|
2848
|
+
parsing, and decoding errors remain observable. No Core capability is selected.
|
|
2849
|
+
|
|
2850
|
+
### Example
|
|
2851
|
+
|
|
2852
|
+
```javascript
|
|
2853
|
+
import {saveMarkdownMedia} from 'arcane-os/modules/MarkdownMedia.js';
|
|
2854
|
+
|
|
2855
|
+
const mural = new Blob([
|
|
2856
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="240" height="80"><text x="10" y="45">Octopus parking only</text></svg>'
|
|
2857
|
+
], {type:'image/svg+xml'});
|
|
2858
|
+
const {reference} = await saveMarkdownMedia({blob:mural});
|
|
2859
|
+
editor.insertMarkdown(``);
|
|
2860
|
+
```
|
|
2861
|
+
|
|
2862
|
+
Here `editor` is the ready shared Markdown Editor. Its normal change/save flow
|
|
2863
|
+
still owns saving the Markdown; this operation saves the image record.
|
|
2864
|
+
|
|
2797
2865
|
## MD.js
|
|
2798
2866
|
|
|
2799
2867
|
### Overview
|
|
@@ -3071,6 +3139,9 @@ assistant content in configured model context and ChatEntity/DBOPFS history.
|
|
|
3071
3139
|
That assistant-only opening survives reload and empty-chat maintenance without
|
|
3072
3140
|
a fabricated user turn. A second opening rejects as `AI_CHAT_OPENING_EXISTS`;
|
|
3073
3141
|
an unavailable durable ChatEntity rejects as `AI_CHAT_PERSISTENCE_UNAVAILABLE`.
|
|
3142
|
+
If retention is disabled while the opening is being prepared, that same error
|
|
3143
|
+
rolls back the opening before its append; it does not leave hidden model context.
|
|
3144
|
+
An opening write already accepted by DBOPFS still completes normally.
|
|
3074
3145
|
|
|
3075
3146
|
`send()` accepts either
|
|
3076
3147
|
`{message:{content,role:'user'|'tool',tool_call_id?,message?,name?,status?,persist},...}`
|
|
@@ -3086,6 +3157,20 @@ the retained transcript, memory extraction, or DBOPFS. A nonpersistent response
|
|
|
3086
3157
|
therefore does not open a retained structural-tool continuation. A retained
|
|
3087
3158
|
structural tool result must use the persistence choice captured by its matching
|
|
3088
3159
|
assistant tool call.
|
|
3160
|
+
An explicitly nonretained tool-result continuation uses its matching pending
|
|
3161
|
+
call only for that operation, then rolls back without settling or changing the
|
|
3162
|
+
retained call. A later retained continuation can still settle it normally.
|
|
3163
|
+
|
|
3164
|
+
The entity-wide `session.chatEntity.persist=false` applies the same no-retention
|
|
3165
|
+
rule to new turns and direct entity additions; it is not a disk-only switch.
|
|
3166
|
+
The session captures that setting when it accepts a request and observes it
|
|
3167
|
+
again before accepting a durable append. Changing it later cannot bring back
|
|
3168
|
+
discarded inputs or responses. A write already accepted by DBOPFS finishes
|
|
3169
|
+
normally; toggling the setting does not undo that write or rewrite saved data.
|
|
3170
|
+
Completed `send()` and `stream()` responses include a session-owned `retained`
|
|
3171
|
+
boolean outside `message` and the provider payload, identifying whether the
|
|
3172
|
+
turn entered recurring history. Existing retained and stored records remain
|
|
3173
|
+
untouched: this boundary applies to new additions and performs no migration.
|
|
3089
3174
|
|
|
3090
3175
|
`history()` returns provider-safe configured model context, including the
|
|
3091
3176
|
system prompt and every complete ordinary visible committed turn. Only a
|
|
@@ -3263,11 +3348,16 @@ print-only surface expands scrollable content without editing the live screen.
|
|
|
3263
3348
|
|
|
3264
3349
|
### Public surface
|
|
3265
3350
|
|
|
3266
|
-
Named `createPrintView({host,content,title,active,priority=0,signal,onError,retain})`
|
|
3351
|
+
Named `createPrintView({host,content,title,active,priority=0,signal,onError,retain,prepare})`
|
|
3267
3352
|
returns `{print,destroy}`. `host` is a connected element; synchronous callbacks
|
|
3268
3353
|
return the rendered content node, string title, and whether the view is active.
|
|
3269
3354
|
Optional `retain()` returns a resource-release callback. It keeps owned media
|
|
3270
3355
|
available until preparation is cancelled or the browser emits `afterprint`.
|
|
3356
|
+
Optional `prepare(signal)` may await view-owned readiness during an explicit
|
|
3357
|
+
print request. After it settles, current content and title are read again
|
|
3358
|
+
synchronously immediately before capture. A view whose content changes during
|
|
3359
|
+
preparation must keep its owned media retained and reject a still-pending
|
|
3360
|
+
snapshot through its synchronous `content()` callback.
|
|
3271
3361
|
|
|
3272
3362
|
`await print()` selects that view, waits for accessible HTML frame, image, and
|
|
3273
3363
|
font readiness, and requests the browser print dialog. It returns `false` when
|
|
@@ -3281,6 +3371,8 @@ on ties. File preview uses priority `1`; editor uses `0`. Native `beforeprint`
|
|
|
3281
3371
|
is synchronous: it snapshots currently rendered content, cannot await a newly
|
|
3282
3372
|
loading resource, and cannot cancel the browser's dialog. `onError(error)` or
|
|
3283
3373
|
the developer console receives complete preparation failures.
|
|
3374
|
+
Native printing never invokes the asynchronous `prepare` callback; prepared
|
|
3375
|
+
views still receive the synchronous content/title read at capture.
|
|
3284
3376
|
|
|
3285
3377
|
`destroy()` unregisters the view and cancels pending preparation. Once the
|
|
3286
3378
|
dialog has been requested, its snapshot and retained media remain until
|