arcane-os 0.3.0 → 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.
Files changed (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -1,5 +1,17 @@
1
1
  import ChatEntity from '../entities/Chat.js';
2
- import ConfiguredAIChatSession from './ConfiguredAIChatSession.js';
2
+ import ConfiguredAIChatSession,{
3
+ normalizeStructuralToolCall
4
+ } from './ConfiguredAIChatSession.js';
5
+
6
+ const SESSION_MANAGED_REQUEST_FIELDS=new Set([
7
+ 'messages',
8
+ 'onChunk',
9
+ 'onResponse',
10
+ 'onToolCall',
11
+ 'signal',
12
+ 'stream',
13
+ ]);
14
+ const PROVIDER_LIFECYCLE_FIELDS=new Set(['onChunk','onResponse','onToolCall']);
3
15
 
4
16
  function coded(error,code){
5
17
  if(!error.code) error.code=code;
@@ -33,6 +45,12 @@ function signalLike(value){
33
45
  );
34
46
  }
35
47
 
48
+ function providerRequestWithoutLifecycleCallbacks(value){
49
+ return Object.fromEntries(
50
+ Object.entries(value).filter(([key])=>!PROVIDER_LIFECYCLE_FIELDS.has(key))
51
+ );
52
+ }
53
+
36
54
  async function configuredArcaneChat(request){
37
55
  const api=globalThis.Arcane?.ai;
38
56
  if(typeof api?.chat!=='function'){
@@ -44,9 +62,90 @@ async function configuredArcaneChat(request){
44
62
  return api.chat(request);
45
63
  }
46
64
 
65
+ function configuredAIChat(ai){
66
+ return function requestConfiguredAI(request){
67
+ return ai.fetchRequest(request);
68
+ };
69
+ }
70
+
71
+ function normalizeStreamHandlers(value){
72
+ if(value===undefined) return {};
73
+ if(!isPlainRecord(value)) throw new TypeError('Persistent chat stream handlers must be a plain object.');
74
+ assertKnownKeys(value,new Set(['onChunk','onToolCall']),'Persistent chat stream handlers');
75
+ for(const key of ['onChunk','onToolCall']){
76
+ if(value[key]!==undefined&&typeof value[key]!=='function'){
77
+ throw new TypeError(`${key} must be a function when provided.`);
78
+ }
79
+ }
80
+ return {
81
+ onChunk:value.onChunk??function ignorePersistentChatChunk(){},
82
+ onToolCall:value.onToolCall??function ignorePersistentChatToolCall(){},
83
+ };
84
+ }
85
+
86
+ function normalizeStreamResponse(terminal,output){
87
+ const value=terminal??output;
88
+ if(typeof value==='string'){
89
+ return {message:{role:'assistant',content:value}};
90
+ }
91
+ if(isPlainRecord(value)) return value;
92
+ throw coded(
93
+ new TypeError('The AI stream did not return a terminal response.'),
94
+ 'AI_CHAT_INVALID_RESPONSE',
95
+ );
96
+ }
97
+
98
+ function terminalStructuralToolCall(response){
99
+ const hasMessage=Object.hasOwn(response,'message');
100
+ const hasChoices=Object.hasOwn(response,'choices');
101
+ if(hasMessage&&hasChoices){
102
+ throw coded(
103
+ new TypeError('The AI stream terminal response cannot mix message and choices envelopes.'),
104
+ 'AI_CHAT_INVALID_RESPONSE',
105
+ );
106
+ }
107
+ const messages=hasMessage
108
+ ?[response.message]
109
+ :hasChoices&&Array.isArray(response.choices)
110
+ ?response.choices.map(choice=>choice?.message).filter(Boolean)
111
+ :[];
112
+ const calls=[];
113
+ for(let messageIndex=0;messageIndex<messages.length;messageIndex++){
114
+ const value=messages[messageIndex]?.tool_calls;
115
+ if(value===undefined) continue;
116
+ if(!Array.isArray(value)){
117
+ throw coded(
118
+ new TypeError('The AI stream terminal response contains invalid structural tool calls.'),
119
+ 'AI_CHAT_INVALID_TOOL_CALL',
120
+ );
121
+ }
122
+ for(let callIndex=0;callIndex<value.length;callIndex++){
123
+ calls.push(normalizeStructuralToolCall(
124
+ value[callIndex],
125
+ `The terminal structural tool call ${messageIndex+1}.${callIndex+1}`,
126
+ ));
127
+ }
128
+ }
129
+ if(calls.length>1){
130
+ throw coded(
131
+ new TypeError('The AI stream terminal response contains parallel structural tool calls.'),
132
+ 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
133
+ );
134
+ }
135
+ return calls[0]??null;
136
+ }
137
+
138
+ function sameStructuralToolCall(left,right){
139
+ return Boolean(left&&right)
140
+ &&left.id===right.id
141
+ &&left.type===right.type
142
+ &&left.function.name===right.function.name
143
+ &&left.function.arguments===right.function.arguments;
144
+ }
145
+
47
146
  function normalizeSend(input){
48
147
  if(!isPlainRecord(input)) throw new TypeError('Persistent chat input must be a plain object.');
49
- assertKnownKeys(input,new Set(['message','response','signal']),'Persistent chat input');
148
+ assertKnownKeys(input,new Set(['message','request','response','signal']),'Persistent chat input');
50
149
  if(!isPlainRecord(input.message)) throw new TypeError('message must be a plain object.');
51
150
  assertKnownKeys(input.message,new Set(['content','persist','role','tool_call_id']),'message');
52
151
  if(typeof input.message.content!=='string'||!input.message.content.trim()){
@@ -74,17 +173,26 @@ function normalizeSend(input){
74
173
  'AI_CHAT_INCOHERENT_PERSISTENCE',
75
174
  );
76
175
  }
176
+ const request=input.request??{};
177
+ if(!isPlainRecord(request)) throw new TypeError('request must be a plain object.');
178
+ const managedRequestField=Object.keys(request).find(
179
+ key=>SESSION_MANAGED_REQUEST_FIELDS.has(key)
180
+ );
181
+ if(managedRequestField){
182
+ throw new TypeError(`request.${managedRequestField} is managed by the chat session.`);
183
+ }
77
184
  if(!signalLike(input.signal)) throw new TypeError('signal must be an AbortSignal.');
78
- return Object.freeze({
185
+ return {
79
186
  messagePersist,
80
- requestMessage:Object.freeze({
187
+ requestMessage:{
81
188
  content:input.message.content,
82
189
  role,
83
190
  ...(toolCallId?{tool_call_id:toolCallId}:{}),
84
- }),
191
+ },
85
192
  responsePersist,
193
+ request:{...request},
86
194
  signal:input.signal??null,
87
- });
195
+ };
88
196
  }
89
197
 
90
198
  function fileName(value){
@@ -100,8 +208,10 @@ function fileName(value){
100
208
  * per-turn persistence affects DBOPFS and memory, never the live model context.
101
209
  */
102
210
  class PersistentAIChatSession{
211
+ #activeStream=null;
103
212
  #configured=null;
104
213
  #entity;
214
+ #fetchChat;
105
215
  #memory;
106
216
  #options;
107
217
  #pending=false;
@@ -113,7 +223,7 @@ class PersistentAIChatSession{
113
223
  assertKnownKeys(
114
224
  options,
115
225
  new Set([
116
- 'chat','chatEntity','chatFileName','contextBuilder','loadExisting','maxContextCharacters',
226
+ 'ai','chat','chatEntity','chatFileName','contextBuilder','loadExisting','maxContextCharacters',
117
227
  'maxMessageCharacters','maxMessages','memory','request','responseLength','systemPrompt'
118
228
  ]),
119
229
  'Persistent chat options',
@@ -121,8 +231,48 @@ class PersistentAIChatSession{
121
231
  if(options.chatEntity!==undefined&&!(options.chatEntity instanceof ChatEntity)){
122
232
  throw new TypeError('chatEntity must be a ChatEntity.');
123
233
  }
124
- const chat=options.chat??configuredArcaneChat;
234
+ if(options.ai!==undefined&&(
235
+ !options.ai
236
+ ||typeof options.ai!=='object'
237
+ ||typeof options.ai.fetchRequest!=='function'
238
+ )){
239
+ throw new TypeError('ai must expose fetchRequest(request).');
240
+ }
241
+ if(options.ai!==undefined&&options.chat!==undefined){
242
+ throw coded(
243
+ new TypeError('Specify either ai or chat, not both.'),
244
+ 'AI_CHAT_AMBIGUOUS_PROVIDER',
245
+ );
246
+ }
247
+ const chat=options.ai===undefined
248
+ ?options.chat??configuredArcaneChat
249
+ :configuredAIChat(options.ai);
125
250
  if(typeof chat!=='function') throw new TypeError('chat must be a function.');
251
+ if(options.request!==undefined&&!isPlainRecord(options.request)){
252
+ throw new TypeError('request must be a plain object.');
253
+ }
254
+ const request={...(options.request??{})};
255
+ const managedRequestField=Object.keys(request).find(
256
+ key=>SESSION_MANAGED_REQUEST_FIELDS.has(key)
257
+ );
258
+ if(managedRequestField){
259
+ throw new TypeError(`request.${managedRequestField} is managed by the chat session.`);
260
+ }
261
+ if(
262
+ request.parallelToolCalls===true
263
+ ||request.parallel_tool_calls===true
264
+ ){
265
+ throw coded(
266
+ new TypeError('Persistent chat supports one structural tool call at a time.'),
267
+ 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
268
+ );
269
+ }
270
+ if(
271
+ request.parallelToolCalls===undefined
272
+ &&request.parallel_tool_calls===undefined
273
+ ){
274
+ request.parallelToolCalls=false;
275
+ }
126
276
  const systemPrompt=options.systemPrompt??'';
127
277
  if(typeof systemPrompt!=='string') throw new TypeError('systemPrompt must be a string.');
128
278
  this.#entity=options.chatEntity??new ChatEntity(systemPrompt);
@@ -138,7 +288,8 @@ class PersistentAIChatSession{
138
288
  throw new TypeError('loadExisting requires chatFileName or chatEntity.');
139
289
  }
140
290
  this.#memory=boolean(options.memory,'memory',true);
141
- this.#options=Object.freeze({...options,chat,loadExisting,systemPrompt});
291
+ this.#fetchChat=chat;
292
+ this.#options={...options,chat,loadExisting,request,systemPrompt};
142
293
  this.#readyPromise=this.#initialize();
143
294
  }
144
295
 
@@ -150,6 +301,54 @@ class PersistentAIChatSession{
150
301
 
151
302
  get chatEntity(){return this.#entity;}
152
303
  get fileName(){return this.#entity.fileName;}
304
+ get ai(){return this.#options.ai??null;}
305
+
306
+ async #requestConfiguredAI(request){
307
+ const providerRequest=providerRequestWithoutLifecycleCallbacks(request);
308
+ if(!this.#activeStream) return this.#fetchChat(providerRequest);
309
+ if(typeof this.#options.ai?.streamRequest!=='function'){
310
+ return this.#fetchChat(providerRequest);
311
+ }
312
+ const activeStream=this.#activeStream;
313
+ let terminal=null;
314
+ let streamedToolCall=null;
315
+ let streamedToolDetails=[];
316
+ const output=await this.#options.ai.streamRequest({
317
+ ...providerRequest,
318
+ onChunk:activeStream.onChunk,
319
+ onToolCall:function retainStructuralToolCall(call,...details){
320
+ const normalized=normalizeStructuralToolCall(
321
+ call,
322
+ 'The streamed structural tool call',
323
+ );
324
+ if(streamedToolCall&&!sameStructuralToolCall(streamedToolCall,normalized)){
325
+ throw coded(
326
+ new TypeError('The AI stream emitted divergent structural tool calls.'),
327
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
328
+ );
329
+ }
330
+ streamedToolCall=normalized;
331
+ streamedToolDetails=details;
332
+ },
333
+ onResponse:async function retainPersistentChatTerminal(response){
334
+ terminal=response;
335
+ },
336
+ });
337
+ const response=normalizeStreamResponse(terminal,output);
338
+ const terminalToolCall=terminalStructuralToolCall(response);
339
+ if(streamedToolCall&&!sameStructuralToolCall(streamedToolCall,terminalToolCall)){
340
+ throw coded(
341
+ new TypeError(
342
+ 'The AI stream terminal response omitted or changed its structural tool call.'
343
+ ),
344
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
345
+ );
346
+ }
347
+ activeStream.streamedToolCall=streamedToolCall;
348
+ activeStream.streamedToolDetails=streamedToolDetails;
349
+ activeStream.terminalToolCall=terminalToolCall;
350
+ return response;
351
+ }
153
352
 
154
353
  async #initialize(){
155
354
  if(this.#options.loadExisting) await this.#entity.load();
@@ -157,9 +356,12 @@ class PersistentAIChatSession{
157
356
  const storedSystem=storedMessages.find(message=>message.role==='system');
158
357
  const initialMessages=storedMessages
159
358
  .filter(message=>['user','assistant','tool'].includes(message.role))
160
- .map(message=>Object.freeze({
359
+ .map(message=>({
161
360
  role:message.role,
162
361
  content:String(message.content??''),
362
+ ...(message.role==='assistant'&&Object.hasOwn(message,'reasoning_content')
363
+ ?{reasoning_content:message.reasoning_content}
364
+ :{}),
163
365
  ...(message.tool_calls?{tool_calls:message.tool_calls}:{}),
164
366
  ...(message.tool_call_id?{tool_call_id:message.tool_call_id}:{}),
165
367
  }));
@@ -171,12 +373,9 @@ class PersistentAIChatSession{
171
373
  }
172
374
  }
173
375
  const configuredOptions={
174
- chat:this.#options.chat,
376
+ chat:request=>this.#requestConfiguredAI(request),
175
377
  contextBuilder:this.#options.contextBuilder,
176
378
  initialMessages,
177
- maxContextCharacters:this.#options.maxContextCharacters,
178
- maxMessageCharacters:this.#options.maxMessageCharacters,
179
- maxMessages:this.#options.maxMessages,
180
379
  request:this.#options.request,
181
380
  systemPrompt:this.#options.systemPrompt||String(storedSystem?.content??''),
182
381
  };
@@ -200,12 +399,17 @@ class PersistentAIChatSession{
200
399
  return this.#configured.history();
201
400
  }
202
401
 
402
+ async transcript(){
403
+ await this.ready();
404
+ return this.#entity.transcript;
405
+ }
406
+
203
407
  async settleMemory(){
204
408
  await this.ready();
205
409
  return this.#entity.settleMemory();
206
410
  }
207
411
 
208
- async send(input){
412
+ async #requestTurn(input,streamHandlers=null){
209
413
  const settings=normalizeSend(input);
210
414
  if(this.#pending){
211
415
  throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
@@ -233,12 +437,51 @@ class PersistentAIChatSession{
233
437
  );
234
438
  }
235
439
  }
236
- prepared=await this.#configured.prepare(settings.requestMessage,{signal:settings.signal});
440
+ const streamState=streamHandlers?{
441
+ ...streamHandlers,
442
+ streamedToolCall:null,
443
+ streamedToolDetails:[],
444
+ terminalToolCall:null,
445
+ }:null;
446
+ if(streamState) this.#activeStream=streamState;
447
+ try{
448
+ prepared=await this.#configured.prepare(
449
+ settings.requestMessage,
450
+ {request:settings.request,signal:settings.signal},
451
+ );
452
+ }finally{
453
+ if(this.#activeStream===streamState) this.#activeStream=null;
454
+ }
237
455
  const result=prepared.response;
456
+ if(streamState){
457
+ const validatedToolCall=result.message.tool_calls?.[0]??null;
458
+ if(
459
+ streamState.terminalToolCall
460
+ &&!sameStructuralToolCall(streamState.terminalToolCall,validatedToolCall)
461
+ ){
462
+ throw coded(
463
+ new TypeError(
464
+ 'The validated AI response changed its terminal structural tool call.'
465
+ ),
466
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
467
+ );
468
+ }
469
+ if(validatedToolCall){
470
+ await streamState.onToolCall(
471
+ validatedToolCall,
472
+ ...streamState.streamedToolDetails,
473
+ );
474
+ }
475
+ }
238
476
  await this.#entity.addTurn({
239
477
  assistantMessage:result.message,
240
478
  extractMemory:this.#memory&&settings.messagePersist&&settings.responsePersist,
241
- memoryRequest:messages=>this.#options.chat({messages}),
479
+ memoryRequest:messages=>this.#fetchChat(
480
+ providerRequestWithoutLifecycleCallbacks({
481
+ ...this.#options.request,
482
+ messages,
483
+ })
484
+ ),
242
485
  messagePersist:settings.messagePersist,
243
486
  requestMessage:settings.requestMessage,
244
487
  responsePersist:settings.responsePersist,
@@ -250,7 +493,19 @@ class PersistentAIChatSession{
250
493
  for(const call of result.message.tool_calls??[]){
251
494
  this.#toolCallPersistence.set(call.id,settings.responsePersist);
252
495
  }
253
- return committed;
496
+ const transcript=this.#entity.transcript;
497
+ const assistantRecord=transcript.at(-1);
498
+ return assistantRecord?.role==='assistant'
499
+ ?{
500
+ ...committed,
501
+ message:{
502
+ ...committed.message,
503
+ ...(assistantRecord.timestamp!==undefined
504
+ ?{timestamp:assistantRecord.timestamp}
505
+ :{}),
506
+ },
507
+ }
508
+ :committed;
254
509
  }catch(error){
255
510
  prepared?.rollback();
256
511
  throw error;
@@ -258,6 +513,14 @@ class PersistentAIChatSession{
258
513
  this.#pending=false;
259
514
  }
260
515
  }
516
+
517
+ async send(input){
518
+ return this.#requestTurn(input);
519
+ }
520
+
521
+ async stream(input,handlers={}){
522
+ return this.#requestTurn(input,normalizeStreamHandlers(handlers));
523
+ }
261
524
  }
262
525
 
263
526
  function createPersistentAIChatSession(options){