@glassly/engine 0.3.3-dev.669 → 0.3.4-dev.674

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 (115) hide show
  1. package/build/engine.d.ts +38 -14
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +3 -0
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/ai.d.ts +32 -0
  6. package/build/facades/ai.d.ts.map +1 -0
  7. package/build/facades/ai.js +38 -0
  8. package/build/facades/ai.js.map +1 -0
  9. package/build/facades/glasses.d.ts +30 -24
  10. package/build/facades/glasses.d.ts.map +1 -1
  11. package/build/facades/glasses.js +49 -30
  12. package/build/facades/glasses.js.map +1 -1
  13. package/build/facades/glassesSettings.d.ts.map +1 -1
  14. package/build/facades/glassesSettings.js +1 -0
  15. package/build/facades/glassesSettings.js.map +1 -1
  16. package/build/facades/pairing.d.ts +6 -5
  17. package/build/facades/pairing.d.ts.map +1 -1
  18. package/build/facades/pairing.js +14 -14
  19. package/build/facades/pairing.js.map +1 -1
  20. package/build/generated/releaseMetadata.js +4 -4
  21. package/build/generated/releaseMetadata.js.map +1 -1
  22. package/build/index.d.ts +5 -2
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +2 -0
  25. package/build/index.js.map +1 -1
  26. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  27. package/build/services/DeviceEventRouter.js +9 -1
  28. package/build/services/DeviceEventRouter.js.map +1 -1
  29. package/build/services/GestureDotsService.d.ts +4 -3
  30. package/build/services/GestureDotsService.d.ts.map +1 -1
  31. package/build/services/GestureDotsService.js +17 -12
  32. package/build/services/GestureDotsService.js.map +1 -1
  33. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  34. package/build/services/GlassesTouchInput.js +11 -9
  35. package/build/services/GlassesTouchInput.js.map +1 -1
  36. package/build/services/LlmRouting.d.ts +35 -0
  37. package/build/services/LlmRouting.d.ts.map +1 -0
  38. package/build/services/LlmRouting.js +25 -0
  39. package/build/services/LlmRouting.js.map +1 -0
  40. package/build/services/LocalLlmModelManager.d.ts +79 -0
  41. package/build/services/LocalLlmModelManager.d.ts.map +1 -0
  42. package/build/services/LocalLlmModelManager.js +278 -0
  43. package/build/services/LocalLlmModelManager.js.map +1 -0
  44. package/build/services/LocalLlmPrompt.d.ts +90 -0
  45. package/build/services/LocalLlmPrompt.d.ts.map +1 -0
  46. package/build/services/LocalLlmPrompt.js +189 -0
  47. package/build/services/LocalLlmPrompt.js.map +1 -0
  48. package/build/services/LocalLlmProvider.d.ts +35 -0
  49. package/build/services/LocalLlmProvider.d.ts.map +1 -0
  50. package/build/services/LocalLlmProvider.js +217 -0
  51. package/build/services/LocalLlmProvider.js.map +1 -0
  52. package/build/services/LocalMiniappRuntime.d.ts +9 -1
  53. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  54. package/build/services/LocalMiniappRuntime.js +91 -17
  55. package/build/services/LocalMiniappRuntime.js.map +1 -1
  56. package/build/services/STTModelManager.d.ts +25 -2
  57. package/build/services/STTModelManager.d.ts.map +1 -1
  58. package/build/services/STTModelManager.js +137 -6
  59. package/build/services/STTModelManager.js.map +1 -1
  60. package/build/services/controllers.d.ts +21 -0
  61. package/build/services/controllers.d.ts.map +1 -0
  62. package/build/services/controllers.js +62 -0
  63. package/build/services/controllers.js.map +1 -0
  64. package/build/services/effectiveCapabilities.d.ts.map +1 -1
  65. package/build/services/effectiveCapabilities.js +18 -24
  66. package/build/services/effectiveCapabilities.js.map +1 -1
  67. package/build/services/neuralBand/NeuralBandInput.d.ts +17 -6
  68. package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -1
  69. package/build/services/neuralBand/NeuralBandInput.js +40 -23
  70. package/build/services/neuralBand/NeuralBandInput.js.map +1 -1
  71. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -1
  72. package/build/services/neuralBand/NeuralBandSettingsSync.js +23 -26
  73. package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -1
  74. package/build/services/settingsBridge.d.ts +26 -18
  75. package/build/services/settingsBridge.d.ts.map +1 -1
  76. package/build/services/settingsBridge.js +36 -15
  77. package/build/services/settingsBridge.js.map +1 -1
  78. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  79. package/build/stores/bluetoothSettingKeys.js +3 -1
  80. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  81. package/build/stores/core.d.ts.map +1 -1
  82. package/build/stores/core.js +0 -1
  83. package/build/stores/core.js.map +1 -1
  84. package/build/stores/glasses.d.ts.map +1 -1
  85. package/build/stores/glasses.js +2 -6
  86. package/build/stores/glasses.js.map +1 -1
  87. package/build/stores/settings.d.ts.map +1 -1
  88. package/build/stores/settings.js +28 -2
  89. package/build/stores/settings.js.map +1 -1
  90. package/package.json +12 -8
  91. package/src/engine.ts +3 -0
  92. package/src/facades/ai.ts +43 -0
  93. package/src/facades/glasses.ts +60 -31
  94. package/src/facades/glassesSettings.ts +1 -0
  95. package/src/facades/pairing.ts +18 -14
  96. package/src/generated/releaseMetadata.ts +4 -4
  97. package/src/index.ts +16 -2
  98. package/src/services/DeviceEventRouter.ts +8 -1
  99. package/src/services/GestureDotsService.ts +20 -14
  100. package/src/services/GlassesTouchInput.ts +11 -9
  101. package/src/services/LlmRouting.ts +49 -0
  102. package/src/services/LocalLlmModelManager.ts +327 -0
  103. package/src/services/LocalLlmPrompt.ts +246 -0
  104. package/src/services/LocalLlmProvider.ts +289 -0
  105. package/src/services/LocalMiniappRuntime.ts +137 -21
  106. package/src/services/STTModelManager.ts +140 -8
  107. package/src/services/controllers.ts +77 -0
  108. package/src/services/effectiveCapabilities.ts +16 -32
  109. package/src/services/neuralBand/NeuralBandInput.ts +50 -26
  110. package/src/services/neuralBand/NeuralBandSettingsSync.ts +28 -26
  111. package/src/services/settingsBridge.ts +60 -22
  112. package/src/stores/bluetoothSettingKeys.ts +3 -1
  113. package/src/stores/core.ts +0 -1
  114. package/src/stores/glasses.ts +2 -6
  115. package/src/stores/settings.ts +28 -2
