@glassly/engine 0.3.5-beta.710 → 0.3.5-beta.760

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 (119) hide show
  1. package/build/engine.d.ts +4 -1
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +5 -1
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/ai.d.ts +3 -3
  6. package/build/facades/ai.d.ts.map +1 -1
  7. package/build/facades/ai.js +3 -3
  8. package/build/facades/ai.js.map +1 -1
  9. package/build/facades/glasses.d.ts +5 -1
  10. package/build/facades/glasses.d.ts.map +1 -1
  11. package/build/facades/glasses.js +5 -0
  12. package/build/facades/glasses.js.map +1 -1
  13. package/build/generated/releaseMetadata.js +3 -3
  14. package/build/generated/releaseMetadata.js.map +1 -1
  15. package/build/services/GestureDotsService.d.ts.map +1 -1
  16. package/build/services/GestureDotsService.js +2 -1
  17. package/build/services/GestureDotsService.js.map +1 -1
  18. package/build/services/GlassesTouchInput.d.ts +6 -4
  19. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  20. package/build/services/GlassesTouchInput.js +76 -37
  21. package/build/services/GlassesTouchInput.js.map +1 -1
  22. package/build/services/LocalDisplayManager.d.ts +12 -4
  23. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  24. package/build/services/LocalDisplayManager.js +21 -7
  25. package/build/services/LocalDisplayManager.js.map +1 -1
  26. package/build/services/LocalLlmModelManager.d.ts +3 -1
  27. package/build/services/LocalLlmModelManager.d.ts.map +1 -1
  28. package/build/services/LocalLlmModelManager.js +13 -5
  29. package/build/services/LocalLlmModelManager.js.map +1 -1
  30. package/build/services/LocalLlmPrompt.d.ts +10 -9
  31. package/build/services/LocalLlmPrompt.d.ts.map +1 -1
  32. package/build/services/LocalLlmPrompt.js +43 -32
  33. package/build/services/LocalLlmPrompt.js.map +1 -1
  34. package/build/services/LocalLlmProvider.d.ts.map +1 -1
  35. package/build/services/LocalLlmProvider.js +39 -20
  36. package/build/services/LocalLlmProvider.js.map +1 -1
  37. package/build/services/LocalMiniappRuntime.d.ts +22 -4
  38. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  39. package/build/services/LocalMiniappRuntime.js +108 -134
  40. package/build/services/LocalMiniappRuntime.js.map +1 -1
  41. package/build/services/MiniappExitGesture.d.ts +6 -3
  42. package/build/services/MiniappExitGesture.d.ts.map +1 -1
  43. package/build/services/MiniappExitGesture.js +4 -4
  44. package/build/services/MiniappExitGesture.js.map +1 -1
  45. package/build/services/MiniappWidgetMode.d.ts +6 -4
  46. package/build/services/MiniappWidgetMode.d.ts.map +1 -1
  47. package/build/services/MiniappWidgetMode.js +7 -6
  48. package/build/services/MiniappWidgetMode.js.map +1 -1
  49. package/build/services/SceneRenderer.d.ts.map +1 -1
  50. package/build/services/SceneRenderer.js +3 -1
  51. package/build/services/SceneRenderer.js.map +1 -1
  52. package/build/services/actionInvoke.d.ts +37 -0
  53. package/build/services/actionInvoke.d.ts.map +1 -0
  54. package/build/services/actionInvoke.js +89 -0
  55. package/build/services/actionInvoke.js.map +1 -0
  56. package/build/services/appleWatch/AppleWatchLink.d.ts +42 -0
  57. package/build/services/appleWatch/AppleWatchLink.d.ts.map +1 -0
  58. package/build/services/appleWatch/AppleWatchLink.js +259 -0
  59. package/build/services/appleWatch/AppleWatchLink.js.map +1 -0
  60. package/build/services/controllers.d.ts.map +1 -1
  61. package/build/services/controllers.js +16 -0
  62. package/build/services/controllers.js.map +1 -1
  63. package/build/services/settingsBridge.d.ts +4 -4
  64. package/build/services/settingsBridge.d.ts.map +1 -1
  65. package/build/services/settingsBridge.js +9 -4
  66. package/build/services/settingsBridge.js.map +1 -1
  67. package/build/stores/apps.d.ts.map +1 -1
  68. package/build/stores/apps.js +13 -11
  69. package/build/stores/apps.js.map +1 -1
  70. package/build/stores/bluetoothSettingKeys.d.ts +2 -0
  71. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  72. package/build/stores/bluetoothSettingKeys.js +17 -0
  73. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  74. package/build/stores/settings.d.ts.map +1 -1
  75. package/build/stores/settings.js +32 -2
  76. package/build/stores/settings.js.map +1 -1
  77. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  78. package/build/types/capabilities/rayneo-io.js +2 -0
  79. package/build/types/capabilities/rayneo-io.js.map +1 -1
  80. package/build/types/enums.d.ts +2 -1
  81. package/build/types/enums.d.ts.map +1 -1
  82. package/build/types/enums.js +1 -0
  83. package/build/types/enums.js.map +1 -1
  84. package/build/utils/display/scene/index.d.ts +1 -1
  85. package/build/utils/display/scene/index.d.ts.map +1 -1
  86. package/build/utils/display/scene/index.js.map +1 -1
  87. package/build/utils/display/scene/process.d.ts.map +1 -1
  88. package/build/utils/display/scene/process.js +20 -1
  89. package/build/utils/display/scene/process.js.map +1 -1
  90. package/build/utils/display/scene/types.d.ts +1 -1
  91. package/build/utils/display/scene/types.d.ts.map +1 -1
  92. package/build/utils/display/scene/types.js.map +1 -1
  93. package/package.json +7 -7
  94. package/src/engine.ts +5 -1
  95. package/src/facades/ai.ts +3 -3
  96. package/src/facades/glasses.ts +7 -0
  97. package/src/generated/releaseMetadata.ts +3 -3
  98. package/src/services/GestureDotsService.ts +2 -1
  99. package/src/services/GlassesTouchInput.ts +81 -34
  100. package/src/services/LocalDisplayManager.ts +22 -8
  101. package/src/services/LocalLlmModelManager.ts +16 -6
  102. package/src/services/LocalLlmPrompt.ts +56 -37
  103. package/src/services/LocalLlmProvider.ts +49 -22
  104. package/src/services/LocalMiniappRuntime.ts +140 -134
  105. package/src/services/MiniappExitGesture.ts +8 -5
  106. package/src/services/MiniappWidgetMode.ts +9 -7
  107. package/src/services/SceneRenderer.ts +3 -1
  108. package/src/services/actionInvoke.ts +105 -0
  109. package/src/services/appleWatch/AppleWatchLink.ts +297 -0
  110. package/src/services/controllers.ts +17 -0
  111. package/src/services/settingsBridge.ts +9 -4
  112. package/src/stores/apps.ts +12 -10
  113. package/src/stores/bluetoothSettingKeys.ts +20 -0
  114. package/src/stores/settings.ts +34 -2
  115. package/src/types/capabilities/rayneo-io.ts +2 -0
  116. package/src/types/enums.ts +1 -0
  117. package/src/utils/display/scene/index.ts +1 -0
  118. package/src/utils/display/scene/process.ts +17 -1
  119. package/src/utils/display/scene/types.ts +1 -0
