arcane-os 0.5.6 → 0.5.7
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 +7 -0
- package/README.md +15 -9
- package/package.json +1 -1
- package/runtime/arcane/entities/Chat.js +3 -3
- 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,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.7
|
|
4
|
+
|
|
5
|
+
- Added `PersistentAIChatSession.open()` for model-authored conversation
|
|
6
|
+
openings. Its application-authored bootstrap remains transient, while the
|
|
7
|
+
complete nonblank assistant response is committed atomically as durable
|
|
8
|
+
assistant-only chat history and survives maintenance and reload.
|
|
9
|
+
|
|
3
10
|
## 0.5.6
|
|
4
11
|
|
|
5
12
|
- 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.7` 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
|
|
|
@@ -176,8 +176,14 @@ The SDK runtime also owns `DBOPFSDocumentLibrary`,
|
|
|
176
176
|
schema-driven and explicit; chat never searches a corpus unless the app wires
|
|
177
177
|
that library into the request context builder. Persistent chat automatically
|
|
178
178
|
maintains recurring model context and `ChatEntity` history/memory. A turn may
|
|
179
|
-
set `persist:false` to
|
|
180
|
-
|
|
179
|
+
set `persist:false` to participate in one request and response only; after that
|
|
180
|
+
operation settles, neither side remains in subsequent model context, the
|
|
181
|
+
retained transcript, durable chat, memory, or DBOPFS.
|
|
182
|
+
For an automatic model-authored opening, call
|
|
183
|
+
`session.open({message:{content:bootstrap,persist:false}})`: the bootstrap is
|
|
184
|
+
request-only, while the complete nonblank assistant response becomes the first
|
|
185
|
+
durable conversation row without a fabricated user turn.
|
|
186
|
+
`createArcaneAI(...).createChatSession(options)` wires
|
|
181
187
|
that session to the same selected LLM controller, creates its `ChatEntity`, and
|
|
182
188
|
uses the same controller for automatic memory extraction.
|
|
183
189
|
|
|
@@ -186,7 +192,7 @@ uses the same controller for automatic memory extraction.
|
|
|
186
192
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
187
193
|
|
|
188
194
|
```bash
|
|
189
|
-
npx arcane-os@0.5.
|
|
195
|
+
npx arcane-os@0.5.7 new my-app --path ./my-app --target portable --git
|
|
190
196
|
cd my-app
|
|
191
197
|
npm install
|
|
192
198
|
npm run check
|
|
@@ -197,7 +203,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
197
203
|
missing Arcane files:
|
|
198
204
|
|
|
199
205
|
```bash
|
|
200
|
-
npm install --save-dev --save-exact arcane-os@0.5.
|
|
206
|
+
npm install --save-dev --save-exact arcane-os@0.5.7
|
|
201
207
|
npm exec -- arcane init my-app --target portable
|
|
202
208
|
```
|
|
203
209
|
|
|
@@ -213,7 +219,7 @@ npm exec -- arcane-os targets
|
|
|
213
219
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
214
220
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
215
221
|
|
|
216
|
-
Use `npx arcane-os@0.5.
|
|
222
|
+
Use `npx arcane-os@0.5.7` for the initial bootstrap because it names this npm
|
|
217
223
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
218
224
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
219
225
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -234,7 +240,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
234
240
|
|
|
235
241
|
# From the generated app repository
|
|
236
242
|
cd ../local-app
|
|
237
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.
|
|
243
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.7.tgz
|
|
238
244
|
npm run check
|
|
239
245
|
npm ci
|
|
240
246
|
```
|
|
@@ -244,7 +250,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
244
250
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
245
251
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
246
252
|
runner also needs that tarball at the locked path. After publication, replace
|
|
247
|
-
the local declaration with the exact `arcane-os@0.5.
|
|
253
|
+
the local declaration with the exact `arcane-os@0.5.7` registry package and
|
|
248
254
|
commit the regenerated lock.
|
|
249
255
|
|
|
250
256
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -382,7 +388,7 @@ package installation, or assertions.
|
|
|
382
388
|
|
|
383
389
|
## Current target support
|
|
384
390
|
|
|
385
|
-
Version `0.5.
|
|
391
|
+
Version `0.5.7` exposes one browser target and five explicitly paired
|
|
386
392
|
native development targets: a non-runnable portable directory, a
|
|
387
393
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
388
394
|
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
|
|
|
@@ -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){
|