@fastagent-sh/fastagent 0.21.1 → 0.22.1

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 (388) hide show
  1. package/README.md +9 -12
  2. package/dist/agent.d.ts +24 -37
  3. package/dist/agent.js +7 -15
  4. package/dist/atomic-write.d.ts +13 -17
  5. package/dist/atomic-write.js +18 -29
  6. package/dist/bind.d.ts +12 -22
  7. package/dist/bind.js +18 -27
  8. package/dist/channel.d.ts +11 -33
  9. package/dist/channels/agentcore-limits.d.ts +2 -13
  10. package/dist/channels/agentcore-limits.js +2 -13
  11. package/dist/channels/agentcore-protocol.d.ts +67 -58
  12. package/dist/channels/agentcore-protocol.js +23 -15
  13. package/dist/channels/agentcore-service.d.ts +19 -33
  14. package/dist/channels/agentcore-service.js +176 -59
  15. package/dist/channels/agentcore.d.ts +39 -33
  16. package/dist/channels/agentcore.js +90 -224
  17. package/dist/channels/body.d.ts +15 -2
  18. package/dist/channels/body.js +24 -2
  19. package/dist/channels/busy.d.ts +1 -21
  20. package/dist/channels/busy.js +2 -39
  21. package/dist/channels/control.d.ts +17 -62
  22. package/dist/channels/control.js +74 -182
  23. package/dist/channels/define-channel.d.ts +39 -0
  24. package/dist/channels/define-channel.js +9 -0
  25. package/dist/channels/discover.d.ts +10 -31
  26. package/dist/channels/discover.js +37 -31
  27. package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
  28. package/dist/channels/feishu/bootstrap-token.js +7 -23
  29. package/dist/channels/feishu/card.d.ts +14 -28
  30. package/dist/channels/feishu/card.js +16 -30
  31. package/dist/channels/feishu/cloud.d.ts +1 -5
  32. package/dist/channels/feishu/context-buffer.d.ts +4 -23
  33. package/dist/channels/feishu/context-buffer.js +8 -31
  34. package/dist/channels/feishu/crypto.d.ts +10 -5
  35. package/dist/channels/feishu/crypto.js +12 -19
  36. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  37. package/dist/channels/feishu/feishu-api.js +5 -4
  38. package/dist/channels/feishu/feishu.d.ts +4 -10
  39. package/dist/channels/feishu/feishu.js +113 -227
  40. package/dist/channels/feishu/invoke-turn.d.ts +5 -4
  41. package/dist/channels/feishu/invoke-turn.js +36 -54
  42. package/dist/channels/feishu/model.d.ts +4 -21
  43. package/dist/channels/feishu/model.js +1 -8
  44. package/dist/channels/feishu/normalize.d.ts +2 -10
  45. package/dist/channels/feishu/normalize.js +7 -42
  46. package/dist/channels/feishu/parse.d.ts +9 -29
  47. package/dist/channels/feishu/parse.js +8 -26
  48. package/dist/channels/feishu/preview.d.ts +12 -14
  49. package/dist/channels/feishu/preview.js +108 -207
  50. package/dist/channels/feishu/register-app.d.ts +8 -23
  51. package/dist/channels/feishu/register-app.js +7 -20
  52. package/dist/channels/feishu/register-webhook.d.ts +2 -9
  53. package/dist/channels/feishu/register-webhook.js +9 -33
  54. package/dist/channels/feishu/scaffold/channel.ts +27 -19
  55. package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
  56. package/dist/channels/feishu/setup-mode.d.ts +11 -36
  57. package/dist/channels/feishu/setup-mode.js +7 -26
  58. package/dist/channels/feishu/ws-ingress.d.ts +1 -2
  59. package/dist/channels/feishu/ws-ingress.js +6 -8
  60. package/dist/channels/http.d.ts +15 -15
  61. package/dist/channels/http.js +19 -9
  62. package/dist/channels/kit/attachment-path.d.ts +4 -5
  63. package/dist/channels/kit/attachment-path.js +10 -34
  64. package/dist/channels/kit/context-buffer.d.ts +14 -21
  65. package/dist/channels/kit/context-buffer.js +25 -38
  66. package/dist/channels/kit/delivery.d.ts +35 -0
  67. package/dist/channels/kit/delivery.js +129 -0
  68. package/dist/channels/kit/event-stream.d.ts +5 -0
  69. package/dist/channels/kit/event-stream.js +29 -0
  70. package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
  71. package/dist/channels/kit/invoke-turn-kit.js +89 -32
  72. package/dist/channels/kit/preview-kit.d.ts +18 -72
  73. package/dist/channels/kit/preview-kit.js +22 -88
  74. package/dist/channels/kit/signature.d.ts +3 -9
  75. package/dist/channels/kit/signature.js +3 -9
  76. package/dist/channels/kit/state.d.ts +1 -2
  77. package/dist/channels/kit/state.js +5 -14
  78. package/dist/channels/kit/stop-command.d.ts +1 -2
  79. package/dist/channels/kit/stop-command.js +6 -9
  80. package/dist/channels/kit/tasks.d.ts +1 -1
  81. package/dist/channels/kit/tasks.js +8 -21
  82. package/dist/channels/kit/text.d.ts +2 -16
  83. package/dist/channels/kit/text.js +23 -31
  84. package/dist/channels/kit/thread-participants.d.ts +4 -17
  85. package/dist/channels/kit/thread-participants.js +8 -55
  86. package/dist/channels/kit/transport.d.ts +26 -0
  87. package/dist/channels/kit/transport.js +20 -0
  88. package/dist/channels/kit/turn-queue.d.ts +7 -11
  89. package/dist/channels/kit/turn-queue.js +21 -39
  90. package/dist/channels/kit/turn-runner.d.ts +32 -18
  91. package/dist/channels/kit/turn-runner.js +98 -53
  92. package/dist/channels/kit/turn-store.d.ts +32 -32
  93. package/dist/channels/kit/turn-store.js +45 -88
  94. package/dist/channels/lark/lark.d.ts +1 -5
  95. package/dist/channels/lark/onboard.d.ts +3 -12
  96. package/dist/channels/lark/onboard.js +3 -13
  97. package/dist/channels/lark/scaffold/channel.ts +27 -19
  98. package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
  99. package/dist/channels/registration.d.ts +9 -35
  100. package/dist/channels/registration.js +6 -37
  101. package/dist/channels/secret.js +2 -5
  102. package/dist/channels/serve.d.ts +47 -44
  103. package/dist/channels/serve.js +165 -101
  104. package/dist/channels/slack/config-api.d.ts +4 -27
  105. package/dist/channels/slack/config-api.js +5 -34
  106. package/dist/channels/slack/context-buffer.d.ts +2 -3
  107. package/dist/channels/slack/context-buffer.js +5 -5
  108. package/dist/channels/slack/invoke-turn.d.ts +3 -1
  109. package/dist/channels/slack/invoke-turn.js +25 -34
  110. package/dist/channels/slack/manifest.js +1 -3
  111. package/dist/channels/slack/model.d.ts +1 -2
  112. package/dist/channels/slack/model.js +1 -1
  113. package/dist/channels/slack/onboard.d.ts +4 -3
  114. package/dist/channels/slack/onboard.js +8 -11
  115. package/dist/channels/slack/onboarding-state.d.ts +3 -5
  116. package/dist/channels/slack/onboarding-state.js +9 -6
  117. package/dist/channels/slack/parse.d.ts +4 -13
  118. package/dist/channels/slack/parse.js +7 -28
  119. package/dist/channels/slack/preview.d.ts +12 -2
  120. package/dist/channels/slack/preview.js +292 -269
  121. package/dist/channels/slack/reaction.d.ts +11 -3
  122. package/dist/channels/slack/reaction.js +19 -8
  123. package/dist/channels/slack/register-webhook.js +4 -7
  124. package/dist/channels/slack/scaffold/channel.ts +20 -13
  125. package/dist/channels/slack/scaffold/slack-send.ts +2 -2
  126. package/dist/channels/slack/setup-server.d.ts +1 -5
  127. package/dist/channels/slack/setup-server.js +1 -5
  128. package/dist/channels/slack/shared-api.d.ts +1 -4
  129. package/dist/channels/slack/shared-api.js +2 -13
  130. package/dist/channels/slack/slack-api.d.ts +2 -1
  131. package/dist/channels/slack/slack-api.js +6 -4
  132. package/dist/channels/slack/slack.d.ts +11 -14
  133. package/dist/channels/slack/slack.js +121 -122
  134. package/dist/channels/sse.d.ts +1 -1
  135. package/dist/channels/sse.js +1 -1
  136. package/dist/channels/telegram/context-buffer.d.ts +7 -22
  137. package/dist/channels/telegram/context-buffer.js +6 -23
  138. package/dist/channels/telegram/invoke-turn.d.ts +7 -7
  139. package/dist/channels/telegram/invoke-turn.js +24 -43
  140. package/dist/channels/telegram/parse.d.ts +24 -51
  141. package/dist/channels/telegram/parse.js +32 -84
  142. package/dist/channels/telegram/preview.d.ts +10 -1
  143. package/dist/channels/telegram/preview.js +79 -100
  144. package/dist/channels/telegram/register-webhook.js +5 -23
  145. package/dist/channels/telegram/scaffold/channel.ts +25 -18
  146. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
  147. package/dist/channels/telegram/telegram-api.d.ts +4 -3
  148. package/dist/channels/telegram/telegram-api.js +9 -7
  149. package/dist/channels/telegram/telegram.d.ts +4 -18
  150. package/dist/channels/telegram/telegram.js +38 -94
  151. package/dist/channels/telegram/turn-store.d.ts +11 -14
  152. package/dist/channels/telegram/turn-store.js +8 -6
  153. package/dist/channels/wait-health.d.ts +4 -6
  154. package/dist/channels/wait-health.js +10 -14
  155. package/dist/cli/add-feishu.d.ts +2 -12
  156. package/dist/cli/add-feishu.js +24 -71
  157. package/dist/cli/add-slack.d.ts +5 -3
  158. package/dist/cli/add-slack.js +53 -76
  159. package/dist/cli/auth-view.d.ts +2 -16
  160. package/dist/cli/auth-view.js +2 -16
  161. package/dist/cli/commands/add.js +25 -40
  162. package/dist/cli/commands/chat.d.ts +0 -1
  163. package/dist/cli/commands/chat.js +5 -6
  164. package/dist/cli/commands/deploy/agentcore.js +60 -65
  165. package/dist/cli/commands/deploy/docker.js +50 -23
  166. package/dist/cli/commands/deploy/fly.js +23 -61
  167. package/dist/cli/commands/deploy/railway.js +23 -18
  168. package/dist/cli/commands/deploy/shared.d.ts +38 -57
  169. package/dist/cli/commands/deploy/shared.js +40 -59
  170. package/dist/cli/commands/deploy.d.ts +10 -27
  171. package/dist/cli/commands/deploy.js +38 -60
  172. package/dist/cli/commands/destroy.d.ts +4 -0
  173. package/dist/cli/commands/destroy.js +56 -0
  174. package/dist/cli/commands/dev.d.ts +2 -1
  175. package/dist/cli/commands/dev.js +20 -23
  176. package/dist/cli/commands/info.d.ts +0 -2
  177. package/dist/cli/commands/info.js +83 -49
  178. package/dist/cli/commands/init.d.ts +1 -2
  179. package/dist/cli/commands/init.js +16 -55
  180. package/dist/cli/commands/invoke.d.ts +0 -1
  181. package/dist/cli/commands/invoke.js +5 -7
  182. package/dist/cli/commands/login.d.ts +2 -1
  183. package/dist/cli/commands/login.js +33 -38
  184. package/dist/cli/commands/logs.js +3 -4
  185. package/dist/cli/commands/models.js +1 -5
  186. package/dist/cli/commands/routine-run.d.ts +6 -0
  187. package/dist/cli/commands/routine-run.js +47 -0
  188. package/dist/cli/commands/routine.d.ts +25 -0
  189. package/dist/cli/commands/routine.js +122 -0
  190. package/dist/cli/commands/start.d.ts +31 -3
  191. package/dist/cli/commands/start.js +131 -81
  192. package/dist/cli/commands/tool.js +26 -12
  193. package/dist/cli/fail.d.ts +13 -23
  194. package/dist/cli/fail.js +20 -23
  195. package/dist/cli/invoke-stream.d.ts +1 -8
  196. package/dist/cli/kernel.d.ts +6 -38
  197. package/dist/cli/kernel.js +14 -77
  198. package/dist/cli/models-view.d.ts +1 -9
  199. package/dist/cli/models-view.js +11 -22
  200. package/dist/cli/program.d.ts +5 -6
  201. package/dist/cli/program.js +120 -157
  202. package/dist/cli/serve.d.ts +57 -41
  203. package/dist/cli/serve.js +94 -121
  204. package/dist/cli/shared.d.ts +15 -61
  205. package/dist/cli/shared.js +55 -181
  206. package/dist/cli.js +1 -8
  207. package/dist/collect.d.ts +6 -0
  208. package/dist/collect.js +6 -0
  209. package/dist/core.d.ts +4 -1
  210. package/dist/core.js +9 -7
  211. package/dist/declared-secrets.d.ts +55 -0
  212. package/dist/declared-secrets.js +88 -0
  213. package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
  214. package/dist/deploy/agentcore/aws-cli.js +64 -0
  215. package/dist/deploy/agentcore/destroy.d.ts +51 -0
  216. package/dist/deploy/agentcore/destroy.js +273 -0
  217. package/dist/deploy/agentcore/forwarder.js +28 -79
  218. package/dist/deploy/agentcore/logs.d.ts +2 -18
  219. package/dist/deploy/agentcore/logs.js +27 -51
  220. package/dist/deploy/agentcore/plan.d.ts +85 -105
  221. package/dist/deploy/agentcore/plan.js +152 -314
  222. package/dist/deploy/agentcore/run.d.ts +28 -28
  223. package/dist/deploy/agentcore/run.js +227 -306
  224. package/dist/deploy/agentcore/zip.d.ts +2 -15
  225. package/dist/deploy/agentcore/zip.js +3 -17
  226. package/dist/deploy/build-context.d.ts +21 -0
  227. package/dist/deploy/build-context.js +137 -0
  228. package/dist/deploy/channel-ingress.d.ts +26 -39
  229. package/dist/deploy/channel-ingress.js +57 -28
  230. package/dist/deploy/container.d.ts +20 -34
  231. package/dist/deploy/container.js +49 -100
  232. package/dist/deploy/docker/plan.d.ts +12 -11
  233. package/dist/deploy/docker/plan.js +43 -24
  234. package/dist/deploy/docker/run.d.ts +22 -25
  235. package/dist/deploy/docker/run.js +39 -37
  236. package/dist/deploy/fly/plan.d.ts +15 -44
  237. package/dist/deploy/fly/plan.js +41 -73
  238. package/dist/deploy/fly/run.d.ts +16 -51
  239. package/dist/deploy/fly/run.js +42 -95
  240. package/dist/deploy/hosts.d.ts +24 -3
  241. package/dist/deploy/hosts.js +24 -3
  242. package/dist/deploy/preflight.d.ts +47 -41
  243. package/dist/deploy/preflight.js +282 -383
  244. package/dist/deploy/railway/plan.d.ts +16 -42
  245. package/dist/deploy/railway/plan.js +42 -60
  246. package/dist/deploy/railway/run.d.ts +30 -68
  247. package/dist/deploy/railway/run.js +86 -100
  248. package/dist/deploy/registration-gate.d.ts +6 -13
  249. package/dist/deploy/residency.d.ts +37 -0
  250. package/dist/deploy/residency.js +24 -0
  251. package/dist/deploy/runner.d.ts +10 -9
  252. package/dist/deploy/runner.js +7 -18
  253. package/dist/deploy/secrets.d.ts +40 -41
  254. package/dist/deploy/secrets.js +117 -103
  255. package/dist/deploy/workspace.d.ts +50 -0
  256. package/dist/deploy/workspace.js +197 -0
  257. package/dist/dev-supervisor.d.ts +1 -9
  258. package/dist/dev-supervisor.js +21 -64
  259. package/dist/effect-port.d.ts +50 -0
  260. package/dist/effect-port.js +77 -0
  261. package/dist/engines/pi/agent-session-factory.d.ts +50 -69
  262. package/dist/engines/pi/agent-session-factory.js +249 -146
  263. package/dist/engines/pi/auth.d.ts +15 -11
  264. package/dist/engines/pi/auth.js +96 -99
  265. package/dist/engines/pi/chat.d.ts +1 -5
  266. package/dist/engines/pi/chat.js +2 -14
  267. package/dist/engines/pi/config.d.ts +53 -51
  268. package/dist/engines/pi/config.js +83 -92
  269. package/dist/engines/pi/create.d.ts +60 -75
  270. package/dist/engines/pi/create.js +108 -141
  271. package/dist/engines/pi/definition.d.ts +14 -58
  272. package/dist/engines/pi/definition.js +21 -85
  273. package/dist/engines/pi/invoke-session.d.ts +5 -25
  274. package/dist/engines/pi/invoke-session.js +233 -244
  275. package/dist/engines/pi/login.d.ts +95 -35
  276. package/dist/engines/pi/login.js +259 -107
  277. package/dist/engines/pi/machine.d.ts +31 -0
  278. package/dist/engines/pi/machine.js +106 -0
  279. package/dist/engines/pi/models.d.ts +70 -89
  280. package/dist/engines/pi/models.js +188 -81
  281. package/dist/engines/pi/open.d.ts +65 -53
  282. package/dist/engines/pi/open.js +129 -69
  283. package/dist/engines/pi/report.d.ts +3 -12
  284. package/dist/engines/pi/report.js +2 -16
  285. package/dist/engines/pi/search-tools.d.ts +1 -9
  286. package/dist/engines/pi/search-tools.js +26 -57
  287. package/dist/engines/pi/service.d.ts +2 -16
  288. package/dist/engines/pi/service.js +2 -16
  289. package/dist/engines/pi/session-builder.d.ts +5 -9
  290. package/dist/engines/pi/session-builder.js +44 -82
  291. package/dist/engines/pi/session-control.js +286 -269
  292. package/dist/engines/pi/session-effects.d.ts +14 -0
  293. package/dist/engines/pi/session-effects.js +30 -0
  294. package/dist/engines/pi/session-inheritance.d.ts +9 -18
  295. package/dist/engines/pi/session-inheritance.js +103 -160
  296. package/dist/engines/pi/session-markers.d.ts +31 -30
  297. package/dist/engines/pi/session-markers.js +26 -25
  298. package/dist/engines/pi/session-settings.d.ts +6 -22
  299. package/dist/engines/pi/session-settings.js +10 -21
  300. package/dist/engines/pi/session-store.d.ts +26 -92
  301. package/dist/engines/pi/session-store.js +96 -310
  302. package/dist/engines/pi/tool-context.d.ts +17 -45
  303. package/dist/engines/pi/tool-context.js +7 -26
  304. package/dist/engines/pi/tool.d.ts +45 -36
  305. package/dist/engines/pi/tool.js +43 -57
  306. package/dist/engines/pi/turn-kit.d.ts +14 -52
  307. package/dist/engines/pi/turn-kit.js +18 -67
  308. package/dist/engines/pi/wake-tool.d.ts +2 -10
  309. package/dist/engines/pi/wake-tool.js +7 -27
  310. package/dist/env.d.ts +27 -28
  311. package/dist/env.js +45 -50
  312. package/dist/index.js +0 -2
  313. package/dist/lark.d.ts +1 -2
  314. package/dist/lark.js +1 -2
  315. package/dist/loader.d.ts +14 -55
  316. package/dist/loader.js +24 -43
  317. package/dist/log.d.ts +2 -12
  318. package/dist/log.js +3 -24
  319. package/dist/node.d.ts +2 -15
  320. package/dist/node.js +2 -18
  321. package/dist/observe.d.ts +2 -8
  322. package/dist/open-url.d.ts +1 -1
  323. package/dist/open-url.js +1 -1
  324. package/dist/paths.d.ts +70 -134
  325. package/dist/paths.js +165 -232
  326. package/dist/pi.d.ts +4 -6
  327. package/dist/pi.js +3 -3
  328. package/dist/proxy.d.ts +16 -6
  329. package/dist/proxy.js +52 -16
  330. package/dist/runtime.d.ts +18 -12
  331. package/dist/runtime.js +18 -11
  332. package/dist/scaffold/add-channel.d.ts +14 -31
  333. package/dist/scaffold/add-channel.js +48 -112
  334. package/dist/scaffold/init.d.ts +9 -31
  335. package/dist/scaffold/init.js +45 -138
  336. package/dist/scaffold/templates/env.example +6 -4
  337. package/dist/scaffold/templates/fastagent.config.ts +36 -0
  338. package/dist/scaffold/templates.d.ts +3 -3
  339. package/dist/scaffold/templates.js +5 -8
  340. package/dist/scaffold/vendor-skill.d.ts +2 -5
  341. package/dist/scaffold/vendor-skill.js +19 -29
  342. package/dist/schedule/cron.d.ts +9 -5
  343. package/dist/schedule/cron.js +9 -11
  344. package/dist/schedule/discover.d.ts +12 -12
  345. package/dist/schedule/discover.js +43 -34
  346. package/dist/schedule/routine.d.ts +43 -0
  347. package/dist/schedule/routine.js +32 -0
  348. package/dist/schedule/run.d.ts +55 -0
  349. package/dist/schedule/run.js +146 -0
  350. package/dist/schedule/scheduler.d.ts +44 -55
  351. package/dist/schedule/scheduler.js +291 -265
  352. package/dist/schedule/state.d.ts +105 -10
  353. package/dist/schedule/state.js +293 -19
  354. package/dist/schedule/wake-alarm.d.ts +10 -23
  355. package/dist/schedule/wake-alarm.js +91 -121
  356. package/dist/schedule/wakeups.d.ts +23 -39
  357. package/dist/schedule/wakeups.js +33 -75
  358. package/dist/secrets-gate.d.ts +32 -0
  359. package/dist/secrets-gate.js +33 -0
  360. package/dist/service.d.ts +137 -98
  361. package/dist/service.js +250 -252
  362. package/dist/session-remote.d.ts +33 -44
  363. package/dist/session-remote.js +203 -192
  364. package/dist/session.d.ts +166 -229
  365. package/dist/session.js +33 -55
  366. package/dist/tunnel.d.ts +8 -36
  367. package/dist/tunnel.js +24 -65
  368. package/dist/version.d.ts +2 -2
  369. package/dist/version.js +2 -2
  370. package/package.json +10 -16
  371. package/dist/channels/agentcore-state.d.ts +0 -77
  372. package/dist/channels/agentcore-state.js +0 -276
  373. package/dist/channels/github/github.d.ts +0 -31
  374. package/dist/channels/github/github.js +0 -89
  375. package/dist/channels/github/scaffold/channel.ts +0 -25
  376. package/dist/cli/commands/attach.d.ts +0 -108
  377. package/dist/cli/commands/attach.js +0 -637
  378. package/dist/cli/commands/fire.d.ts +0 -7
  379. package/dist/cli/commands/fire.js +0 -38
  380. package/dist/cli/commands/schedule.d.ts +0 -12
  381. package/dist/cli/commands/schedule.js +0 -87
  382. package/dist/github.d.ts +0 -3
  383. package/dist/github.js +0 -2
  384. package/dist/scaffold/templates/fastagent.config.mjs +0 -23
  385. package/dist/schedule/audit.d.ts +0 -18
  386. package/dist/schedule/audit.js +0 -57
  387. package/dist/schedule/schedule.d.ts +0 -35
  388. package/dist/schedule/schedule.js +0 -23
