@oh-my-pi/pi-coding-agent 15.5.15 → 15.7.0

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 (274) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/types/capability/rule-buckets.d.ts +30 -0
  3. package/dist/types/capability/rule.d.ts +7 -0
  4. package/dist/types/cli/classify-install-target.d.ts +0 -10
  5. package/dist/types/cli/completion-gen.d.ts +80 -0
  6. package/dist/types/cli/initial-message.d.ts +1 -1
  7. package/dist/types/cli/tiny-models-cli.d.ts +9 -0
  8. package/dist/types/commands/complete.d.ts +6 -0
  9. package/dist/types/commands/completions.d.ts +13 -0
  10. package/dist/types/commands/setup.d.ts +10 -1
  11. package/dist/types/commands/tiny-models.d.ts +22 -0
  12. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  13. package/dist/types/commit/analysis/summary.d.ts +1 -1
  14. package/dist/types/commit/changelog/generate.d.ts +1 -1
  15. package/dist/types/commit/changelog/index.d.ts +2 -2
  16. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  17. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  18. package/dist/types/config/model-id-affixes.d.ts +10 -0
  19. package/dist/types/config/settings-schema.d.ts +402 -17
  20. package/dist/types/discovery/builtin-defaults.d.ts +1 -0
  21. package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
  22. package/dist/types/discovery/helpers.d.ts +1 -1
  23. package/dist/types/discovery/index.d.ts +1 -0
  24. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  25. package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
  26. package/dist/types/edit/hashline/index.d.ts +1 -0
  27. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  28. package/dist/types/eval/py/kernel.d.ts +3 -0
  29. package/dist/types/eval/py/runtime.d.ts +11 -1
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  32. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  33. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  34. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  35. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  36. package/dist/types/internal-urls/router.d.ts +8 -1
  37. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  38. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  39. package/dist/types/internal-urls/types.d.ts +26 -0
  40. package/dist/types/main.d.ts +1 -0
  41. package/dist/types/memory-backend/index.d.ts +1 -0
  42. package/dist/types/memory-backend/resolve.d.ts +2 -1
  43. package/dist/types/memory-backend/types.d.ts +7 -1
  44. package/dist/types/mnemosyne/backend.d.ts +4 -0
  45. package/dist/types/mnemosyne/config.d.ts +29 -0
  46. package/dist/types/mnemosyne/index.d.ts +3 -0
  47. package/dist/types/mnemosyne/state.d.ts +72 -0
  48. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  49. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  50. package/dist/types/modes/components/index.d.ts +2 -0
  51. package/dist/types/modes/components/segment-track.d.ts +22 -0
  52. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  53. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  54. package/dist/types/modes/components/welcome.d.ts +22 -0
  55. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  56. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  57. package/dist/types/modes/interactive-mode.d.ts +7 -4
  58. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  59. package/dist/types/modes/orchestrate.d.ts +10 -0
  60. package/dist/types/modes/setup-wizard/index.d.ts +16 -0
  61. package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
  62. package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
  63. package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
  64. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
  65. package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
  66. package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
  67. package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
  68. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
  69. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
  70. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  71. package/dist/types/modes/theme/shimmer.d.ts +2 -0
  72. package/dist/types/modes/theme/theme.d.ts +11 -0
  73. package/dist/types/modes/types.d.ts +5 -1
  74. package/dist/types/modes/ultrathink.d.ts +3 -3
  75. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  76. package/dist/types/sdk.d.ts +3 -0
  77. package/dist/types/session/agent-session.d.ts +33 -0
  78. package/dist/types/system-prompt.d.ts +2 -0
  79. package/dist/types/task/executor.d.ts +2 -0
  80. package/dist/types/task/render.d.ts +5 -1
  81. package/dist/types/tiny/device.d.ts +78 -0
  82. package/dist/types/tiny/dtype.d.ts +85 -0
  83. package/dist/types/tiny/models.d.ts +185 -0
  84. package/dist/types/tiny/text.d.ts +19 -0
  85. package/dist/types/tiny/title-client.d.ts +32 -0
  86. package/dist/types/tiny/title-protocol.d.ts +74 -0
  87. package/dist/types/tiny/worker.d.ts +2 -0
  88. package/dist/types/tools/bash.d.ts +3 -2
  89. package/dist/types/tools/eval.d.ts +1 -1
  90. package/dist/types/tools/index.d.ts +7 -4
  91. package/dist/types/tools/memory-edit.d.ts +40 -0
  92. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  93. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  94. package/dist/types/tools/memory-render.d.ts +60 -0
  95. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  96. package/dist/types/tools/todo-write.d.ts +8 -0
  97. package/dist/types/tools/tool-result.d.ts +2 -0
  98. package/dist/types/tui/code-cell.d.ts +2 -0
  99. package/dist/types/tui/output-block.d.ts +17 -0
  100. package/dist/types/utils/title-generator.d.ts +3 -0
  101. package/package.json +18 -14
  102. package/scripts/build-binary.ts +1 -0
  103. package/src/capability/rule-buckets.ts +64 -0
  104. package/src/capability/rule.ts +8 -0
  105. package/src/cli/completion-gen.ts +550 -0
  106. package/src/cli/setup-cli.ts +5 -3
  107. package/src/cli/tiny-models-cli.ts +127 -0
  108. package/src/cli-commands.ts +3 -0
  109. package/src/cli.ts +9 -15
  110. package/src/commands/complete.ts +66 -0
  111. package/src/commands/completions.ts +60 -0
  112. package/src/commands/setup.ts +29 -4
  113. package/src/commands/tiny-models.ts +36 -0
  114. package/src/config/model-equivalence.ts +43 -2
  115. package/src/config/model-id-affixes.ts +64 -0
  116. package/src/config/model-registry.ts +84 -10
  117. package/src/config/settings-schema.ts +275 -15
  118. package/src/discovery/builtin-defaults.ts +39 -0
  119. package/src/discovery/builtin-rules/index.ts +48 -0
  120. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  121. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  122. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  123. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  124. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  125. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  126. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  127. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  128. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  129. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  130. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  131. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
  132. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  133. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  134. package/src/discovery/index.ts +1 -0
  135. package/src/edit/hashline/block-resolver.ts +14 -0
  136. package/src/edit/hashline/diff.ts +9 -8
  137. package/src/edit/hashline/execute.ts +2 -1
  138. package/src/edit/hashline/index.ts +1 -0
  139. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  140. package/src/eval/js/shared/local-module-loader.ts +13 -1
  141. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  142. package/src/eval/py/kernel.ts +37 -15
  143. package/src/eval/py/runtime.ts +57 -28
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/export/html/template.js +0 -12
  146. package/src/export/ttsr.ts +2 -0
  147. package/src/internal-urls/agent-protocol.ts +18 -1
  148. package/src/internal-urls/artifact-protocol.ts +19 -1
  149. package/src/internal-urls/docs-index.generated.ts +8 -7
  150. package/src/internal-urls/local-protocol.ts +14 -1
  151. package/src/internal-urls/memory-protocol.ts +6 -1
  152. package/src/internal-urls/omp-protocol.ts +5 -1
  153. package/src/internal-urls/router.ts +20 -1
  154. package/src/internal-urls/rule-protocol.ts +8 -1
  155. package/src/internal-urls/skill-protocol.ts +8 -1
  156. package/src/internal-urls/types.ts +27 -0
  157. package/src/lsp/render.ts +1 -1
  158. package/src/main.ts +18 -1
  159. package/src/mcp/oauth-flow.ts +2 -2
  160. package/src/memory-backend/index.ts +1 -0
  161. package/src/memory-backend/resolve.ts +4 -1
  162. package/src/memory-backend/types.ts +8 -1
  163. package/src/mnemosyne/backend.ts +374 -0
  164. package/src/mnemosyne/config.ts +160 -0
  165. package/src/mnemosyne/index.ts +3 -0
  166. package/src/mnemosyne/state.ts +548 -0
  167. package/src/modes/acp/acp-agent.ts +11 -6
  168. package/src/modes/components/agent-dashboard.ts +4 -4
  169. package/src/modes/components/custom-editor.ts +3 -2
  170. package/src/modes/components/diff.ts +2 -2
  171. package/src/modes/components/extensions/extension-list.ts +3 -2
  172. package/src/modes/components/footer.ts +5 -6
  173. package/src/modes/components/history-search.ts +3 -3
  174. package/src/modes/components/hook-selector.ts +92 -8
  175. package/src/modes/components/index.ts +2 -0
  176. package/src/modes/components/mcp-add-wizard.ts +3 -3
  177. package/src/modes/components/model-selector.ts +5 -4
  178. package/src/modes/components/oauth-selector.ts +3 -3
  179. package/src/modes/components/segment-track.ts +52 -0
  180. package/src/modes/components/session-observer-overlay.ts +19 -13
  181. package/src/modes/components/session-selector.ts +3 -3
  182. package/src/modes/components/settings-defs.ts +7 -0
  183. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  184. package/src/modes/components/status-line/segments.ts +2 -2
  185. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  186. package/src/modes/components/tips.txt +13 -0
  187. package/src/modes/components/tool-execution.ts +72 -4
  188. package/src/modes/components/tree-selector.ts +3 -3
  189. package/src/modes/components/user-message-selector.ts +3 -3
  190. package/src/modes/components/welcome.ts +102 -43
  191. package/src/modes/controllers/command-controller.ts +16 -1
  192. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  193. package/src/modes/controllers/input-controller.ts +69 -21
  194. package/src/modes/gradient-highlight.ts +70 -0
  195. package/src/modes/interactive-mode.ts +75 -114
  196. package/src/modes/internal-url-autocomplete.ts +143 -0
  197. package/src/modes/orchestrate.ts +36 -0
  198. package/src/modes/prompt-action-autocomplete.ts +12 -0
  199. package/src/modes/setup-wizard/index.ts +88 -0
  200. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  201. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  202. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  203. package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
  204. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  205. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  206. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  207. package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
  208. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  209. package/src/modes/theme/shimmer.ts +5 -0
  210. package/src/modes/theme/theme.ts +44 -20
  211. package/src/modes/types.ts +6 -1
  212. package/src/modes/ultrathink.ts +9 -53
  213. package/src/modes/utils/keybinding-matchers.ts +11 -0
  214. package/src/prompts/system/memory-consolidation-system.md +8 -0
  215. package/src/prompts/system/memory-extraction-system.md +26 -0
  216. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +6 -17
  217. package/src/prompts/system/system-prompt.md +2 -0
  218. package/src/prompts/system/tiny-title-system.md +8 -0
  219. package/src/prompts/tools/memory-edit.md +8 -0
  220. package/src/prompts/tools/read.md +4 -0
  221. package/src/prompts/tools/task.md +4 -7
  222. package/src/sdk.ts +13 -21
  223. package/src/session/agent-session.ts +128 -44
  224. package/src/slash-commands/builtin-registry.ts +18 -1
  225. package/src/system-prompt.ts +4 -0
  226. package/src/task/commands.ts +1 -5
  227. package/src/task/executor.ts +8 -0
  228. package/src/task/index.ts +2 -0
  229. package/src/task/render.ts +69 -26
  230. package/src/tiny/device.ts +117 -0
  231. package/src/tiny/dtype.ts +101 -0
  232. package/src/tiny/models.ts +218 -0
  233. package/src/tiny/text.ts +54 -0
  234. package/src/tiny/title-client.ts +395 -0
  235. package/src/tiny/title-protocol.ts +51 -0
  236. package/src/tiny/worker.ts +587 -0
  237. package/src/tools/bash.ts +74 -29
  238. package/src/tools/browser/tab-worker.ts +1 -1
  239. package/src/tools/eval.ts +9 -4
  240. package/src/tools/index.ts +17 -22
  241. package/src/tools/memory-edit.ts +59 -0
  242. package/src/tools/memory-recall.ts +100 -0
  243. package/src/tools/memory-reflect.ts +88 -0
  244. package/src/tools/memory-render.ts +185 -0
  245. package/src/tools/memory-retain.ts +91 -0
  246. package/src/tools/read.ts +1 -0
  247. package/src/tools/renderers.ts +4 -2
  248. package/src/tools/todo-write.ts +128 -29
  249. package/src/tools/tool-result.ts +8 -0
  250. package/src/tui/code-cell.ts +6 -1
  251. package/src/tui/output-block.ts +199 -38
  252. package/src/utils/title-generator.ts +115 -13
  253. package/dist/types/tools/recipe/index.d.ts +0 -46
  254. package/dist/types/tools/recipe/render.d.ts +0 -36
  255. package/dist/types/tools/recipe/runner.d.ts +0 -60
  256. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  257. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  258. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  259. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  260. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  261. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  262. package/src/prompts/tools/recipe.md +0 -16
  263. package/src/tools/hindsight-recall.ts +0 -69
  264. package/src/tools/hindsight-reflect.ts +0 -58
  265. package/src/tools/hindsight-retain.ts +0 -57
  266. package/src/tools/recipe/index.ts +0 -81
  267. package/src/tools/recipe/render.ts +0 -19
  268. package/src/tools/recipe/runner.ts +0 -219
  269. package/src/tools/recipe/runners/cargo.ts +0 -131
  270. package/src/tools/recipe/runners/index.ts +0 -8
  271. package/src/tools/recipe/runners/just.ts +0 -73
  272. package/src/tools/recipe/runners/make.ts +0 -101
  273. package/src/tools/recipe/runners/pkg.ts +0 -167
  274. package/src/tools/recipe/runners/task.ts +0 -72
