arcane-os 0.5.6 → 0.5.8
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 +16 -0
- package/README.md +22 -9
- package/package.json +1 -1
- package/runtime/arcane/entities/Chat.js +3 -3
- package/runtime/arcane/modules/AI.js +70 -18
- package/runtime/arcane/modules/ChatRecords.js +14 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +77 -0
- package/runtime/arcane/modules/DataMaintenance.js +3 -2
- package/runtime/arcane/modules/PersistentAIChatSession.js +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.8
|
|
4
|
+
|
|
5
|
+
- Added provider-neutral `reasoningEffort` to `AI.fetchRequest()` and
|
|
6
|
+
`AI.streamRequest()`, accepting `none`, `low`, `medium`, `high`, or
|
|
7
|
+
`max`. TWiN Cloud forwards an explicit value as `reasoning_effort`, omits
|
|
8
|
+
the field when unspecified, and preserves explicit
|
|
9
|
+
`openai-gpt-oss-120b` or `openai-gpt-oss-20b` model selection with the
|
|
10
|
+
existing streaming and structural-tool behavior.
|
|
11
|
+
|
|
12
|
+
## 0.5.7
|
|
13
|
+
|
|
14
|
+
- Added `PersistentAIChatSession.open()` for model-authored conversation
|
|
15
|
+
openings. Its application-authored bootstrap remains transient, while the
|
|
16
|
+
complete nonblank assistant response is committed atomically as durable
|
|
17
|
+
assistant-only chat history and survives maintenance and reload.
|
|
18
|
+
|
|
3
19
|
## 0.5.6
|
|
4
20
|
|
|
5
21
|
- Sanitized newly persisted chat history into complete human-readable user,
|
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.5.
|
|
22
|
+
This checkout defines the `0.5.8` 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
|
|
|
@@ -93,6 +93,13 @@ on device: Whisper (`LOCAL_SPEACH` / `whisper-small`) owns transcription and
|
|
|
93
93
|
Kokoro (`LOCAL_SPEACH` / `kokoro`) owns speech synthesis. Neither audio route
|
|
94
94
|
uses the TWiN key, and no OpenAI audio key is required.
|
|
95
95
|
|
|
96
|
+
`fetchRequest()` and `streamRequest()` accept the provider-neutral
|
|
97
|
+
`reasoningEffort` option with `none`, `low`, `medium`, `high`, or `max`. TWiN
|
|
98
|
+
Cloud maps it to DigitalOcean Serverless Inference `reasoning_effort`; omitting
|
|
99
|
+
it preserves the provider default. TWiN Cloud defaults to
|
|
100
|
+
`openai-gpt-oss-120b`, and applications may explicitly select
|
|
101
|
+
`openai-gpt-oss-20b` without changing streaming or structural-tool behavior.
|
|
102
|
+
|
|
96
103
|
## Browser-local AI
|
|
97
104
|
|
|
98
105
|
`arcane-os/ai/browser-wasm` provides the shared Wllama LLM provider. The app
|
|
@@ -176,8 +183,14 @@ The SDK runtime also owns `DBOPFSDocumentLibrary`,
|
|
|
176
183
|
schema-driven and explicit; chat never searches a corpus unless the app wires
|
|
177
184
|
that library into the request context builder. Persistent chat automatically
|
|
178
185
|
maintains recurring model context and `ChatEntity` history/memory. A turn may
|
|
179
|
-
set `persist:false` to
|
|
180
|
-
|
|
186
|
+
set `persist:false` to participate in one request and response only; after that
|
|
187
|
+
operation settles, neither side remains in subsequent model context, the
|
|
188
|
+
retained transcript, durable chat, memory, or DBOPFS.
|
|
189
|
+
For an automatic model-authored opening, call
|
|
190
|
+
`session.open({message:{content:bootstrap,persist:false}})`: the bootstrap is
|
|
191
|
+
request-only, while the complete nonblank assistant response becomes the first
|
|
192
|
+
durable conversation row without a fabricated user turn.
|
|
193
|
+
`createArcaneAI(...).createChatSession(options)` wires
|
|
181
194
|
that session to the same selected LLM controller, creates its `ChatEntity`, and
|
|
182
195
|
uses the same controller for automatic memory extraction.
|
|
183
196
|
|
|
@@ -186,7 +199,7 @@ uses the same controller for automatic memory extraction.
|
|
|
186
199
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
187
200
|
|
|
188
201
|
```bash
|
|
189
|
-
npx arcane-os@0.5.
|
|
202
|
+
npx arcane-os@0.5.8 new my-app --path ./my-app --target portable --git
|
|
190
203
|
cd my-app
|
|
191
204
|
npm install
|
|
192
205
|
npm run check
|
|
@@ -197,7 +210,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
197
210
|
missing Arcane files:
|
|
198
211
|
|
|
199
212
|
```bash
|
|
200
|
-
npm install --save-dev --save-exact arcane-os@0.5.
|
|
213
|
+
npm install --save-dev --save-exact arcane-os@0.5.8
|
|
201
214
|
npm exec -- arcane init my-app --target portable
|
|
202
215
|
```
|
|
203
216
|
|
|
@@ -213,7 +226,7 @@ npm exec -- arcane-os targets
|
|
|
213
226
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
214
227
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
215
228
|
|
|
216
|
-
Use `npx arcane-os@0.5.
|
|
229
|
+
Use `npx arcane-os@0.5.8` for the initial bootstrap because it names this npm
|
|
217
230
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
218
231
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
219
232
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -234,7 +247,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
234
247
|
|
|
235
248
|
# From the generated app repository
|
|
236
249
|
cd ../local-app
|
|
237
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.
|
|
250
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.8.tgz
|
|
238
251
|
npm run check
|
|
239
252
|
npm ci
|
|
240
253
|
```
|
|
@@ -244,7 +257,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
244
257
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
245
258
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
246
259
|
runner also needs that tarball at the locked path. After publication, replace
|
|
247
|
-
the local declaration with the exact `arcane-os@0.5.
|
|
260
|
+
the local declaration with the exact `arcane-os@0.5.8` registry package and
|
|
248
261
|
commit the regenerated lock.
|
|
249
262
|
|
|
250
263
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -382,7 +395,7 @@ package installation, or assertions.
|
|
|
382
395
|
|
|
383
396
|
## Current target support
|
|
384
397
|
|
|
385
|
-
Version `0.5.
|
|
398
|
+
Version `0.5.8` exposes one browser target and five explicitly paired
|
|
386
399
|
native development targets: a non-runnable portable directory, a
|
|
387
400
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
388
401
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Is from '../../node_modules/strong-type/index.js';
|
|
2
2
|
import '../modules/DBOPFS.js';
|
|
3
3
|
import '../modules/AI.js';
|
|
4
|
-
import {hasUserEntry} from '../modules/ChatRecords.js';
|
|
4
|
+
import {hasConversationEntry,hasUserEntry} from '../modules/ChatRecords.js';
|
|
5
5
|
import {normalizeMemoryContent} from '../modules/MemoryRecords.js';
|
|
6
6
|
|
|
7
7
|
const is = new Is(false);
|
|
@@ -925,7 +925,7 @@ ${JSON.stringify(transcript)}`
|
|
|
925
925
|
* @returns {Promise<*>}
|
|
926
926
|
*/
|
|
927
927
|
async save(){
|
|
928
|
-
if(!
|
|
928
|
+
if(!hasConversationEntry(this.#durableMessages())){
|
|
929
929
|
this.#saved=false;
|
|
930
930
|
return false;
|
|
931
931
|
}
|
|
@@ -1073,7 +1073,7 @@ ${JSON.stringify(transcript)}`
|
|
|
1073
1073
|
return;
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
1076
|
-
if(!
|
|
1076
|
+
if(!hasConversationEntry(this.#durableMessages())){
|
|
1077
1077
|
return false;
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
@@ -30,6 +30,7 @@ const TTS_ANY_PUNCTUATION_AT_END=/(?<!\p{P})(?:(?<![\p{L}\p{N}])\p{P}+|\p{P}+(?!
|
|
|
30
30
|
credentials='omit';
|
|
31
31
|
|
|
32
32
|
const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
|
|
33
|
+
const AI_REASONING_EFFORTS=new Set(['none','low','medium','high','max']);
|
|
33
34
|
export const AI_READY_EVENT='ai-ready';
|
|
34
35
|
const AI_TTS_FAILURE_EVENT='ai-tts-failure';
|
|
35
36
|
export const AI_INITIALIZATION_ERROR_CODES=completeValue({
|
|
@@ -128,6 +129,20 @@ function normalizeAIRequestAbort(error){
|
|
|
128
129
|
return normalized;
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
function normalizeAIReasoningEffort(value){
|
|
133
|
+
if(value===undefined||value===null||value===''){
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
if(typeof value!=='string'||!AI_REASONING_EFFORTS.has(value)){
|
|
137
|
+
const error=new TypeError(
|
|
138
|
+
'AI reasoningEffort must be none, low, medium, high, or max.'
|
|
139
|
+
);
|
|
140
|
+
error.code='AI_REASONING_EFFORT_INVALID';
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
131
146
|
function legacyAIProviderError(message,code,cause){
|
|
132
147
|
const error=cause===undefined
|
|
133
148
|
?new Error(message)
|
|
@@ -2000,7 +2015,7 @@ class AI {
|
|
|
2000
2015
|
||model;
|
|
2001
2016
|
}
|
|
2002
2017
|
if(service==='OPENAI'){
|
|
2003
|
-
return this.#models.OPENAI;
|
|
2018
|
+
return model==='OPENAI'?this.#models.OPENAI:model;
|
|
2004
2019
|
}
|
|
2005
2020
|
return model;
|
|
2006
2021
|
}
|
|
@@ -4126,7 +4141,8 @@ class AI {
|
|
|
4126
4141
|
parallelToolCalls,
|
|
4127
4142
|
payload.id??Date.now(),
|
|
4128
4143
|
function ignoreLegacyLLMProviderRequest(){},
|
|
4129
|
-
signal
|
|
4144
|
+
signal,
|
|
4145
|
+
payload.reasoningEffort
|
|
4130
4146
|
);
|
|
4131
4147
|
}
|
|
4132
4148
|
|
|
@@ -4153,7 +4169,9 @@ class AI {
|
|
|
4153
4169
|
payload.structuredOutput??false,
|
|
4154
4170
|
false,
|
|
4155
4171
|
true,
|
|
4156
|
-
emitLegacyLLMStreamData
|
|
4172
|
+
emitLegacyLLMStreamData,
|
|
4173
|
+
function ignoreLegacyLLMStreamResult(){},
|
|
4174
|
+
payload.reasoningEffort
|
|
4157
4175
|
);
|
|
4158
4176
|
}
|
|
4159
4177
|
|
|
@@ -4323,9 +4341,13 @@ class AI {
|
|
|
4323
4341
|
minP,
|
|
4324
4342
|
seed,
|
|
4325
4343
|
stop,
|
|
4326
|
-
templateOptions
|
|
4344
|
+
templateOptions,
|
|
4345
|
+
reasoningEffort
|
|
4327
4346
|
}={}){
|
|
4328
4347
|
validateAIStructuralRequest(messages,tools,parallelToolCalls);
|
|
4348
|
+
const normalizedReasoningEffort=normalizeAIReasoningEffort(
|
|
4349
|
+
reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
|
|
4350
|
+
);
|
|
4329
4351
|
if(localOnly!==true&&localOnly!==false){
|
|
4330
4352
|
throw new TypeError('AI localOnly must be a boolean.');
|
|
4331
4353
|
}
|
|
@@ -4355,7 +4377,10 @@ class AI {
|
|
|
4355
4377
|
...(minP!==undefined?{minP}:{}),
|
|
4356
4378
|
...(seed!==undefined?{seed}:{}),
|
|
4357
4379
|
...(stop!==undefined?{stop}:{}),
|
|
4358
|
-
...(templateOptions!==undefined?{templateOptions}:{})
|
|
4380
|
+
...(templateOptions!==undefined?{templateOptions}:{}),
|
|
4381
|
+
...(normalizedReasoningEffort
|
|
4382
|
+
?{reasoningEffort:normalizedReasoningEffort}
|
|
4383
|
+
:{})
|
|
4359
4384
|
};
|
|
4360
4385
|
const displayId=`M-${id}`;
|
|
4361
4386
|
let handle=null;
|
|
@@ -4457,7 +4482,8 @@ class AI {
|
|
|
4457
4482
|
false,
|
|
4458
4483
|
true,
|
|
4459
4484
|
onDataChunk,
|
|
4460
|
-
onDataResult
|
|
4485
|
+
onDataResult,
|
|
4486
|
+
normalizedReasoningEffort
|
|
4461
4487
|
);
|
|
4462
4488
|
const structuralToolCalls=normalizeAICompletionToolCalls(
|
|
4463
4489
|
completion
|
|
@@ -4563,7 +4589,8 @@ class AI {
|
|
|
4563
4589
|
finishSpeech=true,
|
|
4564
4590
|
returnCompletion=false,
|
|
4565
4591
|
dataChunkHandler=function ignoreLegacyStreamDataChunk(){},
|
|
4566
|
-
dataResultHandler=function ignoreLegacyStreamDataResult(){}
|
|
4592
|
+
dataResultHandler=function ignoreLegacyStreamDataResult(){},
|
|
4593
|
+
reasoningEffort
|
|
4567
4594
|
){
|
|
4568
4595
|
let speechTurnCompleted=false;
|
|
4569
4596
|
|
|
@@ -4583,6 +4610,9 @@ class AI {
|
|
|
4583
4610
|
structuredOutput
|
|
4584
4611
|
);
|
|
4585
4612
|
|
|
4613
|
+
const normalizedReasoningEffort=normalizeAIReasoningEffort(
|
|
4614
|
+
reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
|
|
4615
|
+
);
|
|
4586
4616
|
const request={
|
|
4587
4617
|
model:this.model,
|
|
4588
4618
|
messages:messages,
|
|
@@ -4603,8 +4633,11 @@ class AI {
|
|
|
4603
4633
|
}
|
|
4604
4634
|
}
|
|
4605
4635
|
|
|
4606
|
-
if(
|
|
4607
|
-
|
|
4636
|
+
if(
|
|
4637
|
+
normalizedReasoningEffort
|
|
4638
|
+
&&(this.llmService==='OPENAI'||this.llmService==='OLLAMA')
|
|
4639
|
+
){
|
|
4640
|
+
request.reasoning_effort=normalizedReasoningEffort;
|
|
4608
4641
|
}
|
|
4609
4642
|
|
|
4610
4643
|
let isThinking=true;
|
|
@@ -4631,7 +4664,9 @@ class AI {
|
|
|
4631
4664
|
model:this.model,
|
|
4632
4665
|
messages:ollamaMessages,
|
|
4633
4666
|
stream:true,
|
|
4634
|
-
...(
|
|
4667
|
+
...(normalizedReasoningEffort
|
|
4668
|
+
?{think:normalizedReasoningEffort}
|
|
4669
|
+
:{}),
|
|
4635
4670
|
...(structuredOutputFormat?{format:structuredOutputFormat}:{}),
|
|
4636
4671
|
...(ollamaTools.length?{tools:ollamaTools}:{})
|
|
4637
4672
|
};
|
|
@@ -5272,9 +5307,13 @@ class AI {
|
|
|
5272
5307
|
minP,
|
|
5273
5308
|
seed,
|
|
5274
5309
|
stop,
|
|
5275
|
-
templateOptions
|
|
5310
|
+
templateOptions,
|
|
5311
|
+
reasoningEffort
|
|
5276
5312
|
}={}){
|
|
5277
5313
|
validateAIStructuralRequest(messages,tools,parallelToolCalls);
|
|
5314
|
+
const normalizedReasoningEffort=normalizeAIReasoningEffort(
|
|
5315
|
+
reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
|
|
5316
|
+
);
|
|
5278
5317
|
if(localOnly!==true&&localOnly!==false){
|
|
5279
5318
|
throw new TypeError('AI localOnly must be a boolean.');
|
|
5280
5319
|
}
|
|
@@ -5306,7 +5345,10 @@ class AI {
|
|
|
5306
5345
|
...(minP!==undefined?{minP}:{}),
|
|
5307
5346
|
...(seed!==undefined?{seed}:{}),
|
|
5308
5347
|
...(stop!==undefined?{stop}:{}),
|
|
5309
|
-
...(templateOptions!==undefined?{templateOptions}:{})
|
|
5348
|
+
...(templateOptions!==undefined?{templateOptions}:{}),
|
|
5349
|
+
...(normalizedReasoningEffort
|
|
5350
|
+
?{reasoningEffort:normalizedReasoningEffort}
|
|
5351
|
+
:{})
|
|
5310
5352
|
};
|
|
5311
5353
|
await this.#reportRequest(onRequest,request,id);
|
|
5312
5354
|
if(signal?.aborted){
|
|
@@ -5332,7 +5374,7 @@ class AI {
|
|
|
5332
5374
|
return response;
|
|
5333
5375
|
}
|
|
5334
5376
|
|
|
5335
|
-
return this
|
|
5377
|
+
return this.#fetchLegacy(
|
|
5336
5378
|
messages,
|
|
5337
5379
|
onResponse,
|
|
5338
5380
|
structuredOutput,
|
|
@@ -5341,7 +5383,8 @@ class AI {
|
|
|
5341
5383
|
parallelToolCalls,
|
|
5342
5384
|
id,
|
|
5343
5385
|
onRequest,
|
|
5344
|
-
signal
|
|
5386
|
+
signal,
|
|
5387
|
+
normalizedReasoningEffort
|
|
5345
5388
|
);
|
|
5346
5389
|
}
|
|
5347
5390
|
|
|
@@ -5354,7 +5397,7 @@ class AI {
|
|
|
5354
5397
|
parallel_tool_calls,
|
|
5355
5398
|
id=Date.now(),
|
|
5356
5399
|
requestHandler=function ignoreFetchRequest(){},
|
|
5357
|
-
signal=null
|
|
5400
|
+
signal=null
|
|
5358
5401
|
){
|
|
5359
5402
|
if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
|
|
5360
5403
|
return this.fetchRequest({
|
|
@@ -5394,6 +5437,7 @@ class AI {
|
|
|
5394
5437
|
id=Date.now(),
|
|
5395
5438
|
requestHandler=function ignoreFetchRequest(){},
|
|
5396
5439
|
signal=null,
|
|
5440
|
+
reasoningEffort
|
|
5397
5441
|
){
|
|
5398
5442
|
validateAIStructuralRequest(messages,tools,parallel_tool_calls);
|
|
5399
5443
|
this.#assertServiceConfigured(this.llmService);
|
|
@@ -5408,6 +5452,9 @@ class AI {
|
|
|
5408
5452
|
}
|
|
5409
5453
|
const structuredOutputFormat=this.#structuredOutputFormat(structuredOutput);
|
|
5410
5454
|
|
|
5455
|
+
const normalizedReasoningEffort=normalizeAIReasoningEffort(
|
|
5456
|
+
reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
|
|
5457
|
+
);
|
|
5411
5458
|
const request={
|
|
5412
5459
|
model:this.model,
|
|
5413
5460
|
messages:messages,
|
|
@@ -5428,8 +5475,11 @@ class AI {
|
|
|
5428
5475
|
}
|
|
5429
5476
|
}
|
|
5430
5477
|
|
|
5431
|
-
if(
|
|
5432
|
-
|
|
5478
|
+
if(
|
|
5479
|
+
normalizedReasoningEffort
|
|
5480
|
+
&&(this.llmService==='OPENAI'||this.llmService==='OLLAMA')
|
|
5481
|
+
){
|
|
5482
|
+
request.reasoning_effort=normalizedReasoningEffort;
|
|
5433
5483
|
}
|
|
5434
5484
|
|
|
5435
5485
|
const nativeOllama=this.#nativeOllama();
|
|
@@ -5448,7 +5498,9 @@ class AI {
|
|
|
5448
5498
|
model:this.model,
|
|
5449
5499
|
messages:ollamaMessages,
|
|
5450
5500
|
stream:false,
|
|
5451
|
-
...(
|
|
5501
|
+
...(normalizedReasoningEffort
|
|
5502
|
+
?{think:normalizedReasoningEffort}
|
|
5503
|
+
:{}),
|
|
5452
5504
|
...(structuredOutputFormat?{format:structuredOutputFormat}:{}),
|
|
5453
5505
|
...(ollamaTools.length?{tools:ollamaTools}:{})
|
|
5454
5506
|
};
|
|
@@ -8,6 +8,20 @@ function hasUserEntry(chat=[]){
|
|
|
8
8
|
);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function hasConversationEntry(chat=[]){
|
|
12
|
+
const messages=Array.isArray(chat)
|
|
13
|
+
?chat
|
|
14
|
+
:chat?.messages||[];
|
|
15
|
+
|
|
16
|
+
return hasUserEntry(messages)||messages.some(
|
|
17
|
+
message=>
|
|
18
|
+
message?.role==='assistant'
|
|
19
|
+
&&typeof message.content==='string'
|
|
20
|
+
&&Boolean(message.content.trim())
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
export {
|
|
25
|
+
hasConversationEntry,
|
|
12
26
|
hasUserEntry
|
|
13
27
|
};
|
|
@@ -584,6 +584,83 @@ export default class ConfiguredAIChatSession{
|
|
|
584
584
|
return context;
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
+
/** Prepares one transient bootstrap request whose assistant response alone may be committed. */
|
|
588
|
+
async prepareOpening(input,options={}){
|
|
589
|
+
if(!isPlainRecord(options)) throw new TypeError('Chat opening options must be a plain object.');
|
|
590
|
+
const unsupported=Object.keys(options).find(key=>!['request','signal'].includes(key));
|
|
591
|
+
if(unsupported) throw new TypeError(`Unsupported chat opening option: ${unsupported}`);
|
|
592
|
+
if(!signalLike(options.signal)) throw new TypeError('signal must be an AbortSignal.');
|
|
593
|
+
if(options.signal?.aborted) throw abortError();
|
|
594
|
+
const turnRequest=normalizeRequestOptions(options.request,'request');
|
|
595
|
+
const [normalizedInputMessage]=normalizeInputMessages(input);
|
|
596
|
+
if(normalizedInputMessage.role!=='user'){
|
|
597
|
+
throw new TypeError('The chat opening bootstrap must be a user message.');
|
|
598
|
+
}
|
|
599
|
+
const inputMessage=message('user',normalizedInputMessage.content);
|
|
600
|
+
if(this.#pending){
|
|
601
|
+
throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
|
|
602
|
+
}
|
|
603
|
+
if(this.#conversation.length){
|
|
604
|
+
throw coded(
|
|
605
|
+
new Error('The chat already contains a retained conversation turn.'),
|
|
606
|
+
'AI_CHAT_OPENING_EXISTS',
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
this.#pending=true;
|
|
610
|
+
try{
|
|
611
|
+
const context=await this.#contextFor(inputMessage.content,options.signal);
|
|
612
|
+
if(options.signal?.aborted) throw abortError();
|
|
613
|
+
const transientContext=context
|
|
614
|
+
?message('user',context)
|
|
615
|
+
:null;
|
|
616
|
+
const requestMessages=completeHistory(
|
|
617
|
+
this.#systemPrompt,
|
|
618
|
+
[...(transientContext?[transientContext]:[]),inputMessage],
|
|
619
|
+
);
|
|
620
|
+
let providerResponse;
|
|
621
|
+
try{
|
|
622
|
+
providerResponse=await this.#chat({
|
|
623
|
+
...this.#request,
|
|
624
|
+
...turnRequest,
|
|
625
|
+
...(options.signal?{signal:options.signal}:{}),
|
|
626
|
+
messages:requestMessages.map(publicMessage),
|
|
627
|
+
});
|
|
628
|
+
}catch(error){
|
|
629
|
+
if(options.signal?.aborted) throw abortError();
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
const response=normalizeResponse(providerResponse);
|
|
633
|
+
if(options.signal?.aborted) throw abortError();
|
|
634
|
+
if(response.message.tool_calls?.length||!response.message.content.trim()){
|
|
635
|
+
throw coded(
|
|
636
|
+
new TypeError('The model-authored chat opening must contain visible assistant text.'),
|
|
637
|
+
'AI_CHAT_INVALID_OPENING_RESPONSE',
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const openingMessage=message('assistant',response.message.content);
|
|
641
|
+
let settled=false;
|
|
642
|
+
return {
|
|
643
|
+
response,
|
|
644
|
+
commit:()=>{
|
|
645
|
+
if(settled) throw coded(new Error('The prepared chat opening is already settled.'),'AI_CHAT_TRANSACTION_SETTLED');
|
|
646
|
+
this.#conversation=[openingMessage];
|
|
647
|
+
settled=true;
|
|
648
|
+
this.#pending=false;
|
|
649
|
+
return response;
|
|
650
|
+
},
|
|
651
|
+
rollback:()=>{
|
|
652
|
+
if(settled) return false;
|
|
653
|
+
settled=true;
|
|
654
|
+
this.#pending=false;
|
|
655
|
+
return true;
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
}catch(error){
|
|
659
|
+
this.#pending=false;
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
587
664
|
async prepare(input,options={}){
|
|
588
665
|
if(!isPlainRecord(options)) throw new TypeError('Chat send options must be a plain object.');
|
|
589
666
|
const unsupported=Object.keys(options).find(key=>!['request','signal'].includes(key));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import './DBOPFS.js';
|
|
2
|
-
import {hasUserEntry} from './ChatRecords.js';
|
|
2
|
+
import {hasConversationEntry,hasUserEntry} from './ChatRecords.js';
|
|
3
3
|
import {hasMemoryContent} from './MemoryRecords.js';
|
|
4
4
|
import {arcaneEvents} from 'arcane-os/event-manager';
|
|
5
5
|
|
|
@@ -16,7 +16,7 @@ async function clearEmptyChatsAndMemories(){
|
|
|
16
16
|
true
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
-
if(!
|
|
19
|
+
if(!hasConversationEntry(chat)){
|
|
20
20
|
emptyChats.push(chatFileNames[i]);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -96,6 +96,7 @@ function waitForDBOPFS(){
|
|
|
96
96
|
|
|
97
97
|
export {
|
|
98
98
|
clearEmptyChatsAndMemories,
|
|
99
|
+
hasConversationEntry,
|
|
99
100
|
hasMemoryContent,
|
|
100
101
|
hasUserEntry
|
|
101
102
|
};
|
|
@@ -286,6 +286,35 @@ function normalizeSend(input){
|
|
|
286
286
|
};
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
function normalizeOpening(input){
|
|
290
|
+
if(!isPlainRecord(input)) throw new TypeError('Persistent chat opening input must be a plain object.');
|
|
291
|
+
assertKnownKeys(input,new Set(['message','request','signal']),'Persistent chat opening input');
|
|
292
|
+
if(!isPlainRecord(input.message)) throw new TypeError('message must be a plain object.');
|
|
293
|
+
assertKnownKeys(input.message,new Set(['content','persist','role']),'message');
|
|
294
|
+
if(typeof input.message.content!=='string'||!input.message.content.trim()){
|
|
295
|
+
throw new TypeError('message.content must contain text.');
|
|
296
|
+
}
|
|
297
|
+
const role=input.message.role??'user';
|
|
298
|
+
if(role!=='user') throw new TypeError('message.role must be user.');
|
|
299
|
+
if(input.message.persist!==undefined&&input.message.persist!==false){
|
|
300
|
+
throw new TypeError('The chat opening bootstrap is always nonpersistent.');
|
|
301
|
+
}
|
|
302
|
+
const request=input.request??{};
|
|
303
|
+
if(!isPlainRecord(request)) throw new TypeError('request must be a plain object.');
|
|
304
|
+
const managedRequestField=Object.keys(request).find(
|
|
305
|
+
key=>SESSION_MANAGED_REQUEST_FIELDS.has(key)
|
|
306
|
+
);
|
|
307
|
+
if(managedRequestField){
|
|
308
|
+
throw new TypeError(`request.${managedRequestField} is managed by the chat session.`);
|
|
309
|
+
}
|
|
310
|
+
if(!signalLike(input.signal)) throw new TypeError('signal must be an AbortSignal.');
|
|
311
|
+
return {
|
|
312
|
+
message:{role:'user',content:input.message.content},
|
|
313
|
+
request:{...request},
|
|
314
|
+
signal:input.signal??null,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
289
318
|
function fileName(value){
|
|
290
319
|
if(typeof value!=='string'||value.length===0){
|
|
291
320
|
throw new TypeError('chatFileName must be a nonempty string.');
|
|
@@ -506,6 +535,54 @@ class PersistentAIChatSession{
|
|
|
506
535
|
return this.#entity.settleMemory();
|
|
507
536
|
}
|
|
508
537
|
|
|
538
|
+
/** Persists one model-authored opening while retaining none of its bootstrap request. */
|
|
539
|
+
async open(input){
|
|
540
|
+
const settings=normalizeOpening(input);
|
|
541
|
+
if(this.#pending){
|
|
542
|
+
throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
|
|
543
|
+
}
|
|
544
|
+
this.#pending=true;
|
|
545
|
+
let prepared=null;
|
|
546
|
+
try{
|
|
547
|
+
await this.ready();
|
|
548
|
+
if(this.#historyError)throw this.#historyError;
|
|
549
|
+
if(this.#entity.persist!==true){
|
|
550
|
+
throw coded(
|
|
551
|
+
new Error('The model-authored chat opening requires durable chat persistence.'),
|
|
552
|
+
'AI_CHAT_PERSISTENCE_UNAVAILABLE',
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
await this.#entity.settleMemory();
|
|
556
|
+
prepared=await this.#configured.prepareOpening(
|
|
557
|
+
settings.message,
|
|
558
|
+
{request:settings.request,signal:settings.signal},
|
|
559
|
+
);
|
|
560
|
+
await this.#entity.addAIMessage(
|
|
561
|
+
prepared.response.message.content,
|
|
562
|
+
{extractMemory:false,persist:true},
|
|
563
|
+
);
|
|
564
|
+
const committed=prepared.commit();
|
|
565
|
+
prepared=null;
|
|
566
|
+
const assistantRecord=this.#entity.transcript.at(-1);
|
|
567
|
+
return assistantRecord?.role==='assistant'
|
|
568
|
+
?{
|
|
569
|
+
...committed,
|
|
570
|
+
message:{
|
|
571
|
+
...committed.message,
|
|
572
|
+
...(assistantRecord.timestamp!==undefined
|
|
573
|
+
?{timestamp:assistantRecord.timestamp}
|
|
574
|
+
:{}),
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
:committed;
|
|
578
|
+
}catch(error){
|
|
579
|
+
prepared?.rollback();
|
|
580
|
+
throw error;
|
|
581
|
+
}finally{
|
|
582
|
+
this.#pending=false;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
509
586
|
async #requestTurn(input,streamHandlers=null){
|
|
510
587
|
const settings=normalizeSend(input);
|
|
511
588
|
if(this.#pending){
|