@@ -1,32 +1,25 @@
1
1
  /**
2
- * The AgentSession L0's engine binding: fastagent's assembled agent — model, prompt, skills, tools —
3
- * bound to one durable record, per invoke.
4
- *
5
- * The chat path builds the same pi class from the same assembly (session-builder.ts) but keeps ONE
6
- * resident session. What is specific here is the posture: many sessions, one turn each, nothing in
7
- * memory between turns.
8
- *
9
- * Shared once, rebuilt per turn:
10
- * - `services` (ResourceLoader, settings, model runtime) is built lazily and reused — it is the
11
- * expensive half, and it holds nothing session-specific;
12
- * - the `AgentSession` and its tool bindings are per turn, because a tool's `execute` closes over the
13
- * session it runs in and this posture has several in flight at once.
2
+ * The AgentSession L0's engine binding: fastagent's assembled agent — model, prompt, skills, tools — bound to one
3
+ * durable record, per invoke.
14
4
  */
15
- import { dirname, join } from "node:path";
16
- import { createAgentSessionFromServices, createAgentSessionServices, } from "@earendil-works/pi-coding-agent";
5
+ import { dirname } from "node:path";
6
+ import { readMachine, withMachine } from "./machine.js";
7
+ import { ExtensionRunner, ModelRegistry, SessionManager, DefaultResourceLoader, createAgentSessionFromServices, getAgentDir, initTheme, } from "@earendil-works/pi-coding-agent";
17
8
  import { log } from "../../log.js";
