pim-agent 0.0.1 → 0.9.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -7
  3. package/bin/pim.ts +151 -0
  4. package/package.json +82 -4
  5. package/packages/core/src/attachments/AttachmentStore.ts +99 -0
  6. package/packages/core/src/attachments/Attachments.ts +69 -0
  7. package/packages/core/src/extensions/CoreExtensions.ts +51 -0
  8. package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
  9. package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
  10. package/packages/core/src/extensions/apply-patch/index.ts +69 -0
  11. package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
  12. package/packages/core/src/extensions/apply-patch/model.ts +39 -0
  13. package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
  14. package/packages/core/src/extensions/apply-patch/render.ts +216 -0
  15. package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
  16. package/packages/core/src/extensions/apply-patch/types.ts +30 -0
  17. package/packages/core/src/extensions/bash/capture.ts +86 -0
  18. package/packages/core/src/extensions/bash/format.ts +110 -0
  19. package/packages/core/src/extensions/bash/image.ts +31 -0
  20. package/packages/core/src/extensions/bash/index.ts +88 -0
  21. package/packages/core/src/extensions/bash/render.ts +31 -0
  22. package/packages/core/src/extensions/bash/run.ts +221 -0
  23. package/packages/core/src/extensions/bash/schema.ts +64 -0
  24. package/packages/core/src/extensions/edit/edit.ts +366 -0
  25. package/packages/core/src/extensions/edit/index.ts +36 -0
  26. package/packages/core/src/extensions/edit/render.ts +16 -0
  27. package/packages/core/src/extensions/edit/schema.ts +37 -0
  28. package/packages/core/src/extensions/glob/glob.ts +34 -0
  29. package/packages/core/src/extensions/glob/index.ts +83 -0
  30. package/packages/core/src/extensions/glob/render.ts +72 -0
  31. package/packages/core/src/extensions/glob/schema.ts +66 -0
  32. package/packages/core/src/extensions/grep/grep.ts +256 -0
  33. package/packages/core/src/extensions/grep/index.ts +114 -0
  34. package/packages/core/src/extensions/grep/render.ts +271 -0
  35. package/packages/core/src/extensions/grep/schema.ts +92 -0
  36. package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
  37. package/packages/core/src/extensions/read/image.ts +57 -0
  38. package/packages/core/src/extensions/read/index.ts +88 -0
  39. package/packages/core/src/extensions/read/read.ts +191 -0
  40. package/packages/core/src/extensions/read/render.ts +58 -0
  41. package/packages/core/src/extensions/read/schema.ts +49 -0
  42. package/packages/core/src/extensions/subagent/index.ts +33 -0
  43. package/packages/core/src/extensions/subagent/render.ts +127 -0
  44. package/packages/core/src/extensions/subagent/schema.ts +9 -0
  45. package/packages/core/src/extensions/subagent/subagent.ts +419 -0
  46. package/packages/core/src/extensions/system-prompt/index.ts +28 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
  48. package/packages/core/src/extensions/todo/index.ts +123 -0
  49. package/packages/core/src/extensions/todo/render.ts +156 -0
  50. package/packages/core/src/extensions/todo/schema.ts +24 -0
  51. package/packages/core/src/extensions/todo/todo.ts +185 -0
  52. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
  53. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
  54. package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
  55. package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
  56. package/packages/core/src/extensions/web-fetch/image.ts +165 -0
  57. package/packages/core/src/extensions/web-fetch/index.ts +83 -0
  58. package/packages/core/src/extensions/web-fetch/render.ts +100 -0
  59. package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
  60. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
  61. package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
  62. package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
  63. package/packages/core/src/extensions/web-search/index.ts +90 -0
  64. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
  65. package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
  66. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
  67. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
  68. package/packages/core/src/extensions/web-search/render.ts +49 -0
  69. package/packages/core/src/extensions/web-search/schema.ts +29 -0
  70. package/packages/core/src/extensions/web-search/search.ts +23 -0
  71. package/packages/core/src/extensions/write/index.ts +58 -0
  72. package/packages/core/src/extensions/write/render.ts +16 -0
  73. package/packages/core/src/extensions/write/schema.ts +14 -0
  74. package/packages/core/src/extensions/write/write.ts +87 -0
  75. package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
  76. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
  77. package/packages/core/src/picker/PickerItem.ts +6 -0
  78. package/packages/core/src/picker/PickerService.ts +110 -0
  79. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
  80. package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
  81. package/packages/core/src/picker/catalog.ts +220 -0
  82. package/packages/core/src/picker/commandRanker.ts +17 -0
  83. package/packages/core/src/picker/filePickerWorker.ts +66 -0
  84. package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
  85. package/packages/core/src/picker/ranker.ts +245 -0
  86. package/packages/core/src/picker/token.ts +70 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +213 -0
  89. package/packages/core/src/session/MessageText.ts +21 -0
  90. package/packages/core/src/session/ReadCursors.ts +94 -0
  91. package/packages/core/src/session/SessionCache.ts +68 -0
  92. package/packages/core/src/session/SessionHost.ts +876 -0
  93. package/packages/core/src/session/SessionLease.ts +279 -0
  94. package/packages/core/src/session/SessionRegistry.ts +169 -0
  95. package/packages/core/src/session/WriteMark.ts +37 -0
  96. package/packages/core/src/shared/Cli.ts +25 -0
  97. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  98. package/packages/core/src/shared/DiffLines.ts +304 -0
  99. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  100. package/packages/core/src/shared/DiffView.ts +29 -0
  101. package/packages/core/src/shared/Directories.ts +61 -0
  102. package/packages/core/src/shared/EditMatcher.ts +767 -0
  103. package/packages/core/src/shared/Errors.ts +9 -0
  104. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  105. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  106. package/packages/core/src/shared/FileScanner.ts +57 -0
  107. package/packages/core/src/shared/FileWatch.ts +107 -0
  108. package/packages/core/src/shared/Format.ts +74 -0
  109. package/packages/core/src/shared/Fs.ts +91 -0
  110. package/packages/core/src/shared/FsErrors.ts +67 -0
  111. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  112. package/packages/core/src/shared/Git.ts +89 -0
  113. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  114. package/packages/core/src/shared/Http.ts +14 -0
  115. package/packages/core/src/shared/ImageMime.ts +36 -0
  116. package/packages/core/src/shared/Images.ts +357 -0
  117. package/packages/core/src/shared/Json.ts +19 -0
  118. package/packages/core/src/shared/Languages.ts +95 -0
  119. package/packages/core/src/shared/Levenshtein.ts +30 -0
  120. package/packages/core/src/shared/Lines.ts +79 -0
  121. package/packages/core/src/shared/McpClient.ts +444 -0
  122. package/packages/core/src/shared/OutputBudget.ts +84 -0
  123. package/packages/core/src/shared/PatchSummary.ts +40 -0
  124. package/packages/core/src/shared/Paths.ts +80 -0
  125. package/packages/core/src/shared/PimSettings.ts +128 -0
  126. package/packages/core/src/shared/PimVersion.ts +93 -0
  127. package/packages/core/src/shared/Pool.ts +26 -0
  128. package/packages/core/src/shared/Proc.ts +30 -0
  129. package/packages/core/src/shared/RateLimiter.ts +50 -0
  130. package/packages/core/src/shared/Renderer.ts +371 -0
  131. package/packages/core/src/shared/SafePath.ts +17 -0
  132. package/packages/core/src/shared/SearchRender.ts +82 -0
  133. package/packages/core/src/shared/SendFile.ts +23 -0
  134. package/packages/core/src/shared/SpillCache.ts +78 -0
  135. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  136. package/packages/core/src/shared/Supervisor.ts +460 -0
  137. package/packages/core/src/shared/Sweeper.ts +32 -0
  138. package/packages/core/src/shared/Tools.ts +801 -0
  139. package/packages/core/src/shared/Updater.ts +193 -0
  140. package/packages/core/src/view/AnsiPainter.ts +227 -0
  141. package/packages/core/src/view/BodyRenderer.ts +102 -0
  142. package/packages/core/src/view/DiffBlocks.ts +52 -0
  143. package/packages/core/src/view/DiffLayout.ts +69 -0
  144. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  145. package/packages/core/src/view/Painting.ts +182 -0
  146. package/packages/core/src/view/ViewBlock.ts +124 -0
  147. package/packages/daemon/src/Daemon.ts +63 -0
  148. package/packages/daemon/src/DaemonInstall.ts +115 -0
  149. package/packages/daemon/src/Surfaces.ts +66 -0
  150. package/packages/daemon/src/TelegramSurface.ts +30 -0
  151. package/packages/daemon/src/WebSurface.ts +57 -0
  152. package/packages/daemon/src/index.ts +37 -0
  153. package/packages/protocol/src/Command.ts +92 -0
  154. package/packages/protocol/src/ImageRoute.ts +23 -0
  155. package/packages/protocol/src/Protocol.ts +6 -0
  156. package/packages/protocol/src/ServerEvent.ts +251 -0
  157. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  158. package/packages/server/src/ClientConnection.ts +265 -0
  159. package/packages/server/src/ImageEndpoint.ts +36 -0
  160. package/packages/server/src/ProbeClient.ts +312 -0
  161. package/packages/server/src/Reloader.ts +93 -0
  162. package/packages/server/src/SendFileTool.ts +88 -0
  163. package/packages/server/src/SessionCatalogue.ts +202 -0
  164. package/packages/server/src/SessionProjection.ts +194 -0
  165. package/packages/server/src/SessionStream.ts +601 -0
  166. package/packages/server/src/StaticClient.ts +95 -0
  167. package/packages/server/src/WebOptions.ts +72 -0
  168. package/packages/server/src/WsGateway.ts +563 -0
  169. package/packages/server/src/probe.ts +146 -0
  170. package/packages/telegram/src/Bot.ts +229 -0
  171. package/packages/telegram/src/Commands.ts +768 -0
  172. package/packages/telegram/src/Config.ts +148 -0
  173. package/packages/telegram/src/Markdown.ts +235 -0
  174. package/packages/telegram/src/Message.ts +152 -0
  175. package/packages/telegram/src/Renderer.ts +456 -0
  176. package/packages/telegram/src/SendFileSchema.ts +18 -0
  177. package/packages/telegram/src/SendFileTool.ts +65 -0
  178. package/packages/telegram/src/Session.ts +252 -0
  179. package/packages/telegram/src/SessionRegistry.ts +126 -0
  180. package/packages/telegram/src/TaskScheduler.ts +296 -0
  181. package/packages/telegram/src/TaskSchema.ts +88 -0
  182. package/packages/telegram/src/TaskStore.ts +67 -0
  183. package/packages/telegram/src/TaskTool.ts +208 -0
  184. package/packages/telegram/src/TelegramHtml.ts +288 -0
  185. package/packages/telegram/src/TypingIndicator.ts +43 -0
  186. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  187. package/packages/tui/src/extensions/_init/index.ts +144 -0
  188. package/packages/tui/src/extensions/command-picker/index.ts +43 -0
  189. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  190. package/packages/tui/src/extensions/footer/index.ts +117 -0
  191. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  192. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  193. package/packages/tui/src/extensions/pim/index.ts +89 -0
  194. package/packages/tui/src/extensions/session-lease/index.ts +248 -0
  195. package/packages/tui/src/extensions/tps/index.ts +118 -0
  196. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  197. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  198. package/packages/tui/src/themes/pim-dark.json +84 -0
  199. package/packages/tui/src/themes/pim-light.json +84 -0
  200. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  201. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  202. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  203. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  204. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  205. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  206. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  207. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  208. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  209. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  210. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  211. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  212. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  213. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  214. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  215. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  216. package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
  217. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  218. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  219. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  220. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  221. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  222. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  223. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  224. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  225. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  226. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  227. package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
  228. package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
  229. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  230. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  231. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  232. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  233. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  234. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  235. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  236. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  237. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  238. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  239. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  240. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  241. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  242. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  243. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  244. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  245. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  246. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  247. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  248. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  249. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  250. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  251. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  252. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  253. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  254. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  255. package/packages/web/dist/client/favicon.svg +1 -0
  256. package/packages/web/dist/client/icon-192.png +0 -0
  257. package/packages/web/dist/client/icon-512.png +0 -0
  258. package/packages/web/dist/client/icon-maskable.png +0 -0
  259. package/packages/web/dist/client/index.html +24 -0
  260. package/packages/web/dist/client/manifest.webmanifest +21 -0
  261. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,876 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type {
3
+ Api as ModelApi,
4
+ AssistantMessageEvent,
5
+ Model,
6
+ } from "@earendil-works/pi-ai";
7
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
8
+ import {
9
+ AgentSession,
10
+ createAgentSession,
11
+ DefaultResourceLoader,
12
+ ModelRegistry,
13
+ ModelRuntime,
14
+ SessionManager,
15
+ SettingsManager,
16
+ type AgentSessionEvent,
17
+ type CompactionResult,
18
+ type ToolDefinition,
19
+ } from "@earendil-works/pi-coding-agent";
20
+ import { unlink } from "node:fs/promises";
21
+
22
+ import { CoreExtensions } from "../extensions/CoreExtensions";
23
+ import { Directories } from "../shared/Directories";
24
+ import { FuzzyMatcher, type FuzzyCandidate } from "../shared/FuzzyMatcher";
25
+ import { EventLog } from "./EventLog";
26
+ import {
27
+ SessionLease,
28
+ type LeaseFrontend,
29
+ type LeaseRecord,
30
+ } from "./SessionLease";
31
+ import { WriteMark } from "./WriteMark";
32
+
33
+ /** What the agent is doing right now. */
34
+ export type SessionStatus = "idle" | "thinking" | "streaming" | "tool";
35
+
36
+ /** Everything the host persists about a session; frontend-specific settings stay with the adapter. */
37
+ export type HostSettings = {
38
+ readonly cwd?: string;
39
+ readonly model?: string;
40
+ readonly thinkingLevel?: ThinkingLevel;
41
+ readonly sessionPath?: string;
42
+ readonly cumulativeCost?: number;
43
+ };
44
+
45
+ export type SetCwdResult =
46
+ | { readonly ok: true }
47
+ | { readonly ok: false; readonly error: string };
48
+
49
+ export type SetModelResult =
50
+ | { readonly ok: true; readonly id: string }
51
+ | {
52
+ readonly ok: false;
53
+ readonly kind: "none" | "ambiguous";
54
+ readonly candidates: readonly string[];
55
+ };
56
+
57
+ export type SessionCompactResult = {
58
+ readonly compaction: CompactionResult;
59
+ readonly activeMessages: number;
60
+ };
61
+
62
+ /** What a `cancel` did; `restored` messages are the caller's from here on. */
63
+ export type CancelResult = {
64
+ readonly cancelled: boolean;
65
+ readonly restored: readonly string[];
66
+ };
67
+
68
+ /** Whether this host can mutate its session file right now, and who is stopping it. */
69
+ export type LeaseState = {
70
+ readonly writable: boolean;
71
+ readonly heldBy?: {
72
+ readonly frontend: LeaseFrontend;
73
+ readonly pid: number;
74
+ };
75
+ };
76
+
77
+ export type SessionHostDeps = {
78
+ /** Prefix for this host's log lines; also the registry key in practice. */
79
+ readonly label: string;
80
+ readonly settings: HostSettings;
81
+ readonly defaults: { readonly cwd: string; readonly model?: string };
82
+ readonly agentDir: string;
83
+ readonly modelRuntime: ModelRuntime;
84
+ readonly modelRegistry: ModelRegistry;
85
+ readonly settingsManagerFor: (cwd: string) => SettingsManager;
86
+ readonly persistSettings: (patch: Partial<HostSettings>) => Promise<void>;
87
+ /**
88
+ * Take the session file's turn lease around every mutation, as this frontend.
89
+ * Leave unset where nothing else can open the file.
90
+ */
91
+ readonly lease?: LeaseFrontend;
92
+ /** Omit to let pi place the file in its own cwd-grouped sessions directory. */
93
+ readonly mainSessionPath?: () => string;
94
+ readonly isolatedSessionPath?: () => string;
95
+ readonly systemInstruction?: () => Promise<string | undefined>;
96
+ readonly customTools?: (
97
+ context: CustomToolContext
98
+ ) => readonly ToolDefinition[];
99
+ /** Runs after the agent is disposed, before the session file is forgotten. */
100
+ readonly onRetire?: (sessionPath: string) => Promise<void>;
101
+ };
102
+
103
+ /** What a frontend's own tools are built against. */
104
+ export type CustomToolContext = {
105
+ /** Where this session's tools resolve relative paths. */
106
+ readonly cwd: string;
107
+ /** Pi's session uuid, undefined until the agent exists. */
108
+ readonly sessionId: () => string | undefined;
109
+ };
110
+
111
+ type ModelResolveResult =
112
+ | { readonly kind: "ok"; readonly model: Model<ModelApi> }
113
+ | { readonly kind: "ambiguous"; readonly candidates: readonly string[] }
114
+ | { readonly kind: "none"; readonly candidates: readonly string[] };
115
+
116
+ /** A mutation of ours parked behind someone else's lease; the holder is unknown when their file is torn. */
117
+ type LeaseWait = { readonly holder?: LeaseRecord };
118
+
119
+ const FOREIGN_POLL_MS = 2_000;
120
+
121
+ function sameWait(a: LeaseWait | undefined, b: LeaseWait | undefined): boolean {
122
+ return a === undefined || b === undefined
123
+ ? a === b
124
+ : a.holder?.pid === b.holder?.pid &&
125
+ a.holder?.startedAt === b.holder?.startedAt;
126
+ }
127
+
128
+ function isOutput(event: AssistantMessageEvent): boolean {
129
+ switch (event.type) {
130
+ case "text_delta":
131
+ case "toolcall_delta":
132
+ return event.delta.length > 0;
133
+ case "text_end":
134
+ return event.content.length > 0;
135
+ default:
136
+ return false;
137
+ }
138
+ }
139
+
140
+ /** Owns one in-process `createAgentSession()`, its settings and a serialized turn queue. */
141
+ export class SessionHost {
142
+ public readonly label: string;
143
+ public lastUsed = Date.now();
144
+ private readonly deps: SessionHostDeps;
145
+ private currentSettings: HostSettings;
146
+ private cached: AgentSession | undefined;
147
+ private cachedUnsubscribe: (() => void) | undefined;
148
+ private cachedSystemInstruction: string | undefined;
149
+ private cachedLog: EventLog | undefined;
150
+ private queue: Promise<unknown> = Promise.resolve();
151
+ private seqLog: EventLog | undefined;
152
+ /** The `EventLog` head as of this host's own last write to that file. */
153
+ private lastSeen: { readonly path: string; readonly seq: number } | undefined;
154
+ private blockedBy: LeaseWait | undefined;
155
+ private readonly leaseListeners = new Set<() => void>();
156
+ private readonly foreignListeners = new Set<() => void>();
157
+ /** Set only while a held turn is watching the file; re-samples on demand. */
158
+ private sampleWrites: (() => Promise<void>) | undefined;
159
+ private readonly agentListeners = new Set<
160
+ (event: AgentSessionEvent) => void
161
+ >();
162
+ private runningTools = 0;
163
+ private streaming = false;
164
+ private producing = false;
165
+ private outputStartedAt: number | undefined;
166
+ private lastTps: number | undefined;
167
+
168
+ public constructor(deps: SessionHostDeps) {
169
+ this.deps = deps;
170
+ this.label = deps.label;
171
+ this.currentSettings = deps.settings;
172
+ }
173
+
174
+ public get settings(): HostSettings {
175
+ return this.currentSettings;
176
+ }
177
+
178
+ public get agentDir(): string {
179
+ return this.deps.agentDir;
180
+ }
181
+
182
+ public get cwd(): string {
183
+ return this.currentSettings.cwd ?? this.deps.defaults.cwd;
184
+ }
185
+
186
+ public get agentSession(): AgentSession | undefined {
187
+ return this.cached;
188
+ }
189
+
190
+ public get sessionId(): string | undefined {
191
+ return this.cached?.sessionId;
192
+ }
193
+
194
+ /** Reader over this session's JSONL, live only once the agent is built. */
195
+ public get eventLog(): EventLog | undefined {
196
+ return this.cachedLog;
197
+ }
198
+
199
+ /** False only while one of this host's own mutations waits on another process. */
200
+ public get leaseState(): LeaseState {
201
+ const blocked = this.blockedBy;
202
+ if (blocked === undefined) {
203
+ return { writable: true };
204
+ }
205
+ const holder = blocked.holder;
206
+ return holder === undefined
207
+ ? { writable: false }
208
+ : {
209
+ writable: false,
210
+ heldBy: { frontend: holder.frontend, pid: holder.pid },
211
+ };
212
+ }
213
+
214
+ /** Fires whenever `leaseState` changes. */
215
+ public onLeaseChange(listener: () => void): () => void {
216
+ this.leaseListeners.add(listener);
217
+ return () => {
218
+ this.leaseListeners.delete(listener);
219
+ };
220
+ }
221
+
222
+ /** Fires once per turn in which another process appended to this session's file. */
223
+ public onForeignWrite(listener: () => void): () => void {
224
+ this.foreignListeners.add(listener);
225
+ return () => {
226
+ this.foreignListeners.delete(listener);
227
+ };
228
+ }
229
+
230
+ /** Events from whatever agent this host currently holds, across rehydration rebuilds. */
231
+ public subscribe(listener: (event: AgentSessionEvent) => void): () => void {
232
+ this.agentListeners.add(listener);
233
+ return () => {
234
+ this.agentListeners.delete(listener);
235
+ };
236
+ }
237
+
238
+ public get isStreaming(): boolean {
239
+ return this.cached?.isStreaming ?? false;
240
+ }
241
+
242
+ public get status(): SessionStatus {
243
+ if (this.runningTools > 0) {
244
+ return "tool";
245
+ }
246
+ if (this.producing) {
247
+ return "streaming";
248
+ }
249
+ if (this.streaming) {
250
+ return "thinking";
251
+ }
252
+ return "idle";
253
+ }
254
+
255
+ /** Decode tokens per second for the last completed assistant message. */
256
+ public get tps(): number | undefined {
257
+ return this.lastTps;
258
+ }
259
+
260
+ public usage(): ReturnType<AgentSession["getContextUsage"]> | undefined {
261
+ return this.cached?.getContextUsage();
262
+ }
263
+
264
+ public sessionCost(): number | undefined {
265
+ const agent = this.cached;
266
+ return agent ? (agent.getSessionStats().cost ?? 0) : undefined;
267
+ }
268
+
269
+ public get currentModelId(): string | undefined {
270
+ const model = this.cached?.model ?? this.resolveDefaultModel();
271
+ return model ? qualifiedModelId(model) : undefined;
272
+ }
273
+
274
+ /** The model's display name — "Claude Opus 5.0", not `anthropic/claude-opus-5`. */
275
+ public get currentModelLabel(): string | undefined {
276
+ return (this.cached?.model ?? this.resolveDefaultModel())?.name;
277
+ }
278
+
279
+ public get supportedThinkingLevels(): readonly ThinkingLevel[] {
280
+ const model = this.cached?.model ?? this.resolveDefaultModel();
281
+ return model ? getSupportedThinkingLevels(model) : [];
282
+ }
283
+
284
+ public get currentThinkingLevel(): ThinkingLevel {
285
+ const chosen = this.chosenThinkingLevel;
286
+ if (chosen) {
287
+ return chosen;
288
+ }
289
+ const sm = this.deps.settingsManagerFor(this.cwd);
290
+ return (sm.getDefaultThinkingLevel() as ThinkingLevel) ?? "medium";
291
+ }
292
+
293
+ /** The level this session was told to think at, absent when only its directory has a default. */
294
+ public get chosenThinkingLevel(): ThinkingLevel | undefined {
295
+ return this.currentSettings.thinkingLevel ?? this.cached?.thinkingLevel;
296
+ }
297
+
298
+ /** Run `work` as a turn, serialized in submission order; `isolated` uses a throwaway agent and file. */
299
+ public run(
300
+ work: (agent: AgentSession) => Promise<void>,
301
+ opts?: { readonly isolated?: boolean }
302
+ ): Promise<void> {
303
+ // An isolated run owns a throwaway file nobody else can name, so it takes no lease.
304
+ if (opts?.isolated) {
305
+ return this.enqueue(async () => {
306
+ const { agent, sessionPath } = await this.buildIsolatedAgent();
307
+ try {
308
+ await work(agent);
309
+ } finally {
310
+ await disposeAgent(agent);
311
+ await unlink(sessionPath).catch((err: unknown) => {
312
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
313
+ console.warn(`[${this.label}] unlink ${sessionPath}:`, err);
314
+ }
315
+ });
316
+ }
317
+ });
318
+ }
319
+ return this.withLease(async () => {
320
+ const agent = await this.ensureCached();
321
+ await work(agent);
322
+ });
323
+ }
324
+
325
+ /** Build or reuse the cached agent inside the turn queue; its id and file exist once this resolves. */
326
+ public ensureAgent(): Promise<AgentSession> {
327
+ return this.enqueue(() => this.ensureCached());
328
+ }
329
+
330
+ /** Run `work` in this session's turn queue without touching the agent. */
331
+ public serialize<T>(work: () => Promise<T>): Promise<T> {
332
+ return this.enqueue(work);
333
+ }
334
+
335
+ /** Stop the turn in flight; empty the queue before aborting or pi delivers it to the next turn. */
336
+ public async cancel(): Promise<CancelResult> {
337
+ if (!this.cached || !this.cached.isStreaming) {
338
+ return { cancelled: false, restored: [] };
339
+ }
340
+ // Take the queue back *before* aborting: anything left in pi is delivered to the next turn.
341
+ const restored = this.takeBack();
342
+ await this.cached.abort();
343
+ return { cancelled: true, restored };
344
+ }
345
+
346
+ /** Take back every message queued behind the running turn; not serialized, the turn holds the queue. */
347
+ public takeBack(): readonly string[] {
348
+ if (!this.cached || !this.cached.isStreaming) {
349
+ return [];
350
+ }
351
+ const { steering, followUp } = this.cached.clearQueue();
352
+ return [...steering, ...followUp];
353
+ }
354
+
355
+ public clear(): Promise<void> {
356
+ return this.withLease(async () => {
357
+ await this.tearDownCached();
358
+ await this.patchSettings({ sessionPath: undefined });
359
+ });
360
+ }
361
+
362
+ public setCwd(newCwd: string): Promise<SetCwdResult> {
363
+ return this.enqueue(async (): Promise<SetCwdResult> => {
364
+ const reason = await Directories.check(newCwd);
365
+ if (reason !== undefined) {
366
+ return { ok: false, error: reason };
367
+ }
368
+ await this.tearDownCached();
369
+ await this.patchSettings({ cwd: newCwd, sessionPath: undefined });
370
+ return { ok: true };
371
+ });
372
+ }
373
+
374
+ public setModel(pattern: string): Promise<SetModelResult> {
375
+ return this.withLease(async (): Promise<SetModelResult> => {
376
+ const result = this.resolveModel(pattern);
377
+ if (result.kind === "none" || result.kind === "ambiguous") {
378
+ return { ok: false, kind: result.kind, candidates: result.candidates };
379
+ }
380
+ const id = qualifiedModelId(result.model);
381
+ if (this.currentSettings.model === id) {
382
+ return { ok: true, id };
383
+ }
384
+ await this.patchSettings({ model: id });
385
+ if (this.cached) {
386
+ await this.cached.setModel(result.model);
387
+ }
388
+ return { ok: true, id };
389
+ });
390
+ }
391
+
392
+ public setThinkingLevel(level: ThinkingLevel): Promise<void> {
393
+ return this.withLease(async () => {
394
+ if (this.currentSettings.thinkingLevel === level) {
395
+ return;
396
+ }
397
+ await this.patchSettings({ thinkingLevel: level });
398
+ this.cached?.setThinkingLevel(level);
399
+ });
400
+ }
401
+
402
+ public compact(customInstructions?: string): Promise<SessionCompactResult> {
403
+ return this.withLease(async (): Promise<SessionCompactResult> => {
404
+ const agent = await this.ensureCached();
405
+ const compaction = await agent.compact(customInstructions);
406
+ return { compaction, activeMessages: agent.messages.length };
407
+ });
408
+ }
409
+
410
+ /** Drop the cached agent so the next turn rebuilds it from the file; never retires the session. */
411
+ public async invalidate(): Promise<void> {
412
+ if (!this.cached) {
413
+ return;
414
+ }
415
+ const agent = this.cached;
416
+ this.detachCached();
417
+ this.cachedSystemInstruction = undefined;
418
+ await disposeAgent(agent);
419
+ }
420
+
421
+ public async dispose(): Promise<void> {
422
+ await this.invalidate();
423
+ }
424
+
425
+ private enqueue<T>(work: () => Promise<T>): Promise<T> {
426
+ const next = this.queue.then(work);
427
+ // Only the stored tail swallows: the chain must never reject, callers still see their own failure.
428
+ this.queue = next.catch((err: unknown) => {
429
+ console.error(`[${this.label}] work failed:`, err);
430
+ });
431
+ this.lastUsed = Date.now();
432
+ return next;
433
+ }
434
+
435
+ /** Where this session's turn lease lives, once anything has named the file. */
436
+ private get leasePath(): string | undefined {
437
+ return (
438
+ this.cached?.sessionFile ??
439
+ this.currentSettings.sessionPath ??
440
+ this.deps.mainSessionPath?.()
441
+ );
442
+ }
443
+
444
+ /**
445
+ * Serialize `work` behind the session file's turn lease, with a rehydration on
446
+ * either side of the acquire: waiting for the lease is exactly the window in
447
+ * which the other surface appends.
448
+ */
449
+ private withLease<T>(work: () => Promise<T>): Promise<T> {
450
+ return this.enqueue(async () => {
451
+ const frontend = this.deps.lease;
452
+ if (frontend === undefined) {
453
+ return await work();
454
+ }
455
+ await this.rehydrateIfStale();
456
+ // pi names a brand-new session's file itself, and a file nobody else knows cannot be contended.
457
+ const path = this.leasePath ?? (await this.ensureCached()).sessionFile;
458
+ if (path === undefined) {
459
+ return await work();
460
+ }
461
+ try {
462
+ return await SessionLease.hold(
463
+ path,
464
+ frontend,
465
+ async () => {
466
+ this.setBlocked(undefined);
467
+ await this.rehydrateIfStale();
468
+ const settle = await this.watchForeignWrites(path);
469
+ try {
470
+ return await work();
471
+ } finally {
472
+ // A line we cannot account for means the file has moved past our
473
+ // agent: leave `lastSeen` behind the head so the next turn rebuilds.
474
+ if (!(await settle())) {
475
+ await this.markSeen(path);
476
+ }
477
+ }
478
+ },
479
+ {
480
+ onBlocked: (holder) =>
481
+ this.setBlocked(holder === undefined ? {} : { holder }),
482
+ }
483
+ );
484
+ } finally {
485
+ this.setBlocked(undefined);
486
+ }
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Watch, for one held turn, for lines this host cannot account for: a `pi` that
492
+ * took no lease, or a holder killed mid-write. Reports at most once and never
493
+ * aborts — the work in flight is worth more than the inconsistency it races.
494
+ * Resolves to whether anything foreign landed.
495
+ */
496
+ private async watchForeignWrites(
497
+ path: string
498
+ ): Promise<() => Promise<boolean>> {
499
+ let base:
500
+ | { readonly agent: AgentSession; readonly mark: WriteMark }
501
+ | undefined;
502
+ let foreign = false;
503
+
504
+ const check = async (): Promise<void> => {
505
+ if (foreign) {
506
+ return;
507
+ }
508
+ const agent = this.cached;
509
+ // A rebuilt agent re-opened the file, which repairs a torn tail and rewrites
510
+ // on migration, so its own accounting starts from whatever it just read.
511
+ if (agent?.sessionFile !== path) {
512
+ base = undefined;
513
+ return;
514
+ }
515
+ const mark = await WriteMark.of(this.logFor(path), agent.sessionManager);
516
+ const previous = base;
517
+ base = { agent, mark };
518
+ if (
519
+ previous?.agent !== agent ||
520
+ !WriteMark.foreignSince(previous.mark, mark)
521
+ ) {
522
+ return;
523
+ }
524
+ foreign = true;
525
+ console.warn(
526
+ `[${this.label}] ${path} was written by another process during this turn; its history may be inconsistent`
527
+ );
528
+ for (const listener of this.foreignListeners) {
529
+ listener();
530
+ }
531
+ };
532
+
533
+ const sample = (): Promise<void> => check().catch(() => undefined);
534
+ this.sampleWrites = sample;
535
+ await sample();
536
+ const timer = setInterval(() => void sample(), FOREIGN_POLL_MS);
537
+ timer.unref?.();
538
+
539
+ return async (): Promise<boolean> => {
540
+ clearInterval(timer);
541
+ this.sampleWrites = undefined;
542
+ await sample();
543
+ return foreign;
544
+ };
545
+ }
546
+
547
+ private setBlocked(blocked: LeaseWait | undefined): void {
548
+ if (sameWait(this.blockedBy, blocked)) {
549
+ return;
550
+ }
551
+ this.blockedBy = blocked;
552
+ for (const listener of this.leaseListeners) {
553
+ listener();
554
+ }
555
+ }
556
+
557
+ /** Another writer moved the file past what this host's `messages` were loaded from. */
558
+ private async rehydrateIfStale(): Promise<void> {
559
+ const path = this.cached?.sessionFile;
560
+ const seen = this.lastSeen;
561
+ if (path === undefined || seen?.path !== path) {
562
+ return;
563
+ }
564
+ if ((await this.logFor(path).head()) > seen.seq) {
565
+ await this.invalidate();
566
+ }
567
+ }
568
+
569
+ // Reading the head is how our own writes stop looking foreign: pi repairs a torn tail and
570
+ // rewrites on migration, so a rebuild moves the head without anyone else touching the file.
571
+ private async markSeen(path: string): Promise<void> {
572
+ this.lastSeen = { path, seq: await this.logFor(path).head() };
573
+ }
574
+
575
+ private logFor(path: string): EventLog {
576
+ if (this.seqLog?.path !== path) {
577
+ this.seqLog = new EventLog(path);
578
+ }
579
+ return this.seqLog;
580
+ }
581
+
582
+ private async ensureCached(): Promise<AgentSession> {
583
+ const systemInstruction = await this.deps.systemInstruction?.();
584
+ if (this.cached) {
585
+ if (this.cachedSystemInstruction !== systemInstruction) {
586
+ this.cachedSystemInstruction = systemInstruction;
587
+ await this.cached.reload();
588
+ }
589
+ return this.cached;
590
+ }
591
+ const { agent, cwd } = await this.buildAgent(
592
+ this.currentSettings.sessionPath ?? this.deps.mainSessionPath?.(),
593
+ systemInstruction
594
+ );
595
+ this.cached = agent;
596
+ this.cachedSystemInstruction = systemInstruction;
597
+ this.cachedLog = agent.sessionFile
598
+ ? new EventLog(agent.sessionFile)
599
+ : undefined;
600
+ this.cachedUnsubscribe = this.observe(agent);
601
+ if (this.deps.lease !== undefined && agent.sessionFile) {
602
+ await this.markSeen(agent.sessionFile);
603
+ }
604
+ // Anchor a watching turn on the agent it is about to run, not on the file as
605
+ // it was before this rebuild read it.
606
+ await this.sampleWrites?.();
607
+ await this.patchSettings({ cwd, sessionPath: agent.sessionFile });
608
+ return agent;
609
+ }
610
+
611
+ private observe(agent: AgentSession): () => void {
612
+ const stopCostTracking = this.observeCost(agent);
613
+ const stop = agent.subscribe((event: AgentSessionEvent) => {
614
+ switch (event.type) {
615
+ case "agent_start":
616
+ this.streaming = true;
617
+ this.producing = false;
618
+ this.outputStartedAt = undefined;
619
+ break;
620
+ case "message_start":
621
+ this.producing = false;
622
+ this.outputStartedAt = undefined;
623
+ break;
624
+ case "message_update":
625
+ if (
626
+ event.message.role === "assistant" &&
627
+ isOutput(event.assistantMessageEvent)
628
+ ) {
629
+ this.producing = true;
630
+ this.outputStartedAt ??= Date.now();
631
+ }
632
+ break;
633
+ case "message_end":
634
+ if (event.message.role === "assistant") {
635
+ this.recordTps(event.message.usage?.output ?? 0);
636
+ }
637
+ this.producing = false;
638
+ break;
639
+ case "tool_execution_start":
640
+ this.runningTools += 1;
641
+ break;
642
+ case "tool_execution_end":
643
+ this.runningTools = Math.max(0, this.runningTools - 1);
644
+ break;
645
+ case "agent_settled":
646
+ this.streaming = false;
647
+ this.producing = false;
648
+ this.runningTools = 0;
649
+ break;
650
+ }
651
+ for (const listener of this.agentListeners) {
652
+ listener(event);
653
+ }
654
+ });
655
+ return () => {
656
+ stop();
657
+ stopCostTracking();
658
+ };
659
+ }
660
+
661
+ private observeCost(agent: AgentSession): () => void {
662
+ let last = agent.getSessionStats().cost ?? 0;
663
+ return agent.subscribe((event) => {
664
+ if (event.type !== "turn_end") {
665
+ return;
666
+ }
667
+ const total = agent.getSessionStats().cost ?? 0;
668
+ const delta = total - last;
669
+ if (delta <= 0) {
670
+ return;
671
+ }
672
+ last = total;
673
+ void this.patchSettings({
674
+ cumulativeCost: (this.currentSettings.cumulativeCost ?? 0) + delta,
675
+ });
676
+ });
677
+ }
678
+
679
+ private recordTps(outputTokens: number): void {
680
+ const startedAt = this.outputStartedAt;
681
+ this.outputStartedAt = undefined;
682
+ const elapsed = startedAt === undefined ? 0 : Date.now() - startedAt;
683
+ if (outputTokens > 0 && elapsed > 0) {
684
+ this.lastTps = (outputTokens * 1000) / elapsed;
685
+ }
686
+ }
687
+
688
+ private async buildIsolatedAgent(): Promise<{
689
+ readonly agent: AgentSession;
690
+ readonly sessionPath: string;
691
+ }> {
692
+ const sessionPath = this.deps.isolatedSessionPath?.();
693
+ if (!sessionPath) {
694
+ throw new Error(`[${this.label}] isolated runs need isolatedSessionPath`);
695
+ }
696
+ const { agent } = await this.buildAgent(
697
+ sessionPath,
698
+ await this.deps.systemInstruction?.()
699
+ );
700
+ this.observeCost(agent);
701
+ return { agent, sessionPath };
702
+ }
703
+
704
+ private async buildAgent(
705
+ sessionPath: string | undefined,
706
+ wrapped: string | undefined
707
+ ): Promise<{ readonly agent: AgentSession; readonly cwd: string }> {
708
+ const cwd = this.cwd;
709
+ const sessionManager = sessionPath
710
+ ? SessionManager.open(sessionPath, undefined, cwd)
711
+ : SessionManager.create(cwd);
712
+ const settingsManager = this.deps.settingsManagerFor(cwd);
713
+ const promptRef = { wrapped };
714
+ const loader = new DefaultResourceLoader({
715
+ cwd,
716
+ agentDir: this.deps.agentDir,
717
+ settingsManager,
718
+ // Load core tools in-process: a disk-loaded copy registers views into a second `Tools`.
719
+ extensionFactories: CoreExtensions.gated(),
720
+ appendSystemPromptOverride: (base) => {
721
+ return promptRef.wrapped ? [...base, promptRef.wrapped] : base;
722
+ },
723
+ });
724
+ await loader.reload();
725
+
726
+ const defaultModelId =
727
+ this.currentSettings.model ?? this.deps.defaults.model;
728
+ let model: Model<ModelApi> | undefined;
729
+ if (defaultModelId) {
730
+ const resolved = this.resolveModel(defaultModelId);
731
+ if (resolved.kind === "ok") {
732
+ model = resolved.model;
733
+ } else {
734
+ console.warn(
735
+ `[${this.label}] model "${defaultModelId}" did not resolve cleanly (${resolved.kind})`
736
+ );
737
+ }
738
+ }
739
+
740
+ const { session: agent } = await createAgentSession({
741
+ cwd,
742
+ agentDir: this.deps.agentDir,
743
+ modelRuntime: this.deps.modelRuntime,
744
+ settingsManager,
745
+ resourceLoader: loader,
746
+ sessionManager,
747
+ model,
748
+ thinkingLevel: this.currentSettings.thinkingLevel,
749
+ customTools: [
750
+ ...(this.deps.customTools?.({
751
+ cwd,
752
+ sessionId: () => this.sessionId,
753
+ }) ?? []),
754
+ ],
755
+ });
756
+
757
+ // Emits session_start; without it extension tools are registered but never usable.
758
+ await agent.bindExtensions({
759
+ mode: "print",
760
+ onError: (err) => {
761
+ console.warn(
762
+ `[${this.label}] extension ${err.extensionPath} (${err.event}):`,
763
+ err.error
764
+ );
765
+ },
766
+ });
767
+
768
+ return { agent, cwd };
769
+ }
770
+
771
+ private detachCached(): void {
772
+ this.cachedUnsubscribe?.();
773
+ this.cached = undefined;
774
+ this.cachedUnsubscribe = undefined;
775
+ this.cachedLog = undefined;
776
+ this.runningTools = 0;
777
+ this.streaming = false;
778
+ this.producing = false;
779
+ }
780
+
781
+ private async tearDownCached(): Promise<void> {
782
+ await this.invalidate();
783
+ const path =
784
+ this.currentSettings.sessionPath ?? this.deps.mainSessionPath?.();
785
+ if (path) {
786
+ await this.deps.onRetire?.(path);
787
+ }
788
+ }
789
+
790
+ private async patchSettings(patch: Partial<HostSettings>): Promise<void> {
791
+ this.currentSettings = { ...this.currentSettings, ...patch };
792
+ await this.deps.persistSettings(patch);
793
+ }
794
+
795
+ private resolveDefaultModel(): Model<ModelApi> | undefined {
796
+ this.deps.modelRegistry.refresh();
797
+ for (const candidate of [
798
+ this.currentSettings.model,
799
+ this.deps.defaults.model,
800
+ ]) {
801
+ if (candidate) {
802
+ const r = this.resolveModel(candidate);
803
+ if (r.kind === "ok") {
804
+ return r.model;
805
+ }
806
+ }
807
+ }
808
+ const sm = this.deps.settingsManagerFor(this.cwd);
809
+ const provider = sm.getDefaultProvider();
810
+ const modelId = sm.getDefaultModel();
811
+ if (provider && modelId) {
812
+ const m = this.deps.modelRegistry.find(provider, modelId);
813
+ if (m) {
814
+ return m;
815
+ }
816
+ }
817
+ return this.deps.modelRegistry.getAvailable()[0];
818
+ }
819
+
820
+ private resolveModel(pattern: string): ModelResolveResult {
821
+ this.deps.modelRegistry.refresh();
822
+ const available = this.deps.modelRegistry.getAvailable();
823
+ const candidates: FuzzyCandidate<Model<ModelApi>>[] = available.map(
824
+ (m) => ({
825
+ item: m,
826
+ haystacks: [qualifiedModelId(m), m.id, m.name],
827
+ })
828
+ );
829
+
830
+ const exact = available.find(
831
+ (m) =>
832
+ qualifiedModelId(m) === pattern.trim() ||
833
+ m.id === pattern.trim() ||
834
+ m.name === pattern.trim()
835
+ );
836
+ if (exact) {
837
+ return { kind: "ok", model: exact };
838
+ }
839
+
840
+ const hits = FuzzyMatcher.rank(pattern, candidates, { limit: 5 });
841
+ if (hits.length === 0) {
842
+ return {
843
+ kind: "none",
844
+ candidates: available.slice(0, 8).map(qualifiedModelId),
845
+ };
846
+ }
847
+ if (hits.length === 1) {
848
+ return { kind: "ok", model: hits[0]!.item };
849
+ }
850
+ const top = hits[0]!;
851
+ const second = hits[1]!;
852
+ if (top.score > second.score * 1.5) {
853
+ return { kind: "ok", model: top.item };
854
+ }
855
+ return {
856
+ kind: "ambiguous",
857
+ candidates: hits.map((h) => qualifiedModelId(h.item)),
858
+ };
859
+ }
860
+ }
861
+
862
+ function qualifiedModelId(model: Model<ModelApi>): string {
863
+ return `${model.provider}/${model.id}`;
864
+ }
865
+
866
+ async function disposeAgent(agent: AgentSession): Promise<void> {
867
+ try {
868
+ await agent.extensionRunner.emit({
869
+ type: "session_shutdown",
870
+ reason: "quit",
871
+ });
872
+ } catch (err) {
873
+ console.warn(`[session] extension shutdown failed:`, err);
874
+ }
875
+ agent.dispose();
876
+ }