arcane-os 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
- package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -148
- package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1042 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +27 -67
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +109 -1558
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -185
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1295
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -719
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2965
- package/docs/reference/sdk-api.md +0 -6698
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
|
@@ -13,21 +13,68 @@ function plainRecord(value){
|
|
|
13
13
|
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function coded(error,code){
|
|
17
|
+
error.code=code;
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
function copyToolCalls(value){
|
|
17
22
|
if(value===undefined) return null;
|
|
18
23
|
if(!Array.isArray(value)||value.length!==1){
|
|
19
|
-
|
|
24
|
+
const error=new TypeError(
|
|
25
|
+
'assistantMessage.tool_calls must contain exactly one structural tool call.'
|
|
26
|
+
);
|
|
27
|
+
error.code=Array.isArray(value)&&value.length>1
|
|
28
|
+
?'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
|
|
29
|
+
:'AI_CHAT_INVALID_TOOL_CALL';
|
|
30
|
+
throw error;
|
|
20
31
|
}
|
|
21
32
|
const ids=new Set();
|
|
22
33
|
return value.map((call,index)=>{
|
|
23
34
|
if(!plainRecord(call)||!plainRecord(call.function)||call.type!=='function'){
|
|
24
|
-
|
|
35
|
+
const error=new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
|
|
36
|
+
error.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
37
|
+
throw error;
|
|
25
38
|
}
|
|
26
|
-
const id=
|
|
27
|
-
const name=
|
|
39
|
+
const id=call.id;
|
|
40
|
+
const name=call.function.name;
|
|
28
41
|
const argumentValue=call.function.arguments;
|
|
29
|
-
if(
|
|
30
|
-
|
|
42
|
+
if(
|
|
43
|
+
typeof id!=='string'
|
|
44
|
+
||!id.trim()
|
|
45
|
+
||ids.has(id)
|
|
46
|
+
||typeof name!=='string'
|
|
47
|
+
||!name.trim()
|
|
48
|
+
||typeof argumentValue!=='string'
|
|
49
|
+
){
|
|
50
|
+
const error=new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
|
|
51
|
+
error.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
let argumentRecord;
|
|
55
|
+
try{
|
|
56
|
+
argumentRecord=JSON.parse(argumentValue);
|
|
57
|
+
}catch(cause){
|
|
58
|
+
const error=new TypeError(
|
|
59
|
+
`assistantMessage.tool_calls[${index}].function.arguments must encode a JSON object.`,
|
|
60
|
+
{cause}
|
|
61
|
+
);
|
|
62
|
+
error.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
if(!plainRecord(argumentRecord)){
|
|
66
|
+
const error=new TypeError(
|
|
67
|
+
`assistantMessage.tool_calls[${index}].function.arguments must encode a JSON object.`
|
|
68
|
+
);
|
|
69
|
+
error.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
if(typeof argumentRecord.message!=='string'||!argumentRecord.message.trim()){
|
|
73
|
+
const error=new TypeError(
|
|
74
|
+
`assistantMessage.tool_calls[${index}].function.arguments.message must contain user-facing text.`
|
|
75
|
+
);
|
|
76
|
+
error.code='AI_CHAT_TOOL_MESSAGE_REQUIRED';
|
|
77
|
+
throw error;
|
|
31
78
|
}
|
|
32
79
|
ids.add(id);
|
|
33
80
|
return {
|
|
@@ -43,8 +90,19 @@ function turnMessage(value,label){
|
|
|
43
90
|
throw new TypeError(`${label} must be a user or tool message.`);
|
|
44
91
|
}
|
|
45
92
|
if(value.role==='tool'){
|
|
46
|
-
const toolCallId=
|
|
47
|
-
if(
|
|
93
|
+
const toolCallId=value.tool_call_id;
|
|
94
|
+
if(typeof toolCallId!=='string'||!toolCallId.trim()){
|
|
95
|
+
throw coded(
|
|
96
|
+
new TypeError(`${label}.tool_call_id is invalid.`),
|
|
97
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if(!value.content.trim()){
|
|
101
|
+
throw coded(
|
|
102
|
+
new TypeError(`${label}.content must contain a user-facing tool result.`),
|
|
103
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
48
106
|
return {content:value.content,role:'tool',tool_call_id:toolCallId};
|
|
49
107
|
}
|
|
50
108
|
return {content:value.content,role:'user'};
|
|
@@ -53,20 +111,67 @@ function turnMessage(value,label){
|
|
|
53
111
|
function pendingToolCallId(messages){
|
|
54
112
|
let pending=null;
|
|
55
113
|
for(const [index,message] of messages.entries()){
|
|
114
|
+
if(!plainRecord(message)||!['assistant','system','tool','user'].includes(message.role)){
|
|
115
|
+
throw coded(
|
|
116
|
+
new TypeError(`Chat message ${index+1} is not a supported persisted record.`),
|
|
117
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if(message.role!=='assistant'&&message.tool_calls!==undefined){
|
|
121
|
+
throw coded(
|
|
122
|
+
new TypeError(`Chat message ${index+1} places structural tool calls on a non-assistant record.`),
|
|
123
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if(message.role!=='tool'&&message.tool_call_id!==undefined){
|
|
127
|
+
throw coded(
|
|
128
|
+
new TypeError(`Chat message ${index+1} places a tool-call ID on a non-tool record.`),
|
|
129
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if(message.role==='system'&&pending){
|
|
133
|
+
throw coded(
|
|
134
|
+
new TypeError(`Chat message ${index+1} precedes the pending tool result.`),
|
|
135
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
56
138
|
if(message.role==='user'&&pending){
|
|
57
|
-
throw
|
|
139
|
+
throw coded(
|
|
140
|
+
new TypeError(`Chat message ${index+1} starts a user turn before the pending tool result.`),
|
|
141
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
142
|
+
);
|
|
58
143
|
}
|
|
59
|
-
if(message.role==='assistant'
|
|
60
|
-
const calls=copyToolCalls(message.tool_calls);
|
|
144
|
+
if(message.role==='assistant'){
|
|
61
145
|
if(pending){
|
|
62
|
-
throw
|
|
146
|
+
throw coded(
|
|
147
|
+
new TypeError(`Chat message ${index+1} precedes the pending tool result.`),
|
|
148
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if(message.tool_calls){
|
|
152
|
+
const calls=copyToolCalls(message.tool_calls);
|
|
153
|
+
pending=calls[0].id;
|
|
63
154
|
}
|
|
64
|
-
pending=calls[0].id;
|
|
65
155
|
}
|
|
66
156
|
if(message.role==='tool'){
|
|
67
|
-
const toolCallId=
|
|
157
|
+
const toolCallId=message.tool_call_id;
|
|
158
|
+
if(typeof toolCallId!=='string'||!toolCallId.trim()){
|
|
159
|
+
throw coded(
|
|
160
|
+
new TypeError(`Chat message ${index+1} has an invalid tool_call_id.`),
|
|
161
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if(typeof message.content!=='string'||!message.content.trim()){
|
|
165
|
+
throw coded(
|
|
166
|
+
new TypeError(`Chat message ${index+1} has a blank tool result.`),
|
|
167
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
168
|
+
);
|
|
169
|
+
}
|
|
68
170
|
if(!pending||pending!==toolCallId){
|
|
69
|
-
throw
|
|
171
|
+
throw coded(
|
|
172
|
+
new TypeError(`Chat message ${index+1} does not match the pending tool call.`),
|
|
173
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
174
|
+
);
|
|
70
175
|
}
|
|
71
176
|
pending=null;
|
|
72
177
|
}
|
|
@@ -91,6 +196,8 @@ function pendingToolCallId(messages){
|
|
|
91
196
|
* Internal marker for session-only messages omitted from durable chat and memory.
|
|
92
197
|
* @property {Array<Object>} [tool_calls]
|
|
93
198
|
* Assistant tool calls retained in the saved conversation log.
|
|
199
|
+
* @property {string} [reasoning_content]
|
|
200
|
+
* Optional complete provider reasoning retained with an assistant record.
|
|
94
201
|
* @property {string} [tool_call_id]
|
|
95
202
|
* Matching tool-call identifier for a tool result.
|
|
96
203
|
*/
|
|
@@ -225,21 +332,47 @@ class ChatEntity{
|
|
|
225
332
|
* @returns {ChatMessage[]}
|
|
226
333
|
*/
|
|
227
334
|
get messages(){
|
|
228
|
-
return
|
|
335
|
+
return this.#messages.map(function publicChatMessage(message){
|
|
229
336
|
const copy={...message};
|
|
230
337
|
if(copy.tool_calls){
|
|
231
|
-
copy.tool_calls=
|
|
232
|
-
function:
|
|
338
|
+
copy.tool_calls=copyToolCalls(copy.tool_calls).map(call=>({
|
|
339
|
+
function:{...call.function},
|
|
233
340
|
id:call.id,
|
|
234
341
|
type:call.type,
|
|
235
|
-
}))
|
|
342
|
+
}));
|
|
236
343
|
}
|
|
237
344
|
delete copy.memory_excluded;
|
|
238
345
|
delete copy.persistence_excluded;
|
|
239
346
|
delete copy.ui_hidden;
|
|
240
347
|
delete copy.timestamp;
|
|
241
|
-
return
|
|
242
|
-
})
|
|
348
|
+
return copy;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Returns the UI-visible conversation records with their persisted timestamps.
|
|
354
|
+
* Model-facing callers should continue to use `messages`, which intentionally
|
|
355
|
+
* omits display metadata.
|
|
356
|
+
*
|
|
357
|
+
* @returns {ChatMessage[]}
|
|
358
|
+
*/
|
|
359
|
+
get transcript(){
|
|
360
|
+
return this.#messages
|
|
361
|
+
.filter(message=>message.ui_hidden!==true)
|
|
362
|
+
.map(function publicTranscriptMessage(message){
|
|
363
|
+
const copy={...message};
|
|
364
|
+
if(copy.tool_calls){
|
|
365
|
+
copy.tool_calls=copyToolCalls(copy.tool_calls).map(call=>({
|
|
366
|
+
function:{...call.function},
|
|
367
|
+
id:call.id,
|
|
368
|
+
type:call.type,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
delete copy.memory_excluded;
|
|
372
|
+
delete copy.persistence_excluded;
|
|
373
|
+
delete copy.ui_hidden;
|
|
374
|
+
return copy;
|
|
375
|
+
});
|
|
243
376
|
}
|
|
244
377
|
|
|
245
378
|
/**
|
|
@@ -300,7 +433,10 @@ class ChatEntity{
|
|
|
300
433
|
throw new Error('persist must be boolean');
|
|
301
434
|
}
|
|
302
435
|
if(pendingToolCallId(this.#messages)){
|
|
303
|
-
throw
|
|
436
|
+
throw coded(
|
|
437
|
+
new TypeError('The pending structural tool result must be supplied before a new user turn.'),
|
|
438
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
439
|
+
);
|
|
304
440
|
}
|
|
305
441
|
|
|
306
442
|
const message={
|
|
@@ -341,6 +477,12 @@ class ChatEntity{
|
|
|
341
477
|
if(!is.boolean(persist)){
|
|
342
478
|
throw new Error('persist must be boolean');
|
|
343
479
|
}
|
|
480
|
+
if(pendingToolCallId(this.#messages)){
|
|
481
|
+
throw coded(
|
|
482
|
+
new TypeError('The pending structural tool result must be supplied before another assistant turn.'),
|
|
483
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
484
|
+
);
|
|
485
|
+
}
|
|
344
486
|
|
|
345
487
|
const message={
|
|
346
488
|
role:'assistant',
|
|
@@ -366,16 +508,24 @@ class ChatEntity{
|
|
|
366
508
|
* The host application decides whether anything is rendered in the chat UI.
|
|
367
509
|
*/
|
|
368
510
|
addToolExchange({id='',name='',arguments:argumentValue='',result='',persist=true}={}){
|
|
369
|
-
const toolCallId=
|
|
370
|
-
const toolName=
|
|
371
|
-
if(
|
|
511
|
+
const toolCallId=id;
|
|
512
|
+
const toolName=name;
|
|
513
|
+
if(
|
|
514
|
+
typeof toolCallId!=='string'
|
|
515
|
+
||!toolCallId.trim()
|
|
516
|
+
||typeof toolName!=='string'
|
|
517
|
+
||!toolName.trim()
|
|
518
|
+
){
|
|
372
519
|
throw new TypeError('Tool exchanges require an id and name.');
|
|
373
520
|
}
|
|
374
521
|
if(!is.boolean(persist)){
|
|
375
522
|
throw new TypeError('persist must be boolean.');
|
|
376
523
|
}
|
|
377
524
|
if(pendingToolCallId(this.#messages)){
|
|
378
|
-
throw
|
|
525
|
+
throw coded(
|
|
526
|
+
new TypeError('The pending structural tool result must be supplied before another tool exchange.'),
|
|
527
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
528
|
+
);
|
|
379
529
|
}
|
|
380
530
|
|
|
381
531
|
const serializedArguments=typeof argumentValue==='string'
|
|
@@ -387,6 +537,14 @@ class ChatEntity{
|
|
|
387
537
|
if(typeof serializedArguments!=='string'||typeof serializedResult!=='string'){
|
|
388
538
|
throw new TypeError('Tool exchange arguments and results must be JSON-compatible.');
|
|
389
539
|
}
|
|
540
|
+
const toolCall=copyToolCalls([{
|
|
541
|
+
id:toolCallId,
|
|
542
|
+
type:'function',
|
|
543
|
+
function:{
|
|
544
|
+
name:toolName,
|
|
545
|
+
arguments:serializedArguments
|
|
546
|
+
}
|
|
547
|
+
}])[0];
|
|
390
548
|
|
|
391
549
|
const timestamp=Date.now();
|
|
392
550
|
return this.#appendMessages([
|
|
@@ -394,14 +552,7 @@ class ChatEntity{
|
|
|
394
552
|
role:'assistant',
|
|
395
553
|
content:'',
|
|
396
554
|
tool_calls:[
|
|
397
|
-
|
|
398
|
-
id:toolCallId,
|
|
399
|
-
type:'function',
|
|
400
|
-
function:{
|
|
401
|
-
name:toolName,
|
|
402
|
-
arguments:serializedArguments
|
|
403
|
-
}
|
|
404
|
-
}
|
|
555
|
+
toolCall
|
|
405
556
|
],
|
|
406
557
|
timestamp,
|
|
407
558
|
memory_excluded:true
|
|
@@ -440,19 +591,41 @@ class ChatEntity{
|
|
|
440
591
|
throw new TypeError('memoryRequest must be a function.');
|
|
441
592
|
}
|
|
442
593
|
if(messagePersist!==responsePersist){
|
|
443
|
-
throw
|
|
594
|
+
throw coded(
|
|
595
|
+
new TypeError('messagePersist and responsePersist must match for one coherent durable turn.'),
|
|
596
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
597
|
+
);
|
|
444
598
|
}
|
|
445
599
|
const pendingTool=pendingToolCallId(this.#messages);
|
|
446
600
|
if(request.role==='user'&&pendingTool){
|
|
447
|
-
throw
|
|
601
|
+
throw coded(
|
|
602
|
+
new TypeError('The pending structural tool result must be supplied before a new user turn.'),
|
|
603
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
604
|
+
);
|
|
448
605
|
}
|
|
449
606
|
if(request.role==='tool'&&pendingTool!==request.tool_call_id){
|
|
450
|
-
throw
|
|
607
|
+
throw coded(
|
|
608
|
+
new TypeError('requestMessage does not match the pending structural tool call.'),
|
|
609
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE'
|
|
610
|
+
);
|
|
451
611
|
}
|
|
452
612
|
const toolCalls=copyToolCalls(assistantMessage.tool_calls);
|
|
453
613
|
const assistantContent=assistantMessage.content??'';
|
|
454
|
-
|
|
455
|
-
|
|
614
|
+
const assistantReasoning=assistantMessage.reasoning_content;
|
|
615
|
+
if(assistantReasoning!==undefined&&typeof assistantReasoning!=='string'){
|
|
616
|
+
throw new TypeError('assistantMessage.reasoning_content must be a string when provided.');
|
|
617
|
+
}
|
|
618
|
+
if(
|
|
619
|
+
!is.union(assistantContent,'string','number')
|
|
620
|
+
||(
|
|
621
|
+
!String(assistantContent)
|
|
622
|
+
&&!toolCalls
|
|
623
|
+
&&!(typeof assistantReasoning==='string'&&assistantReasoning.length)
|
|
624
|
+
)
|
|
625
|
+
){
|
|
626
|
+
throw new TypeError(
|
|
627
|
+
'assistantMessage must contain text, reasoning, or structural tool calls.'
|
|
628
|
+
);
|
|
456
629
|
}
|
|
457
630
|
const timestamp=Date.now();
|
|
458
631
|
const requestRecord={...request,timestamp};
|
|
@@ -460,6 +633,7 @@ class ChatEntity{
|
|
|
460
633
|
content:assistantContent,
|
|
461
634
|
role:'assistant',
|
|
462
635
|
timestamp,
|
|
636
|
+
...(assistantReasoning!==undefined?{reasoning_content:assistantReasoning}:{}),
|
|
463
637
|
...(toolCalls?{tool_calls:toolCalls}:{})
|
|
464
638
|
};
|
|
465
639
|
if(request.role==='tool') requestRecord.memory_excluded=true;
|
|
@@ -502,13 +676,15 @@ class ChatEntity{
|
|
|
502
676
|
return this.messages;
|
|
503
677
|
}
|
|
504
678
|
|
|
505
|
-
|
|
679
|
+
const loadedMessages=Array.isArray(content)
|
|
506
680
|
?content
|
|
507
681
|
:String(content)
|
|
508
682
|
.split('\n')
|
|
509
683
|
.map(row=>row.trim())
|
|
510
684
|
.filter(Boolean)
|
|
511
685
|
.map(row=>JSON.parse(row));
|
|
686
|
+
pendingToolCallId(loadedMessages);
|
|
687
|
+
this.#messages=loadedMessages;
|
|
512
688
|
this.#persistedMessageCount=this.#durableMessages().length;
|
|
513
689
|
this.#saved=true;
|
|
514
690
|
|
|
@@ -735,13 +911,19 @@ ${JSON.stringify(transcript)}`
|
|
|
735
911
|
try{
|
|
736
912
|
const messageIndex=this.#messages.indexOf(records[0]);
|
|
737
913
|
if(messageIndex<0){
|
|
738
|
-
|
|
914
|
+
throw coded(
|
|
915
|
+
new Error('The chat persistence transaction is already settled.'),
|
|
916
|
+
'AI_CHAT_TRANSACTION_SETTLED'
|
|
917
|
+
);
|
|
739
918
|
}
|
|
740
919
|
const recordsAreContiguous=records.every(
|
|
741
920
|
(record,index)=>this.#messages[messageIndex+index]===record
|
|
742
921
|
);
|
|
743
922
|
if(!recordsAreContiguous){
|
|
744
|
-
throw
|
|
923
|
+
throw coded(
|
|
924
|
+
new Error('Chat records changed before persistence completed.'),
|
|
925
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
926
|
+
);
|
|
745
927
|
}
|
|
746
928
|
const durableSnapshot=this.#durableMessages();
|
|
747
929
|
const durableIndex=durableSnapshot.indexOf(durableRecords[0]);
|
|
@@ -749,7 +931,10 @@ ${JSON.stringify(transcript)}`
|
|
|
749
931
|
(record,index)=>durableSnapshot[durableIndex+index]===record
|
|
750
932
|
);
|
|
751
933
|
if(durableIndex<0||!durableRecordsAreContiguous){
|
|
752
|
-
throw
|
|
934
|
+
throw coded(
|
|
935
|
+
new Error('Durable chat records changed before persistence completed.'),
|
|
936
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
937
|
+
);
|
|
753
938
|
}
|
|
754
939
|
const lastDurableIndex=durableIndex+durableRecords.length-1;
|
|
755
940
|
if(this.#persistedMessageCount===durableIndex){
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const KEY_PATTERN=/^[A-Za-z0-9][A-Za-z0-9._:-]
|
|
1
|
+
const KEY_PATTERN=/^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
2
2
|
const TYPES=new Set(['boolean','number','select','text']);
|
|
3
3
|
|
|
4
4
|
function copy(value){
|
|
@@ -25,8 +25,6 @@ export default class Preference{
|
|
|
25
25
|
if(type==='select'&&!this.options.some(option=>Object.is(option.value,this.defaultValue))){
|
|
26
26
|
throw new TypeError(`Preference ${key} has a default value outside its options.`);
|
|
27
27
|
}
|
|
28
|
-
Object.freeze(this.options);
|
|
29
|
-
Object.freeze(this);
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
normalizeOptions(options=[]){
|
|
@@ -35,7 +33,7 @@ export default class Preference{
|
|
|
35
33
|
const normalized=typeof option==='object'&&option!==null
|
|
36
34
|
?{label:String(option.label??option.value),value:this.normalize(option.value)}
|
|
37
35
|
:{label:String(option),value:this.normalize(option)};
|
|
38
|
-
return
|
|
36
|
+
return normalized;
|
|
39
37
|
});
|
|
40
38
|
}
|
|
41
39
|
|
|
@@ -79,5 +77,5 @@ export default class Preference{
|
|
|
79
77
|
export function preferenceSchema(definitions=[]){
|
|
80
78
|
const schema=definitions.map(definition=>definition instanceof Preference?definition:new Preference(definition));
|
|
81
79
|
if(new Set(schema.map(item=>item.key)).size!==schema.length) throw new TypeError('Preference schema keys must be unique.');
|
|
82
|
-
return
|
|
80
|
+
return schema;
|
|
83
81
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
function number(value,label,{minimum=-Infinity,maximum=Infinity}={}){const result=Number(value);if(!Number.isFinite(result)||result<minimum||result>maximum)throw new TypeError(`${label} is invalid.`);return result;}
|
|
2
|
-
function text(value,label){const result=String(value??'')
|
|
2
|
+
function text(value,label){const result=String(value??'');if(!result.trim())throw new TypeError(`${label} is required.`);return result;}
|
|
3
3
|
function instant(value,label){const date=new Date(value);if(Number.isNaN(date.valueOf()))throw new TypeError(`${label} is invalid.`);return date.toISOString();}
|
|
4
4
|
|
|
5
5
|
export class WeatherLocation{
|
|
6
|
-
constructor({id='',name,region='',country='',latitude,longitude,timezone='auto'}={}){this.id=String(id||`${latitude},${longitude}`);this.name=text(name,'Weather location name');this.region=String(region||'');this.country=String(country||'');this.latitude=number(latitude,'Latitude',{minimum:-90,maximum:90});this.longitude=number(longitude,'Longitude',{minimum:-180,maximum:180});this.timezone=String(timezone||'auto');
|
|
6
|
+
constructor({id='',name,region='',country='',latitude,longitude,timezone='auto'}={}){this.id=String(id||`${latitude},${longitude}`);this.name=text(name,'Weather location name');this.region=String(region||'');this.country=String(country||'');this.latitude=number(latitude,'Latitude',{minimum:-90,maximum:90});this.longitude=number(longitude,'Longitude',{minimum:-180,maximum:180});this.timezone=String(timezone||'auto');}
|
|
7
7
|
toJSON(){return {...this};}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export class WeatherObservation{
|
|
11
|
-
constructor({time,temperature,apparentTemperature=temperature,humidity=0,precipitation=0,weatherCode=0,windSpeed=0,isDay=true,temperatureUnit='°',windUnit=''}={}){this.time=instant(time,'Observation time');this.temperature=number(temperature,'Temperature');this.apparentTemperature=number(apparentTemperature,'Apparent temperature');this.humidity=number(humidity,'Humidity',{minimum:0,maximum:100});this.precipitation=number(precipitation,'Precipitation',{minimum:0});this.weatherCode=Math.max(0,Math.trunc(Number(weatherCode)||0));this.windSpeed=number(windSpeed,'Wind speed',{minimum:0});this.isDay=Boolean(isDay);this.temperatureUnit=String(temperatureUnit||'°');this.windUnit=String(windUnit||'');
|
|
11
|
+
constructor({time,temperature,apparentTemperature=temperature,humidity=0,precipitation=0,weatherCode=0,windSpeed=0,isDay=true,temperatureUnit='°',windUnit=''}={}){this.time=instant(time,'Observation time');this.temperature=number(temperature,'Temperature');this.apparentTemperature=number(apparentTemperature,'Apparent temperature');this.humidity=number(humidity,'Humidity',{minimum:0,maximum:100});this.precipitation=number(precipitation,'Precipitation',{minimum:0});this.weatherCode=Math.max(0,Math.trunc(Number(weatherCode)||0));this.windSpeed=number(windSpeed,'Wind speed',{minimum:0});this.isDay=Boolean(isDay);this.temperatureUnit=String(temperatureUnit||'°');this.windUnit=String(windUnit||'');}
|
|
12
12
|
toJSON(){return {...this};}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export class WeatherDay{
|
|
16
|
-
constructor({date,weatherCode=0,temperatureMax,temperatureMin,precipitationProbability=0,sunrise='',sunset='',temperatureUnit='°'}={}){this.date=instant(date,'Forecast date');this.weatherCode=Math.max(0,Math.trunc(Number(weatherCode)||0));this.temperatureMax=number(temperatureMax,'Maximum temperature');this.temperatureMin=number(temperatureMin,'Minimum temperature');this.precipitationProbability=number(precipitationProbability,'Precipitation probability',{minimum:0,maximum:100});this.sunrise=sunrise?instant(sunrise,'Sunrise'):'';this.sunset=sunset?instant(sunset,'Sunset'):'';this.temperatureUnit=String(temperatureUnit||'°');
|
|
16
|
+
constructor({date,weatherCode=0,temperatureMax,temperatureMin,precipitationProbability=0,sunrise='',sunset='',temperatureUnit='°'}={}){this.date=instant(date,'Forecast date');this.weatherCode=Math.max(0,Math.trunc(Number(weatherCode)||0));this.temperatureMax=number(temperatureMax,'Maximum temperature');this.temperatureMin=number(temperatureMin,'Minimum temperature');this.precipitationProbability=number(precipitationProbability,'Precipitation probability',{minimum:0,maximum:100});this.sunrise=sunrise?instant(sunrise,'Sunrise'):'';this.sunset=sunset?instant(sunset,'Sunset'):'';this.temperatureUnit=String(temperatureUnit||'°');}
|
|
17
17
|
toJSON(){return {...this};}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export class WeatherSnapshot{
|
|
21
|
-
constructor({location,current,daily=[],source='',fetchedAt=new Date()}={}){this.location=location instanceof WeatherLocation?location:new WeatherLocation(location);this.current=current instanceof WeatherObservation?current:new WeatherObservation(current);this.daily=
|
|
21
|
+
constructor({location,current,daily=[],source='',fetchedAt=new Date()}={}){this.location=location instanceof WeatherLocation?location:new WeatherLocation(location);this.current=current instanceof WeatherObservation?current:new WeatherObservation(current);this.daily=Array.from(daily,value=>value instanceof WeatherDay?value:new WeatherDay(value));this.source=String(source||'');this.fetchedAt=instant(fetchedAt,'Weather fetch time');}
|
|
22
22
|
toJSON(){return {location:this.location.toJSON(),current:this.current.toJSON(),daily:this.daily.map(day=>day.toJSON()),source:this.source,fetchedAt:this.fetchedAt};}
|
|
23
23
|
}
|