@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,36 +1,34 @@
1
+ import { contentText } from "@earendil-works/pi-ai";
2
+ import * as Deferred from "effect/Deferred";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Fiber from "effect/Fiber";
5
+ import * as Queue from "effect/Queue";
1
6
  import { ABORTED_CODE, SESSION_BUSY_CODE, } from "../../agent.js";
2
- import { toRetryScheduledEvent } from "./retry-event.js";
3
7
  import { cancellableStream } from "../../collect.js";
4
8
  import { log } from "../../log.js";
5
- import { EventQueue, errorToTerminal, inProcessLease, projectAgentEvent, toPiPromptOptions, toTerminal, } from "./turn-kit.js";
6
- /**
7
- * pi session events into the rich `SessionEvent` vocabulary — the SINGLE translation point. Events
8
- * with no vocabulary yet (agent_start, turn_start, entry_appended, …) are dropped.
9
- *
10
- * `auto_retry_start` reports as a `retry_scheduled` with `operation: "assistant"`: pi retries a
11
- * failed ANSWER request itself, which the summarization-only cases of that vocabulary predate.
12
- */
9
+ import { toRetryScheduledEvent } from "./retry-event.js";
10
+ import { PortFailure, port, portAbort, portCleanup, portError } from "../../effect-port.js";
11
+ import { SessionBusy, acquireSession, acquireSessionLease } from "./session-effects.js";
12
+ import { errorToTerminal, inProcessLease, projectAgentEvent, toPiPromptOptions, toTerminal, } from "./turn-kit.js";
13
13
  function toSessionEvent(event, runId) {
14
14
  const at = Date.now();
15
15
  switch (event.type) {
16
16
  case "message_start":
17
- // Assistant streaming only — a user/toolResult message is not a live message boundary.
18
17
  if (event.message.role !== "assistant")
19
18
  return null;
20
19
  return { type: "message_started", timestamp: at, runId, data: {} };
21
20
  case "message_update": {
22
- // An empty delta is not output: it moves no consumer's state, and treating it as output would
23
- // spend the silent window that auto-retry is allowed to use (see runOnSession).
24
21
  const ev = event.assistantMessageEvent;
25
- if (ev.type === "text_delta") {
22
+ if (ev.type === "text_delta" || ev.type === "thinking_delta") {
23
+ // Empty deltas must not spend the silent window in which a provider retry is safe.
26
24
  return ev.delta === ""
27
25
  ? null
28
- : { type: "message_delta", timestamp: at, runId, data: { channel: "text", delta: ev.delta } };
29
- }
30
- if (ev.type === "thinking_delta") {
31
- return ev.delta === ""
32
- ? null
33
- : { type: "message_delta", timestamp: at, runId, data: { channel: "thinking", delta: ev.delta } };
26
+ : {
27
+ type: "message_delta",
28
+ timestamp: at,
29
+ runId,
30
+ data: { channel: ev.type === "text_delta" ? "text" : "thinking", delta: ev.delta },
31
+ };
34
32
  }
35
33
  return null;
36
34
  }
@@ -64,7 +62,7 @@ function toSessionEvent(event, runId) {
64
62
  type: "queue_changed",
65
63
  timestamp: at,
66
64
  runId,
67
- data: { steering: event.steering.length, followUp: event.followUp.length },
65
+ data: { steering: [...event.steering], followUp: [...event.followUp] },
68
66
  };
69
67
  case "auto_retry_start":
70
68
  case "summarization_retry_scheduled":
@@ -73,46 +71,62 @@ function toSessionEvent(event, runId) {
73
71
  return null;
74
72
  }
75
73
  }
74
+ /** pi's error-channel events for work an invoke owes its caller: the command it ran, and the turns it started. */
75
+ const OWED_BY_THE_RUN = new Set(["command", "send_user_message", "send_message"]);
76
76
  /**
77
- * The turn's outcome. `prompt()` resolves void and never throws for an engine-side failure (measured:
78
- * a provider error and an abort both resolve normally), so the terminal comes from the assistant
79
- * message the run ended on.
80
- *
81
- * That message is taken from the EVENT STREAM, not from `session.state.messages`. Session state is
82
- * mutable mid-turn: compaction replaces the array, and overflow recovery splices the last assistant
83
- * message out of it outright (`state.messages = messages.slice(0, -1)`). Any index into it is a
84
- * turn boundary that the engine is free to invalidate, while a `message_end` payload is a fact that
85
- * already happened. Auto-compaction runs its own model call outside the agent's event stream, so it
86
- * cannot masquerade as the turn's answer here.
77
+ * The turns extensions start on this session and have not finished. `pi.sendUserMessage` and `pi.sendMessage` reach
78
+ * these two session methods, and pi discards the promise; wrapping the methods keeps it. The session is this run's
79
+ * alone, so the wrap never outlives the run.
87
80
  */
88
- const ENGINE_PRODUCED_NOTHING = {
89
- // Unreachable on a settled run: pi ends every outcome, error and abort included, with an assistant
90
- // message. Reaching it means the engine broke its own contract — name the engine, not the turn.
91
- type: "failed",
92
- details: "the engine settled the run without ending an assistant message",
93
- retryable: false,
94
- };
81
+ function trackExtensionTurns(session) {
82
+ const pending = new Set();
83
+ const track = (sent) => {
84
+ pending.add(sent);
85
+ const done = () => void pending.delete(sent);
86
+ sent.then(done, done); // pi's own `.catch` on `sent` reports the failure
87
+ return sent;
88
+ };
89
+ const sendUserMessage = session.sendUserMessage.bind(session);
90
+ const sendCustomMessage = session.sendCustomMessage.bind(session);
91
+ session.sendUserMessage = (...args) => track(sendUserMessage(...args));
92
+ session.sendCustomMessage = (...args) => track(sendCustomMessage(...args));
93
+ return pending;
94
+ }
95
+ /**
96
+ * Report each user message when it is WRITTEN, which is later than pi announces it: pi awaits extension `message_end`
97
+ * handlers before it appends. Emitting from the write is what makes `user_message.entryId` readable through
98
+ * `entries()` by the time the event arrives, so a reconnecting client's backfill cannot miss a message whose event
99
+ * fired before it subscribed. pi's agent awaits each listener before the next event, so the message still reports
100
+ * before the answer to it. The in-memory store reuses one record across runs, so the release restores the method.
101
+ */
102
+ function reportUserMessages(session, report) {
103
+ const record = session.sessionManager;
104
+ const append = record.appendMessage;
105
+ return Effect.acquireRelease(Effect.sync(() => {
106
+ record.appendMessage = (message) => {
107
+ const entryId = append.call(record, message);
108
+ // The same text `entries()` publishes for this entry (session-control.ts toSessionEntry).
109
+ if (message.role === "user")
110
+ report(entryId, contentText(message.content, ""));
111
+ return entryId;
112
+ };
113
+ }), () => Effect.sync(() => {
114
+ record.appendMessage = append;
115
+ }));
116
+ }
95
117
  export function createPiAgentFromSession(options) {
96
118
  const { sessionFactory, lease = inProcessLease(), observer } = options;
97
119
  async function* turn(scope, prompt, { onCancelReady, wasCancelled }) {
98
- const release = lease.tryAcquire(scope.session);
99
- if (!release) {
100
- // Rejected BEFORE acceptance: no run exists, so the observer sees nothing (replay-safe).
101
- yield {
102
- type: "failed",
103
- details: "session busy: a turn is already in flight for this session",
104
- retryable: true,
105
- code: SESSION_BUSY_CODE,
106
- };
107
- return;
108
- }
109
- // The run exists from here: exactly one run_started, exactly one run_settled. The settlement is
110
- // emitted in the outer finally, immediately before the lease releases, so the observation
111
- // plane's "running" window equals the lease window — state() must never read idle while a new
112
- // invoke would still be rejected session_busy. A run with no recorded outcome was cancelled by
113
- // the caller (SPEC: cancellation has no terminal event), which settles as aborted.
120
+ const queue = Effect.runSync(Queue.unbounded());
121
+ const consumed = Deferred.makeUnsafe();
122
+ const bound = Deferred.makeUnsafe();
123
+ const abort = new AbortController();
124
+ onCancelReady(() => abort.abort());
114
125
  const runId = crypto.randomUUID();
126
+ let settled = false;
115
127
  let outcome;
128
+ let abortsInFlight = 0;
129
+ let abortSucceeded = false;
116
130
  const observe = (event, run) => {
117
131
  if (!event || !observer)
118
132
  return;
@@ -120,220 +134,195 @@ export function createPiAgentFromSession(options) {
120
134
  observer(scope.session, event, run);
121
135
  }
122
136
  catch (error) {
123
- // The observation plane must never break the data plane; a broken hub is its own problem.
124
137
  log.warn(`[fastagent] session observer threw (event ${event.type}): ${String(error)}`);
125
138
  }
126
139
  };
127
- // run_started is published BEFORE the session is built, so no early event can outrun the
128
- // registration — which means the controls have to await the build rather than reject during it:
129
- // a dispatch that races it simply queues on the freshly bound session. A build failure rejects
130
- // the gate, so a pending dispatch learns why instead of hanging.
131
- let sessionReady;
132
- let sessionFailed;
133
- const bound = new Promise((resolve, reject) => {
134
- sessionReady = resolve;
135
- sessionFailed = reject;
136
- });
137
- bound.catch(() => { }); // observed through the controls only when a dispatch actually happens
138
- // Stale-controls guard: after settlement pi's steer()/followUp()/abort() would still resolve
139
- // (they queue onto a session about to be disposed), which is a silent acceptance of a command
140
- // that can never take effect. The check and the engine call share one synchronous block — pi
141
- // enqueues at method entry, so a check behind its own await would only shrink the race.
142
- let settled = false;
143
- const settledError = () => new Error("run already settled; the command cannot take effect");
144
- // Aborted classification has two sources, either sufficient: pi's own stopReason "aborted", and
145
- // control-plane INTENT — providers do not uniformly attribute an aborted stream, so an abort
146
- // that was still in flight when the terminal arrived counts too.
147
- let abortsInFlight = 0;
148
- let abortSucceeded = false;
140
+ const ready = Deferred.await(bound);
141
+ /**
142
+ * Every control command obeys one admission rule: wait for this run's binding, then refuse once it has settled.
143
+ * Kept in one place so a fourth command cannot arrive with a fifth spelling of it.
144
+ */
145
+ const command = (run) => Effect.runPromise(Effect.gen(function* () {
146
+ const session = yield* ready;
147
+ if (settled)
148
+ return yield* Effect.fail(new PortFailure(new Error("run already settled; the command cannot take effect")));
149
+ yield* run(session);
150
+ }));
151
+ // Prompt preparation stays OUTSIDE admission. An await between the settled check and the enqueue would let the run
152
+ // end in between, and pi accepts a message for a finished run without complaint (`_steeringMessages.push`) — the
153
+ // command would be dropped and still reported as success. Image resizing is exactly such an await, hundreds of
154
+ // milliseconds of dynamic import and Photon work, and doing it here also keeps it overlapping session acquisition.
155
+ const enqueue = async (p, kind) => {
156
+ const opts = await toPiPromptOptions(p, "queued");
157
+ return command((session) => port(() => session[kind](p.text, opts?.images)));
158
+ };
149
159
  const controls = {
150
- async steer(p) {
151
- const opts = await toPiPromptOptions(p);
152
- const session = await bound;
153
- if (settled)
154
- throw settledError();
155
- await session.steer(p.text, opts?.images);
156
- },
157
- async followUp(p) {
158
- const opts = await toPiPromptOptions(p);
159
- const session = await bound;
160
- if (settled)
161
- throw settledError();
162
- await session.followUp(p.text, opts?.images);
163
- },
164
- async abort() {
165
- const session = await bound;
166
- if (settled)
167
- throw settledError();
160
+ steer: (p) => enqueue(p, "steer"),
161
+ followUp: (p) => enqueue(p, "followUp"),
162
+ abort: () => command((session) => Effect.gen(function* () {
168
163
  abortsInFlight++;
169
- try {
170
- await session.abort();
164
+ yield* port(() => session.abort()).pipe(Effect.tap(() => Effect.sync(() => {
171
165
  abortSucceeded = true;
172
- }
173
- finally {
166
+ })), Effect.ensuring(Effect.sync(() => {
174
167
  abortsInFlight--;
175
- }
176
- },
168
+ })));
169
+ })),
177
170
  };
178
- observe({ type: "run_started", timestamp: Date.now(), runId, data: {} }, controls);
179
- try {
180
- let session;
181
- try {
182
- // The scope's lineage reaches the store's CREATE path only — an existing session opens
183
- // exactly as before, whatever the scope names.
184
- session = await sessionFactory(scope.session, scope.parentSession === undefined
185
- ? undefined
186
- : {
187
- parentSession: scope.parentSession,
188
- ...(scope.branchHints !== undefined ? { branchHints: scope.branchHints } : {}),
189
- });
190
- sessionReady(session);
191
- }
192
- catch (error) {
193
- // Setup failures (session open, auth, a broken definition) are EVENTS, never throws
194
- // (MUST 2) — and they settle the run as failed: an unrecorded outcome means the caller
195
- // cancelled, which this is not.
196
- sessionFailed(error); // a pending dispatch learns the run cannot take commands
197
- const terminal = errorToTerminal(error);
198
- outcome = { status: "failed", error: { message: terminal.details, retryable: terminal.retryable } };
171
+ const execute = Effect.gen(function* () {
172
+ yield* acquireSessionLease(lease, scope.session);
173
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
199
174
  settled = true;
200
- yield terminal;
201
- return;
202
- }
203
- try {
204
- const abort = () => session.abort().catch(() => { });
205
- onCancelReady(() => void abort());
206
- const queue = new EventQueue();
207
- let finalAssistant;
208
- /** Whether any of THIS attempt's answer has been streamed — the only output a retry duplicates. */
209
- let streamedAnswer = false;
210
- /** Set when a retry is refused because the answer already streamed — carries the ending error. */
211
- let retriedAfterAnswer;
212
- const unsub = session.subscribe((event) => {
213
- if (retriedAfterAnswer !== undefined)
214
- return; // decided; the retry's output is not ours
215
- if (event.type === "message_end" && event.message.role === "assistant") {
216
- finalAssistant = event.message;
217
- // Error messages and details can contain provider payloads; log only diagnostic metadata.
218
- for (const diagnostic of finalAssistant.diagnostics ?? []) {
219
- log.warn(`[fastagent] provider diagnostic ${diagnostic.type} (${finalAssistant.provider}/${finalAssistant.model}, session ${scope.session}, run ${runId})`);
220
- }
221
- }
222
- if (event.type === "compaction_end" && event.reason !== "manual") {
223
- const status = event.aborted ? "aborted" : event.errorMessage ? "failed" : "completed";
224
- const emit = event.errorMessage && !event.aborted ? log.warn : log.debug;
225
- emit(`[fastagent] automatic compaction ${event.reason} (session ${scope.session}, run ${runId}): ${status}`);
226
- }
227
- // pi retries a failed assistant request by DISCARDING that attempt's assistant message and
228
- // asking again. Everything the turn achieved before it survives — executed tools keep
229
- // their persisted results, and the retry resumes from them — so the only thing a retry can
230
- // duplicate is answer text already streamed, which SPEC deltas cannot retract. Refuse it
231
- // exactly there: refusing on tool events instead would push the retry out to the CALLER,
232
- // who can only re-run the whole prompt and execute the tool a second time.
233
- if (event.type === "auto_retry_start" && streamedAnswer) {
234
- retriedAfterAnswer = event.errorMessage;
235
- // Not synchronously: pi emits this event BEFORE creating the controller that makes its
236
- // backoff abortable, so an abort from inside the listener would find nothing to cancel
237
- // and the turn would still pay the delay and burn a provider call on a discarded answer.
238
- queueMicrotask(() => void abort());
239
- return;
240
- }
241
- const rich = toSessionEvent(event, runId);
242
- if (!rich)
243
- return;
244
- observe(rich);
245
- const projected = projectAgentEvent(rich);
246
- if (!projected)
247
- return;
248
- if (projected.type === "text" || projected.type === "thinking")
249
- streamedAnswer = true;
250
- queue.push(projected);
175
+ observe({ type: "run_settled", timestamp: Date.now(), runId, data: outcome ?? { status: "aborted" } });
176
+ }));
177
+ // Admission precedes binding so a racing control command waits on this run's readiness gate.
178
+ observe({ type: "run_started", timestamp: Date.now(), runId, data: {} }, controls);
179
+ const session = yield* acquireSession(sessionFactory, scope.session, scope.parentSession === undefined
180
+ ? undefined
181
+ : {
182
+ parentSession: scope.parentSession,
183
+ ...(scope.branchHints !== undefined ? { branchHints: scope.branchHints } : {}),
251
184
  });
252
- try {
253
- // Resolving prompt options lazy-loads the image pipeline and re-encodes every attachment,
254
- // so it both takes time and can throw before any engine work exists to fail. Hence the two
255
- // guards, in this order and no earlier: its failure is a turn failure (MUST 2), and the
256
- // latch has to be read after the LAST await before the call — the door armed above only
257
- // stops a RUNNING session, so a consumer who walked away during the build or the resize
258
- // would knock on an idle one and have the turn start anyway.
259
- let promptOptions;
260
- try {
261
- promptOptions = await toPiPromptOptions(prompt);
262
- }
263
- catch (error) {
264
- const terminal = errorToTerminal(error);
265
- outcome = { status: "failed", error: { message: terminal.details, retryable: terminal.retryable } };
266
- settled = true;
267
- yield terminal;
185
+ let finalAssistant;
186
+ let runStarted = false;
187
+ let extensionFailure;
188
+ let streamedAnswer = false;
189
+ let retriedAfterAnswer;
190
+ let eventFailure;
191
+ const stop = () => {
192
+ void Effect.runPromise(portCleanup("event-fault abort", () => session.abort()));
193
+ };
194
+ yield* Effect.acquireRelease(Effect.try({
195
+ try: () => session.subscribe((event) => {
196
+ if (retriedAfterAnswer !== undefined || eventFailure)
268
197
  return;
269
- }
270
- if (wasCancelled()) {
271
- settled = true;
272
- await abort();
273
- return; // cancelled: the outer finally settles it as aborted
274
- }
275
- const run = session.prompt(prompt.text, promptOptions);
276
- yield* queue.drainUntil(run);
277
- let terminal;
278
198
  try {
279
- await run;
280
- terminal =
281
- retriedAfterAnswer !== undefined
282
- ? { type: "failed", details: retriedAfterAnswer, retryable: true }
283
- : finalAssistant
284
- ? toTerminal(finalAssistant)
285
- : ENGINE_PRODUCED_NOTHING;
199
+ if (event.type === "agent_start")
200
+ runStarted = true;
201
+ // Compaction rewrites session history; the event's assistant message is the turn's fact.
202
+ if (event.type === "message_end" && event.message.role === "assistant") {
203
+ finalAssistant = event.message;
204
+ for (const diagnostic of finalAssistant.diagnostics ?? []) {
205
+ log.warn(`[fastagent] provider diagnostic ${diagnostic.type} (${finalAssistant.provider}/${finalAssistant.model}, session ${scope.session}, run ${runId})`);
206
+ }
207
+ }
208
+ if (event.type === "compaction_end" && event.reason !== "manual") {
209
+ const status = event.aborted ? "aborted" : event.errorMessage ? "failed" : "completed";
210
+ const emit = event.errorMessage && !event.aborted ? log.warn : log.debug;
211
+ emit(`[fastagent] automatic compaction ${event.reason} (session ${scope.session}, run ${runId}): ${status}`);
212
+ }
213
+ if (event.type === "auto_retry_start" && streamedAnswer) {
214
+ retriedAfterAnswer = event.errorMessage;
215
+ // Pi installs the retry controller after emitting this event.
216
+ queueMicrotask(stop);
217
+ return;
218
+ }
219
+ const rich = toSessionEvent(event, runId);
220
+ observe(rich);
221
+ if (!rich)
222
+ return;
223
+ const projected = projectAgentEvent(rich);
224
+ if (!projected)
225
+ return;
226
+ if (projected.type === "text" || projected.type === "thinking")
227
+ streamedAnswer = true;
228
+ Queue.offerUnsafe(queue, projected);
286
229
  }
287
230
  catch (error) {
288
- terminal = errorToTerminal(error);
231
+ eventFailure = new PortFailure(error);
232
+ queueMicrotask(stop);
289
233
  }
290
- if ((abortSucceeded || abortsInFlight > 0) && terminal.type === "failed") {
291
- terminal = { type: "failed", details: terminal.details, retryable: false, code: ABORTED_CODE };
234
+ }),
235
+ catch: (error) => new PortFailure(error),
236
+ }), (unsubscribe) => portCleanup("unsubscribe", unsubscribe));
237
+ // pi swallows a failing extension command, and a turn an extension failed to start, into its error channel;
238
+ // both are work this run owes its caller, so the run reports them.
239
+ yield* Effect.acquireRelease(Effect.sync(() => session.extensionRunner.onError((error) => {
240
+ if (OWED_BY_THE_RUN.has(error.event))
241
+ extensionFailure ??= `${error.event} failed (${error.extensionPath}): ${error.error}`;
242
+ })), (unsubscribe) => Effect.sync(unsubscribe));
243
+ yield* reportUserMessages(session, (entryId, text) => observe({ type: "user_message", timestamp: Date.now(), runId, data: { entryId, text } }));
244
+ const extensionTurns = trackExtensionTurns(session);
245
+ // Completing the gate can run waiting controls synchronously; their queue events must be observed.
246
+ yield* Deferred.succeed(bound, session);
247
+ const promptOptions = yield* port(() => toPiPromptOptions(prompt, "prompt"));
248
+ if (eventFailure)
249
+ return yield* Effect.fail(eventFailure);
250
+ // Whether an extension could take this input instead of the model (a command, or an `input` handler): only
251
+ // then may a prompt settle with no model run and still have done its job.
252
+ const extensionMayTakeInput = session.extensionRunner.getRegisteredCommands().length > 0 || session.extensionRunner.hasHandlers("input");
253
+ yield* portAbort("prompt", async () => {
254
+ await session.prompt(prompt.text, promptOptions);
255
+ // An extension command returns from `prompt()` as soon as its handler does, and a turn it started may not
256
+ // have begun yet, so idleness alone proves nothing. This run owns those turns until they settle, and any
257
+ // turn they start in turn.
258
+ do {
259
+ await Promise.allSettled([...extensionTurns]);
260
+ await session.waitForIdle();
261
+ } while (extensionTurns.size > 0);
262
+ }, () => session.abort());
263
+ if (eventFailure)
264
+ return yield* Effect.fail(eventFailure);
265
+ if (extensionFailure !== undefined)
266
+ return { type: "failed", details: extensionFailure, retryable: false };
267
+ if (retriedAfterAnswer !== undefined)
268
+ return { type: "failed", details: retriedAfterAnswer, retryable: true };
269
+ if (finalAssistant)
270
+ return toTerminal(finalAssistant);
271
+ // No model run at all: an extension took the input (a command, or an `input` handler) and did its work.
272
+ if (!runStarted && extensionMayTakeInput)
273
+ return { type: "completed" };
274
+ return {
275
+ type: "failed",
276
+ details: "the engine settled the run without ending an assistant message",
277
+ retryable: false,
278
+ };
279
+ }).pipe(Effect.onError((cause) => Deferred.failCause(bound, cause)));
280
+ const work = Effect.runFork(Effect.scoped(Effect.gen(function* () {
281
+ const terminal = yield* execute.pipe(Effect.catchCause((cause) => {
282
+ const error = portError(cause);
283
+ return Effect.succeed(error instanceof SessionBusy
284
+ ? {
285
+ type: "failed",
286
+ details: "session busy: a turn is already in flight for this session",
287
+ retryable: true,
288
+ code: SESSION_BUSY_CODE,
292
289
  }
293
- if (terminal.type === "failed") {
290
+ : errorToTerminal(error));
291
+ }));
292
+ settled = true;
293
+ Queue.offerUnsafe(queue, terminal.type === "failed" && (abortSucceeded || abortsInFlight > 0)
294
+ ? { ...terminal, retryable: false, code: ABORTED_CODE }
295
+ : terminal);
296
+ Queue.endUnsafe(queue);
297
+ // Producer completion is earlier than consumer completion.
298
+ yield* Deferred.await(consumed);
299
+ })).pipe(Effect.ensuring(Effect.sync(() => {
300
+ Queue.endUnsafe(queue);
301
+ }))), { signal: abort.signal });
302
+ const read = Queue.takeAll(queue).pipe(Effect.catchTag("Done", () => Effect.succeed([])));
303
+ try {
304
+ for (;;) {
305
+ const events = await Effect.runPromise(read);
306
+ if (events.length === 0)
307
+ return;
308
+ for (const event of events) {
309
+ if (wasCancelled())
310
+ return;
311
+ if (event.type === "failed") {
294
312
  outcome =
295
- terminal.code === ABORTED_CODE
296
- ? // Carry the detail: an independent error that raced an accepted abort must stay
297
- // diagnosable in the settlement, which is what audit consumers read.
298
- { status: "aborted", error: { message: terminal.details, retryable: false } }
299
- : {
300
- status: "failed",
301
- error: { code: terminal.code, message: terminal.details, retryable: terminal.retryable },
302
- };
313
+ event.code === ABORTED_CODE
314
+ ? { status: "aborted", error: { message: event.details, retryable: false } }
315
+ : { status: "failed", error: { code: event.code, message: event.details, retryable: event.retryable } };
303
316
  }
304
- else {
317
+ else if (event.type === "completed")
305
318
  outcome = { status: "completed" };
306
- }
307
- // Commands become ineffective the moment the run resolved — not at the outer finally,
308
- // which sits behind a consumer-paced `yield`.
309
- settled = true;
310
- yield terminal;
311
- }
312
- finally {
313
- settled = true;
314
- unsub();
315
- }
316
- }
317
- finally {
318
- // NO session_shutdown here, deliberately. A per-invoke session makes one look right, but the
319
- // extension INSTANCE it would tear down is not per-invoke: extensions belong to the agent's
320
- // assembly and every turn shares one. Emitting a shutdown per turn had a finished
321
- // turn clearing a timer a concurrent turn had just opened (measured, and pinned in
322
- // definition-extensions.test.ts). The lifecycle has to match the instance, not the session
323
- // wrapper: one agent, one instance, no per-turn teardown. Extensions that need per-turn
324
- // cleanup do it in the tool or handler that opened the resource.
325
- try {
326
- session.dispose();
327
- }
328
- catch (error) {
329
- log.warn(`[fastagent] session dispose failed during cleanup: ${String(error)}`);
319
+ yield event;
330
320
  }
331
321
  }
332
322
  }
333
323
  finally {
334
- settled = true;
335
- observe({ type: "run_settled", timestamp: Date.now(), runId, data: outcome ?? { status: "aborted" } });
336
- release(); // after the settlement, so the next invoke for this session cannot outrun it
324
+ Deferred.doneUnsafe(consumed, Effect.void);
325
+ await Effect.runPromise(Fiber.await(work));
337
326
  }
338
327
  }
339
328
  return { invoke: (scope, prompt) => cancellableStream((hooks) => turn(scope, prompt, hooks)) };