@@ -24,8 +24,10 @@ export interface LocalLlmModelConfig {
24
24
  id: LocalLlmModelId
25
25
  /** Human label for the Settings dropdown. */
26
26
  label: string
27
- /** Approximate download size in bytes (the .pte dominates). */
27
+ /** Total download size in bytes (the .pte dominates). */
28
28
  sizeBytes: number
29
+ /** Exact size of the published .pte; the availability check compares against it. */
30
+ modelBytes: number
29
31
  /** Rough resident memory while loaded; the Settings copy warns above ~half of device RAM. */
30
32
  ramBytes: number
31
33
  urls: {
@@ -54,8 +56,10 @@ export interface LocalLlmDownloadStatus {
54
56
 
55
57
  // XNNPACK 8da4w exports from software-mansion/react-native-executorch-qwen-3,
56
58
  // pinned to the v0.10.0 release tag that react-native-executorch 0.10.x
57
- // expects. Sizes are the .pte file sizes on the Hub (tokenizer adds ~11 MB).
59
+ // expects. modelBytes are the .pte sizes on the Hub; sizeBytes adds the
60
+ // tokenizer files (~11 MB).
58
61
  const QWEN3_BASE = "https://huggingface.co/software-mansion/react-native-executorch-qwen-3/resolve/v0.10.0"
62
+ const TOKENIZER_BYTES = 11_431_080
59
63
  const MB = 1024 * 1024
60
64
  const GB = 1024 * MB
61
65
 
@@ -63,7 +67,8 @@ const MODELS: Record<LocalLlmModelId, LocalLlmModelConfig> = {
63
67
  "qwen3-0.6b": {
64
68
  id: "qwen3-0.6b",
65
69
  label: "Qwen3 0.6B",
66
- sizeBytes: 517 * MB,
70
+ sizeBytes: 505_686_400 + TOKENIZER_BYTES,
71
+ modelBytes: 505_686_400,
67
72
  ramBytes: 0.9 * GB,
68
73
  urls: {
69
74
  model: `${QWEN3_BASE}/0_6b/xnnpack/qwen_3_0_6b_xnnpack_8da4w.pte`,
@@ -74,7 +79,8 @@ const MODELS: Record<LocalLlmModelId, LocalLlmModelConfig> = {
74
79
  "qwen3-1.7b": {
75
80
  id: "qwen3-1.7b",
76
81
  label: "Qwen3 1.7B",
77
- sizeBytes: 1315 * MB,
82
+ sizeBytes: 1_303_738_496 + TOKENIZER_BYTES,
83
+ modelBytes: 1_303_738_496,
78
84
  ramBytes: 1.8 * GB,
79
85
  urls: {
80
86
  model: `${QWEN3_BASE}/1_7b/xnnpack/qwen_3_1_7b_xnnpack_8da4w.pte`,
@@ -85,7 +91,8 @@ const MODELS: Record<LocalLlmModelId, LocalLlmModelConfig> = {
85
91
  "qwen3-4b": {
86
92
  id: "qwen3-4b",
87
93
  label: "Qwen3 4B",
88
- sizeBytes: 2693 * MB,
94
+ sizeBytes: 2_681_646_976 + TOKENIZER_BYTES,
95
+ modelBytes: 2_681_646_976,
89
96
  ramBytes: 3.4 * GB,
90
97
  urls: {
91
98
  model: `${QWEN3_BASE}/4b/xnnpack/qwen_3_4b_xnnpack_8da4w.pte`,
@@ -161,7 +168,7 @@ class LocalLlmModelManager {
161
168
  // most of the published size. Downloads land in a .part file and are
162
169
  // renamed only once whole, so this only catches external tampering.
163
170
  const stat = await RNFS.stat(paths.model)
164
- return Number(stat.size) >= MODELS[id].sizeBytes * 0.95
171
+ return Number(stat.size) >= MODELS[id].modelBytes * 0.95
165
172
  } catch (error) {
166
173
  console.warn("LOCAL_LLM: availability check failed:", error)
167
174
  return false
@@ -243,6 +250,9 @@ class LocalLlmModelManager {
243
250
  await this.fetchFile(config.urls.model, paths.model, (percent) =>
244
251
  this.setStatus({modelId: id, stage: "downloading", percent}),
245
252
  )
253
+ if (!(await this.isModelAvailable(id))) {
254
+ throw new Error(`downloaded model is smaller than the expected ${config.modelBytes} bytes`)
255
+ }
246
256
  this.setStatus({modelId: id, stage: "complete", percent: 100})
247
257
  for (const listener of this.modelReadyListeners) {
248
258
  try {
@@ -85,55 +85,74 @@ export function toTemplateTools(tools?: LlmTool[]): TemplateTool[] | undefined {
85
85
  }))
86
86
  }
87
87
 
88
- /** Rough tokens-per-character for a BPE vocabulary over mixed prose/JSON. */
89
- const CHARS_PER_TOKEN = 3.2
90
- /** Template framing per message (`<|im_start|>role … <|im_end|>`). */
91
- const TOKENS_PER_MESSAGE = 6
92
-
93
- export function estimateTokens(text: string): number {
94
- return Math.ceil(text.length / CHARS_PER_TOKEN)
95
- }
96
-
97
- export function estimateMessageTokens(message: TemplateMessage): number {
98
- let total = TOKENS_PER_MESSAGE + estimateTokens(message.content)
99
- if (message.role === "assistant" && message.tool_calls) {
100
- for (const call of message.tool_calls) total += estimateTokens(JSON.stringify(call.function)) + 8
101
- }
102
- return total
88
+ export interface FittedLocalPrompt {
89
+ prompt: string
90
+ promptTokens: number
91
+ truncated: boolean
103
92
  }
104
93
 
105
- const MIN_TOOL_CONTENT_CHARS = 1_200
106
-
107
94
  /**
108
- * Fit the conversation into `budgetTokens`. History is grouped into turns
109
- * (a user message plus everything up to the next user message, so a tool
110
- * round stays with the question it answers); the oldest turns go first and
111
- * the last turn is never dropped — the model must at least see the question.
112
- * When that last turn alone still does not fit, tool outputs are cut to
113
- * their head.
95
+ * Measure the complete rendered prompt with the model's tokenizer, including
96
+ * template framing, system instructions and tool schemas. Drop old turns first,
97
+ * then shorten tool results / message text in the final turn. Never cut template
98
+ * delimiters, system instructions, or structured tool calls to make them fit.
114
99
  */
115
- export function fitToBudget(messages: TemplateMessage[], budgetTokens: number, toolTokens = 0): TemplateMessage[] {
116
- const total = (list: TemplateMessage[]) => list.reduce((sum, m) => sum + estimateMessageTokens(m), toolTokens)
100
+ export async function fitPromptToContext(
101
+ messages: TemplateMessage[],
102
+ budgetTokens: number,
103
+ render: (messages: TemplateMessage[]) => string,
104
+ countTokens: (prompt: string) => Promise<number>,
105
+ ): Promise<FittedLocalPrompt> {
106
+ if (!Number.isInteger(budgetTokens) || budgetTokens < 1) {
107
+ throw new Error("On-device model has no room for a prompt after reserving output tokens")
108
+ }
117
109
  const system = messages[0]?.role === "system" ? [messages[0]] : []
118
110
  const turns: TemplateMessage[][] = []
119
111
  for (const message of messages.slice(system.length)) {
120
112
  if (message.role === "user" || turns.length === 0) turns.push([message])
121
113
  else turns[turns.length - 1]!.push(message)
122
114
  }
123
-
124
- while (turns.length > 1 && total([...system, ...turns.flat()]) > budgetTokens) {
125
- turns.shift()
126
- }
127
-
128
115
  let fitted = [...system, ...turns.flat()]
129
- if (total(fitted) > budgetTokens) {
130
- fitted = fitted.map((m) =>
131
- m.role === "tool" && m.content.length > MIN_TOOL_CONTENT_CHARS
132
- ? {...m, content: `${m.content.slice(0, MIN_TOOL_CONTENT_CHARS)}\n…[truncated]`}
133
- : m,
134
- )
116
+ let truncated = false
117
+ const marker = "\n…[truncated]…\n"
118
+ while (true) {
119
+ const prompt = render(fitted)
120
+ const promptTokens = await countTokens(prompt)
121
+ if (!Number.isInteger(promptTokens) || promptTokens < 1) {
122
+ throw new Error("On-device tokenizer returned an invalid prompt token count")
123
+ }
124
+ if (promptTokens <= budgetTokens) return {prompt, promptTokens, truncated}
125
+ truncated = true
126
+ if (turns.length > 1) {
127
+ turns.shift()
128
+ fitted = [...system, ...turns.flat()]
129
+ continue
130
+ }
131
+ // Preserve the user's question while tool output can still be shortened.
132
+ const candidates = fitted
133
+ .map((message, index) => ({message, index}))
134
+ .filter(({message}) => message.role !== "system" && Array.from(message.content).length > marker.length * 2)
135
+ .sort(
136
+ (a, b) =>
137
+ Number(b.message.role === "tool") - Number(a.message.role === "tool") ||
138
+ b.message.content.length - a.message.content.length,
139
+ )
140
+ const candidate = candidates[0]
141
+ if (!candidate) {
142
+ throw new Error(
143
+ `On-device prompt instructions and tool definitions exceed the context budget (${promptTokens} > ${budgetTokens} tokens)`,
144
+ )
145
+ }
146
+ // Keep both ends of long transcripts and questions, without splitting a
147
+ // Unicode code point. Each pass shrinks strictly, then remeasures the whole
148
+ // rendered prompt; character ratios are never used as a safety check.
149
+ const chars = Array.from(candidate.message.content)
150
+ const keep = Math.floor(chars.length / 4)
151
+ fitted[candidate.index] = {
152
+ ...candidate.message,
153
+ content: `${chars.slice(0, keep).join("")}${marker}${chars.slice(-keep).join("")}`,
154
+ }
135
155
  }
136
- return fitted
137
156
  }
138
157
 
139
158
  /** Remove a leading `<think>…</think>` block (or an unterminated one). */
@@ -22,9 +22,8 @@ import {BgTimer} from "../utils/timers"
22
22
  import localLlmModelManager, {type LocalLlmModelId} from "./LocalLlmModelManager"
23
23
  import {
24
24
  buildTemplateMessages,
25
- estimateTokens,
26
25
  finalizeLocalResult,
27
- fitToBudget,
26
+ fitPromptToContext,
28
27
  toTemplateTools,
29
28
  type LocalCompletion,
30
29
  } from "./LocalLlmPrompt"
@@ -33,6 +32,7 @@ import {SETTINGS, useSettingsStore} from "../stores/settings"
33
32
  type Rne = typeof import("react-native-executorch")
34
33
  // The runner API lives on the `llm` namespace; derive its types from there so
35
34
  // the import stays a type-only reference (no native module at load time).
35
+ type Tokenizer = ReturnType<Rne["nlp"]["loadTokenizer"]>
36
36
  type LLMRunner = ReturnType<Rne["llm"]["createLLMRunner"]>
37
37
  type LLMGenerationStats = ReturnType<LLMRunner["generate"]>
38
38
  type LLMGenerationConfig = NonNullable<Parameters<LLMRunner["generate"]>[1]>
@@ -45,12 +45,14 @@ const IDLE_UNLOAD_MS = 5 * 60_000
45
45
  const GENERATION_DEADLINE_MS = 100_000
46
46
  const DEFAULT_MAX_NEW_TOKENS = 512
47
47
  const MAX_NEW_TOKENS_CEILING = 1_024
48
- /** Headroom between the estimated prompt and the context end; estimates are rough. */
48
+ /** Reserve space for runner-added BOS/EOS tokens and context boundary bookkeeping. */
49
49
  const CONTEXT_MARGIN_TOKENS = 96
50
50
 
51
51
  interface LoadedModel {
52
52
  modelId: LocalLlmModelId
53
53
  runner: LLMRunner
54
+ tokenizer: Tokenizer
55
+ countTokens: (tokenizer: Tokenizer, prompt: string) => Promise<number>
54
56
  template: Template
55
57
  stopTokens: string[]
56
58
  maxSeqLen: number
@@ -85,6 +87,11 @@ function generateWorklet(
85
87
  return {text, stats}
86
88
  }
87
89
 
90
+ function countTokensWorklet(tokenizer: Tokenizer, prompt: string): number {
91
+ "worklet"
92
+ return tokenizer.encode(prompt).length
93
+ }
94
+
88
95
  class LocalLlmProvider {
89
96
  private static instance: LocalLlmProvider
90
97
 
@@ -150,6 +157,8 @@ class LocalLlmProvider {
150
157
  loaded.runner.dispose()
151
158
  } catch (error) {
152
159
  console.warn(`${LOG_TAG}: runner dispose failed:`, error)
160
+ } finally {
161
+ loaded.tokenizer.dispose()
153
162
  }
154
163
  }
155
164
 
@@ -159,21 +168,21 @@ class LocalLlmProvider {
159
168
  const model = await this.ensureLoaded()
160
169
  const jsonMode = request.jsonMode === true
161
170
  const tools = toTemplateTools(request.tools)
162
- const toolTokens = tools ? estimateTokens(JSON.stringify(tools)) + 64 : 0
163
171
 
164
172
  const maxNewTokens = Math.max(
165
173
  16,
166
174
  Math.min(request.maxTokens ?? DEFAULT_MAX_NEW_TOKENS, MAX_NEW_TOKENS_CEILING, Math.floor(model.maxSeqLen / 4)),
167
175
  )
168
176
  const promptBudget = model.maxSeqLen - maxNewTokens - CONTEXT_MARGIN_TOKENS
169
- const messages = fitToBudget(buildTemplateMessages({...request, jsonMode}), promptBudget, toolTokens)
170
-
171
- const prompt = model.template.render({
172
- messages,
173
- tools,
174
- add_generation_prompt: true,
175
- enable_thinking: false,
176
- })
177
+ const {prompt, promptTokens, truncated} = await fitPromptToContext(
178
+ buildTemplateMessages({...request, jsonMode}),
179
+ promptBudget,
180
+ (messages) => model.template.render({messages, tools, add_generation_prompt: true, enable_thinking: false}),
181
+ (text) => model.countTokens(model.tokenizer, text),
182
+ )
183
+ console.log(
184
+ `${LOG_TAG}: ${model.modelId} prompt=${promptTokens} budget=${promptBudget} context=${model.maxSeqLen} maxNewTokens=${maxNewTokens} truncated=${truncated}`,
185
+ )
177
186
 
178
187
  model.runner.reset(0)
179
188
  const deadline = BgTimer.setTimeout(() => {
@@ -189,6 +198,12 @@ class LocalLlmProvider {
189
198
  {maxNewTokens, temperature: request.temperature ?? 0.7},
190
199
  model.stopTokens,
191
200
  )
201
+ } catch (error) {
202
+ console.warn(
203
+ `${LOG_TAG}: generation failed: model=${model.modelId} prompt=${promptTokens} context=${model.maxSeqLen} maxNewTokens=${maxNewTokens}`,
204
+ error,
205
+ )
206
+ throw error
192
207
  } finally {
193
208
  BgTimer.clearTimeout(deadline)
194
209
  }
@@ -246,16 +261,28 @@ class LocalLlmProvider {
246
261
  const startedAt = Date.now()
247
262
  const createRunner = rne.wrapAsync(rne.llm.createLLMRunner)
248
263
  const runner = await createRunner(paths.model, paths.tokenizer, [])
249
- const maxSeqLen = runner.getKVCacheState().maxSeqLen
250
- console.log(`${LOG_TAG}: loaded ${modelId} (context ${maxSeqLen}) in ${Date.now() - startedAt} ms`)
251
-
252
- return {
253
- modelId,
254
- runner,
255
- template,
256
- stopTokens: [...tokenizerConfig.stopTokens],
257
- maxSeqLen,
258
- generate: rne.wrapAsync(generateWorklet),
264
+ let tokenizer: Tokenizer | undefined
265
+ try {
266
+ const maxSeqLen = runner.getKVCacheState().maxSeqLen
267
+ if (!Number.isInteger(maxSeqLen) || maxSeqLen <= CONTEXT_MARGIN_TOKENS + 16) {
268
+ throw new Error(`On-device model ${modelId} reported an invalid context size: ${maxSeqLen}`)
269
+ }
270
+ tokenizer = await rne.wrapAsync(rne.nlp.loadTokenizer)(paths.tokenizer)
271
+ console.log(`${LOG_TAG}: loaded ${modelId} (context ${maxSeqLen}) in ${Date.now() - startedAt} ms`)
272
+ return {
273
+ modelId,
274
+ runner,
275
+ tokenizer,
276
+ countTokens: rne.wrapAsync(countTokensWorklet),
277
+ template,
278
+ stopTokens: [...tokenizerConfig.stopTokens],
279
+ maxSeqLen,
280
+ generate: rne.wrapAsync(generateWorklet),
281
+ }
282
+ } catch (error) {
283
+ tokenizer?.dispose()
284
+ runner.dispose()
285
+ throw error
259
286
  }
260
287
  }
261
288