18
9
  import { isDeferredTool } from "./tool.js";
19
10
  import { activePath, resolveSessionSettings } from "./session-settings.js";
20
11
  import { DEFAULT_THINKING_LEVEL } from "./models.js";
21
12
  import { agentSessionManager, sessionToolActivation, turnContext } from "./tool-context.js";
22
13
  /**
23
- * The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
24
- * tools a loader activated in that call.
14
+ * The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred tools a loader
15
+ * activated in that call.
25
16
  *
26
- * A DEDICATED record, not pi's own `active_tools_change`: that one is a full SNAPSHOT of everything
27
- * active at the moment, so replaying it would keep a tool active in old sessions after the author
28
- * flips it to `deferred` — the session never discovered it. A delta carries only what was actually
29
- * found, and is layered onto whatever the workspace mounts TODAY.
17
+ * Not replaceable by pi's own transcript record. Since 0.86 the journal carries `toolsAdded` system messages and
18
+ * `getCurrentTools()` replays them, but that answers a different question: it reports every tool ever DECLARED to
19
+ * the model, which cannot tell a tool this conversation DISCOVERED from one that merely happened to be in the
20
+ * initial set at the time. Restoring from it would keep a tool later flipped to `deferred` active in sessions that
21
+ * never discovered it. This entry holds only what a loader activated, which is what makes the restore below
22
+ * "today's non-deferred tools PLUS what this conversation found".
30
23
  */