@@ -0,0 +1,395 @@
1
+ import { $env, isCompiledBinary, logger } from "@oh-my-pi/pi-utils";
2
+ import { settings } from "../config/settings";
3
+ import { tinyModelDeviceSettingToEnv } from "./device";
4
+ import { tinyModelDtypeSettingToEnv } from "./dtype";
5
+ import {
6
+ isTinyLocalModelKey,
7
+ isTinyMemoryLocalModelKey,
8
+ isTinyTitleLocalModelKey,
9
+ type TinyLocalModelKey,
10
+ type TinyMemoryLocalModelKey,
11
+ type TinyTitleLocalModelKey,
12
+ } from "./models";
13
+ import type { TinyTitleProgressEvent, TinyTitleWorkerInbound, TinyTitleWorkerOutbound } from "./title-protocol";
14
+
15
+ interface WorkerHandle {
16
+ send(message: TinyTitleWorkerInbound): void;
17
+ onMessage(handler: (message: TinyTitleWorkerOutbound) => void): () => void;
18
+ onError(handler: (error: Error) => void): () => void;
19
+ terminate(): Promise<void>;
20
+ }
21
+
22
+ type PendingRequest =
23
+ | { kind: "generate"; modelKey: TinyTitleLocalModelKey; resolve: (title: string | null) => void }
24
+ | { kind: "complete"; modelKey: TinyMemoryLocalModelKey; resolve: (text: string | null) => void }
25
+ | { kind: "download"; modelKey: TinyLocalModelKey; resolve: (ok: boolean) => void };
26
+
27
+ export interface TinyTitleDownloadOptions {
28
+ signal?: AbortSignal;
29
+ onProgress?: (event: TinyTitleProgressEvent) => void;
30
+ }
31
+
32
+ const SMOKE_TEST_TIMEOUT_MS = 5_000;
33
+
34
+ function readTinyModelSetting(path: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
35
+ try {
36
+ const value = settings.get(path);
37
+ return typeof value === "string" ? value : undefined;
38
+ } catch {
39
+ // Settings may be uninitialized (e.g. `omp --smoke-test`); fall back to env/default.
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Decide which `PI_TINY_DEVICE` / `PI_TINY_DTYPE` vars to overlay onto the worker
46
+ * env. A present env var wins (left untouched); otherwise the mapped persisted
47
+ * setting is used. Returns only the keys to add — never the default sentinel.
48
+ * Pure for testability; see {@link tinyWorkerEnv} for the spawn-time glue.
49
+ * @internal
50
+ */
51
+ export function tinyWorkerEnvOverlay(
52
+ env: Record<string, string | undefined>,
53
+ deviceSetting: string | undefined,
54
+ dtypeSetting: string | undefined,
55
+ ): Record<string, string> {
56
+ const overlay: Record<string, string> = {};
57
+ if (!env.PI_TINY_DEVICE) {
58
+ const device = tinyModelDeviceSettingToEnv(deviceSetting);
59
+ if (device) overlay.PI_TINY_DEVICE = device;
60
+ }
61
+ if (!env.PI_TINY_DTYPE) {
62
+ const dtype = tinyModelDtypeSettingToEnv(dtypeSetting);
63
+ if (dtype) overlay.PI_TINY_DTYPE = dtype;
64
+ }
65
+ return overlay;
66
+ }
67
+
68
+ /**
69
+ * Env handed to the tiny-model worker. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE` env
70
+ * vars win; otherwise the persisted `providers.tinyModelDevice` /
71
+ * `providers.tinyModelDtype` settings are mapped onto those vars so the worker's
72
+ * env-based resolution picks them up. Resolved once at spawn (pipelines are cached).
73
+ */
74
+ function tinyWorkerEnv(): Record<string, string> | undefined {
75
+ const overlay = tinyWorkerEnvOverlay(
76
+ $env,
77
+ readTinyModelSetting("providers.tinyModelDevice"),
78
+ readTinyModelSetting("providers.tinyModelDtype"),
79
+ );
80
+ if (Object.keys(overlay).length === 0) return undefined;
81
+ return { ...($env as Record<string, string>), ...overlay };
82
+ }
83
+
84
+ export function createTinyTitleWorker(): Worker {
85
+ const env = tinyWorkerEnv();
86
+ const options: WorkerOptions = env ? { type: "module", env } : { type: "module" };
87
+ return isCompiledBinary()
88
+ ? new Worker("./packages/coding-agent/src/tiny/worker.ts", options)
89
+ : new Worker(new URL("./worker.ts", import.meta.url).href, options);
90
+ }
91
+
92
+ function wrapBunWorker(worker: Worker): WorkerHandle {
93
+ (worker as Worker & { unref?: () => void }).unref?.();
94
+ return {
95
+ send(message) {
96
+ worker.postMessage(message);
97
+ },
98
+ onMessage(handler) {
99
+ const wrap = (event: MessageEvent): void => handler(event.data as TinyTitleWorkerOutbound);
100
+ worker.addEventListener("message", wrap);
101
+ return () => worker.removeEventListener("message", wrap);
102
+ },
103
+ onError(handler) {
104
+ const wrap = (event: ErrorEvent): void => {
105
+ handler(event.error instanceof Error ? event.error : new Error(event.message || "tiny title worker error"));
106
+ };
107
+ worker.addEventListener("error", wrap);
108
+ return () => worker.removeEventListener("error", wrap);
109
+ },
110
+ async terminate() {
111
+ worker.terminate();
112
+ },
113
+ };
114
+ }
115
+
116
+ function spawnInlineUnavailableWorker(error: unknown): WorkerHandle {
117
+ const listeners = new Set<(message: TinyTitleWorkerOutbound) => void>();
118
+ const errorMessage = error instanceof Error ? error.message : String(error);
119
+ const emit = (message: TinyTitleWorkerOutbound): void => {
120
+ for (const listener of listeners) listener(message);
121
+ };
122
+ return {
123
+ send(message) {
124
+ queueMicrotask(() => {
125
+ if (message.type === "ping") {
126
+ emit({ type: "pong", id: message.id });
127
+ return;
128
+ }
129
+ if (message.type === "close") {
130
+ emit({ type: "closed" });
131
+ return;
132
+ }
133
+ emit({ type: "error", id: message.id, error: errorMessage });
134
+ });
135
+ },
136
+ onMessage(handler) {
137
+ listeners.add(handler);
138
+ return () => listeners.delete(handler);
139
+ },
140
+ onError() {
141
+ return () => {};
142
+ },
143
+ async terminate() {
144
+ listeners.clear();
145
+ },
146
+ };
147
+ }
148
+
149
+ function spawnTinyTitleWorker(): WorkerHandle {
150
+ try {
151
+ return wrapBunWorker(createTinyTitleWorker());
152
+ } catch (error) {
153
+ logger.warn("Tiny title Worker spawn failed; local titles disabled", {
154
+ error: error instanceof Error ? error.message : String(error),
155
+ });
156
+ return spawnInlineUnavailableWorker(error);
157
+ }
158
+ }
159
+
160
+ function logWorkerMessage(message: Extract<TinyTitleWorkerOutbound, { type: "log" }>): void {
161
+ if (message.level === "debug") logger.debug(message.msg, message.meta);
162
+ else if (message.level === "warn") logger.warn(message.msg, message.meta);
163
+ else logger.error(message.msg, message.meta);
164
+ }
165
+
166
+ export class TinyTitleClient {
167
+ #worker: WorkerHandle | null = null;
168
+ #unsubscribeMessage: (() => void) | null = null;
169
+ #unsubscribeError: (() => void) | null = null;
170
+ #pending = new Map<string, PendingRequest>();
171
+ #progressListeners = new Set<(event: TinyTitleProgressEvent) => void>();
172
+ #nextRequestId = 0;
173
+
174
+ onProgress(listener: (event: TinyTitleProgressEvent) => void): () => void {
175
+ this.#progressListeners.add(listener);
176
+ return () => this.#progressListeners.delete(listener);
177
+ }
178
+
179
+ async generate(modelKey: string, message: string, signal?: AbortSignal): Promise<string | null> {
180
+ if (!isTinyTitleLocalModelKey(modelKey)) return null;
181
+ if (signal?.aborted) return null;
182
+
183
+ try {
184
+ const worker = this.#ensureWorker();
185
+ const id = String(++this.#nextRequestId);
186
+ const { promise, resolve } = Promise.withResolvers<string | null>();
187
+ this.#pending.set(id, { kind: "generate", modelKey, resolve });
188
+ const abort = (): void => {
189
+ const pending = this.#pending.get(id);
190
+ if (pending?.kind !== "generate") return;
191
+ this.#pending.delete(id);
192
+ pending.resolve(null);
193
+ };
194
+ signal?.addEventListener("abort", abort, { once: true });
195
+ try {
196
+ worker.send({ type: "generate", id, modelKey, message });
197
+ return await promise;
198
+ } finally {
199
+ signal?.removeEventListener("abort", abort);
200
+ this.#pending.delete(id);
201
+ }
202
+ } catch (error) {
203
+ logger.debug("tiny-title: local generation failed", {
204
+ modelKey,
205
+ error: error instanceof Error ? error.message : String(error),
206
+ });
207
+ return null;
208
+ }
209
+ }
210
+
211
+ async complete(
212
+ modelKey: string,
213
+ prompt: string,
214
+ options: { maxTokens?: number; signal?: AbortSignal } = {},
215
+ ): Promise<string | null> {
216
+ if (!isTinyMemoryLocalModelKey(modelKey)) return null;
217
+ if (options.signal?.aborted) return null;
218
+
219
+ try {
220
+ const worker = this.#ensureWorker();
221
+ const id = String(++this.#nextRequestId);
222
+ const { promise, resolve } = Promise.withResolvers<string | null>();
223
+ this.#pending.set(id, { kind: "complete", modelKey, resolve });
224
+ const abort = (): void => {
225
+ const pending = this.#pending.get(id);
226
+ if (pending?.kind !== "complete") return;
227
+ this.#pending.delete(id);
228
+ pending.resolve(null);
229
+ };
230
+ options.signal?.addEventListener("abort", abort, { once: true });
231
+ try {
232
+ worker.send({ type: "complete", id, modelKey, prompt, maxTokens: options.maxTokens });
233
+ return await promise;
234
+ } finally {
235
+ options.signal?.removeEventListener("abort", abort);
236
+ this.#pending.delete(id);
237
+ }
238
+ } catch (error) {
239
+ logger.debug("tiny-model: local completion failed", {
240
+ modelKey,
241
+ error: error instanceof Error ? error.message : String(error),
242
+ });
243
+ return null;
244
+ }
245
+ }
246
+
247
+ async downloadModel(modelKey: string, options: TinyTitleDownloadOptions = {}): Promise<boolean> {
248
+ if (!isTinyLocalModelKey(modelKey)) return false;
249
+ if (options.signal?.aborted) return false;
250
+
251
+ const unsubscribe = options.onProgress ? this.onProgress(options.onProgress) : undefined;
252
+ try {
253
+ const worker = this.#ensureWorker();
254
+ const id = String(++this.#nextRequestId);
255
+ const { promise, resolve } = Promise.withResolvers<boolean>();
256
+ this.#pending.set(id, { kind: "download", modelKey, resolve });
257
+ const abort = (): void => {
258
+ const pending = this.#pending.get(id);
259
+ if (pending?.kind !== "download") return;
260
+ this.#pending.delete(id);
261
+ pending.resolve(false);
262
+ };
263
+ options.signal?.addEventListener("abort", abort, { once: true });
264
+ try {
265
+ worker.send({ type: "download", id, modelKey });
266
+ return await promise;
267
+ } finally {
268
+ options.signal?.removeEventListener("abort", abort);
269
+ this.#pending.delete(id);
270
+ }
271
+ } catch (error) {
272
+ logger.debug("tiny-title: local model download failed", {
273
+ modelKey,
274
+ error: error instanceof Error ? error.message : String(error),
275
+ });
276
+ return false;
277
+ } finally {
278
+ unsubscribe?.();
279
+ }
280
+ }
281
+
282
+ async terminate(): Promise<void> {
283
+ const worker = this.#worker;
284
+ this.#worker = null;
285
+ this.#unsubscribeMessage?.();
286
+ this.#unsubscribeMessage = null;
287
+ this.#unsubscribeError?.();
288
+ this.#unsubscribeError = null;
289
+ for (const pending of this.#pending.values()) {
290
+ this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
291
+ if (pending.kind === "generate" || pending.kind === "complete") pending.resolve(null);
292
+ else pending.resolve(false);
293
+ }
294
+ this.#pending.clear();
295
+ try {
296
+ worker?.send({ type: "close" });
297
+ } catch {
298
+ // Worker may already be gone.
299
+ }
300
+ }
301
+
302
+ #ensureWorker(): WorkerHandle {
303
+ if (this.#worker) return this.#worker;
304
+ const worker = spawnTinyTitleWorker();
305
+ this.#worker = worker;
306
+ this.#unsubscribeMessage = worker.onMessage(message => this.#handleMessage(message));
307
+ this.#unsubscribeError = worker.onError(error => this.#handleWorkerError(error));
308
+ return worker;
309
+ }
310
+
311
+ #handleMessage(message: TinyTitleWorkerOutbound): void {
312
+ if (message.type === "log") {
313
+ logWorkerMessage(message);
314
+ return;
315
+ }
316
+ if (message.type === "progress") {
317
+ this.#emitProgress(message.event);
318
+ return;
319
+ }
320
+ if (message.type === "closed") return;
321
+ if (message.type === "pong") return;
322
+
323
+ const pending = this.#pending.get(message.id);
324
+ if (!pending) return;
325
+ this.#pending.delete(message.id);
326
+ if (message.type === "title") {
327
+ if (pending.kind === "generate") pending.resolve(message.title);
328
+ return;
329
+ }
330
+ if (message.type === "downloaded") {
331
+ if (pending.kind === "download") pending.resolve(true);
332
+ return;
333
+ }
334
+ if (message.type === "completion") {
335
+ if (pending.kind === "complete") pending.resolve(message.text);
336
+ return;
337
+ }
338
+ logger.debug("tiny-title: worker returned error", { error: message.error });
339
+ this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
340
+ if (pending.kind === "generate" || pending.kind === "complete") pending.resolve(null);
341
+ else pending.resolve(false);
342
+ }
343
+
344
+ #emitProgress(event: TinyTitleProgressEvent): void {
345
+ for (const listener of this.#progressListeners) listener(event);
346
+ }
347
+
348
+ #handleWorkerError(error: Error): void {
349
+ logger.warn("tiny-title: worker error", { error: error.message });
350
+ for (const pending of this.#pending.values()) {
351
+ this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
352
+ if (pending.kind === "generate" || pending.kind === "complete") pending.resolve(null);
353
+ else pending.resolve(false);
354
+ }
355
+ this.#pending.clear();
356
+ void this.terminate();
357
+ }
358
+ }
359
+
360
+ export const tinyTitleClient = new TinyTitleClient();
361
+
362
+ /** Alias for the shared tiny-model worker client (titles + memory completions). */
363
+ export const tinyModelClient = tinyTitleClient;
364
+
365
+ export async function shutdownTinyTitleClient(): Promise<void> {
366
+ await tinyTitleClient.terminate();
367
+ }
368
+
369
+ export async function smokeTestTinyTitleWorker({
370
+ timeoutMs = SMOKE_TEST_TIMEOUT_MS,
371
+ }: {
372
+ timeoutMs?: number;
373
+ } = {}): Promise<void> {
374
+ const worker = createTinyTitleWorker();
375
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
376
+ const timer = setTimeout(() => reject(new Error(`tiny title worker did not pong within ${timeoutMs}ms`)), timeoutMs);
377
+ worker.onmessage = (event: MessageEvent<TinyTitleWorkerOutbound>) => {
378
+ const message = event.data;
379
+ if (message.type === "pong") {
380
+ resolve();
381
+ return;
382
+ }
383
+ reject(new Error(`tiny title worker: expected pong, got ${JSON.stringify(message)}`));
384
+ };
385
+ worker.onerror = (event: ErrorEvent) => {
386
+ reject(event.error instanceof Error ? event.error : new Error(event.message || "tiny title worker error"));
387
+ };
388
+ try {
389
+ worker.postMessage({ type: "ping", id: "smoke" } satisfies TinyTitleWorkerInbound);
390
+ await promise;
391
+ } finally {
392
+ clearTimeout(timer);
393
+ worker.terminate();
394
+ }
395
+ }
@@ -0,0 +1,51 @@
1
+ import type { TinyLocalModelKey, TinyTitleLocalModelKey } from "./models";
2
+
3
+ export type TinyTitleProgressStatus =
4
+ | "initiate"
5
+ | "download"
6
+ | "progress"
7
+ | "progress_total"
8
+ | "done"
9
+ | "ready"
10
+ | "error";
11
+
12
+ export interface TinyTitleProgressFileState {
13
+ loaded: number;
14
+ total: number;
15
+ }
16
+
17
+ export interface TinyTitleProgressEvent {
18
+ modelKey: TinyLocalModelKey;
19
+ status: TinyTitleProgressStatus;
20
+ name?: string;
21
+ file?: string;
22
+ progress?: number;
23
+ loaded?: number;
24
+ total?: number;
25
+ files?: Record<string, TinyTitleProgressFileState>;
26
+ task?: string;
27
+ model?: string;
28
+ }
29
+
30
+ export type TinyTitleWorkerInbound =
31
+ | { type: "ping"; id: string }
32
+ | { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string }
33
+ | { type: "complete"; id: string; modelKey: TinyLocalModelKey; prompt: string; maxTokens?: number }
34
+ | { type: "download"; id: string; modelKey: TinyLocalModelKey }
35
+ | { type: "close" };
36
+
37
+ export type TinyTitleWorkerOutbound =
38
+ | { type: "pong"; id: string }
39
+ | { type: "title"; id: string; title: string | null }
40
+ | { type: "completion"; id: string; text: string | null }
41
+ | { type: "downloaded"; id: string }
42
+ | { type: "error"; id: string; error: string }
43
+ | { type: "progress"; id: string; event: TinyTitleProgressEvent }
44
+ | { type: "log"; level: "debug" | "warn" | "error"; msg: string; meta?: Record<string, unknown> }
45
+ | { type: "closed" };
46
+
47
+ export interface TinyTitleTransport {
48
+ send(message: TinyTitleWorkerOutbound): void;
49
+ onMessage(handler: (message: TinyTitleWorkerInbound) => void): () => void;
50
+ close(): void;
51
+ }