arcane-os 0.2.0 → 0.2.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 (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
  4. package/browser-runtime/ai/browser-speech-providers.mjs +331 -25
  5. package/docs/architecture.md +2 -2
  6. package/docs/reference/README.md +79 -13
  7. package/docs/reference/ai/browser-speech.md +336 -0
  8. package/docs/reference/ai/browser-wasm.md +207 -82
  9. package/docs/reference/availability-and-normalization.md +30 -4
  10. package/docs/reference/behavioral-testing.md +4 -1
  11. package/docs/reference/cli.md +29 -10
  12. package/docs/reference/core/arcane-ai-contracts.md +43 -9
  13. package/docs/reference/inventory/package-api.json +110 -14
  14. package/docs/reference/inventory/runtime-components.json +19 -6
  15. package/docs/reference/inventory/runtime-modules.json +113 -9
  16. package/docs/reference/protocols.md +260 -38
  17. package/docs/reference/runtime-components.md +108 -15
  18. package/docs/reference/runtime-modules.md +422 -8
  19. package/docs/reference/sdk-api.md +626 -85
  20. package/package.json +1 -1
  21. package/runtime/ARCANE_RUNTIME_RELEASE.json +19 -19
  22. package/runtime/arcane/components/chat.html +288 -52
  23. package/runtime/arcane/components/speech.html +339 -15
  24. package/runtime/arcane/modules/AI.js +1245 -136
  25. package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
  26. package/runtime/arcane/modules/AIRuntimeState.js +23 -4
  27. package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
  28. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
  29. package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
  30. package/schemas/arcane-lock.schema.json +6 -4
  31. package/src/dev-server.mjs +29 -13
  32. package/src/doctor.mjs +1 -3
  33. package/src/import-map.mjs +134 -83
  34. package/src/packager/core.mjs +311 -39
  35. package/src/scaffold.mjs +45 -17
  36. package/src/templates/workspace-template.mjs +23 -4
  37. package/src/toolchain.mjs +10 -2
  38. package/src/workspace.mjs +177 -24
@@ -53,6 +53,17 @@ function usageCount(value){
53
53
  return Number.isSafeInteger(value)&&value>=0?value:null;
54
54
  }
55
55
 
56
+ function providerUsageCount(value,label){
57
+ if(value===undefined) return null;
58
+ if(!Number.isSafeInteger(value)||value<0){
59
+ throw coded(
60
+ new TypeError(`${label} must be a nonnegative integer when provided.`),
61
+ 'AI_CHAT_INVALID_RESPONSE',
62
+ );
63
+ }
64
+ return value;
65
+ }
66
+
56
67
  function signalLike(value){
57
68
  return value===undefined||value===null||(
58
69
  typeof value==='object'
@@ -271,14 +282,14 @@ async function configuredArcaneChat(request){
271
282
  return api.chat(request);
272
283
  }
273
284
 
274
- function normalizeResponse(response,maxMessageCharacters){
275
- if(!isPlainRecord(response)||!isPlainRecord(response.message)){
285
+ function normalizeAssistantResponseMessage(value,maxMessageCharacters,{requireRole=false}={}){
286
+ if(!isPlainRecord(value)){
276
287
  throw coded(
277
288
  new TypeError('The chat provider returned an invalid response.'),
278
289
  'AI_CHAT_INVALID_RESPONSE',
279
290
  );
280
291
  }
281
- if(response.message.role!==undefined&&response.message.role!=='assistant'){
292
+ if((requireRole&&value.role!=='assistant')||(value.role!==undefined&&value.role!=='assistant')){
282
293
  throw coded(
283
294
  new TypeError('The chat provider response must contain an assistant message.'),
284
295
  'AI_CHAT_INVALID_RESPONSE',
@@ -288,7 +299,7 @@ function normalizeResponse(response,maxMessageCharacters){
288
299
  let responseMessage;
289
300
  try{
290
301
  responseMessage=normalizeMessage(
291
- {...response.message,role:'assistant'},
302
+ {...value,role:'assistant'},
292
303
  'The assistant message',
293
304
  maxMessageCharacters,
294
305
  new Set(['assistant']),
@@ -296,6 +307,14 @@ function normalizeResponse(response,maxMessageCharacters){
296
307
  }catch(error){
297
308
  throw coded(error,'AI_CHAT_INVALID_RESPONSE');
298
309
  }
310
+ return responseMessage;
311
+ }
312
+
313
+ function normalizeSessionResponse(response,maxMessageCharacters){
314
+ const responseMessage=normalizeAssistantResponseMessage(
315
+ response.message,
316
+ maxMessageCharacters,
317
+ );
299
318
 
300
319
  return Object.freeze({
301
320
  provider:optionalMetadata(response.provider,'The provider name',128),
@@ -308,6 +327,63 @@ function normalizeResponse(response,maxMessageCharacters){
308
327
  });
309
328
  }
310
329
 
330
+ function normalizeOpenAICompatibleResponse(response,maxMessageCharacters){
331
+ if(!Array.isArray(response.choices)||response.choices.length!==1){
332
+ throw coded(
333
+ new TypeError('The chat provider completion must contain exactly one choice.'),
334
+ 'AI_CHAT_INVALID_RESPONSE',
335
+ );
336
+ }
337
+ const choice=response.choices[0];
338
+ if(!isPlainRecord(choice)||(choice.index!==undefined&&choice.index!==0)){
339
+ throw coded(
340
+ new TypeError('The chat provider completion contains an invalid choice.'),
341
+ 'AI_CHAT_INVALID_RESPONSE',
342
+ );
343
+ }
344
+ if(response.usage!==undefined&&response.usage!==null&&!isPlainRecord(response.usage)){
345
+ throw coded(
346
+ new TypeError('The chat provider completion usage must be a plain object when provided.'),
347
+ 'AI_CHAT_INVALID_RESPONSE',
348
+ );
349
+ }
350
+ const usage=response.usage??{};
351
+ const responseMessage=normalizeAssistantResponseMessage(
352
+ choice.message,
353
+ maxMessageCharacters,
354
+ {requireRole:true},
355
+ );
356
+
357
+ return Object.freeze({
358
+ provider:optionalMetadata(response.provider,'The provider name',128),
359
+ model:optionalMetadata(response.model,'The model name',256),
360
+ message:responseMessage,
361
+ done:true,
362
+ doneReason:optionalMetadata(choice.finish_reason,'The completion reason',128),
363
+ promptEvalCount:providerUsageCount(
364
+ usage.prompt_tokens,
365
+ 'The provider prompt token count',
366
+ ),
367
+ evalCount:providerUsageCount(
368
+ usage.completion_tokens,
369
+ 'The provider completion token count',
370
+ ),
371
+ });
372
+ }
373
+
374
+ function normalizeResponse(response,maxMessageCharacters){
375
+ if(!isPlainRecord(response)){
376
+ throw coded(
377
+ new TypeError('The chat provider returned an invalid response.'),
378
+ 'AI_CHAT_INVALID_RESPONSE',
379
+ );
380
+ }
381
+ if(Object.hasOwn(response,'message')){
382
+ return normalizeSessionResponse(response,maxMessageCharacters);
383
+ }
384
+ return normalizeOpenAICompatibleResponse(response,maxMessageCharacters);
385
+ }
386
+
311
387
  /**
312
388
  * Maintains one bounded, in-memory conversation through a configured chat provider.
313
389
  *
@@ -315,7 +391,9 @@ function normalizeResponse(response,maxMessageCharacters){
315
391
  * provider selection. Applications own their prompt policy and may inject an
316
392
  * asynchronous contextBuilder that returns additional system text for each send.
317
393
  * An explicitly supplied responseLength augments only this conversational
318
- * session's system prompt through the shared response-length policy.
394
+ * session's system prompt through the shared response-length policy. An injected
395
+ * chat function may return either the normalized session response or one
396
+ * non-stream OpenAI-compatible completion containing exactly one choice.
319
397
  */
320
398
  export default class ConfiguredAIChatSession{
321
399
  #chat;
@@ -498,12 +576,19 @@ export default class ConfiguredAIChatSession{
498
576
  ?this.#conversation.length-toolCallIndex+transientTail.length
499
577
  :transientTail.length,
500
578
  );
501
- const response=normalizeResponse(
502
- await this.#chat({
579
+ let providerResponse;
580
+ try{
581
+ providerResponse=await this.#chat({
503
582
  ...this.#request,
504
583
  ...(options.signal?{signal:options.signal}:{}),
505
584
  messages:requestMessages.map(publicMessage),
506
- }),
585
+ });
586
+ }catch(error){
587
+ if(options.signal?.aborted) throw abortError();
588
+ throw error;
589
+ }
590
+ const response=normalizeResponse(
591
+ providerResponse,
507
592
  this.#limits.maxMessageCharacters,
508
593
  );
509
594
  if(options.signal?.aborted) throw abortError();