@@ -34,6 +34,9 @@ import { markTranscriptSourceRecent, useTranscriptionDebugStore } from "../store
34
34
  import { resolveMapboxRenderToken } from "./MapboxTokenService";
35
35
  import { createAnthropicProvider, createGeminiProvider, createOpenAiProvider, } from "@glassly/llm-providers";
36
36
  import { normalizeAiTier, resolveAiTier } from "./AiTierResolution";
37
+ import { decideLlmRoute, localLlmReason } from "./LlmRouting";
38
+ import localLlmModelManager from "./LocalLlmModelManager";
39
+ import localLlmProvider from "./LocalLlmProvider";
37
40
  import { HttpError } from "@glassly/cloud-client";
38
41
  import localDisplayManager from "./LocalDisplayManager";
39
42
  import sttModelManager from "./STTModelManager";
@@ -45,7 +48,8 @@ import dashboardOpenTrace from "./debug/DashboardOpenTrace";
45
48
  import { isAppForegrounded, isPhoneActive, onAppForegroundChange, onPhoneActivityChange } from "./PhoneActivity";
46
49
  import { installedNotificationApps } from "./PhoneNotificationsSync";
47
50
  import { MiniappIcons } from "./MiniappIcons";
48
- import { DERIVED_SETTINGS_KEYS, SETTINGS_BRIDGE_KEYS, input2dFrom, isDerivedSettingsKey, ringBatteryFrom, selectSettingsBridgeKeys, validateSettingsSet, } from "./settingsBridge";
51
+ import { pairedControllersFrom, readyControllersKey } from "./controllers";
52
+ import { DERIVED_SETTINGS_KEYS, SETTINGS_BRIDGE_KEYS, controllersSettingFrom, isDerivedSettingsKey, selectSettingsBridgeKeys, validateSettingsSet, } from "./settingsBridge";
49
53
  import { mayUsePermission } from "./miniappPermissions";
