arcane-os 0.5.5 → 0.5.6
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 +10 -0
- package/README.md +7 -7
- package/package.json +1 -1
- package/runtime/arcane/components/chat.html +67 -39
- package/runtime/arcane/entities/Chat.js +154 -61
- package/runtime/arcane/modules/PersistentAIChatSession.js +49 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.6
|
|
4
|
+
|
|
5
|
+
- Sanitized newly persisted chat history into complete human-readable user,
|
|
6
|
+
assistant, and public tool-result records without storing internal prompts,
|
|
7
|
+
provider envelopes, raw tool protocol fields, or hidden request metadata.
|
|
8
|
+
Existing stored rows remain untouched.
|
|
9
|
+
- Made `persist: false` turn-scoped: the request can use the complete temporary
|
|
10
|
+
turn, then retains neither side in model history, memory, DBOPFS, nor the Chat
|
|
11
|
+
transcript after the operation settles.
|
|
12
|
+
|
|
3
13
|
## 0.5.5
|
|
4
14
|
|
|
5
15
|
- Added configurable complete text-to-speech stream segmentation through
|
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.6` 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
|
|
|
@@ -186,7 +186,7 @@ uses the same controller for automatic memory extraction.
|
|
|
186
186
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
|
-
npx arcane-os@0.5.
|
|
189
|
+
npx arcane-os@0.5.6 new my-app --path ./my-app --target portable --git
|
|
190
190
|
cd my-app
|
|
191
191
|
npm install
|
|
192
192
|
npm run check
|
|
@@ -197,7 +197,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
197
197
|
missing Arcane files:
|
|
198
198
|
|
|
199
199
|
```bash
|
|
200
|
-
npm install --save-dev --save-exact arcane-os@0.5.
|
|
200
|
+
npm install --save-dev --save-exact arcane-os@0.5.6
|
|
201
201
|
npm exec -- arcane init my-app --target portable
|
|
202
202
|
```
|
|
203
203
|
|
|
@@ -213,7 +213,7 @@ npm exec -- arcane-os targets
|
|
|
213
213
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
214
214
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
215
215
|
|
|
216
|
-
Use `npx arcane-os@0.5.
|
|
216
|
+
Use `npx arcane-os@0.5.6` for the initial bootstrap because it names this npm
|
|
217
217
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
218
218
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
219
219
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -234,7 +234,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
234
234
|
|
|
235
235
|
# From the generated app repository
|
|
236
236
|
cd ../local-app
|
|
237
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.
|
|
237
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.6.tgz
|
|
238
238
|
npm run check
|
|
239
239
|
npm ci
|
|
240
240
|
```
|
|
@@ -244,7 +244,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
244
244
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
245
245
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
246
246
|
runner also needs that tarball at the locked path. After publication, replace
|
|
247
|
-
the local declaration with the exact `arcane-os@0.5.
|
|
247
|
+
the local declaration with the exact `arcane-os@0.5.6` registry package and
|
|
248
248
|
commit the regenerated lock.
|
|
249
249
|
|
|
250
250
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -382,7 +382,7 @@ package installation, or assertions.
|
|
|
382
382
|
|
|
383
383
|
## Current target support
|
|
384
384
|
|
|
385
|
-
Version `0.5.
|
|
385
|
+
Version `0.5.6` exposes one browser target and five explicitly paired
|
|
386
386
|
native development targets: a non-runnable portable directory, a
|
|
387
387
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
388
388
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
package/package.json
CHANGED
|
@@ -1333,12 +1333,13 @@
|
|
|
1333
1333
|
}
|
|
1334
1334
|
|
|
1335
1335
|
function sameTranscriptRequest(actual,expected){
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
expected.
|
|
1340
|
-
||actual.
|
|
1341
|
-
|
|
1336
|
+
if(actual?.role!==expected?.role) return false;
|
|
1337
|
+
if(expected.role==='tool'){
|
|
1338
|
+
return actual.content===(expected.message??expected.content)
|
|
1339
|
+
&&(expected.name===undefined||actual.name===expected.name)
|
|
1340
|
+
&&(expected.status===undefined||actual.status===expected.status);
|
|
1341
|
+
}
|
|
1342
|
+
return actual.content===expected.content;
|
|
1342
1343
|
}
|
|
1343
1344
|
|
|
1344
1345
|
function latestCommittedTranscriptTurn(transcript,requestMessages){
|
|
@@ -1460,18 +1461,20 @@
|
|
|
1460
1461
|
}
|
|
1461
1462
|
if(actionable){
|
|
1462
1463
|
if(message.role==='tool'){
|
|
1463
|
-
if(
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1464
|
+
if(message.tool_call_id!==undefined){
|
|
1465
|
+
if(
|
|
1466
|
+
typeof message.tool_call_id!=='string'
|
|
1467
|
+
||!message.tool_call_id.trim()
|
|
1468
|
+
||!message.content.trim()
|
|
1469
|
+
||!restoredPendingToolCalls.has(message.tool_call_id)
|
|
1470
|
+
){
|
|
1471
|
+
throw chatError(
|
|
1472
|
+
'Saved chat contains a tool result that does not match a pending structural tool call.',
|
|
1473
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE'
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
restoredPendingToolCalls.delete(message.tool_call_id);
|
|
1473
1477
|
}
|
|
1474
|
-
restoredPendingToolCalls.delete(message.tool_call_id);
|
|
1475
1478
|
}else{
|
|
1476
1479
|
if(restoredPendingToolCalls.size){
|
|
1477
1480
|
throw chatError(
|
|
@@ -1489,7 +1492,9 @@
|
|
|
1489
1492
|
const name=message.role==='user'
|
|
1490
1493
|
?host.name
|
|
1491
1494
|
:message.role==='tool'
|
|
1492
|
-
?'
|
|
1495
|
+
?typeof message.name==='string'&&message.name.trim()
|
|
1496
|
+
?`Tool · ${message.name}`
|
|
1497
|
+
:'Tool'
|
|
1493
1498
|
:message.role==='system'
|
|
1494
1499
|
?'System'
|
|
1495
1500
|
:host.aiName;
|
|
@@ -3069,6 +3074,7 @@
|
|
|
3069
3074
|
if(!sessionRequestMessages.length){
|
|
3070
3075
|
throw new TypeError('A chat session request must contain at least one message.');
|
|
3071
3076
|
}
|
|
3077
|
+
const retainTurn=sessionRequestMessages.every(message=>message.persist!==false);
|
|
3072
3078
|
const request={
|
|
3073
3079
|
...(sessionRequestMessages.length===1
|
|
3074
3080
|
?{message:sessionRequestMessages[0]}
|
|
@@ -3179,22 +3185,29 @@
|
|
|
3179
3185
|
??committedTurn?.request?.timestamp
|
|
3180
3186
|
);
|
|
3181
3187
|
}
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
if(!streamedStructuralToolCalls.length){
|
|
3193
|
-
for(const call of terminalToolCalls){
|
|
3194
|
-
appendVisibleToolCall(message,call);
|
|
3188
|
+
if(retainTurn){
|
|
3189
|
+
const terminalToolCalls=normalizeVisibleToolCalls(result.message.tool_calls);
|
|
3190
|
+
if(
|
|
3191
|
+
streamedStructuralToolCalls.length
|
|
3192
|
+
&&!sameStructuralToolCalls(streamedStructuralToolCalls,terminalToolCalls)
|
|
3193
|
+
){
|
|
3194
|
+
throw chatError(
|
|
3195
|
+
'The terminal structural tool calls do not match the streamed calls.',
|
|
3196
|
+
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
|
|
3197
|
+
);
|
|
3195
3198
|
}
|
|
3199
|
+
if(!streamedStructuralToolCalls.length){
|
|
3200
|
+
for(const call of terminalToolCalls){
|
|
3201
|
+
appendVisibleToolCall(message,call);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
setPendingStructuralToolCalls(terminalToolCalls);
|
|
3205
|
+
}else{
|
|
3206
|
+
for(const requestMessage of requestMessages) requestMessage.remove();
|
|
3207
|
+
message.remove();
|
|
3208
|
+
setPendingStructuralToolCalls(previousPendingToolCalls);
|
|
3209
|
+
scrollTranscriptToBottom();
|
|
3196
3210
|
}
|
|
3197
|
-
setPendingStructuralToolCalls(terminalToolCalls);
|
|
3198
3211
|
setSessionStatus(
|
|
3199
3212
|
pendingStructuralToolMessage?'tool':'ready',
|
|
3200
3213
|
pendingStructuralToolMessage||'Chat ready.'
|
|
@@ -3339,7 +3352,7 @@
|
|
|
3339
3352
|
if(!Array.isArray(options.results)||options.results.length===0){
|
|
3340
3353
|
throw new TypeError('Tool results must be a nonempty array.');
|
|
3341
3354
|
}
|
|
3342
|
-
const
|
|
3355
|
+
const statuses=new Map([
|
|
3343
3356
|
['executed','Executed'],
|
|
3344
3357
|
['declined','Declined'],
|
|
3345
3358
|
['cancelled','Cancelled'],
|
|
@@ -3369,7 +3382,7 @@
|
|
|
3369
3382
|
throw new TypeError(`Tool result ${index+1} must be a plain object.`);
|
|
3370
3383
|
}
|
|
3371
3384
|
const unsupportedResultField=Object.keys(result).find(
|
|
3372
|
-
key=>!['disposition','message','persist','toolCallId'].includes(key)
|
|
3385
|
+
key=>!['disposition','message','persist','status','toolCallId'].includes(key)
|
|
3373
3386
|
);
|
|
3374
3387
|
if(unsupportedResultField){
|
|
3375
3388
|
throw new TypeError(
|
|
@@ -3388,9 +3401,17 @@
|
|
|
3388
3401
|
error.code='AI_CHAT_INVALID_TOOL_MESSAGE';
|
|
3389
3402
|
throw error;
|
|
3390
3403
|
}
|
|
3391
|
-
if(
|
|
3404
|
+
if(
|
|
3405
|
+
result.status!==undefined
|
|
3406
|
+
&&result.disposition!==undefined
|
|
3407
|
+
&&result.status!==result.disposition
|
|
3408
|
+
){
|
|
3409
|
+
throw new TypeError('Tool-result status and legacy disposition must match.');
|
|
3410
|
+
}
|
|
3411
|
+
const status=result.status??result.disposition;
|
|
3412
|
+
if(!statuses.has(status)){
|
|
3392
3413
|
throw new TypeError(
|
|
3393
|
-
'Tool-result
|
|
3414
|
+
'Tool-result status must be executed, declined, cancelled, or not-executed.'
|
|
3394
3415
|
);
|
|
3395
3416
|
}
|
|
3396
3417
|
if(typeof result.message!=='string'||!result.message.trim()){
|
|
@@ -3401,9 +3422,12 @@
|
|
|
3401
3422
|
throw new TypeError('Tool-result persist must be boolean.');
|
|
3402
3423
|
}
|
|
3403
3424
|
resultsById.set(toolCallId,{
|
|
3404
|
-
content:`${
|
|
3425
|
+
content:`${statuses.get(status)} — ${result.message}`,
|
|
3426
|
+
message:result.message,
|
|
3427
|
+
name:pendingById.get(toolCallId).function.name,
|
|
3405
3428
|
persist,
|
|
3406
3429
|
role:'tool',
|
|
3430
|
+
status,
|
|
3407
3431
|
tool_call_id:toolCallId
|
|
3408
3432
|
});
|
|
3409
3433
|
}
|
|
@@ -3440,7 +3464,11 @@
|
|
|
3440
3464
|
}
|
|
3441
3465
|
try{
|
|
3442
3466
|
for(const result of orderedResults){
|
|
3443
|
-
const visibleMessage=appendTranscriptMessage(
|
|
3467
|
+
const visibleMessage=appendTranscriptMessage(
|
|
3468
|
+
'tool',
|
|
3469
|
+
result.content,
|
|
3470
|
+
`Tool · ${result.name}`
|
|
3471
|
+
);
|
|
3444
3472
|
visibleMessage.dataset.operationId=operationId;
|
|
3445
3473
|
visibleMessage.dataset.toolCallId=result.tool_call_id;
|
|
3446
3474
|
}
|
|
@@ -3472,7 +3500,7 @@
|
|
|
3472
3500
|
throw new TypeError('Tool-result options must be a plain object.');
|
|
3473
3501
|
}
|
|
3474
3502
|
const unsupportedOption=Object.keys(options).find(
|
|
3475
|
-
key=>!['disposition','message','persist','request','toolCallId'].includes(key)
|
|
3503
|
+
key=>!['disposition','message','persist','request','status','toolCallId'].includes(key)
|
|
3476
3504
|
);
|
|
3477
3505
|
if(unsupportedOption){
|
|
3478
3506
|
throw new TypeError(`Unsupported tool-result option: ${unsupportedOption}.`);
|
|
@@ -102,6 +102,96 @@ function copyToolCalls(value){
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function structuralToolMessage(call){
|
|
106
|
+
const argumentRecord=JSON.parse(call.function.arguments);
|
|
107
|
+
return argumentRecord.message;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function storedStructuralToolCalls(value){
|
|
111
|
+
if(value===undefined) return [];
|
|
112
|
+
if(!Array.isArray(value)){
|
|
113
|
+
console.error('Arcane stored assistant tool calls were not an array and were not retained.');
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const result=[];
|
|
117
|
+
for(const [index,call] of value.entries()){
|
|
118
|
+
try{
|
|
119
|
+
result.push(copyToolCalls([call])[0]);
|
|
120
|
+
}catch(error){
|
|
121
|
+
console.error(
|
|
122
|
+
`Arcane stored assistant tool call ${index+1} had no usable user-facing message and was not retained.`,
|
|
123
|
+
error
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function storedToolRecord({content,name,status,timestamp}){
|
|
131
|
+
if(typeof content!=='string'||!content.trim()) return [];
|
|
132
|
+
return [{
|
|
133
|
+
role:'tool',
|
|
134
|
+
content,
|
|
135
|
+
...(typeof name==='string'&&name.trim()?{name}:{}),
|
|
136
|
+
...(typeof status==='string'&&status.trim()?{status}:{}),
|
|
137
|
+
...(timestamp!==undefined?{timestamp}:{}),
|
|
138
|
+
}];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function storedChatRecords(messages,{memoryOnly=false}={}){
|
|
142
|
+
const result=[];
|
|
143
|
+
for(const message of messages){
|
|
144
|
+
if(!plainRecord(message)) continue;
|
|
145
|
+
if(message.persistence_excluded===true) continue;
|
|
146
|
+
if(memoryOnly&&message.memory_excluded===true) continue;
|
|
147
|
+
const timestamp=message.timestamp;
|
|
148
|
+
if(message.role==='user'){
|
|
149
|
+
if(typeof message.content!=='string') continue;
|
|
150
|
+
result.push({
|
|
151
|
+
role:'user',
|
|
152
|
+
content:message.content,
|
|
153
|
+
...(timestamp!==undefined?{timestamp}:{}),
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if(message.role==='assistant'){
|
|
158
|
+
const content=message.content===undefined||message.content===null
|
|
159
|
+
?''
|
|
160
|
+
:String(message.content);
|
|
161
|
+
if(content){
|
|
162
|
+
result.push({
|
|
163
|
+
role:'assistant',
|
|
164
|
+
content,
|
|
165
|
+
...(timestamp!==undefined?{timestamp}:{}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
for(const call of storedStructuralToolCalls(message.tool_calls)){
|
|
169
|
+
result.push(...storedToolRecord({
|
|
170
|
+
content:structuralToolMessage(call),
|
|
171
|
+
name:call.function.name,
|
|
172
|
+
status:'requested',
|
|
173
|
+
timestamp,
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if(message.role==='tool'){
|
|
179
|
+
const protocolResult=typeof message.tool_call_id==='string'&&message.tool_call_id;
|
|
180
|
+
result.push(...storedToolRecord({
|
|
181
|
+
content:protocolResult?message.persistence_message:message.content,
|
|
182
|
+
name:message.persistence_name??message.name,
|
|
183
|
+
status:message.persistence_status??message.status,
|
|
184
|
+
timestamp,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function retainedChatMessages(messages){
|
|
192
|
+
return messages.filter(message=>message?.persistence_excluded!==true);
|
|
193
|
+
}
|
|
194
|
+
|
|
105
195
|
function turnMessage(value,label){
|
|
106
196
|
if(!plainRecord(value)||!['tool','user'].includes(value.role)||typeof value.content!=='string'){
|
|
107
197
|
throw new TypeError(`${label} must be a user or tool message.`);
|
|
@@ -179,6 +269,9 @@ function pendingToolCalls(messages){
|
|
|
179
269
|
}
|
|
180
270
|
if(message.role==='tool'){
|
|
181
271
|
const toolCallId=message.tool_call_id;
|
|
272
|
+
if(toolCallId===undefined){
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
182
275
|
if(typeof toolCallId!=='string'||!toolCallId.trim()){
|
|
183
276
|
throw coded(
|
|
184
277
|
new TypeError(`Chat message ${index+1} has an invalid tool_call_id.`),
|
|
@@ -231,18 +324,8 @@ function turnMessages(requestMessage,requestMessages){
|
|
|
231
324
|
*
|
|
232
325
|
* @property {string|number} content
|
|
233
326
|
* Message text content.
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* @property {boolean} [ui_hidden]
|
|
237
|
-
* Legacy persistence metadata retained without suppressing the public transcript.
|
|
238
|
-
* @property {boolean} [persistence_excluded]
|
|
239
|
-
* Internal marker for session-only messages omitted from durable chat and memory.
|
|
240
|
-
* @property {Array<Object>} [tool_calls]
|
|
241
|
-
* Assistant tool calls retained in the saved conversation log.
|
|
242
|
-
* @property {string} [reasoning_content]
|
|
243
|
-
* Optional complete provider reasoning retained with an assistant record.
|
|
244
|
-
* @property {string} [tool_call_id]
|
|
245
|
-
* Matching tool-call identifier for a tool result.
|
|
327
|
+
* Active provider messages may also contain transient protocol and internal
|
|
328
|
+
* fields. The DBOPFS projection never retains those fields.
|
|
246
329
|
*/
|
|
247
330
|
|
|
248
331
|
/**
|
|
@@ -258,13 +341,12 @@ function turnMessages(requestMessage,requestMessages){
|
|
|
258
341
|
* chat-1719930112231.json
|
|
259
342
|
* ```
|
|
260
343
|
*
|
|
261
|
-
*
|
|
344
|
+
* New file contents:
|
|
262
345
|
*
|
|
263
346
|
* ```
|
|
264
347
|
* [
|
|
265
|
-
* { "role":"
|
|
266
|
-
* { "role":"
|
|
267
|
-
* { "role":"assistant","content":"Hi there" }
|
|
348
|
+
* { "role":"user","content":"Hello","timestamp":1719930112231 },
|
|
349
|
+
* { "role":"assistant","content":"Hi there","timestamp":1719930112240 }
|
|
268
350
|
* ]
|
|
269
351
|
* ```
|
|
270
352
|
*
|
|
@@ -328,6 +410,9 @@ class ChatEntity{
|
|
|
328
410
|
/** Number of leading in-memory messages durably present in the chat file. */
|
|
329
411
|
#persistedMessageCount=0;
|
|
330
412
|
|
|
413
|
+
/** Existing stored records preserved exactly while new records use the narrow format. */
|
|
414
|
+
#preservedStoredMessageCount=0;
|
|
415
|
+
|
|
331
416
|
/** Serializes snapshot and append writes for this chat instance. */
|
|
332
417
|
#persistenceQueue=Promise.resolve();
|
|
333
418
|
|
|
@@ -375,8 +460,10 @@ class ChatEntity{
|
|
|
375
460
|
* @returns {Array<*>}
|
|
376
461
|
*/
|
|
377
462
|
get messages(){
|
|
378
|
-
|
|
379
|
-
|
|
463
|
+
const retainedMessages=retainedChatMessages(this.#messages);
|
|
464
|
+
pendingToolCalls(retainedMessages);
|
|
465
|
+
return retainedMessages
|
|
466
|
+
.map(function publicChatMessage(message){
|
|
380
467
|
const copy={...message};
|
|
381
468
|
if(copy.tool_calls){
|
|
382
469
|
copy.tool_calls=copyToolCalls(copy.tool_calls).map(call=>({
|
|
@@ -388,19 +475,27 @@ class ChatEntity{
|
|
|
388
475
|
delete copy.persistence_excluded;
|
|
389
476
|
delete copy.ui_hidden;
|
|
390
477
|
delete copy.timestamp;
|
|
478
|
+
delete copy.persistence_message;
|
|
479
|
+
delete copy.persistence_name;
|
|
480
|
+
delete copy.persistence_status;
|
|
481
|
+
if(copy.role==='tool'&©.tool_call_id===undefined){
|
|
482
|
+
delete copy.name;
|
|
483
|
+
delete copy.status;
|
|
484
|
+
copy.role='assistant';
|
|
485
|
+
}
|
|
391
486
|
return copy;
|
|
392
487
|
});
|
|
393
488
|
}
|
|
394
489
|
|
|
395
490
|
/**
|
|
396
|
-
* Returns the
|
|
397
|
-
*
|
|
398
|
-
*
|
|
491
|
+
* Returns the sanitized human-readable conversation records. User and
|
|
492
|
+
* assistant records contain only role, complete visible content, and timestamp.
|
|
493
|
+
* Tool records may also contain their public name and result status.
|
|
399
494
|
*
|
|
400
495
|
* @returns {Array<*>}
|
|
401
496
|
*/
|
|
402
497
|
get transcript(){
|
|
403
|
-
return this.#messages.map(function publicTranscriptMessage(message){
|
|
498
|
+
return storedChatRecords(this.#messages).map(function publicTranscriptMessage(message){
|
|
404
499
|
return copyCompleteValue(message);
|
|
405
500
|
});
|
|
406
501
|
}
|
|
@@ -417,6 +512,7 @@ class ChatEntity{
|
|
|
417
512
|
}
|
|
418
513
|
this.#messages=v.map(message=>copyCompleteValue(message));
|
|
419
514
|
this.#persistedMessageCount=0;
|
|
515
|
+
this.#preservedStoredMessageCount=0;
|
|
420
516
|
this.#saved=false;
|
|
421
517
|
return this.transcript;
|
|
422
518
|
}
|
|
@@ -465,7 +561,7 @@ class ChatEntity{
|
|
|
465
561
|
if(!is.boolean(persist)){
|
|
466
562
|
throw new Error('persist must be boolean');
|
|
467
563
|
}
|
|
468
|
-
if(pendingToolCalls(this.#messages).size){
|
|
564
|
+
if(pendingToolCalls(retainedChatMessages(this.#messages)).size){
|
|
469
565
|
throw coded(
|
|
470
566
|
new TypeError('The pending structural tool results must be supplied before a new user turn.'),
|
|
471
567
|
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
@@ -509,7 +605,7 @@ class ChatEntity{
|
|
|
509
605
|
if(!is.boolean(persist)){
|
|
510
606
|
throw new Error('persist must be boolean');
|
|
511
607
|
}
|
|
512
|
-
if(pendingToolCalls(this.#messages).size){
|
|
608
|
+
if(pendingToolCalls(retainedChatMessages(this.#messages)).size){
|
|
513
609
|
throw coded(
|
|
514
610
|
new TypeError('The pending structural tool results must be supplied before another assistant turn.'),
|
|
515
611
|
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
@@ -537,7 +633,8 @@ class ChatEntity{
|
|
|
537
633
|
|
|
538
634
|
/**
|
|
539
635
|
* Adds an assistant tool call and its result as one hidden, atomic log exchange.
|
|
540
|
-
* The
|
|
636
|
+
* The active provider context keeps the complete exchange. New durable history
|
|
637
|
+
* retains only the call's required user-facing arguments.message record.
|
|
541
638
|
*/
|
|
542
639
|
addToolExchange({id='',name='',arguments:argumentValue='',result='',persist=true}={}){
|
|
543
640
|
const toolCallId=id;
|
|
@@ -553,7 +650,7 @@ class ChatEntity{
|
|
|
553
650
|
if(!is.boolean(persist)){
|
|
554
651
|
throw new TypeError('persist must be boolean.');
|
|
555
652
|
}
|
|
556
|
-
if(pendingToolCalls(this.#messages).size){
|
|
653
|
+
if(pendingToolCalls(retainedChatMessages(this.#messages)).size){
|
|
557
654
|
throw coded(
|
|
558
655
|
new TypeError('The pending structural tool results must be supplied before another tool exchange.'),
|
|
559
656
|
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
@@ -629,7 +726,7 @@ class ChatEntity{
|
|
|
629
726
|
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
630
727
|
);
|
|
631
728
|
}
|
|
632
|
-
const pendingTools=pendingToolCalls(this.#messages);
|
|
729
|
+
const pendingTools=pendingToolCalls(retainedChatMessages(this.#messages));
|
|
633
730
|
const userRequest=requests.find(message=>message.role==='user');
|
|
634
731
|
const toolRequests=requests.filter(message=>message.role==='tool');
|
|
635
732
|
if(userRequest&&pendingTools.size){
|
|
@@ -690,15 +787,10 @@ class ChatEntity{
|
|
|
690
787
|
};
|
|
691
788
|
if(!extractMemory||toolCalls?.length) assistantRecord.memory_excluded=true;
|
|
692
789
|
else delete assistantRecord.memory_excluded;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
if(!responsePersist) assistantRecord.persistence_excluded=true;
|
|
699
|
-
else delete assistantRecord.persistence_excluded;
|
|
700
|
-
|
|
701
|
-
const appended=this.#appendMessages([...requestRecords,assistantRecord],{prepared:true});
|
|
790
|
+
const appended=this.#appendMessages(
|
|
791
|
+
[...requestRecords,assistantRecord],
|
|
792
|
+
{persist:messagePersist,prepared:true}
|
|
793
|
+
);
|
|
702
794
|
if(extractMemory&&messagePersist&&responsePersist&&!toolCalls?.length&&this.persist){
|
|
703
795
|
return Promise.resolve(appended).then(result=>{
|
|
704
796
|
this.#queueMemoryUpdate(memoryRequest);
|
|
@@ -729,6 +821,7 @@ class ChatEntity{
|
|
|
729
821
|
const systemMessage=this.#messages.find(message=>message.role==='system');
|
|
730
822
|
this.#messages=systemMessage?[systemMessage]:[];
|
|
731
823
|
this.#persistedMessageCount=0;
|
|
824
|
+
this.#preservedStoredMessageCount=0;
|
|
732
825
|
this.#saved=true;
|
|
733
826
|
return this.transcript;
|
|
734
827
|
}
|
|
@@ -751,7 +844,8 @@ class ChatEntity{
|
|
|
751
844
|
}
|
|
752
845
|
});
|
|
753
846
|
this.#messages=loadedMessages;
|
|
754
|
-
this.#
|
|
847
|
+
this.#preservedStoredMessageCount=loadedMessages.length;
|
|
848
|
+
this.#persistedMessageCount=loadedMessages.length;
|
|
755
849
|
this.#saved=true;
|
|
756
850
|
|
|
757
851
|
return this.transcript;
|
|
@@ -762,7 +856,8 @@ class ChatEntity{
|
|
|
762
856
|
throw new TypeError('Memory request must be a function.');
|
|
763
857
|
}
|
|
764
858
|
return this.#writeMemory(
|
|
765
|
-
this.#
|
|
859
|
+
storedChatRecords(this.#messages,{memoryOnly:true})
|
|
860
|
+
.map(message=>({...message})),
|
|
766
861
|
request
|
|
767
862
|
);
|
|
768
863
|
}
|
|
@@ -844,7 +939,11 @@ ${JSON.stringify(transcript)}`
|
|
|
844
939
|
}
|
|
845
940
|
|
|
846
941
|
#queueMemoryUpdate(request){
|
|
847
|
-
const snapshot=
|
|
942
|
+
const snapshot=storedChatRecords(
|
|
943
|
+
this.#messages,
|
|
944
|
+
{memoryOnly:true}
|
|
945
|
+
)
|
|
946
|
+
.map(message=>copyCompleteValue(message));
|
|
848
947
|
const queued=this.#memoryQueue.then(()=>this.#writeMemory(snapshot,request));
|
|
849
948
|
this.#memoryQueue=queued.catch(()=>{
|
|
850
949
|
console.warn('Unable to update chat memory.');
|
|
@@ -889,12 +988,11 @@ ${JSON.stringify(transcript)}`
|
|
|
889
988
|
*
|
|
890
989
|
* The log file is stored in **NDJSON format** (newline-delimited JSON).
|
|
891
990
|
*
|
|
892
|
-
* Example stored file:
|
|
991
|
+
* Example newly stored file:
|
|
893
992
|
*
|
|
894
993
|
* ```
|
|
895
|
-
* {"role":"
|
|
896
|
-
* {"role":"
|
|
897
|
-
* {"role":"assistant","content":"Hi there"}
|
|
994
|
+
* {"role":"user","content":"Hello","timestamp":1719930112231}
|
|
995
|
+
* {"role":"assistant","content":"Hi there","timestamp":1719930112240}
|
|
898
996
|
* ```
|
|
899
997
|
*
|
|
900
998
|
* Each call to `appendMessage` writes one JSON object followed
|
|
@@ -948,21 +1046,25 @@ ${JSON.stringify(transcript)}`
|
|
|
948
1046
|
}
|
|
949
1047
|
|
|
950
1048
|
#durableMessages(){
|
|
951
|
-
return
|
|
952
|
-
|
|
953
|
-
|
|
1049
|
+
return [
|
|
1050
|
+
...this.#messages
|
|
1051
|
+
.slice(0,this.#preservedStoredMessageCount)
|
|
1052
|
+
.map(message=>copyCompleteValue(message)),
|
|
1053
|
+
...storedChatRecords(
|
|
1054
|
+
this.#messages.slice(this.#preservedStoredMessageCount)
|
|
1055
|
+
),
|
|
1056
|
+
];
|
|
954
1057
|
}
|
|
955
1058
|
|
|
956
1059
|
async #appendMessages(messages,{persist=true,prepared=false}={}){
|
|
957
|
-
const records=Array.from(messages||[]).map(message=>
|
|
958
|
-
prepared?message:(persist?message:{...message,persistence_excluded:true})
|
|
959
|
-
);
|
|
1060
|
+
const records=Array.from(messages||[]).map(message=>prepared?message:{...message});
|
|
960
1061
|
if(!records.length){
|
|
961
1062
|
return false;
|
|
962
1063
|
}
|
|
1064
|
+
if(!persist) return false;
|
|
963
1065
|
|
|
964
1066
|
this.#messages.push(...records);
|
|
965
|
-
const durableRecords=records
|
|
1067
|
+
const durableRecords=storedChatRecords(records);
|
|
966
1068
|
if(durableRecords.length){
|
|
967
1069
|
this.#saved=false;
|
|
968
1070
|
}
|
|
@@ -994,17 +1096,8 @@ ${JSON.stringify(transcript)}`
|
|
|
994
1096
|
);
|
|
995
1097
|
}
|
|
996
1098
|
const durableSnapshot=this.#durableMessages();
|
|
997
|
-
const durableIndex=durableSnapshot.
|
|
998
|
-
const
|
|
999
|
-
(record,index)=>durableSnapshot[durableIndex+index]===record
|
|
1000
|
-
);
|
|
1001
|
-
if(durableIndex<0||!durableRecordsAreContiguous){
|
|
1002
|
-
throw coded(
|
|
1003
|
-
new Error('Durable chat records changed before persistence completed.'),
|
|
1004
|
-
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
const lastDurableIndex=durableIndex+durableRecords.length-1;
|
|
1099
|
+
const durableIndex=durableSnapshot.length-durableRecords.length;
|
|
1100
|
+
const lastDurableIndex=durableSnapshot.length-1;
|
|
1008
1101
|
if(this.#persistedMessageCount===durableIndex){
|
|
1009
1102
|
await dbopfs.set(
|
|
1010
1103
|
this.#tableName,
|
|
@@ -197,24 +197,57 @@ function normalizeSend(input){
|
|
|
197
197
|
throw new TypeError('messages accepts only tool-result messages.');
|
|
198
198
|
}
|
|
199
199
|
let toolCallId=null;
|
|
200
|
+
let persistenceMessage=null;
|
|
201
|
+
let persistenceName=null;
|
|
202
|
+
let persistenceStatus=null;
|
|
200
203
|
if(role==='tool'){
|
|
201
204
|
if(typeof value.tool_call_id!=='string'||!value.tool_call_id.trim()){
|
|
202
205
|
throw new TypeError(`${label}.tool_call_id is required for tool messages.`);
|
|
203
206
|
}
|
|
204
207
|
toolCallId=value.tool_call_id;
|
|
208
|
+
if(value.message!==undefined){
|
|
209
|
+
if(typeof value.message!=='string'||!value.message.trim()){
|
|
210
|
+
throw new TypeError(`${label}.message must contain user-facing text when provided.`);
|
|
211
|
+
}
|
|
212
|
+
persistenceMessage=value.message;
|
|
213
|
+
}
|
|
214
|
+
if(value.name!==undefined){
|
|
215
|
+
if(typeof value.name!=='string'||!value.name.trim()){
|
|
216
|
+
throw new TypeError(`${label}.name must contain text when provided.`);
|
|
217
|
+
}
|
|
218
|
+
persistenceName=value.name;
|
|
219
|
+
}
|
|
220
|
+
if(value.status!==undefined){
|
|
221
|
+
if(typeof value.status!=='string'||!value.status.trim()){
|
|
222
|
+
throw new TypeError(`${label}.status must contain text when provided.`);
|
|
223
|
+
}
|
|
224
|
+
persistenceStatus=value.status;
|
|
225
|
+
}
|
|
205
226
|
}else if(value.tool_call_id!==undefined){
|
|
206
227
|
throw new TypeError(`${label}.tool_call_id is supported only for tool messages.`);
|
|
207
228
|
}
|
|
208
229
|
const completeMessage={...value};
|
|
209
230
|
delete completeMessage.persist;
|
|
231
|
+
delete completeMessage.message;
|
|
232
|
+
delete completeMessage.name;
|
|
233
|
+
delete completeMessage.status;
|
|
234
|
+
const providerMessage={
|
|
235
|
+
...completeMessage,
|
|
236
|
+
content:value.content,
|
|
237
|
+
role,
|
|
238
|
+
...(toolCallId?{tool_call_id:toolCallId}:{})
|
|
239
|
+
};
|
|
210
240
|
return {
|
|
211
241
|
persist:boolean(value.persist,`${label}.persist`,true),
|
|
212
|
-
message:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
242
|
+
message:providerMessage,
|
|
243
|
+
entityMessage:role==='tool'
|
|
244
|
+
?{
|
|
245
|
+
...providerMessage,
|
|
246
|
+
...(persistenceMessage?{persistence_message:persistenceMessage}:{}),
|
|
247
|
+
...(persistenceName?{persistence_name:persistenceName}:{}),
|
|
248
|
+
...(persistenceStatus?{persistence_status:persistenceStatus}:{}),
|
|
249
|
+
}
|
|
250
|
+
:providerMessage,
|
|
218
251
|
};
|
|
219
252
|
});
|
|
220
253
|
const messagePersist=normalizedMessages[0].persist;
|
|
@@ -245,6 +278,7 @@ function normalizeSend(input){
|
|
|
245
278
|
if(!signalLike(input.signal)) throw new TypeError('signal must be an AbortSignal.');
|
|
246
279
|
return {
|
|
247
280
|
messagePersist,
|
|
281
|
+
entityRequestMessages:normalizedMessages.map(item=>item.entityMessage),
|
|
248
282
|
requestMessages:normalizedMessages.map(item=>item.message),
|
|
249
283
|
responsePersist,
|
|
250
284
|
request:{...request},
|
|
@@ -262,7 +296,7 @@ function fileName(value){
|
|
|
262
296
|
/**
|
|
263
297
|
* Composes the configured chat session with one automatically selected
|
|
264
298
|
* ChatEntity. Request-only context is delegated to ConfiguredAIChatSession;
|
|
265
|
-
*
|
|
299
|
+
* persist:false uses the turn for one request and retains it nowhere afterward.
|
|
266
300
|
*/
|
|
267
301
|
class PersistentAIChatSession{
|
|
268
302
|
#activeStream=null;
|
|
@@ -550,6 +584,11 @@ class PersistentAIChatSession{
|
|
|
550
584
|
);
|
|
551
585
|
}
|
|
552
586
|
}
|
|
587
|
+
if(!settings.messagePersist){
|
|
588
|
+
prepared.rollback();
|
|
589
|
+
prepared=null;
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
553
592
|
await this.#entity.addTurn({
|
|
554
593
|
assistantMessage:result.message,
|
|
555
594
|
extractMemory:this.#memory&&settings.messagePersist&&settings.responsePersist,
|
|
@@ -560,9 +599,9 @@ class PersistentAIChatSession{
|
|
|
560
599
|
})
|
|
561
600
|
),
|
|
562
601
|
messagePersist:settings.messagePersist,
|
|
563
|
-
...(settings.
|
|
564
|
-
?{requestMessage:settings.
|
|
565
|
-
:{requestMessages:settings.
|
|
602
|
+
...(settings.entityRequestMessages.length===1
|
|
603
|
+
?{requestMessage:settings.entityRequestMessages[0]}
|
|
604
|
+
:{requestMessages:settings.entityRequestMessages}),
|
|
566
605
|
responsePersist:settings.responsePersist,
|
|
567
606
|
});
|
|
568
607
|
const committed=prepared.commit();
|