@fastagent-sh/fastagent 0.24.3 → 0.25.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 (480) hide show
  1. package/README.md +53 -82
  2. package/dist/agent-api.d.ts +452 -0
  3. package/dist/agent-api.js +201 -0
  4. package/dist/answer-text.d.ts +8 -0
  5. package/dist/answer-text.js +18 -0
  6. package/dist/atomic-write.d.ts +2 -2
  7. package/dist/atomic-write.js +2 -2
  8. package/dist/authoring.d.ts +77 -0
  9. package/dist/authoring.js +238 -0
  10. package/dist/channel.d.ts +29 -15
  11. package/dist/channel.js +4 -1
  12. package/dist/channels/agentcore-protocol.d.ts +36 -50
  13. package/dist/channels/agentcore-protocol.js +3 -3
  14. package/dist/channels/agentcore-service.d.ts +8 -18
  15. package/dist/channels/agentcore-service.js +108 -127
  16. package/dist/channels/agentcore.d.ts +8 -24
  17. package/dist/channels/agentcore.js +32 -65
  18. package/dist/channels/body.d.ts +0 -16
  19. package/dist/channels/body.js +0 -25
  20. package/dist/channels/busy.js +6 -1
  21. package/dist/channels/context.d.ts +4 -0
  22. package/dist/channels/context.js +35 -0
  23. package/dist/channels/discover.d.ts +18 -20
  24. package/dist/channels/discover.js +41 -99
  25. package/dist/channels/feishu/feishu-api.d.ts +54 -4
  26. package/dist/channels/feishu/feishu-api.js +192 -149
  27. package/dist/channels/feishu/feishu.d.ts +3 -0
  28. package/dist/channels/feishu/feishu.js +117 -106
  29. package/dist/channels/feishu/history.d.ts +55 -0
  30. package/dist/channels/feishu/history.js +199 -0
  31. package/dist/channels/feishu/invoke-turn.d.ts +13 -11
  32. package/dist/channels/feishu/invoke-turn.js +9 -10
  33. package/dist/channels/feishu/model.d.ts +2 -1
  34. package/dist/channels/feishu/normalize.js +100 -0
  35. package/dist/channels/feishu/preview.d.ts +2 -2
  36. package/dist/channels/feishu/register-webhook.d.ts +1 -0
  37. package/dist/channels/feishu/register-webhook.js +3 -2
  38. package/dist/channels/feishu/scaffold/channel.ts +65 -46
  39. package/dist/channels/feishu/scaffold/feishu-send.ts +38 -33
  40. package/dist/channels/feishu/scaffold/feishu-threads.ts +25 -0
  41. package/dist/channels/feishu/setup-mode.d.ts +45 -25
  42. package/dist/channels/feishu/setup-mode.js +74 -23
  43. package/dist/channels/feishu/shared-api.d.ts +24 -6
  44. package/dist/channels/feishu/shared-api.js +30 -24
  45. package/dist/channels/kit/context-buffer.d.ts +11 -2
  46. package/dist/channels/kit/context-buffer.js +7 -4
  47. package/dist/channels/kit/delivery.d.ts +19 -3
  48. package/dist/channels/kit/delivery.js +74 -15
  49. package/dist/channels/kit/event-stream.js +2 -1
  50. package/dist/channels/kit/invoke-turn-kit.d.ts +41 -41
  51. package/dist/channels/kit/invoke-turn-kit.js +62 -71
  52. package/dist/channels/kit/place-history.d.ts +107 -0
  53. package/dist/channels/kit/place-history.js +182 -0
  54. package/dist/channels/kit/preview-kit.d.ts +9 -5
  55. package/dist/channels/kit/preview-kit.js +36 -15
  56. package/dist/channels/kit/room-threads.d.ts +46 -0
  57. package/dist/channels/kit/room-threads.js +81 -0
  58. package/dist/channels/kit/seen.js +1 -1
  59. package/dist/channels/kit/stop-command.d.ts +6 -3
  60. package/dist/channels/kit/stop-command.js +20 -18
  61. package/dist/channels/kit/thread-fork.d.ts +17 -0
  62. package/dist/channels/kit/thread-fork.js +43 -0
  63. package/dist/channels/kit/thread-participants.js +1 -1
  64. package/dist/channels/kit/transport.d.ts +5 -0
  65. package/dist/channels/kit/transport.js +5 -0
  66. package/dist/channels/kit/turn-runner.d.ts +10 -9
  67. package/dist/channels/kit/turn-runner.js +17 -19
  68. package/dist/channels/kit/turn-store.d.ts +5 -4
  69. package/dist/channels/kit/turn-store.js +5 -5
  70. package/dist/channels/lark/lark.d.ts +3 -0
  71. package/dist/channels/lark/lark.js +5 -0
  72. package/dist/channels/lark/onboard.d.ts +5 -3
  73. package/dist/channels/lark/onboard.js +14 -14
  74. package/dist/channels/lark/scaffold/channel.ts +67 -44
  75. package/dist/channels/lark/scaffold/lark-send.ts +38 -33
  76. package/dist/channels/lark/scaffold/lark-threads.ts +25 -0
  77. package/dist/channels/respond.d.ts +0 -4
  78. package/dist/channels/respond.js +1 -1
  79. package/dist/channels/router.d.ts +18 -0
  80. package/dist/channels/router.js +68 -0
  81. package/dist/channels/serve.d.ts +5 -70
  82. package/dist/channels/serve.js +105 -287
  83. package/dist/channels/slack/history.d.ts +42 -0
  84. package/dist/channels/slack/history.js +282 -0
  85. package/dist/channels/slack/invoke-turn.d.ts +12 -5
  86. package/dist/channels/slack/invoke-turn.js +7 -5
  87. package/dist/channels/slack/manifest.d.ts +1 -4
  88. package/dist/channels/slack/manifest.js +22 -10
  89. package/dist/channels/slack/model.d.ts +2 -1
  90. package/dist/channels/slack/onboard.d.ts +0 -2
  91. package/dist/channels/slack/onboard.js +2 -4
  92. package/dist/channels/slack/onboarding-state.d.ts +0 -2
  93. package/dist/channels/slack/onboarding-state.js +0 -1
  94. package/dist/channels/slack/parse.d.ts +0 -5
  95. package/dist/channels/slack/parse.js +1 -16
  96. package/dist/channels/slack/preview.d.ts +2 -3
  97. package/dist/channels/slack/preview.js +78 -52
  98. package/dist/channels/slack/register-webhook.d.ts +1 -0
  99. package/dist/channels/slack/register-webhook.js +6 -5
  100. package/dist/channels/slack/scaffold/channel.ts +23 -24
  101. package/dist/channels/slack/scaffold/slack-send.ts +50 -43
  102. package/dist/channels/slack/scaffold/slack-threads.ts +29 -0
  103. package/dist/channels/slack/setup-server.d.ts +19 -0
  104. package/dist/channels/slack/setup-server.js +113 -11
  105. package/dist/channels/slack/shared-api.d.ts +15 -4
  106. package/dist/channels/slack/shared-api.js +19 -14
  107. package/dist/channels/slack/slack-api.d.ts +61 -2
  108. package/dist/channels/slack/slack-api.js +28 -5
  109. package/dist/channels/slack/slack.js +97 -52
  110. package/dist/channels/slack/welcomed.js +1 -1
  111. package/dist/channels/telegram/context-buffer.d.ts +6 -1
  112. package/dist/channels/telegram/context-buffer.js +19 -0
  113. package/dist/channels/telegram/invoke-turn.d.ts +8 -4
  114. package/dist/channels/telegram/invoke-turn.js +7 -7
  115. package/dist/channels/telegram/parse.d.ts +7 -2
  116. package/dist/channels/telegram/parse.js +7 -0
  117. package/dist/channels/telegram/preview.d.ts +2 -2
  118. package/dist/channels/telegram/register-webhook.d.ts +1 -0
  119. package/dist/channels/telegram/register-webhook.js +3 -2
  120. package/dist/channels/telegram/scaffold/channel.ts +27 -28
  121. package/dist/channels/telegram/scaffold/telegram-send.ts +61 -108
  122. package/dist/channels/telegram/shared-api.d.ts +34 -0
  123. package/dist/channels/telegram/shared-api.js +79 -0
  124. package/dist/channels/telegram/telegram-api.d.ts +15 -5
  125. package/dist/channels/telegram/telegram-api.js +10 -6
  126. package/dist/channels/telegram/telegram.js +27 -21
  127. package/dist/channels/telegram/turn-store.js +1 -1
  128. package/dist/channels/{kit/turn-queue.d.ts → turn-queue.d.ts} +2 -2
  129. package/dist/channels/{kit/turn-queue.js → turn-queue.js} +10 -10
  130. package/dist/cli/add-feishu.d.ts +42 -12
  131. package/dist/cli/add-feishu.js +177 -124
  132. package/dist/cli/add-slack.d.ts +8 -3
  133. package/dist/cli/add-slack.js +115 -63
  134. package/dist/cli/auth-view.d.ts +10 -3
  135. package/dist/cli/auth-view.js +8 -6
  136. package/dist/cli/box-login.d.ts +17 -7
  137. package/dist/cli/box-login.js +46 -12
  138. package/dist/cli/commands/add.d.ts +15 -4
  139. package/dist/cli/commands/add.js +252 -143
  140. package/dist/cli/commands/auth.d.ts +6 -0
  141. package/dist/cli/commands/auth.js +50 -0
  142. package/dist/cli/commands/content.d.ts +6 -0
  143. package/dist/cli/commands/content.js +45 -0
  144. package/dist/cli/commands/deploy/agentcore.js +50 -77
  145. package/dist/cli/commands/deploy/docker.js +38 -39
  146. package/dist/cli/commands/deploy/fly.js +21 -18
  147. package/dist/cli/commands/deploy/host-login.d.ts +26 -0
  148. package/dist/cli/commands/deploy/host-login.js +90 -0
  149. package/dist/cli/commands/deploy/railway.js +21 -26
  150. package/dist/cli/commands/deploy/shared.d.ts +45 -17
  151. package/dist/cli/commands/deploy/shared.js +34 -15
  152. package/dist/cli/commands/deploy.d.ts +0 -4
  153. package/dist/cli/commands/deploy.js +237 -50
  154. package/dist/cli/commands/destroy.js +8 -6
  155. package/dist/cli/commands/dev.d.ts +0 -2
  156. package/dist/cli/commands/dev.js +20 -19
  157. package/dist/cli/commands/env.d.ts +12 -0
  158. package/dist/cli/commands/env.js +190 -0
  159. package/dist/cli/commands/info.js +95 -156
  160. package/dist/cli/commands/init.d.ts +2 -2
  161. package/dist/cli/commands/init.js +35 -33
  162. package/dist/cli/commands/invoke.d.ts +6 -0
  163. package/dist/cli/commands/invoke.js +131 -16
  164. package/dist/cli/commands/login.d.ts +31 -2
  165. package/dist/cli/commands/login.js +413 -64
  166. package/dist/cli/commands/logout.d.ts +4 -0
  167. package/dist/cli/commands/logout.js +53 -0
  168. package/dist/cli/commands/logs.js +8 -6
  169. package/dist/cli/commands/models.d.ts +2 -1
  170. package/dist/cli/commands/models.js +39 -37
  171. package/dist/cli/commands/schedules.d.ts +21 -0
  172. package/dist/cli/commands/schedules.js +143 -0
  173. package/dist/cli/commands/start.d.ts +11 -10
  174. package/dist/cli/commands/start.js +35 -62
  175. package/dist/cli/content-view.d.ts +9 -0
  176. package/dist/cli/content-view.js +26 -0
  177. package/dist/cli/fail.d.ts +9 -12
  178. package/dist/cli/fail.js +17 -14
  179. package/dist/cli/invoke-stream.d.ts +12 -3
  180. package/dist/cli/invoke-stream.js +85 -23
  181. package/dist/cli/kernel.d.ts +9 -2
  182. package/dist/cli/kernel.js +15 -6
  183. package/dist/cli/login-relay.d.ts +3 -2
  184. package/dist/cli/login-relay.js +18 -6
  185. package/dist/cli/models-view.d.ts +5 -1
  186. package/dist/cli/models-view.js +1 -1
  187. package/dist/cli/person.d.ts +10 -0
  188. package/dist/cli/person.js +23 -0
  189. package/dist/cli/program.js +421 -203
  190. package/dist/cli/serve.d.ts +13 -63
  191. package/dist/cli/serve.js +31 -121
  192. package/dist/cli/shared.d.ts +21 -48
  193. package/dist/cli/shared.js +68 -89
  194. package/dist/cli/ticket.d.ts +136 -0
  195. package/dist/cli/ticket.js +476 -0
  196. package/dist/client.d.ts +7 -0
  197. package/dist/client.js +2 -0
  198. package/dist/content/declare.d.ts +70 -0
  199. package/dist/content/declare.js +163 -0
  200. package/dist/content/file.d.ts +32 -0
  201. package/dist/content/file.js +119 -0
  202. package/dist/content/git.d.ts +41 -0
  203. package/dist/content/git.js +217 -0
  204. package/dist/content/mount.d.ts +2 -0
  205. package/dist/content/mount.js +15 -0
  206. package/dist/content/resolve.d.ts +52 -0
  207. package/dist/content/resolve.js +151 -0
  208. package/dist/content/source.d.ts +27 -0
  209. package/dist/content/source.js +53 -0
  210. package/dist/conversation-sessions.d.ts +26 -0
  211. package/dist/conversation-sessions.js +99 -0
  212. package/dist/declared-secrets.d.ts +19 -37
  213. package/dist/declared-secrets.js +16 -58
  214. package/dist/deploy/agentcore/destroy.d.ts +3 -3
  215. package/dist/deploy/agentcore/destroy.js +46 -28
  216. package/dist/deploy/agentcore/forwarder.js +105 -22
  217. package/dist/deploy/agentcore/logs.js +1 -1
  218. package/dist/deploy/agentcore/plan.d.ts +10 -24
  219. package/dist/deploy/agentcore/plan.js +31 -135
  220. package/dist/deploy/agentcore/run.d.ts +1 -1
  221. package/dist/deploy/agentcore/run.js +8 -5
  222. package/dist/deploy/agentcore/shell.d.ts +1 -1
  223. package/dist/deploy/agentcore/shell.js +5 -5
  224. package/dist/deploy/api.d.ts +34 -0
  225. package/dist/deploy/api.js +50 -0
  226. package/dist/deploy/box-shell.d.ts +6 -0
  227. package/dist/deploy/box-shell.js +7 -0
  228. package/dist/deploy/build-context.d.ts +7 -5
  229. package/dist/deploy/build-context.js +41 -35
  230. package/dist/deploy/channel-ingress.d.ts +6 -1
  231. package/dist/deploy/channel-ingress.js +18 -5
  232. package/dist/deploy/container.d.ts +22 -9
  233. package/dist/deploy/container.js +96 -55
  234. package/dist/deploy/docker/plan.d.ts +3 -2
  235. package/dist/deploy/docker/plan.js +13 -14
  236. package/dist/deploy/docker/run.d.ts +2 -2
  237. package/dist/deploy/docker/run.js +8 -4
  238. package/dist/deploy/fly/plan.d.ts +1 -1
  239. package/dist/deploy/fly/plan.js +12 -21
  240. package/dist/deploy/fly/run.d.ts +10 -6
  241. package/dist/deploy/fly/run.js +11 -6
  242. package/dist/deploy/hosts.d.ts +2 -1
  243. package/dist/deploy/hosts.js +2 -1
  244. package/dist/deploy/preflight.d.ts +21 -23
  245. package/dist/deploy/preflight.js +222 -201
  246. package/dist/deploy/railway/plan.d.ts +8 -6
  247. package/dist/deploy/railway/plan.js +15 -58
  248. package/dist/deploy/railway/run.d.ts +8 -3
  249. package/dist/deploy/railway/run.js +12 -8
  250. package/dist/deploy/residency.d.ts +5 -11
  251. package/dist/deploy/residency.js +0 -6
  252. package/dist/deploy/runner.d.ts +1 -1
  253. package/dist/deploy/runner.js +1 -1
  254. package/dist/deploy/secrets.d.ts +16 -2
  255. package/dist/deploy/secrets.js +33 -6
  256. package/dist/deploy/workspace.d.ts +11 -6
  257. package/dist/deploy/workspace.js +55 -38
  258. package/dist/dev-supervisor.d.ts +11 -2
  259. package/dist/dev-supervisor.js +51 -26
  260. package/dist/effect-port.d.ts +4 -4
  261. package/dist/effect-port.js +4 -4
  262. package/dist/environment/apt.d.ts +4 -0
  263. package/dist/environment/apt.js +32 -0
  264. package/dist/environment/declare.d.ts +59 -0
  265. package/dist/environment/declare.js +210 -0
  266. package/dist/environment/mise.d.ts +50 -0
  267. package/dist/environment/mise.js +268 -0
  268. package/dist/feishu.d.ts +2 -2
  269. package/dist/feishu.js +2 -2
  270. package/dist/git.d.ts +13 -0
  271. package/dist/git.js +55 -0
  272. package/dist/harnesses/models/catalog.d.ts +20 -0
  273. package/dist/harnesses/models/catalog.js +254 -0
  274. package/dist/{engines/pi/auth.d.ts → harnesses/models/credentials.d.ts} +3 -3
  275. package/dist/{engines/pi/auth.js → harnesses/models/credentials.js} +3 -3
  276. package/dist/harnesses/models/environment.d.ts +43 -0
  277. package/dist/harnesses/models/environment.js +82 -0
  278. package/dist/{engines/pi → harnesses/models}/login.d.ts +57 -26
  279. package/dist/{engines/pi → harnesses/models}/login.js +187 -79
  280. package/dist/harnesses/models/models.d.ts +83 -0
  281. package/dist/harnesses/models/models.js +158 -0
  282. package/dist/harnesses/pi-durable/agent.d.ts +42 -0
  283. package/dist/harnesses/pi-durable/agent.js +579 -0
  284. package/dist/harnesses/pi-durable/defaults.d.ts +28 -0
  285. package/dist/harnesses/pi-durable/defaults.js +26 -0
  286. package/dist/harnesses/pi-durable/define.d.ts +77 -0
  287. package/dist/harnesses/pi-durable/define.js +28 -0
  288. package/dist/harnesses/pi-durable/entries.d.ts +32 -0
  289. package/dist/harnesses/pi-durable/entries.js +126 -0
  290. package/dist/harnesses/pi-durable/events.d.ts +25 -0
  291. package/dist/harnesses/pi-durable/events.js +238 -0
  292. package/dist/harnesses/pi-durable/ids.d.ts +16 -0
  293. package/dist/harnesses/pi-durable/ids.js +55 -0
  294. package/dist/harnesses/pi-durable/messages.d.ts +41 -0
  295. package/dist/harnesses/pi-durable/messages.js +116 -0
  296. package/dist/harnesses/pi-durable/open.d.ts +88 -0
  297. package/dist/harnesses/pi-durable/open.js +329 -0
  298. package/dist/harnesses/pi-durable/reads.d.ts +30 -0
  299. package/dist/harnesses/pi-durable/reads.js +94 -0
  300. package/dist/harnesses/pi-durable/registry.d.ts +18 -0
  301. package/dist/harnesses/pi-durable/registry.js +56 -0
  302. package/dist/harnesses/pi-durable/release.d.ts +13 -0
  303. package/dist/harnesses/pi-durable/release.js +40 -0
  304. package/dist/harnesses/pi-durable/serve.d.ts +80 -0
  305. package/dist/harnesses/pi-durable/serve.js +156 -0
  306. package/dist/harnesses/pi-durable/sessions.d.ts +27 -0
  307. package/dist/harnesses/pi-durable/sessions.js +47 -0
  308. package/dist/harnesses/pi-durable/storage.d.ts +4 -0
  309. package/dist/harnesses/pi-durable/storage.js +49 -0
  310. package/dist/harnesses/pi-durable/stream.d.ts +20 -0
  311. package/dist/harnesses/pi-durable/stream.js +221 -0
  312. package/dist/harnesses/pi-durable/text.d.ts +30 -0
  313. package/dist/harnesses/pi-durable/text.js +292 -0
  314. package/dist/harnesses/pi-durable/wake.d.ts +4 -0
  315. package/dist/harnesses/pi-durable/wake.js +81 -0
  316. package/dist/http/client.d.ts +6 -0
  317. package/dist/http/client.js +256 -0
  318. package/dist/http/contract.d.ts +541 -0
  319. package/dist/http/contract.js +179 -0
  320. package/dist/http/server.d.ts +34 -0
  321. package/dist/http/server.js +194 -0
  322. package/dist/index.d.ts +17 -4
  323. package/dist/index.js +10 -5
  324. package/dist/lark.d.ts +2 -2
  325. package/dist/lark.js +2 -2
  326. package/dist/locked-file.d.ts +17 -0
  327. package/dist/{engines/pi/locked-file.js → locked-file.js} +16 -10
  328. package/dist/once.d.ts +6 -0
  329. package/dist/once.js +23 -0
  330. package/dist/paths.d.ts +36 -52
  331. package/dist/paths.js +66 -182
  332. package/dist/runtime.d.ts +25 -2
  333. package/dist/runtime.js +27 -0
  334. package/dist/scaffold/add-channel.d.ts +14 -27
  335. package/dist/scaffold/add-channel.js +82 -180
  336. package/dist/scaffold/init.d.ts +17 -18
  337. package/dist/scaffold/init.js +87 -99
  338. package/dist/scaffold/templates/SYSTEM.md +12 -0
  339. package/dist/scaffold/templates/env.example +4 -10
  340. package/dist/scaffold/templates/fastagent.config.ts +21 -33
  341. package/dist/scaffold/templates/gitignore +8 -1
  342. package/dist/scaffold/templates/skills/writing-great-skills/SKILL.md +1 -2
  343. package/dist/scaffold/templates.d.ts +9 -4
  344. package/dist/scaffold/templates.js +7 -5
  345. package/dist/scaffold/vendor-skill.d.ts +1 -4
  346. package/dist/scaffold/vendor-skill.js +5 -6
  347. package/dist/schedule/cron.d.ts +13 -0
  348. package/dist/schedule/cron.js +52 -0
  349. package/dist/schedule/eventbridge-cron.d.ts +17 -0
  350. package/dist/schedule/eventbridge-cron.js +88 -0
  351. package/dist/schedule/schedule.d.ts +23 -0
  352. package/dist/schedule/schedule.js +34 -0
  353. package/dist/schedule/scheduler.d.ts +34 -33
  354. package/dist/schedule/scheduler.js +251 -137
  355. package/dist/schedule/state.d.ts +4 -14
  356. package/dist/schedule/state.js +7 -7
  357. package/dist/schedule/wake-alarm.d.ts +22 -2
  358. package/dist/schedule/wake-alarm.js +66 -20
  359. package/dist/schedule/wakeups.d.ts +16 -15
  360. package/dist/schedule/wakeups.js +47 -45
  361. package/dist/service.d.ts +56 -169
  362. package/dist/service.js +91 -187
  363. package/dist/serving-command.d.ts +10 -0
  364. package/dist/serving-command.js +16 -0
  365. package/dist/slack.d.ts +1 -1
  366. package/dist/slack.js +1 -1
  367. package/dist/{channels/kit/state.js → state-file.js} +6 -6
  368. package/dist/telegram.d.ts +1 -0
  369. package/dist/telegram.js +1 -0
  370. package/dist/tunnel.d.ts +3 -0
  371. package/dist/tunnel.js +12 -4
  372. package/dist/value-file.d.ts +21 -0
  373. package/dist/value-file.js +108 -0
  374. package/dist/version.d.ts +5 -0
  375. package/dist/version.js +14 -1
  376. package/package.json +18 -28
  377. package/dist/agent.d.ts +0 -106
  378. package/dist/agent.js +0 -21
  379. package/dist/channels/control.d.ts +0 -31
  380. package/dist/channels/control.js +0 -329
  381. package/dist/channels/define-channel.d.ts +0 -39
  382. package/dist/channels/define-channel.js +0 -9
  383. package/dist/channels/feishu/context-buffer.d.ts +0 -35
  384. package/dist/channels/feishu/context-buffer.js +0 -65
  385. package/dist/channels/feishu/scaffold/channel.websocket.ts +0 -31
  386. package/dist/channels/http.d.ts +0 -23
  387. package/dist/channels/http.js +0 -57
  388. package/dist/channels/lark/scaffold/channel.websocket.ts +0 -31
  389. package/dist/channels/slack/context-buffer.d.ts +0 -23
  390. package/dist/channels/slack/context-buffer.js +0 -37
  391. package/dist/channels/sse.d.ts +0 -4
  392. package/dist/channels/sse.js +0 -66
  393. package/dist/cli/commands/chat.d.ts +0 -3
  394. package/dist/cli/commands/chat.js +0 -14
  395. package/dist/cli/commands/routine-run.d.ts +0 -6
  396. package/dist/cli/commands/routine-run.js +0 -46
  397. package/dist/cli/commands/routine.d.ts +0 -25
  398. package/dist/cli/commands/routine.js +0 -122
  399. package/dist/cli/commands/tool.d.ts +0 -1
  400. package/dist/cli/commands/tool.js +0 -57
  401. package/dist/collect.d.ts +0 -63
  402. package/dist/collect.js +0 -90
  403. package/dist/core.d.ts +0 -13
  404. package/dist/core.js +0 -15
  405. package/dist/engines/pi/agent-models.d.ts +0 -83
  406. package/dist/engines/pi/agent-models.js +0 -120
  407. package/dist/engines/pi/agent-session-factory.d.ts +0 -121
  408. package/dist/engines/pi/agent-session-factory.js +0 -517
  409. package/dist/engines/pi/chat.d.ts +0 -3
  410. package/dist/engines/pi/chat.js +0 -8
  411. package/dist/engines/pi/config.d.ts +0 -71
  412. package/dist/engines/pi/config.js +0 -199
  413. package/dist/engines/pi/create.d.ts +0 -170
  414. package/dist/engines/pi/create.js +0 -343
  415. package/dist/engines/pi/definition.d.ts +0 -74
  416. package/dist/engines/pi/definition.js +0 -236
  417. package/dist/engines/pi/entry-images.d.ts +0 -14
  418. package/dist/engines/pi/entry-images.js +0 -23
  419. package/dist/engines/pi/invoke-session.d.ts +0 -18
  420. package/dist/engines/pi/invoke-session.js +0 -403
  421. package/dist/engines/pi/locked-file.d.ts +0 -9
  422. package/dist/engines/pi/machine.d.ts +0 -53
  423. package/dist/engines/pi/machine.js +0 -137
  424. package/dist/engines/pi/models.d.ts +0 -200
  425. package/dist/engines/pi/models.js +0 -533
  426. package/dist/engines/pi/open.d.ts +0 -180
  427. package/dist/engines/pi/open.js +0 -270
  428. package/dist/engines/pi/openai-account-models.d.ts +0 -35
  429. package/dist/engines/pi/openai-account-models.js +0 -116
  430. package/dist/engines/pi/report.d.ts +0 -18
  431. package/dist/engines/pi/report.js +0 -59
  432. package/dist/engines/pi/retry-event.d.ts +0 -6
  433. package/dist/engines/pi/retry-event.js +0 -15
  434. package/dist/engines/pi/service.d.ts +0 -9
  435. package/dist/engines/pi/service.js +0 -14
  436. package/dist/engines/pi/session-builder.d.ts +0 -13
  437. package/dist/engines/pi/session-builder.js +0 -117
  438. package/dist/engines/pi/session-control.d.ts +0 -77
  439. package/dist/engines/pi/session-control.js +0 -908
  440. package/dist/engines/pi/session-effects.d.ts +0 -14
  441. package/dist/engines/pi/session-effects.js +0 -30
  442. package/dist/engines/pi/session-inheritance.d.ts +0 -21
  443. package/dist/engines/pi/session-inheritance.js +0 -204
  444. package/dist/engines/pi/session-markers.d.ts +0 -49
  445. package/dist/engines/pi/session-markers.js +0 -60
  446. package/dist/engines/pi/session-settings.d.ts +0 -67
  447. package/dist/engines/pi/session-settings.js +0 -119
  448. package/dist/engines/pi/session-store.d.ts +0 -69
  449. package/dist/engines/pi/session-store.js +0 -430
  450. package/dist/engines/pi/tool-context.d.ts +0 -45
  451. package/dist/engines/pi/tool-context.js +0 -31
  452. package/dist/engines/pi/tool.d.ts +0 -99
  453. package/dist/engines/pi/tool.js +0 -118
  454. package/dist/engines/pi/turn-kit.d.ts +0 -83
  455. package/dist/engines/pi/turn-kit.js +0 -275
  456. package/dist/engines/pi/wake-tool.d.ts +0 -9
  457. package/dist/engines/pi/wake-tool.js +0 -92
  458. package/dist/loader.d.ts +0 -41
  459. package/dist/loader.js +0 -116
  460. package/dist/node.d.ts +0 -3
  461. package/dist/node.js +0 -4
  462. package/dist/observe.d.ts +0 -6
  463. package/dist/observe.js +0 -47
  464. package/dist/pi.d.ts +0 -25
  465. package/dist/pi.js +0 -15
  466. package/dist/scaffold/templates/APPEND_SYSTEM.md +0 -12
  467. package/dist/scaffold/templates/tools/fetch-url.ts +0 -25
  468. package/dist/schedule/discover.d.ts +0 -17
  469. package/dist/schedule/discover.js +0 -61
  470. package/dist/schedule/routine.d.ts +0 -43
  471. package/dist/schedule/routine.js +0 -32
  472. package/dist/schedule/run.d.ts +0 -55
  473. package/dist/schedule/run.js +0 -146
  474. package/dist/secrets-gate.d.ts +0 -32
  475. package/dist/secrets-gate.js +0 -33
  476. package/dist/session-remote.d.ts +0 -47
  477. package/dist/session-remote.js +0 -460
  478. package/dist/session.d.ts +0 -466
  479. package/dist/session.js +0 -57
  480. /package/dist/{channels/kit/state.d.ts → state-file.d.ts} +0 -0