50
54
  import { activeDashboard, addDashboardApp, displayViewsFor, isDashboardApp, isOverlayApp, onOverlayAppsChange, removeDashboardApp, setOverlayViewer, } from "./pages/overlayApps";
51
55
  import { pagesEligible } from "./pages/pageEligibility";
@@ -262,6 +266,7 @@ function directLlmProvider(name) {
262
266
  directLlmProviders.set(name, built);
263
267
  return built;
264
268
  }
269
+ const QUOTA_EXCEEDED_MESSAGE = "Monthly AI credit is used up. Upgrade in Settings > Membership, or add your own API key in Settings > API Keys.";
265
270
  /** The user's saved key (Settings > API Keys) for an LLM provider, "" if none. */
266
271
  function userLlmKey(provider) {
267
272
  const raw = useSettingsStore.getState().getSetting(MINIAPP_READABLE_API_KEYS[provider]);
@@ -2201,11 +2206,10 @@ class LocalMiniappRuntime {
2201
2206
  return isPhoneActive();
2202
2207
  case "foreground_app_on_screen":
2203
2208
  return localDisplayManager.foregroundAppOnScreen();
2204
- case "ring_battery":
2205
- return ringBatteryFrom(useGlassesStore.getState());
2206
- case "input_2d": {
2207
- const controller = useSettingsStore.getState().getSetting(SETTINGS.default_controller.key);
2208
- return input2dFrom(useGlassesStore.getState(), controller);
2209
+ case "controllers": {
2210
+ const glasses = useGlassesStore.getState();
2211
+ const records = pairedControllersFrom(useSettingsStore.getState().getSetting(SETTINGS.controllers.key));
2212
+ return controllersSettingFrom(records, glasses.controllers, glasses);
2209
2213
  }
2210
2214
  }
2211
2215
  }
@@ -2254,7 +2258,7 @@ class LocalMiniappRuntime {
2254
2258
  /**
2255
2259
  * Push bridged settings as they change: stored keys from the store, the
2256
2260
  * derived keys whenever their inputs move (phone activity, the display,
2257
- * the apps store, the ring, the dashboard role). Subscribers declared
2261
+ * the apps store, the controllers, the dashboard role). Subscribers declared
2258
2262
  * PLATFORM_SETTINGS (gated at SUBSCRIBE), so this is a no-op fan-out for
2259
2263
  * everyone else.
2260
2264
  */
@@ -2292,8 +2296,8 @@ class LocalMiniappRuntime {
2292
2296
  localDisplayManager.onActiveDisplayChange(pushDerived);
2293
2297
  useAppStatusStore.subscribe(pushDerived);
2294
2298
  onOverlayAppsChange(pushDerived);
2295
- useGlassesStore.subscribe((s) => `${s.ringConnected}:${s.ringBatteryLevel}:${s.controllerConnected}:${s.controllerBatteryLevel}`, pushDerived);
2296
- useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.default_controller.key), pushDerived);
2299
+ useGlassesStore.subscribe((s) => `${s.ringConnected}:${s.ringBatteryLevel}:${s.ringCharging}:${JSON.stringify(s.controllers ?? {})}`, pushDerived);
2300
+ useSettingsStore.subscribe((s) => JSON.stringify(s.getSetting(SETTINGS.controllers.key) ?? null), pushDerived);
2297
2301
  }