31
24
  const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
32
25
  /** Every deferred tool this session has ever discovered, oldest first. */
@@ -44,17 +37,12 @@ function recordedActivations(session) {
44
37
  }
45
38
  return names;
46
39
  }
47
- /** Warned once per session+missing set: a fresh session is built per invoke and channel sessions run
48
- * for weeks, so an un-deduped warn would repeat every turn and dilute its own signal. */
49
- const warnedDroppedActivations = new Set();
50
40
  /**
51
- * fastagent's tools as pi tool definitions, bound to ONE session.
52
- *
53
- * `bound` is filled after the session exists — pi needs the definitions to build the session, and a
54
- * tool needs the session to reach the turn context. A tool that somehow runs before that binding
55
- * throws rather than executing outside the turn: a broken lifecycle must not look like a normal
56
- * out-of-turn call.
41
+ * Warned once per session+missing set: a fresh session is built per invoke and channel sessions run for weeks, so an
42
+ * un-deduped warn would repeat every turn and dilute its own signal.
57
43
  */
44
+ const warnedDroppedActivations = new Set();
45
+ /** fastagent's tools as pi tool definitions, bound to ONE session. */
58
46
  function toolDefinitions(tools, bound, sessionId) {
59
47
  return tools.map((tool) => ({
60
48
  ...tool,
@@ -85,12 +73,8 @@ function toolDefinitions(tools, bound, sessionId) {
85
73
  }));
86
74
  }
87
75
  /**
88
- * Bind ONE pi session to a record: the definition's tools as pi definitions over one turn context,
89
- * pi's own tool copies kept off, and deferral applied. The per-invoke factory and the resident chat
90
- * runtime both bind here — they differ in what they hand in (a shared vs a per-session `services`,
91
- * record-resolved vs configured settings) and in whether a discovered activation has a record to
92
- * land in, and in nothing else. Two copies of this drifted once (the tool adapter and the deferral
93
- * narrowing each existed twice, identical but for those parameters).
76
+ * Bind ONE pi session to a record: the definition's tools as pi definitions over one turn context, pi's own tool
77
+ * copies kept off, and deferral applied.
94
78
  */
95
79
  export async function bindPiSession(options) {
96
80
  const { services, sessionManager, model, thinkingLevel, tools, cwd, recordActivations } = options;
@@ -105,16 +89,12 @@ export async function bindPiSession(options) {
105
89
  model,
106
90
  thinkingLevel,
107
91
  // pi would otherwise mount its built-ins on top of fastagent's copies, offering duplicate names.
108
- // Lower-level callers with an explicit list also rely on omitted built-ins staying omitted. And
109
- // NO `tools` allowlist: it would freeze the set at bind time, while pi lets an extension register
110
- // from `session_start` — `noTools: "builtin"` gives the guarantee the allowlist was there for.
111
92
  noTools: "builtin",
112
93
  ...(excludedToolNames.length > 0 ? { excludeTools: [...excludedToolNames] } : {}),
113
94
  customTools: toolDefinitions(tools, bound, sessionId),
114
95
  });
115
96
  const { session } = result;
116
- // One context for the whole session: it describes the SESSION, not the call. The activation
117
- // bridge above all — a tool call has to see what the previous one activated.
97
+ // One context for the whole session: it describes the SESSION, not the call.
118
98
  bound.current = {
119
99
  session,
120
100
  context: {
@@ -126,15 +106,14 @@ export async function bindPiSession(options) {
126
106
  : undefined),
127
107
  },
128
108
  };
129
- // Deferral, then restoration: pi starts every mounted tool active, so narrow by SUBTRACTING the
130
- // deferred names (robust to pi mounting tools of its own, unlike an exact-set replacement), then
131
- // add back what THIS session has already discovered.
109
+ // Deferral, then restoration: pi starts every mounted tool active, so narrow by SUBTRACTING the deferred names
110
+ // (robust to pi mounting tools of its own, unlike an exact-set replacement), then add back what THIS session has
111
+ // already discovered.
132
112
  if (deferred.length > 0) {
133
113
  const active = session.getActiveToolNames();
134
114
  const mounted = new Set(session.getAllTools().map((tool) => tool.name));
135
115
  const recorded = recordActivations ? recordedActivations(session) : [];
136
- // A recorded name that is no longer mounted is dropped rather than replayed: pi's setter
137
- // THROWS on an unknown name, so replaying one would brick every future turn of this session.
116
+ // A recorded name that is no longer mounted is dropped rather than replayed.
138
117
  const restored = recorded.filter((name) => mounted.has(name));
139
118
  const dropped = recorded.filter((name) => !mounted.has(name));
140
119
  if (dropped.length > 0) {
@@ -150,135 +129,251 @@ export async function bindPiSession(options) {
150
129
  }
151
130
  return result;
152
131
  }
132
+ /** Each distinct load failure is said once per process: serving loads the extensions again for every session. */
133
+ const reportedExtensionErrors = new Set();
153
134
  /**
154
- * Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and
155
- * carries on with the rest — sound for a TUI that shows them, silent for a server that never looks.
156
- * A definition running without the extension it ships is exactly the "quietly missing" failure this
157
- * exists to remove. CHAT calls it, once per built services — serving does not load extensions at
158
- * all, and announces that instead (see PiAgentSessionFactoryOptions.extensionPaths).
135
+ * Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and carries on with the
136
+ * rest.
159
137
  */
160
138
  export function reportExtensionErrors(services) {
161
139
  for (const { path, error } of services.resourceLoader.getExtensions().errors) {
140
+ const key = `${path}\u0000${error}`;
141
+ if (reportedExtensionErrors.has(key))
142
+ continue;
143
+ reportedExtensionErrors.add(key);
162
144
  log.warn(`[fastagent] extension ${path} failed to load: ${error}`);
163
145
  }
164
146
  }
147
+ const PROVIDER_REFUSAL = "extensions cannot register model providers when serving: a provider is process-wide, shared by every " +
148
+ "conversation, and never unregistered. Declare it in the agent's models.json";
165
149
  /**
166
- * The resource posture a fastagent definition asks pi for — ONE definition of it, for both
167
- * assemblies. Serving (`piAgentSessionFactory`) and chat (`buildAgentSessionRuntime`) build
168
- * different sessions on top, but what pi is allowed to DISCOVER is not one of the differences:
169
- * everything comes from the definition, nothing from the machine that happens to be running it.
170
- *
171
- * Two copies of this drifted once already: `additionalExtensionPaths` was added to both, and only
172
- * one of them also passed the resulting tool names through pi's `tools` allowlist — so extensions
173
- * worked when served and vanished in chat. A difference between the two has to be visible AS a
174
- * difference, which is what the parameters are for: serving reads a prompt and skills that change
175
- * per turn and passes NO extension paths (it does not run them — see
176
- * {@link PiAgentSessionFactoryOptions.extensionPaths}); chat reads a fixed assembly and passes its
177
- * own. Both are arguments now, rather than two files that happen to disagree.
150
+ * Serving refuses an extension that registers a provider while loading. pi would write it into the ONE `ModelRuntime`
151
+ * every conversation resolves against, merge a re-registration into the old entry, and never drop one the code stopped
152
+ * registering. The whole extension is left out, the way a tool file that fails to load is, and the refusal is its
153
+ * load error.
154
+ */
155
+ function refuseLoadTimeProviders(base) {
156
+ const offenders = new Set([
157
+ ...base.runtime.pendingProviderRegistrations.map((r) => r.extensionPath),
158
+ ...base.runtime.pendingNativeProviderRegistrations.map((r) => r.extensionPath),
159
+ ]);
160
+ if (offenders.size === 0)
161
+ return base;
162
+ base.runtime.pendingProviderRegistrations = [];
163
+ base.runtime.pendingNativeProviderRegistrations = [];
164
+ return {
165
+ ...base,
166
+ extensions: base.extensions.filter((extension) => !offenders.has(extension.path)),
167
+ errors: [...base.errors, ...[...offenders].map((path) => ({ path, error: PROVIDER_REFUSAL }))],
168
+ };
169
+ }
170
+ /**
171
+ * ...and a registration made AFTER loading (from an event handler or a command), which pi applies to the shared
172
+ * runtime directly. Must run after the session is created (pi installs its own actions then) and before
173
+ * `bindExtensions` (which fires `session_start`).
174
+ */
175
+ function refuseLateProviders(services) {
176
+ const runtime = services.resourceLoader.getExtensions().runtime;
177
+ const refuse = () => {
178
+ throw new Error(PROVIDER_REFUSAL);
179
+ };
180
+ runtime.registerProvider = refuse;
181
+ runtime.registerNativeProvider = refuse;
182
+ runtime.unregisterProvider = refuse;
183
+ }
184
+ /**
185
+ * What a command's `ctx` can do to the session when serving. Each turn binds its own session and disposes it, so
186
+ * there is no current session to replace, fork or reload; pi's unbound default would report `{ cancelled: false }`
187
+ * for all of them without doing anything.
178
188
  */
189
+ function servingCommandActions(session) {
190
+ const unavailable = (action) => async () => {
191
+ throw new Error(`ctx.${action}() is not available when serving: every turn runs on its own session`);
192
+ };
193
+ return {
194
+ waitForIdle: () => session.waitForIdle(),
195
+ newSession: unavailable("newSession"),
196
+ fork: unavailable("fork"),
197
+ navigateTree: unavailable("navigateTree"),
198
+ switchSession: unavailable("switchSession"),
199
+ reload: unavailable("reload"),
200
+ };
201
+ }
202
+ /** The resource posture a fastagent definition asks pi for — ONE definition of it, for both assemblies. */
179
203
  export function definitionResourceLoaderOptions(source) {
180
204
  return {
181
- // Definition-only, like dev/start: pi's machine-global discovery (the operator's own ~/.pi
182
- // extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) stays out, so the agent that
183
- // runs is the artifact, not the artifact plus whoever's laptop it is.
205
+ // The machine's extensions are its owner's setup, not this agent's.
184
206
  noExtensions: true,
185
- // ...except the definition's OWN extensions/: pi honours additionalExtensionPaths even under
186
- // noExtensions, which is exactly the split wanted here — the artifact travels with its
187
- // extensions, the machine's stay out.
207
+ // ...except the definition's OWN extensions/: pi honours additionalExtensionPaths even under noExtensions, which
208
+ // is exactly the split wanted here.
188
209
  ...(source.extensionPaths?.length ? { additionalExtensionPaths: [...source.extensionPaths] } : {}),
189
- noPromptTemplates: true,
210
+ // Not pi's: fastagent already loads the SAME files into segment ② (`loadProjectContextFiles` in
211
+ // definition.ts). Leaving both on would put every AGENTS.md in the prompt twice.
190
212
  noContextFiles: true,
191
- // A SPACE, not "", when the assembly has no prompt: pi treats an empty custom prompt as absent
192
- // and substitutes its own coding-assistant identity, which an L1 agent
193
- // (`createPiAgent({ model, tools })`) never asked for. pi appends its own working-directory line
194
- // either way — that is engine behaviour this binding does not fight.
213
+ // The IDENTITY is the definition's, whatever the machine thinks. Inheriting skills is inheriting capability;
214
+ // inheriting a system prompt would be the agent becoming someone else's agent.
195
215
  systemPromptOverride: () => source.systemPrompt() || " ",
196
216
  appendSystemPromptOverride: () => [],
197
- skillsOverride: (base) => ({
198
- skills: toPiSkills(source.skills()),
199
- diagnostics: base.diagnostics,
217
+ /**
218
+ * THE DEFINITION'S SKILLS, PLUS THE MACHINE'S — an agent inherits the box it runs on (machine.ts), and the
219
+ * definition wins a name collision.
220
+ *
221
+ * NO DISCOVERY OF ITS OWN: the machine's half is the process's single read, so a bound session and `commands()`
222
+ * describe the same box. A loader discovering for itself is what made the menu live while a session was not.
223
+ */
224
+ noSkills: true,
225
+ skillsOverride: () => ({
226
+ skills: withMachine(toPiSkills(source.skills()), source.machine.skills),
227
+ diagnostics: [],
200
228
  }),
229
+ noPromptTemplates: true,
230
+ promptsOverride: () => ({ prompts: [...source.machine.prompts], diagnostics: [] }),
201
231
  };
202
232
  }
203
- /** Open-or-create the record, then bind a fresh session to it. One call per invoke. */
233
+ /** Manual compactions waiting for pi's admission, keyed by the session's record (what an extension's `ctx` names). */
234
+ const compactAdmissions = new WeakMap();
235
+ const COMPACT_ADMISSION = "fastagent-compact-admission";
236
+ /**
237
+ * pi's own admission signal for a manual compaction: `session_before_compact` fires only once `prepareCompaction`
238
+ * found work, and before the model call. It must be the FIRST listener ({@link admissionFirst}): pi awaits each
239
+ * handler in turn, and a definition's own handler (a custom summarizer, a cancel) runs after admission, like the
240
+ * model call it may replace.
241
+ */
242
+ const compactAdmission = {
243
+ name: COMPACT_ADMISSION,
244
+ hidden: true,
245
+ factory: (pi) => {
246
+ pi.on("session_before_compact", (event, ctx) => {
247
+ if (event.reason === "manual")
248
+ admitCompaction(ctx.sessionManager);
249
+ });
250
+ },
251
+ };
252
+ /**
253
+ * pi loads path extensions first and appends inline factories after them, so {@link compactAdmission} arrives last.
254
+ * Moved to the front here, since pi dispatches handlers in extension order.
255
+ */
256
+ function admissionFirst(base) {
257
+ const path = `<inline:${COMPACT_ADMISSION}>`;
258
+ const admission = base.extensions.filter((extension) => extension.path === path);
259
+ return { ...base, extensions: [...admission, ...base.extensions.filter((extension) => extension.path !== path)] };
260
+ }
261
+ /** Tell a waiting {@link startCompaction} that pi admitted the compaction on this record. */
262
+ export function admitCompaction(record) {
263
+ compactAdmissions.get(record)?.();
264
+ }
265
+ /**
266
+ * pi's refusals when `prepareCompaction` finds nothing. They are plain Errors, so their text is the only way to tell
267
+ * them from any other error pi raises before admission. If pi rewords them, the refusal still fails visibly, as
268
+ * `boundary_command_failed` instead of `nothing_to_compact`, and the admission tests in session-control.test.ts,
269
+ * which run pi's real refusal, fail with it.
270
+ */
271
+ const NOTHING_TO_COMPACT = /^(Nothing to compact|Already compacted)\b/;
272
+ /**
273
+ * Start pi's manual compaction on a bound session. `onAdmitted` runs INSIDE pi's admission event, so everything it
274
+ * publishes precedes anything pi does next (the model call, its retries). `admission` settles before the model call.
275
+ * pi's own pre-admission failures come back as `refused` rather than a rejection: the caller answers them to its
276
+ * Caller as a result. `done` is the compaction itself.
277
+ */
278
+ export function startCompaction(session, instructions, onAdmitted) {
279
+ const record = session.sessionManager;
280
+ const admitted = new Promise((resolve) => {
281
+ compactAdmissions.set(record, () => {
282
+ try {
283
+ onAdmitted();
284
+ }
285
+ finally {
286
+ resolve("admitted");
287
+ }
288
+ });
289
+ });
290
+ const done = session.compact(instructions);
291
+ const forget = () => void compactAdmissions.delete(record);
292
+ done.then(forget, forget);
293
+ const settledFirst = done.then(() => {
294
+ throw new Error("pi finished a compaction without announcing its admission (session_before_compact)");
295
+ }, (error) => error instanceof Error && NOTHING_TO_COMPACT.test(error.message) ? "nothing_to_compact" : { refused: error });
296
+ return { admission: Promise.race([admitted, settledFirst]), done };
297
+ }
298
+ /**
299
+ * The resources ONE served session runs on: a fresh loader, so fresh extension instances.
300
+ *
301
+ * Assembled here rather than by pi's `createAgentSessionServices`, which ends by refreshing the model runtime it is
302
+ * given — re-reading `models.json` and rebuilding every provider on the ONE runtime all conversations share. That is
303
+ * there to fold in providers extensions registered, which serving refuses; per session it would be a full provider
304
+ * rebuild per turn.
305
+ */
306
+ async function servingServices(options) {
307
+ const { cwd, modelRuntime, definition, extensionPaths } = options;
308
+ const machine = await readMachine(cwd);
309
+ const agentDir = getAgentDir();
310
+ // The machine's engine settings, as read at boot and without `packages` — a turn never resolves one.
311
+ const settingsManager = machine.settingsManager();
312
+ const resourceLoader = new DefaultResourceLoader({
313
+ ...definitionResourceLoaderOptions({
314
+ systemPrompt: () => definition.systemPrompt,
315
+ skills: () => definition.skills,
316
+ machine,
317
+ extensionPaths,
318
+ }),
319
+ extensionsOverride: (base) => admissionFirst(refuseLoadTimeProviders(base)),
320
+ extensionFactories: [compactAdmission],
321
+ cwd,
322
+ agentDir,
323
+ settingsManager,
324
+ });
325
+ await resourceLoader.reload();
326
+ const services = { cwd, agentDir, modelRuntime, settingsManager, resourceLoader, diagnostics: [] };
327
+ reportExtensionErrors(services);
328
+ return services;
329
+ }
330
+ /**
331
+ * The `/name` commands a served session dispatches, named the way pi resolves them (a name two extensions share gets
332
+ * a `:N` suffix). Loaded through the same {@link servingServices} a turn binds, so the menu and the dispatch cannot
333
+ * disagree. Loading runs the extensions' factories; no session opens, so `session_start` does not fire, and the
334
+ * instances are never bound (any action they call throws).
335
+ */
336
+ export async function servedExtensionCommands(options) {
337
+ if (options.extensionPaths.length === 0)
338
+ return [];
339
+ const services = await servingServices({ ...options, definition: { skills: [] } });
340
+ const { extensions, runtime } = services.resourceLoader.getExtensions();
341
+ const runner = new ExtensionRunner(extensions, runtime, options.cwd, SessionManager.inMemory(options.cwd), new ModelRegistry(options.modelRuntime));
342
+ return runner.getRegisteredCommands();
343
+ }
344
+ /**
345
+ * Open-or-create the record, then bind a fresh session to it — on a fresh resource loader.
346
+ *
347
+ * ONE LOADER PER SESSION, because pi's extension runtime belongs to its loader: a loader shared across concurrent
348
+ * turns would let one conversation's extension act on another's session. It also makes the definition live with no
349
+ * bookkeeping: every bind reads the prompt and skills this invoke read. The machine's half is the process's one read
350
+ * (machine.ts). Extension CODE is imported once per process, so an edit to `extensions/` needs a restart, which is
351
+ * what `dev` does on one.
352
+ */
204
353
  export function piAgentSessionFactory(options) {
205
354
  const { sessions, thinkingLevel, cwd } = options;
206
355
  const extensionPaths = options.extensionPaths ?? [];
207
356
  const excludedToolNames = options.excludedToolNames ?? [];
208
- if (extensionPaths.length > 0) {
209
- log.warn(`[fastagent] ${extensionPaths.length} extension(s) in the definition are NOT loaded when serving ` +
210
- "(they run in `fastagent chat`): pi's extension runtime is shared across sessions, and serving " +
211
- "runs concurrent turns for different conversations. See docs/configuration.md#extensions.");
212
- }
213
357
  const tools = options.tools ?? [];
214
- // What the shared ResourceLoader serves, refreshed per turn before the session is built.
215
- let definition;
216
- let services;
358
+ // `ctx.ui.theme` reads pi's global theme, which only pi's own entry points initialize.
359
+ if (extensionPaths.length > 0)
360
+ initTheme();
217
361
  let engine;
218
- const buildServices = async (modelRuntime) => createAgentSessionServices({
219
- cwd,
220
- agentDir: options.agentDir ?? join(cwd, ".fastagent", "pi"),
221
- modelRuntime,
222
- // No extensionPaths: serving does not run them (see PiAgentSessionFactoryOptions), which is
223
- // the one resource question the two assemblies answer differently. The accessors read the
224
- // CURRENT prompt/skills — serving refreshes both per turn, so a snapshot taken here would
225
- // serve a stale definition after the first edit.
226
- resourceLoaderOptions: definitionResourceLoaderOptions({
227
- systemPrompt: () => definition.systemPrompt,
228
- skills: () => definition.skills,
229
- }),
230
- });
231
362
  return async (sessionId, inherit) => {
232
- const next = await options.readDefinition();
363
+ const definition = await options.readDefinition();
233
364
  engine ??= options.engine();
234
365
  const { modelRuntime, model } = await engine;
235
- if (services === undefined) {
236
- definition = next;
237
- services = buildServices(modelRuntime); // assigned before any await: concurrent turns share it
238
- }
239
- else {
240
- // The ResourceLoader reads the overrides once and caches, so a re-read of the definition only
241
- // reaches the model after a reload. Reload only when the definition ACTUALLY changed — an
242
- // author edits persona.md far less often than the agent takes a turn, and a reload costs ~5ms
243
- // against ~0.6ms to bind a session.
244
- //
245
- // "Changed" is measured against the LOADER, not against what this factory last wrote. Serving
246
- // is concurrent across sessions, and a shared variable makes the check lie: one turn writes
247
- // its new prompt, awaits before reloading, and the next turn sees that value already present,
248
- // concludes nothing changed, and skips the reload — so the edit reaches neither the loader nor
249
- // any error. Asking the loader what it is actually serving cannot go stale that way. The cost
250
- // of losing the race is one redundant reload, not a swallowed edit.
251
- //
252
- // Skill CONTENT is not part of this — pi reads a skill from its file at invocation time, so
253
- // only the declared set matters.
254
- //
255
- // What this deliberately does NOT provide is a per-turn snapshot. The definition is an AGENT
256
- // property, not a session one: two turns running either side of an edit each get a definition
257
- // that genuinely existed, and the product promise — an edit is live on the next turn — holds
258
- // for both. Pinning a snapshot per turn would cost either a loader per turn or a queue in
259
- // front of every bind, to buy a guarantee nothing asks for.
260
- const loader = (await services).resourceLoader;
261
- const definitionChanged = loader.getSystemPrompt() !== (next.systemPrompt || " ") ||
262
- skillSet(loader.getSkills().skills) !== skillSet(next.skills);
263
- if (definitionChanged) {
264
- definition = next;
265
- await loader.reload();
266
- }
267
- }
366
+ // The record first: a control write that races this turn must find the session and be refused busy.
268
367
  const sessionManager = await sessions.openOrCreate(sessionId, inherit);
269
- // What the session RUNS on: the boundary plane records model/thinking overrides as entries, and
270
- // pi does not read them back — a binding that ignored them would silently run every turn on the
271
- // assembly default, and `state()` would report a setting no turn uses.
272
- // The SAME read the control plane performs, including its integrity check: a record whose chain
273
- // is broken must not run on assembly defaults while `state()` rejects it — one of the two planes
274
- // would be lying. A throw here becomes this turn's `failed` event, which is where the fault has
275
- // a channel to be reported through.
368
+ const services = await servingServices({ cwd, modelRuntime, definition, extensionPaths });
369
+ // What the session RUNS on: the boundary plane records model/thinking overrides as entries, and pi does not read
370
+ // them back.
276
371
  const settings = resolveSessionSettings(activePath(sessionManager), modelRuntime, {
277
372
  model,
278
373
  thinkingLevel: thinkingLevel ?? DEFAULT_THINKING_LEVEL,
279
374
  });
280
375
  const { session } = await bindPiSession({
281
- services: await services,
376
+ services,
282
377
  sessionManager,
283
378
  model: settings.model,
284
379
  thinkingLevel: settings.thinkingLevel,
@@ -288,15 +383,23 @@ export function piAgentSessionFactory(options) {
288
383
  sessionId,
289
384
  recordActivations: true,
290
385
  });
386
+ refuseLateProviders(services);
387
+ // `onError` is THE ONLY LISTENER for a fault pi reports nowhere else: `/skill:<name>` is expanded by reading
388
+ // `filePath` at prompt time, and when that read fails pi raises `skill_expansion` on the extension error channel
389
+ // and sends the line to the model unexpanded. The list can outlive the file: it is refreshed per invoke, so a
390
+ // steer or follow-up inside a run, or a definition replaced under a running container (`src/deploy/workspace.ts`),
391
+ // reaches exactly that state. The same channel carries every extension handler's failure.
392
+ //
393
+ // No `uiContext`: a served turn has no human at a terminal, and pi's default is what extensions are written to
394
+ // detect (`ctx.hasUI === false`; dialogs resolve as cancelled). This call fires `session_start`.
395
+ await session.bindExtensions({
396
+ onError: ({ extensionPath, event, error }) => log.warn(`[fastagent] session ${sessionId}: ${event} failed for ${extensionPath}: ${error}`),
397
+ commandContextActions: servingCommandActions(session),
398
+ shutdownHandler: () => log.warn(`[fastagent] session ${sessionId}: an extension called ctx.shutdown(); a served process is stopped by its host, not by a turn`),
399
+ });
291
400
  return session;
292
401
  };
293
402
  }
294
- /** What a reload has to notice: the declared set, not the files behind it. */
295
- function skillSet(skills) {
296
- return skills
297
- .map((s) => `${s.name}\u0000${s.filePath ?? ""}\u0000${s.description}\u0000${s.disableModelInvocation ?? false}`)
298
- .join("\u0001");
299
- }
300
403
  /** fastagent's Skill (content inline) as pi's (read from filePath at invocation time). */
301
404
  function toPiSkills(skills) {
302
405
  return skills.map((skill) => {
@@ -1,18 +1,22 @@
1
1
  import type { CredentialStore } from "@earendil-works/pi-ai";
2
2
  /**
3
- * The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery
4
- * home `~/.fastagent/` — which carries the same unified shape as a workspace (`.secrets/auth.json`).
5
- * The project-level default is `<agentDir>/.secrets/auth.json` (computed by the opener and by
6
- * `fastagent login`); this is only the `loginFlow()` PROGRAMMATIC fallback (when a caller omits
7
- * `authPath`) and the path to point `--auth-path`/`FASTAGENT_AUTH_PATH` at to deliberately share ONE
8
- * credential file across projects (safe — one file, one lock-serialized refresh lifecycle). The
9
- * `fastagent login` CLI is project-level by default, never this.
3
+ * The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery home
4
+ * `~/.fastagent/`.
10
5
  */
11
6
  export declare const GLOBAL_AUTH_PATH: string;
12
7
  export interface FastagentAuthOptions {
13
- /** Sink for non-fatal auth anomalies (unreadable/corrupt file). Defaults to the process logger (warn). */
8
+ /** Sink for non-fatal auth anomalies (unreadable/corrupt file). */
14
9
  warn?: (message: string) => void;
15
10
  }
16
- /** A read-write `CredentialStore` backed by the given credentials file (default {@link GLOBAL_AUTH_PATH};
17
- * the directory opener passes the project-level `<root>/.secrets/auth.json`). */
18
- export declare function fastagentCredentialStore(authPath?: string, options?: FastagentAuthOptions): CredentialStore;
11
+ /**
12
+ * A read-write `CredentialStore` over the given credentials file (default {@link GLOBAL_AUTH_PATH}; the directory
13
+ * opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file.
14
+ *
15
+ * The fallback is PER PROVIDER, not per file: logging one provider into a project must not hide the others a person
16
+ * already has globally. And the layer a credential was READ from is the layer its refresh is written back to —
17
+ * anything else would conjure a second holder of the same OAuth grant, which is the failure this whole area exists
18
+ * to avoid. A provider present in neither layer is new, and new credentials belong to the primary.
19
+ */
20
+ export declare function fastagentCredentialStore(authPath?: string, options?: FastagentAuthOptions & {
21
+ fallbackPath?: string;
22
+ }): CredentialStore;