@@ -1,908 +0,0 @@
1
- // The L0 rendering payload reads message text the way the engine does — ONE reading, joined without a
2
- // separator because this payload is a transcript, not a preview line.
3
- import { contentText } from "@earendil-works/pi-ai";
4
- import * as Deferred from "effect/Deferred";
5
- import * as Effect from "effect/Effect";
6
- import * as Queue from "effect/Queue";
7
- import { PortFailure, port, portAbort, portCleanup, portError, portJoin } from "../../effect-port.js";
8
- import { acquireSession, acquireSessionLease } from "./session-effects.js";
9
- import { calculateContextTokens, estimateTokens, getLastAssistantUsage, } from "@earendil-works/pi-coding-agent";
10
- import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
11
- import { MISSING_MODEL_CODE, SESSION_BUSY_CODE } from "../../agent.js";
12
- import { BOUNDARY_COMMAND_FAILED_CODE, INVALID_COMMAND_CODE, isAddressableSession, NO_ACTIVE_RUN_CODE, NOTHING_TO_COMPACT_CODE, NO_SUCH_SESSION_CODE, PARTIAL_UPDATE_CODE, UPDATE_FIELDS, RUN_COMMAND_FAILED_CODE, UNSUPPORTED_CAPABILITY_CODE, } from "../../session.js";
13
- import { forkProvenance, isConversationMessage, isNavigable, publishedLeaf } from "./session-markers.js";
14
- import { entryImages, imageAt } from "./entry-images.js";
15
- import { answerOutcome, answerThinking, endsRun, } from "./turn-kit.js";
16
- import { startCompaction } from "./agent-session-factory.js";
17
- import { toRetryScheduledEvent } from "./retry-event.js";
18
- import { MissingModel, THINKING_LEVELS, activePath, describeModels, resolveSessionSettings, } from "./session-settings.js";
19
- import { log } from "../../log.js";
20
- // ── Usage (read from the record) ─────────────────────────────────────────────
21
- /**
22
- * What the newest answer cost and how full the context is, from the record rather than a live session (a served
23
- * session exists only during its run). The numbers are the provider's own, on the newest assistant pi counts as
24
- * carrying usage (`getLastAssistantUsage`). The context size adds pi's estimate of what followed that answer, which
25
- * after a finished turn is nothing: then it is exactly pi's `getContextUsage()`.
26
- *
27
- * `contextTokens` is omitted, never guessed, once a compaction or a context edit follows that answer: the provider's
28
- * count no longer describes the context (pi's own rule). pi then estimates from characters; this reports unknown
29
- * until the next response.
30
- */
31
- function sessionUsage(path, record, contextWindow) {
32
- const usage = getLastAssistantUsage(path);
33
- if (!usage)
34
- return undefined;
35
- let at = path.length - 1;
36
- while (at >= 0) {
37
- const entry = path[at];
38
- if (entry?.type === "message" && entry.message.usage === usage)
39
- break;
40
- at--;
41
- }
42
- const answer = path[at];
43
- const stale = path.slice(at + 1).some((entry) => entry.type === "compaction" || entry.type === "context_edit");
44
- let contextTokens;
45
- if (answer && !stale) {
46
- let trailing = 0;
47
- let after = false;
48
- for (const { sourceEntry, messages } of record.buildSessionProjection().entries) {
49
- if (after)
50
- for (const message of messages)
51
- trailing += estimateTokens(message);
52
- if (sourceEntry.id === answer.id)
53
- after = true;
54
- }
55
- contextTokens = calculateContextTokens(usage) + trailing;
56
- }
57
- return {
58
- inputTokens: usage.input,
59
- outputTokens: usage.output,
60
- cacheReadTokens: usage.cacheRead,
61
- cacheWriteTokens: usage.cacheWrite,
62
- cost: usage.cost.total,
63
- ...(contextTokens !== undefined ? { contextTokens } : {}),
64
- ...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),
65
- };
66
- }
67
- // ── Entry normalization (durable plane) ──────────────────────────────────────
68
- /**
69
- * pi `PiSessionEntry` → neutral {@link SessionEntry}. Message entries map onto the guaranteed
70
- * kind vocabulary (user/assistant/tool) with a minimal render payload; every other engine record
71
- * keeps its pi type as an open-set kind with an EMPTY payload — present so `parentId` chains and
72
- * cursors stay intact, skippable by contract, and no pi message class leaks through the adapter.
73
- * One exception: `context_edit` carries `{ targetId, omitted }`, never the replacement content.
74
- */
75
- function toSessionEntry(entry, parentId) {
76
- const base = {
77
- id: entry.id,
78
- parentId,
79
- timestamp: Date.parse(entry.timestamp),
80
- };
81
- if (entry.type === "message") {
82
- const m = entry.message;
83
- if (m.role === "user") {
84
- return { ...base, kind: "user", data: { text: contentText(m.content, ""), ...entryImages(entry.id, m) } };
85
- }
86
- if (m.role === "assistant") {
87
- // `args` is what `tool_started` carries live (pi emits the call's recorded `arguments` there), so a reopened
88
- // conversation shows the same call a watcher saw.
89
- const toolCalls = m.content
90
- .filter((b) => b.type === "toolCall")
91
- .map((b) => ({ id: b.id ?? "", name: b.name ?? "", args: b.arguments ?? {} }));
92
- const data = { text: contentText(m.content, "") };
93
- if (toolCalls.length > 0)
94
- data.toolCalls = toolCalls;
95
- // The same text the live `message_delta { channel: "thinking" }` added up to (turn-kit.ts answerThinking).
96
- const thinking = answerThinking(m);
97
- if (thinking !== undefined)
98
- data.thinking = thinking;
99
- const outcome = answerOutcome(m);
100
- if (outcome)
101
- data.outcome = outcome;
102
- return { ...base, kind: "assistant", data };
103
- }
104
- if (m.role === "toolResult") {
105
- return {
106
- ...base,
107
- kind: "tool",
108
- data: {
109
- toolCallId: m.toolCallId,
110
- toolName: m.toolName,
111
- isError: m.isError ?? false,
112
- text: contentText(m.content, ""),
113
- ...entryImages(entry.id, m),
114
- // Same flag as the live `tool_finished` (turn-kit.ts asksToEndRun).
115
- ...(endsRun(m) ? { terminate: true } : {}),
116
- },
117
- };
118
- }
119
- // Not a conversation turn (`isConversationMessage`): the engine's own `system` bookkeeping, or a custom
120
- // AgentMessage role a channel/extension defined. Open-set kind with an EMPTY payload, skippable by contract —
121
- // which is also what keeps the assembled prompt out of everything this plane publishes.
122
- return { ...base, kind: `message:${m.role}`, data: {} };
123
- }
124
- // Which transcript entry the model no longer sees as written: `omitted` when it left the model context (pi's own
125
- // abandoned retry and overflow attempts, or an extension's edit), otherwise its content was replaced. The target
126
- // stays in the transcript either way, and the edit applies only on a path that contains it.
127
- if (entry.type === "context_edit") {
128
- return { ...base, kind: entry.type, data: { targetId: entry.targetId, omitted: entry.replacement === null } };
129
- }
130
- return { ...base, kind: entry.type, data: {} };
131
- }
132
- // ── Live fan-out (events plane) ──────────────────────────────────────────────
133
- /** Per-subscriber backlog limits: event count and UTF-8 JSON bytes. Overflow freezes the accepted
134
- * prefix; the reader drains it before seeing done, then reconnects and backfills via entries(). */
135
- export const SUBSCRIBER_BUFFER_CAP = 10_000;
136
- export const SUBSCRIBER_BUFFER_BYTES = 8 * 1024 * 1024;
137
- /**
138
- * One subscriber's bounded push→pull queue.
139
- *
140
- * The mechanics are Effect's, because both halves are things this class hand-rolled and got wrong
141
- * once each. `Queue.end` stops accepting offers, lets the reader drain what WAS accepted, and only
142
- * then reports done — which is both the overflow policy above and the door teardown needs (an async
143
- * generator suspended on a quiet stream cannot be ended by `return()` alone: that queues behind the
144
- * never-settling await). And a queue has a SET of takers, so two concurrent `next()` calls — legal,
145
- * any wrapper may poll twice — cannot overwrite each other's resolver.
146
- *
147
- * What stays here is the arithmetic Queue has no opinion on: the two backlog limits, and the JSON
148
- * snapshot (taken at push, so a producer that later mutates its event or tool result cannot grow a
149
- * queued one). Cost of the swap: one fiber per delivered event instead of an array shift, on a
150
- * stream that carries per-token deltas — cheap next to a model call, and paid only while someone is
151
- * attached.
152
- */
153
- class Subscriber {
154
- /** For the overflow diagnostic only — a warn without the session is not actionable on a
155
- * multi-session serve. (Explicit assignment: TS parameter properties break Node's strip-only
156
- * type erasure, which the CLI runs under.) */
157
- session;
158
- constructor(session) {
159
- this.session = session;
160
- }
161
- queue = Effect.runSync(Queue.unbounded());
162
- buffered = 0;
163
- bufferedBytes = 0;
164
- ended = false;
165
- push(event) {
166
- if (this.ended)
167
- return;
168
- const json = JSON.stringify(event);
169
- const bytes = Buffer.byteLength(json);
170
- if (this.buffered >= SUBSCRIBER_BUFFER_CAP || this.bufferedBytes + bytes > SUBSCRIBER_BUFFER_BYTES) {
171
- log.warn(`[fastagent] session-control subscriber for session "${this.session}" exceeded its buffer limit (${SUBSCRIBER_BUFFER_CAP} events / ${SUBSCRIBER_BUFFER_BYTES} bytes; ${this.bufferedBytes} bytes queued, ${bytes} incoming); its stream ends after draining the backlog, then the client resyncs via entries()`);
172
- this.close();
173
- return;
174
- }
175
- Queue.offerUnsafe(this.queue, { json, bytes });
176
- this.buffered++;
177
- this.bufferedBytes += bytes;
178
- }
179
- close() {
180
- this.ended = true;
181
- Queue.endUnsafe(this.queue);
182
- }
183
- next() {
184
- return Effect.runPromise(Queue.take(this.queue).pipe(Effect.map((item) => {
185
- this.buffered--;
186
- this.bufferedBytes -= item.bytes;
187
- return { done: false, value: JSON.parse(item.json) };
188
- }), Effect.catchTag("Done", () => Effect.succeed({ done: true, value: undefined }))));
189
- }
190
- }
191
- /**
192
- * Build the observation hub. Wire `observer` into the SAME agent assembly that serves the sessions:
193
- *
194
- * ```ts
195
- * const { control, observer } = createPiSessionControl({ sessions });
196
- * const agent = createPiAgent({ ..., sessions, observer });
197
- * ```
198
- */
199
- export function createPiSessionControl(options) {
200
- const { sessions, boundary } = options;
201
- /** Live run state per session — derived purely from run_started/run_settled and the controls
202
- * registered with run_started. */
203
- const active = new Map();
204
- const subscribers = new Map();
205
- /** Sessions with a manual compaction in flight — reported as `status: "compacting"`, keyed to
206
- * the summarization's AbortController so `abort` has a door into it (run/compaction symmetry:
207
- * both are model calls a client must be able to stop). Set at ADMISSION, cleared by the
208
- * detached task before `compaction_finished`. */
209
- const compacting = new Map();
210
- /** Fan an event out to this session's subscribers — shared by the observer (run events) and the
211
- * boundary mutations (session-level events, no runId). */
212
- const fanOut = (session, event) => {
213
- const subs = subscribers.get(session);
214
- if (subs)
215
- for (const sub of [...subs])
216
- sub.push(event);
217
- };
218
- /** Emit a HUB-generated event: subscribers first, then the external tap — the boundary-event
219
- * half of a full-vocabulary tap (run events reach it through the observer composition). */
220
- const emitOwn = (session, event) => {
221
- fanOut(session, event);
222
- try {
223
- options.tap?.(session, event);
224
- }
225
- catch (error) {
226
- // Same discipline as the data plane's observer guard: a broken tap is its own problem.
227
- log.warn(`[fastagent] session-control tap threw (event ${event.type}): ${String(error)}`);
228
- }
229
- };
230
- const observer = (session, event, run) => {
231
- if (event.type === "run_started" && event.runId) {
232
- active.set(session, { runId: event.runId, controls: run, pending: { steering: [], followUp: [] } });
233
- }
234
- else if (event.type === "run_settled") {
235
- active.delete(session);
236
- }
237
- else if (event.type === "queue_changed") {
238
- const entry = active.get(session);
239
- if (entry)
240
- entry.pending = event.data;
241
- }
242
- fanOut(session, event);
243
- if (event.type === "run_settled")
244
- publishUsage(session);
245
- };
246
- /**
247
- * Report `usage` as the record now reads it: after a run (a new answer) and after a compaction (the context it
248
- * described is gone). Read after the fact, so it follows the event that caused it.
249
- */
250
- const publishUsage = (session) => {
251
- // Every serve runs a hub, most with nobody attached (a chat channel): reading the whole record each turn for an
252
- // empty audience would cost a full parse per turn, growing with the conversation.
253
- if (!subscribers.get(session)?.size && !options.tap)
254
- return;
255
- reads.state(session).then(({ usage }) => {
256
- if (usage)
257
- emitOwn(session, { type: "state_changed", timestamp: Date.now(), data: { usage } });
258
- }, (error) => log.warn(`[fastagent] session ${session}: usage not published: ${String(error)}`));
259
- };
260
- // The reads, plus the two sessionless declarations. Bound onto a handle below.
261
- const reads = {
262
- async commands() {
263
- return (await options.commands?.()) ?? [];
264
- },
265
- capabilities: () => {
266
- const b = boundary;
267
- return {
268
- steering: true,
269
- followUp: true,
270
- compaction: !!b,
271
- // Every write — a property, a copied record, a removed one — needs the boundary wiring for
272
- // its LEASE, so they answer the same question: a write that races a run would hang the next
273
- // turn off a stale branch, or pull the record out from under it.
274
- fork: !!b,
275
- delete: !!b,
276
- // The CONTRACT's list, not a copy of it: a field added to SessionUpdate is advertised
277
- // without anyone remembering to, and one removed cannot linger here.
278
- updatable: b ? [...UPDATE_FIELDS] : [],
279
- // The registry is a deployment fact (any session may be pointed at any of it). Each model carries the levels
280
- // a session on it accepts, so a picker can offer them before the session runs on that model;
281
- // `state().availableThinkingLevels` answers for the model the session is on.
282
- ...(b ? { allowedModels: describeModels(b.models.getModels()) } : {}),
283
- toolProgress: true, // tool_progress IS delivered (replace-semantics snapshots)
284
- usage: true, // state().usage, and state_changed{usage} after each run and compaction
285
- };
286
- },
287
- async state(session) {
288
- const run = active.get(session);
289
- const opened = await sessions.openIfExists(session);
290
- const leafEntryId = opened ? publishedLeaf(opened) : undefined;
291
- // What will RUN, not the raw record: a client steering a session needs the pair that executes.
292
- // Without a boundary there is no model to resolve against, and the fields are absent.
293
- // OBSERVATION IS TOTAL: an unreadable entry chain leaves the pair absent too (the same shape a
294
- // control-less deployment answers with) rather than rejecting a read that has no error-code
295
- // channel to explain itself. The fault is not swallowed — it surfaces where codes exist: the
296
- // next invoke fails (binding a session walks the same chain) and a boundary dispatch answers
297
- // `boundary_command_failed`. Here it is a server-side warn.
298
- const b = boundary;
299
- let settings;
300
- let usage;
301
- if (!opened) {
302
- // An id with no record is an empty conversation at the defaults: its first turn would run on these.
303
- if (b)
304
- settings = resolveSessionSettings([], b.models, b.defaults);
305
- }
306
- else {
307
- try {
308
- const path = activePath(opened);
309
- if (b)
310
- settings = resolveSessionSettings(path, b.models, b.defaults);
311
- const selected = settings?.model;
312
- const latest = selected?.api === "pi-virtual"
313
- ? opened
314
- .buildSessionContext()
315
- .messages.filter((message) => message.role === "assistant" &&
316
- message.stopReason !== "error" &&
317
- message.stopReason !== "aborted")
318
- .at(-1)
319
- : undefined;
320
- const physical = latest?.role === "assistant" ? b?.models.getModel(latest.provider, latest.model) : undefined;
321
- usage = sessionUsage(path, opened, (physical ?? selected)?.contextWindow);
322
- }
323
- catch (error) {
324
- log.warn(`[fastagent] session ${session}: state unreadable (entry chain): ${String(error)}`);
325
- }
326
- }
327
- const name = opened?.getSessionName();
328
- return {
329
- status: run ? "running" : compacting.has(session) ? "compacting" : "idle",
330
- ...(name ? { name } : {}),
331
- ...(run ? { activeRunId: run.runId } : {}),
332
- ...(settings
333
- ? {
334
- model: `${settings.model.provider}/${settings.model.id}`,
335
- thinkingLevel: settings.thinkingLevel,
336
- availableThinkingLevels: settings.availableThinkingLevels,
337
- }
338
- : {}),
339
- pending: run
340
- ? { steering: [...run.pending.steering], followUp: [...run.pending.followUp] }
341
- : { steering: [], followUp: [] },
342
- ...(usage ? { usage } : {}),
343
- ...(leafEntryId ? { leafEntryId } : {}),
344
- };
345
- },
346
- async entries(session, opts) {
347
- const opened = await sessions.openIfExists(session);
348
- if (!opened)
349
- return { entries: [] };
350
- // LEAF FIRST, then the journal: `getEntries()` hands back a SNAPSHOT, so reading it first
351
- // would race any concurrent append into a leaf the snapshot cannot contain — a live turn
352
- // reading as a dangling head. This order makes the journal a superset of the leaf's chain,
353
- // which is what lets the published head be trusted as one of the published entries.
354
- const leafEntryId = publishedLeaf(opened);
355
- const journal = opened.getEntries();
356
- // The published tree must be SELF-CONTAINED: a `parentId` pointing at an entry this plane does
357
- // not publish (a label, one of our markers) would break the walk a client does from
358
- // `leafEntryId` upward — it would stop at an id it cannot look up and report a short path.
359
- // So a skipped entry is spliced out: its children point at the nearest published ancestor.
360
- const byId = new Map(journal.map((e) => [e.id, e]));
361
- const publishedParent = (entry) => {
362
- let parent = entry.parentId ?? undefined;
363
- while (parent) {
364
- const found = byId.get(parent);
365
- // A gap in the chain is left as a gap — `state()` reports it and the next invoke fails on
366
- // it (design §7); inventing a parent here would hide a corrupt journal.
367
- if (!found)
368
- return parent;
369
- if (isNavigable(found))
370
- return parent;
371
- parent = found.parentId ?? undefined;
372
- }
373
- return undefined;
374
- };
375
- const all = journal.filter(isNavigable).map((e) => toSessionEntry(e, publishedParent(e)));
376
- let entries = all;
377
- if (opts?.since !== undefined) {
378
- const idx = all.findIndex((e) => e.id === opts.since);
379
- // Unknown cursor → full backfill (correct, merely larger): the client's cursor may predate
380
- // a repository the session was rebuilt into; silently skipping records would lose history.
381
- if (idx >= 0)
382
- entries = all.slice(idx + 1);
383
- }
384
- return { entries, ...(leafEntryId ? { leafEntryId } : {}) };
385
- },
386
- async image(session, ref) {
387
- // ponytail: opens (parses) the whole record per image; an index when long image-heavy sessions make it show.
388
- const opened = await sessions.openIfExists(session);
389
- return opened ? imageAt(opened, ref) : undefined;
390
- },
391
- events(session) {
392
- // ONE CALL IS ONE SUBSCRIPTION, and `ready` is its boundary: the moment after which nothing emitted for this
393
- // session can be missed (session.ts, SessionEventStream). A second iteration of the same stream would be a
394
- // second subscription inheriting the first one's readiness — a guarantee it never earned — so it is refused
395
- // rather than documented. Matching the remote client, which opens one connection per stream.
396
- //
397
- // Registration happens on the FIRST next(), not at iterator creation: subscription semantics = you are
398
- // subscribed while you iterate; an iterator obtained but never driven must not buffer. Teardown goes through
399
- // Subscriber.close() so a `return()` on a QUIET stream resolves promptly instead of queueing behind a
400
- // never-settling pull — without it every attach/detach against an idle session would leak a permanently
401
- // registered subscriber.
402
- let registered;
403
- let unestablished;
404
- const ready = new Promise((resolve, reject) => {
405
- registered = resolve;
406
- unestablished = reject;
407
- });
408
- // An awaiter gets the rejection; a consumer that only iterates would otherwise take an unhandled one for a
409
- // cancellation it performed itself.
410
- ready.catch(() => { });
411
- let iterated = false;
412
- return {
413
- ready,
414
- [Symbol.asyncIterator]() {
415
- if (iterated) {
416
- throw new Error("session events: this stream is one subscription — call events() again for another");
417
- }
418
- iterated = true;
419
- let sub;
420
- // `finished` is its own state: `sub === undefined` alone would conflate "not yet
421
- // registered" with "terminated", and a post-done next() would silently REGISTER A FRESH
422
- // subscription — the exact ghost-subscriber leak this class exists to prevent, reachable
423
- // by any wrapper that polls one extra time. done is terminal, per the iterator protocol.
424
- let finished = false;
425
- const cleanup = () => {
426
- finished = true;
427
- if (!sub) {
428
- // Cancelled before it registered: `ready` has a promise it cannot keep, and hanging on it is the one
429
- // outcome a caller cannot diagnose. The remote client answers the same way.
430
- unestablished(new Error("session events: cancelled before the subscription was established"));
431
- return;
432
- }
433
- sub.close();
434
- const set = subscribers.get(session);
435
- if (set) {
436
- set.delete(sub);
437
- if (set.size === 0)
438
- subscribers.delete(session);
439
- }
440
- sub = undefined;
441
- };
442
- return {
443
- async next() {
444
- if (finished)
445
- return { done: true, value: undefined };
446
- if (!sub) {
447
- sub = new Subscriber(session);
448
- let set = subscribers.get(session);
449
- if (!set) {
450
- set = new Set();
451
- subscribers.set(session, set);
452
- }
453
- set.add(sub);
454
- registered();
455
- }
456
- const result = await sub.next();
457
- if (result.done)
458
- cleanup();
459
- return result;
460
- },
461
- async return(value) {
462
- cleanup();
463
- return { done: true, value: value };
464
- },
465
- };
466
- },
467
- };
468
- },
469
- };
470
- /** No boundary wiring — the write path does not exist in this deployment. A capability-gating
471
- * client never lands here; one that does gets the same answer every gate publishes. */
472
- const unsupported = (what) => ({
473
- ok: false,
474
- error: {
475
- code: UNSUPPORTED_CAPABILITY_CODE,
476
- message: `${what} is not supported by this runtime (no boundary wiring)`,
477
- retryable: false,
478
- },
479
- });
480
- const noSuchSession = (session) => ({
481
- ok: false,
482
- error: { code: NO_SUCH_SESSION_CODE, message: `session "${session}" does not exist`, retryable: false },
483
- });
484
- /**
485
- * The refusal for an id no client could then open: the empty string, `.` and `..` are not URL path segments
486
- * (isAddressableSession), so minting one would put a row in list() that nothing can address — listed, unopenable by
487
- * the client that just listed it. Both writes that create a record (fork's `into`, and an update to an id with no
488
- * record) refuse it.
489
- */
490
- const unaddressable = (session) => invalid(`${JSON.stringify(session)} cannot be a session id — the control plane could not address it`);
491
- const nothingToCompact = () => ({
492
- ok: false,
493
- error: {
494
- code: NOTHING_TO_COMPACT_CODE,
495
- message: "nothing to compact — the session has no compactable history yet; retry after more turns",
496
- retryable: false,
497
- },
498
- });
499
- const invalid = (message) => ({
500
- ok: false,
501
- error: { code: INVALID_COMMAND_CODE, message, retryable: false },
502
- });
503
- const busy = () => ({
504
- ok: false,
505
- error: {
506
- code: SESSION_BUSY_CODE,
507
- message: "session busy: a run (or another write) is in flight — retry at idle",
508
- retryable: true,
509
- },
510
- });
511
- const failed = (error) => ({
512
- ok: false,
513
- // A command that binds the session (compaction is a model call) on a session with no model: the same code the
514
- // invoke answers, since the same fix (`update({ model })`) applies.
515
- error: error instanceof MissingModel
516
- ? { code: MISSING_MODEL_CODE, message: error.message, retryable: false }
517
- : { code: BOUNDARY_COMMAND_FAILED_CODE, message: String(error), retryable: true },
518
- });
519
- const runBoundary = (command) => Effect.runPromise(Effect.scoped(command).pipe(Effect.catchTag("SessionBusy", () => Effect.succeed(busy())), Effect.catchCause((cause) => Effect.succeed(failed(portError(cause))))));
520
- /** steer / follow_up / abort — the run actions. They reach the LIVE run through the controls
521
- * registered with `run_started`; nothing durable is written. */
522
- const runAction = (session, action) => Effect.runPromise(Effect.gen(function* () {
523
- // pi dequeues a queued prompt by matching its text when it enters the conversation, and skips an empty one
524
- // (`_handleAgentEvent`): an image-only prompt would sit in `pending` for the whole run and then read as dropped.
525
- if ((action.type === "steer" || action.type === "follow_up") && action.prompt.text === "")
526
- return invalid(`${action.type} needs non-empty text: a queued prompt is reported by its text`);
527
- const run = active.get(session);
528
- if (!run) {
529
- // Run/compaction symmetry: an in-flight compaction is a model call too, and `abort` is its
530
- // only door — interrupting it converges through the detached task's catch into
531
- // `compaction_finished{aborted}` with the lease released; answering no_active_run against a
532
- // state() that says "compacting" would be a lie.
533
- const comp = action.type === "abort" ? compacting.get(session) : undefined;
534
- if (comp) {
535
- comp.abort();
536
- return { ok: true }; // no runId — the outcome travels as compaction_finished{aborted}
537
- }
538
- // Rejected BEFORE acceptance: no run exists, nothing happened. retryable: false — the same
539
- // call fails again; call it after state() shows an active run.
540
- return {
541
- ok: false,
542
- error: {
543
- code: NO_ACTIVE_RUN_CODE,
544
- message: `no active run for this session — ${action.type} modulates a run an invoke is driving`,
545
- retryable: false,
546
- },
547
- };
548
- }
549
- if (!run.controls) {
550
- // A run EXISTS (state() rightly reports running) but was registered observation-only (the
551
- // observer seam allows run_started without controls). That is a CAPABILITY problem, not a run
552
- // problem — permanent for this wiring, so neither no_active_run (would poll forever) nor
553
- // run_command_failed (transient) fits.
554
- return {
555
- ok: false,
556
- error: {
557
- code: UNSUPPORTED_CAPABILITY_CODE,
558
- message: `the active run registered without modulation controls (observation-only) — ${action.type} cannot reach it`,
559
- retryable: false,
560
- },
561
- };
562
- }
563
- const controls = run.controls;
564
- if (action.type === "abort") {
565
- yield* port(() => controls.abort());
566
- return { ok: true, runId: run.runId };
567
- }
568
- const disposition = yield* port(() => action.type === "steer" ? controls.steer(action.prompt) : controls.followUp(action.prompt));
569
- return { ok: true, runId: run.runId, disposition };
570
- }).pipe(Effect.catchCause((cause) => Effect.succeed({
571
- ok: false,
572
- error: { code: RUN_COMMAND_FAILED_CODE, message: String(portError(cause)), retryable: false },
573
- }))));
574
- /**
575
- * {@link Session.update} — validate the whole patch, take the lease once, hand the writes to the
576
- * store, report what landed.
577
- *
578
- * Everything before the lease is validation, which is what makes a rejected patch leave nothing
579
- * behind. What a value MEANS is decided here (a model spec against the registry, a level against
580
- * the model it lands on); how a record takes it is the store's.
581
- */
582
- const updateOf = (session, patch) => runBoundary(Effect.gen(function* () {
583
- // Keys, not values: a field this runtime does not know must not be silently skipped — that is a
584
- // client typo, or a newer client talking to an older serve, and both need to hear about it.
585
- const named = Object.keys(patch);
586
- const unknown = named.filter((f) => !UPDATE_FIELDS.includes(f));
587
- if (unknown.length > 0) {
588
- return unsupported(`update field(s) ${unknown.join(", ")} — capabilities().updatable lists what this serve sets`);
589
- }
590
- const fields = named.filter((f) => patch[f] !== undefined);
591
- if (fields.length === 0)
592
- return { ok: true }; // an empty patch asks for nothing, and gets it
593
- const b = boundary;
594
- if (!b)
595
- return unsupported(`update(${fields.join(", ")})`);
596
- // PAYLOAD validation first — before the session is even opened, and long before the lease: an
597
- // invalid value must not briefly block a run.
598
- let model;
599
- if (patch.model !== undefined) {
600
- const slash = patch.model.indexOf("/");
601
- model = slash > 0 ? b.models.getModel(patch.model.slice(0, slash), patch.model.slice(slash + 1)) : undefined;
602
- if (!model) {
603
- return invalid(`unknown model "${patch.model}" — capabilities().allowedModels lists the accepted models`);
604
- }
605
- }
606
- if (patch.thinkingLevel !== undefined && !THINKING_LEVELS.has(patch.thinkingLevel)) {
607
- return invalid(`unknown thinking level "${patch.thinkingLevel}" — state().availableThinkingLevels lists what this session accepts`);
608
- }
609
- // A name is the client's own label; the only thing that cannot be one is nothing.
610
- if (patch.name !== undefined && patch.name.trim() === "")
611
- return invalid("a session name cannot be empty");
612
- // An id with no record takes properties too: its first turn is the "next turn" they apply to, so the write
613
- // creates its record (below, under the lease). (Read-only handle — the WRITE one is opened under the lease,
614
- // and this one is discarded.)
615
- const existing = yield* port(() => sessions.openIfExists(session));
616
- if (!existing && !isAddressableSession(session))
617
- return unaddressable(session);
618
- if (patch.leafEntryId !== undefined) {
619
- // A target that cannot BE a leaf is a permanent payload error, not a session error — the same
620
- // disposition as an unknown model spec. Same predicate `entries()` publishes by, so
621
- // "everything published is a position" holds by construction rather than by two literals
622
- // agreeing.
623
- const entry = existing?.getEntry(patch.leafEntryId);
624
- if (!entry || !isNavigable(entry)) {
625
- return invalid(entry
626
- ? `entry "${patch.leafEntryId}" is not a position — entries() publishes every id you can move to, and this is not one of them`
627
- : `entry "${patch.leafEntryId}" does not exist in session "${session}" — entries() lists the positions`);
628
- }
629
- }
630
- if (patch.thinkingLevel !== undefined) {
631
- // The same set `state()` showed the client. Reject here rather than record a level the run
632
- // would not use. The read is guarded because this must never REJECT — the contract promises a
633
- // SessionResult, so an unreadable chain has to arrive as a code.
634
- let resolved;
635
- try {
636
- // Against the path this patch LANDS on: a leaf move is written first, and the branch it
637
- // moves to can carry a model override of its own — validating on the path being left would
638
- // reject a level the destination supports, and accept one it does not.
639
- const path = existing ? activePath(existing, patch.leafEntryId) : [];
640
- resolved = resolveSessionSettings(path, b.models, b.defaults);
641
- }
642
- catch (error) {
643
- return failed(error);
644
- }
645
- // An explicit model in the same patch wins over the one that path resolves to: it is applied
646
- // after the move, so it is what the session ends up running. With neither (no model recorded, no
647
- // default), any level of the vocabulary is recorded, and clamped once the session has a model, like a
648
- // recorded level after a model change.
649
- const target = model ?? resolved?.model;
650
- if (target) {
651
- const levels = getSupportedThinkingLevels(target);
652
- if (!levels.includes(patch.thinkingLevel)) {
653
- return invalid(`thinking level "${patch.thinkingLevel}" is not supported by ${target.provider}/${target.id} (allowed: ${levels.join(", ")})`);
654
- }
655
- }
656
- }
657
- // Release before publishing state_changed: an observer may immediately issue another command.
658
- const applied = yield* Effect.scoped(Effect.gen(function* () {
659
- yield* acquireSessionLease(b.lease, session);
660
- // portJoin, not port: the lease is held for the duration of this write, so an interrupted
661
- // fiber must not release it while the store is still writing (effect-port.ts, rule 1).
662
- // A never-run session's record is created through the data plane's own path, under the same lease an
663
- // invoke creating it takes; one that invoke created meanwhile is simply opened.
664
- if (!existing)
665
- yield* portJoin(() => sessions.openOrCreate(session));
666
- return yield* portJoin(() => sessions.applyProperties(session, {
667
- ...(patch.name !== undefined ? { name: patch.name } : {}),
668
- ...(model ? { model: { provider: model.provider, id: model.id } } : {}),
669
- ...(patch.thinkingLevel !== undefined ? { thinkingLevel: patch.thinkingLevel } : {}),
670
- ...(patch.leafEntryId !== undefined ? { leafEntryId: patch.leafEntryId } : {}),
671
- }));
672
- }));
673
- if (!applied)
674
- return noSuchSession(session); // deleted in the window between the read and the lease
675
- if (applied.landed.length > 0) {
676
- // ONE event for the patch, built from what the RECORD holds. The settings pair rides along
677
- // whenever anything but the name changed: model and thinking level are one setting, and a
678
- // moved leaf can drop an override that used to apply.
679
- let settings;
680
- if (applied.path) {
681
- try {
682
- settings = resolveSessionSettings(applied.path, b.models, b.defaults);
683
- }
684
- catch (error) {
685
- // Already durable, so an unresolvable pair must NOT read as "nothing took effect": report
686
- // the position without it and let the next invoke — which walks the same path — be where
687
- // the fault surfaces.
688
- log.warn(`[fastagent] session ${session}: updated, settings unresolvable: ${String(error)}`);
689
- }
690
- }
691
- // A moved head reads a different newest answer, and another model has another window: the event carries
692
- // the usage the session now has, and carries none when it has none. Read back through `state()`, the one
693
- // derivation; a read failure only costs the field, since the write has already landed.
694
- const usage = applied.landed.some((f) => f === "leafEntryId" || f === "model")
695
- ? yield* port(() => reads.state(session)).pipe(Effect.map((state) => state.usage), Effect.catchCause((cause) => Effect.sync(() => {
696
- log.warn(`[fastagent] session ${session}: updated, usage unreadable: ${String(portError(cause))}`);
697
- return undefined;
698
- })))
699
- : undefined;
700
- emitOwn(session, {
701
- type: "state_changed",
702
- timestamp: Date.now(),
703
- data: {
704
- ...(applied.landed.includes("leafEntryId") ? { leafEntryId: applied.leafEntryId } : {}),
705
- ...(settings && applied.landed.some((f) => f !== "name")
706
- ? { model: `${settings.model.provider}/${settings.model.id}`, thinkingLevel: settings.thinkingLevel }
707
- : {}),
708
- ...(applied.landed.includes("name") && applied.name ? { name: applied.name } : {}),
709
- ...(usage ? { usage } : {}),
710
- },
711
- });
712
- }
713
- if (applied.failure !== undefined) {
714
- // `boundary_command_failed` means nothing durable landed. When something did, the client needs
715
- // a different sentence — and the fields, so it knows what its retry would repeat.
716
- return applied.landed.length === 0
717
- ? failed(applied.failure)
718
- : {
719
- ok: false,
720
- error: {
721
- code: PARTIAL_UPDATE_CODE,
722
- message: `applied ${applied.landed.join(", ")}, then failed: ${String(applied.failure)} — read state() before retrying`,
723
- retryable: false,
724
- },
725
- };
726
- }
727
- return { ok: true };
728
- }));
729
- /** Admission waits for binding and pi's own admission (its cut point), not the model call.
730
- * Its execution scope survives the control response; completion is published only after releasing the shared
731
- * lease. */
732
- const compactOf = (session, instructions) => {
733
- const admitted = Deferred.makeUnsafe();
734
- let accepted = false;
735
- let aborted = false;
736
- const work = Effect.scoped(Effect.gen(function* () {
737
- const b = boundary;
738
- if (!b)
739
- return unsupported("compact()");
740
- const existing = yield* port(() => sessions.openIfExists(session));
741
- if (!existing)
742
- return noSuchSession(session);
743
- // A record with no conversation in it (the list's `messageCount: 0`, e.g. one `update()` created before a first
744
- // turn) has nothing to compact on any branch. Said before binding, because binding is a write: pi records the
745
- // model and thinking level on a session that has no messages, so asking pi would add two entries and then
746
- // refuse. Whether a record WITH messages has a cut point stays pi's decision (below).
747
- if (!existing.getEntries().some(isConversationMessage))
748
- return nothingToCompact();
749
- yield* acquireSessionLease(b.lease, session);
750
- const bound = yield* acquireSession(b.sessionFactory, session);
751
- yield* Effect.acquireRelease(Effect.try({
752
- try: () => bound.subscribe((event) => {
753
- if (event.type !== "summarization_retry_scheduled")
754
- return;
755
- log.warn(`[fastagent] compaction retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms (session ${session}): ${event.errorMessage}`);
756
- emitOwn(session, toRetryScheduledEvent(event));
757
- }),
758
- catch: (error) => new PortFailure(error),
759
- }), (unsubscribe) => portCleanup("unsubscribe", unsubscribe));
760
- // Admission is pi's own decision, observed rather than predicted: a prediction has to track pi's private
761
- // cut-point rules release by release. It settles before the model call.
762
- const compaction = startCompaction(bound, instructions, () => {
763
- accepted = true;
764
- compacting.set(session, {
765
- abort: () => {
766
- aborted = true;
767
- bound.abortCompaction();
768
- },
769
- });
770
- emitOwn(session, { type: "compaction_started", timestamp: Date.now(), data: {} });
771
- Deferred.doneUnsafe(admitted, Effect.succeed({ ok: true }));
772
- });
773
- const admission = yield* portAbort("compact admission", () => compaction.admission, () => bound.abortCompaction());
774
- if (admission === "nothing_to_compact")
775
- return nothingToCompact();
776
- // Any other error pi raises before admission. A served session has no known trigger: it always binds a
777
- // model, and pi swallows a credential failure here and meets it at the model call (compaction_finished).
778
- // NOT retryable all the same: it is pi's decision on this session, not a transport or lease fault that
779
- // clears on its own, so repeating the call would meet it again.
780
- if (admission !== "admitted") {
781
- return {
782
- ok: false,
783
- error: { code: BOUNDARY_COMMAND_FAILED_CODE, message: String(admission.refused), retryable: false },
784
- };
785
- }
786
- const done = yield* portAbort("compact", () => compaction.done, () => bound.abortCompaction());
787
- return { summary: done.summary };
788
- })).pipe(Effect.catchTag("SessionBusy", () => Effect.succeed(busy())), Effect.catchCause((cause) => Effect.succeed(failed(portError(cause)))), Effect.tap((result) => Effect.sync(() => {
789
- if (accepted) {
790
- compacting.delete(session);
791
- emitOwn(session, {
792
- type: "compaction_finished",
793
- timestamp: Date.now(),
794
- data: "summary" in result ? result : aborted ? { aborted: true } : { error: result.error.message },
795
- });
796
- if ("summary" in result)
797
- publishUsage(session);
798
- }
799
- Deferred.doneUnsafe(admitted, Effect.succeed("summary" in result ? { ok: true } : result));
800
- })));
801
- // The admitted operation, rather than the request waiting on this Deferred, owns the scope.
802
- Effect.runFork(work);
803
- return Effect.runPromise(Deferred.await(admitted));
804
- };
805
- /**
806
- * Copy a history into a new session. IDEMPOTENT by construction: `into` is the caller's id, so a
807
- * repeat of a fork that already landed answers `ok: true` and writes nothing — a client retrying a
808
- * request whose response it never saw does not get a second record. Provenance is what makes that
809
- * safe rather than merely quiet: the same `into` naming a session that came from somewhere else is
810
- * a rejection, not an overwrite.
811
- */
812
- const forkOf = (options) => runBoundary(Effect.gen(function* () {
813
- const { from, at, into } = options;
814
- /** WHICH fork this is: source + branch point. Two forks of one session at different entries are
815
- * different requests, so a retry of one must not be answered by the other. */
816
- const provenance = `${from}@${at}`;
817
- const b = boundary;
818
- if (!b)
819
- return unsupported("fork()");
820
- if (!isAddressableSession(into))
821
- return unaddressable(into);
822
- const source = yield* port(() => sessions.openIfExists(from));
823
- if (!source)
824
- return noSuchSession(from);
825
- // The entry predicate is the one `entries()` publishes by, so "everything published is forkable"
826
- // holds by construction — the same argument the leaf move makes.
827
- const entry = source.getEntry(at);
828
- if (!entry || !isNavigable(entry)) {
829
- return invalid(`entry "${at}" is not a forkable position in session "${from}" — entries() lists the ids`);
830
- }
831
- const existingTarget = yield* port(() => sessions.openIfExists(into));
832
- if (existingTarget) {
833
- // Already forked from HERE: the request already happened, so answering ok is the truth rather
834
- // than a convenience. Anything else under that id is a different history, and saying yes would
835
- // be the id lying about what it holds.
836
- return forkProvenance(existingTarget) === provenance
837
- ? { ok: true }
838
- : invalid(`session "${into}" already exists with a different history — fork mints nothing over it`);
839
- }
840
- // BOTH ends. The source lease keeps the copy from reading a history a run is mid-write on; the
841
- // destination lease closes the window the existence check above leaves open — an invoke creating
842
- // `into`, or a second fork from a DIFFERENT source (whose source lease is another key entirely),
843
- // otherwise lands between that check and this write. tryAcquire never blocks, so taking two
844
- // cannot deadlock.
845
- yield* acquireSessionLease(b.lease, from);
846
- yield* acquireSessionLease(b.lease, into);
847
- // Recheck under both leases: another creator may have taken the target since validation.
848
- const raced = yield* port(() => sessions.openIfExists(into));
849
- if (raced) {
850
- return forkProvenance(raced) === provenance
851
- ? { ok: true }
852
- : invalid(`session "${into}" already exists with a different history — fork mints nothing over it`);
853
- }
854
- // portJoin: both leases are held for this write and must outlive it (effect-port.ts, rule 1).
855
- yield* portJoin(() => sessions.fork(from, at, into, provenance));
856
- return { ok: true };
857
- }));
858
- const deleteOf = (session) => runBoundary(Effect.gen(function* () {
859
- const b = boundary;
860
- if (!b)
861
- return unsupported("delete()");
862
- const existing = yield* port(() => sessions.openIfExists(session));
863
- if (!existing)
864
- return noSuchSession(session);
865
- const removed = yield* Effect.scoped(Effect.gen(function* () {
866
- yield* acquireSessionLease(b.lease, session);
867
- // portJoin: the lease must outlive the removal (effect-port.ts, rule 1).
868
- return yield* portJoin(() => sessions.delete(session));
869
- }));
870
- if (!removed)
871
- return noSuchSession(session);
872
- // The session is gone, so its live streams have nothing left to report: end them rather than
873
- // hold connections open on a record that no longer exists. A client's reconnect then reads an
874
- // empty `state()`, which is the truth.
875
- for (const sub of [...(subscribers.get(session) ?? [])])
876
- sub.close();
877
- subscribers.delete(session);
878
- return { ok: true };
879
- }));
880
- const control = {
881
- capabilities: reads.capabilities,
882
- commands: reads.commands,
883
- sessions: {
884
- // The one read that may REJECT (design §13): `[]` is what a deployment with no sessions
885
- // answers, so a store that cannot be read must not borrow that shape. The transport turns the
886
- // throw into a coded non-2xx; nothing here swallows it.
887
- list: () => sessions.list(),
888
- fork: forkOf,
889
- // A PURE BINDING: an id and the closures above it. Nothing is checked here — the calls answer
890
- // that, each in its own vocabulary — and nothing is cached, so two handles for one id are
891
- // interchangeable.
892
- get: (session) => ({
893
- id: session,
894
- state: () => reads.state(session),
895
- entries: (options) => reads.entries(session, options),
896
- image: (ref) => reads.image(session, ref),
897
- events: () => reads.events(session),
898
- update: (patch) => updateOf(session, patch),
899
- steer: (prompt) => runAction(session, { type: "steer", prompt }),
900
- followUp: (prompt) => runAction(session, { type: "follow_up", prompt }),
901
- abort: () => runAction(session, { type: "abort" }),
902
- compact: (options) => compactOf(session, options?.instructions),
903
- delete: () => deleteOf(session),
904
- }),
905
- },
906
- };
907
- return { control, observer };
908
- }