2298
2302
  /**
2299
2303
  * Native nav UI feed (display.sendNativeNav*). Gated on the device's nav
@@ -3529,15 +3533,29 @@ class LocalMiniappRuntime {
3529
3533
  // here (the user's Settings > AI Models dropdowns, or "auto" from which
3530
3534
  // keys are saved — see AiTierResolution). Legacy provider/model pins from
3531
3535
  // pre-tier miniapps are deliberately stripped, not honored.
3532
- const { tier: rawTier, provider: _legacyProvider, model: _legacyModel, ...neutralRequest } = request;
3536
+ const { tier: rawTier, provider: _legacyProvider, model: _legacyModel, forceLocal: rawForceLocal, localFallback: rawLocalFallback, ...neutralRequest } = request;
3533
3537
  const tier = normalizeAiTier(rawTier);
3534
3538
  const resolved = resolveAiTier(tier, tierSettingValue(tier), savedLlmKeys());
3539
+ // ON-DEVICE ROUTING — the request's forceLocal / localFallback flags
3540
+ // against the primary path's health (see LlmRouting). The primary is the
3541
+ // vendor-direct call when a user key is saved, else the cloud relay.
3542
+ const directKey = resolved.provider ? userLlmKey(resolved.provider) : "";
3543
+ const forceLocal = rawForceLocal === true;
3544
+ const localFallback = rawLocalFallback === true;
3545
+ const cloudConnected = cloudClientService.isConnected();
3546
+ const cloudDisabled = cloudClientService.isCloudDisabled();
3547
+ const primaryUp = directKey.length > 0 || (cloudConnected && !cloudDisabled && !this.llmQuotaBlocked);
3548
+ const route = decideLlmRoute({ forceLocal, localFallback, primaryUp });
3549
+ if (route === "local") {
3550
+ const reason = localLlmReason({ forceLocal, cloudDisabled, cloudConnected, quotaBlocked: this.llmQuotaBlocked });
3551
+ await this.serveLocalAi(packageName, requestId, neutralRequest, reason, null);
3552
+ return;
3553
+ }
3535
3554
  try {
3536
3555
  // Direct when the resolved provider has a user key (BYO bill, unmetered,
3537
3556
  // works with the cloud disabled); otherwise relay to the cloud LLM
3538
3557
  // service, forwarding the resolved pin so platform-funded calls land on
3539
3558
  // the same model the user chose.
3540
- const directKey = resolved.provider ? userLlmKey(resolved.provider) : "";
3541
3559
  if (resolved.provider && directKey) {
3542
3560
  const result = await directLlmProvider(resolved.provider).complete({
3543
3561
  ...neutralRequest,
@@ -3560,17 +3578,30 @@ class LocalMiniappRuntime {
3560
3578
  // is spent. Distinct code (miniapps may fall back to a local path) and
3561
3579
  // a host hook so the app can offer the upgrade paywall — the miniapp
3562
3580
  // itself must never own that conversation.
3563
- if (err instanceof HttpError && (err.code === "quota_exceeded" || err.status === 402)) {
3581
+ const quotaExceeded = err instanceof HttpError && (err.code === "quota_exceeded" || err.status === 402);
3582
+ if (quotaExceeded && !this.llmQuotaBlocked) {
3564
3583
  console.warn(`${LOG_TAG}: ai.complete quota exceeded for ${packageName}`);
3565
3584
  this.llmQuotaBlocked = true;
3566
3585
  this.onQuotaExceeded?.("llm");
3586
+ }
3587
+ const message = err instanceof Error ? err.message : "ai.complete failed";
3588
+ // localFallback: the same request, on-device. The primary's error
3589
+ // stands only when the phone cannot serve it either.
3590
+ if (route === "primary_then_local") {
3591
+ console.warn(`${LOG_TAG}: ai.complete primary failed for ${packageName} (${message}); trying on-device`);
3592
+ await this.serveLocalAi(packageName, requestId, neutralRequest, quotaExceeded ? "llm_quota" : "primary_failed", {
3593
+ code: quotaExceeded ? MiniappErrorCode.QUOTA_EXCEEDED : MiniappErrorCode.INTERNAL,
3594
+ message: quotaExceeded ? QUOTA_EXCEEDED_MESSAGE : message,
3595
+ });
3596
+ return;
3597
+ }
3598
+ if (quotaExceeded) {
3567
3599
  this.sendResult(packageName, requestId, false, undefined, {
3568
3600
  code: MiniappErrorCode.QUOTA_EXCEEDED,
3569
- message: "Monthly AI credit is used up. Upgrade in Settings > Membership, or add your own API key in Settings > API Keys.",
3601
+ message: QUOTA_EXCEEDED_MESSAGE,
3570
3602
  });
3571
3603
  return;
3572
3604
  }
3573
- const message = err instanceof Error ? err.message : "ai.complete failed";
3574
3605
  console.warn(`${LOG_TAG}: ai.complete failed for ${packageName}: ${message}`);
3575
3606
  this.sendResult(packageName, requestId, false, undefined, {
3576
3607
  code: MiniappErrorCode.INTERNAL,
@@ -3578,6 +3609,50 @@ class LocalMiniappRuntime {
3578
3609
  });
3579
3610
  }
3580
3611
  }
3612
+ /**
3613
+ * Serve one ai.complete on the phone's own model (LocalLlmProvider). With
3614
+ * no model on disk the download starts (or continues) in the background
3615
+ * and the miniapp gets AI_LOCAL_UNAVAILABLE naming the state — unless
3616
+ * `primaryError` is given (a failed fallback), in which case the primary's
3617
+ * own error is what the miniapp sees.
3618
+ */
3619
+ async serveLocalAi(packageName, requestId, request, reason, primaryError) {
3620
+ const modelId = localLlmModelManager.selectedModelId();
3621
+ if (!(await localLlmModelManager.ensureSelectedModel())) {
3622
+ const status = localLlmModelManager.getStatus();
3623
+ const state = status?.modelId === modelId && status.stage === "downloading"
3624
+ ? `is downloading (${status.percent}%)`
3625
+ : status?.modelId === modelId && status.stage === "failed"
3626
+ ? `failed to download (${status.error ?? "unknown error"})`
3627
+ : "is not downloaded yet";
3628
+ console.warn(`${LOG_TAG}: ai.complete on-device for ${packageName} (${reason}): model ${modelId} ${state}`);
3629
+ this.sendResult(packageName, requestId, false, undefined, primaryError ?? {
3630
+ code: MiniappErrorCode.AI_LOCAL_UNAVAILABLE,
3631
+ message: `The on-device model (${localLlmModelManager.model(modelId).label}) ${state}. Manage it in Settings > AI Models.`,
3632
+ });
3633
+ return;
3634
+ }
3635
+ try {
3636
+ // webSearch is a vendor server-side grant; the phone has none.
3637
+ const { webSearch: _webSearch, ...localRequest } = request;
3638
+ const result = await localLlmProvider.complete(localRequest);
3639
+ console.log(`${LOG_TAG}: ai.complete served on-device for ${packageName} (${reason}, ${result.modelId})`);
3640
+ this.sendResult(packageName, requestId, true, {
3641
+ text: result.text,
3642
+ stopReason: result.stopReason,
3643
+ ...(result.toolCalls ? { toolCalls: result.toolCalls } : {}),
3644
+ usage: result.usage,
3645
+ provider: "device",
3646
+ model: result.modelId,
3647
+ billedToUserKey: false,
3648
+ });
3649
+ }
3650
+ catch (err) {
3651
+ const message = err instanceof Error ? err.message : "on-device completion failed";
3652
+ console.warn(`${LOG_TAG}: ai.complete on-device failed for ${packageName}: ${message}`);
3653
+ this.sendResult(packageName, requestId, false, undefined, primaryError ?? { code: MiniappErrorCode.AI_LOCAL_UNAVAILABLE, message });
3654
+ }
3655
+ }
3581
3656
  /**
3582
3657
  * ai.generate — agentic visual generation. Always relayed to the cloud
3583
3658
  * runtime (the server drives the provider's agentic loop; there is no
@@ -4640,9 +4715,8 @@ class LocalMiniappRuntime {
4640
4715
  this.capabilitiesWired = true;
4641
4716
  const broadcast = () => this.broadcastCapabilities();
4642
4717
  // Connection, model and firmware also decide the dashboard view's eligibility;
4643
- // a connected Neural Band adds its swipe2d surface to `button`.
4644
- useGlassesStore.subscribe((s) => `${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}:${s.controllerConnected}`, broadcast);
4645
- useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.default_controller.key), broadcast);
4718
+ // connected controllers add their input surfaces to `button`.
4719
+ useGlassesStore.subscribe((s) => `${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}:${readyControllersKey(s.controllers)}`, broadcast);
4646
4720
  // The debug knob flips hasNativeNavigation on custom firmware.
4647
4721
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast);
4648
4722
  // The dashboard master switch flips the dashboard view.