@fastagent-sh/fastagent 0.24.3 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. package/README.md +53 -82
  2. package/dist/agent-api.d.ts +452 -0
  3. package/dist/agent-api.js +201 -0
  4. package/dist/answer-text.d.ts +8 -0
  5. package/dist/answer-text.js +18 -0
  6. package/dist/atomic-write.d.ts +2 -2
  7. package/dist/atomic-write.js +2 -2
  8. package/dist/authoring.d.ts +77 -0
  9. package/dist/authoring.js +238 -0
  10. package/dist/channel.d.ts +29 -15
  11. package/dist/channel.js +4 -1
  12. package/dist/channels/agentcore-protocol.d.ts +36 -50
  13. package/dist/channels/agentcore-protocol.js +3 -3
  14. package/dist/channels/agentcore-service.d.ts +8 -18
  15. package/dist/channels/agentcore-service.js +108 -127
  16. package/dist/channels/agentcore.d.ts +8 -24
  17. package/dist/channels/agentcore.js +32 -65
  18. package/dist/channels/body.d.ts +0 -16
  19. package/dist/channels/body.js +0 -25
  20. package/dist/channels/busy.js +6 -1
  21. package/dist/channels/context.d.ts +4 -0
  22. package/dist/channels/context.js +35 -0
  23. package/dist/channels/discover.d.ts +18 -20
  24. package/dist/channels/discover.js +41 -99
  25. package/dist/channels/feishu/feishu-api.d.ts +54 -4
  26. package/dist/channels/feishu/feishu-api.js +192 -149
  27. package/dist/channels/feishu/feishu.d.ts +3 -0
  28. package/dist/channels/feishu/feishu.js +117 -106
  29. package/dist/channels/feishu/history.d.ts +55 -0
  30. package/dist/channels/feishu/history.js +199 -0
  31. package/dist/channels/feishu/invoke-turn.d.ts +13 -11
  32. package/dist/channels/feishu/invoke-turn.js +9 -10
  33. package/dist/channels/feishu/model.d.ts +2 -1
  34. package/dist/channels/feishu/normalize.js +100 -0
  35. package/dist/channels/feishu/preview.d.ts +2 -2
  36. package/dist/channels/feishu/register-webhook.d.ts +1 -0
  37. package/dist/channels/feishu/register-webhook.js +3 -2
  38. package/dist/channels/feishu/scaffold/channel.ts +65 -46
  39. package/dist/channels/feishu/scaffold/feishu-send.ts +38 -33
  40. package/dist/channels/feishu/scaffold/feishu-threads.ts +25 -0
  41. package/dist/channels/feishu/setup-mode.d.ts +45 -25
  42. package/dist/channels/feishu/setup-mode.js +74 -23
  43. package/dist/channels/feishu/shared-api.d.ts +24 -6
  44. package/dist/channels/feishu/shared-api.js +30 -24
  45. package/dist/channels/kit/context-buffer.d.ts +11 -2
  46. package/dist/channels/kit/context-buffer.js +7 -4
  47. package/dist/channels/kit/delivery.d.ts +19 -3
  48. package/dist/channels/kit/delivery.js +74 -15
  49. package/dist/channels/kit/event-stream.js +2 -1
  50. package/dist/channels/kit/invoke-turn-kit.d.ts +41 -41
  51. package/dist/channels/kit/invoke-turn-kit.js +62 -71
  52. package/dist/channels/kit/place-history.d.ts +107 -0
  53. package/dist/channels/kit/place-history.js +182 -0
  54. package/dist/channels/kit/preview-kit.d.ts +9 -5
  55. package/dist/channels/kit/preview-kit.js +36 -15
  56. package/dist/channels/kit/room-threads.d.ts +46 -0
  57. package/dist/channels/kit/room-threads.js +81 -0
  58. package/dist/channels/kit/seen.js +1 -1
  59. package/dist/channels/kit/stop-command.d.ts +6 -3
  60. package/dist/channels/kit/stop-command.js +20 -18
  61. package/dist/channels/kit/thread-fork.d.ts +17 -0
  62. package/dist/channels/kit/thread-fork.js +43 -0
  63. package/dist/channels/kit/thread-participants.js +1 -1
  64. package/dist/channels/kit/transport.d.ts +5 -0
  65. package/dist/channels/kit/transport.js +5 -0
  66. package/dist/channels/kit/turn-runner.d.ts +10 -9
  67. package/dist/channels/kit/turn-runner.js +17 -19
  68. package/dist/channels/kit/turn-store.d.ts +5 -4
  69. package/dist/channels/kit/turn-store.js +5 -5
  70. package/dist/channels/lark/lark.d.ts +3 -0
  71. package/dist/channels/lark/lark.js +5 -0
  72. package/dist/channels/lark/onboard.d.ts +5 -3
  73. package/dist/channels/lark/onboard.js +14 -14
  74. package/dist/channels/lark/scaffold/channel.ts +67 -44
  75. package/dist/channels/lark/scaffold/lark-send.ts +38 -33
  76. package/dist/channels/lark/scaffold/lark-threads.ts +25 -0
  77. package/dist/channels/respond.d.ts +0 -4
  78. package/dist/channels/respond.js +1 -1
  79. package/dist/channels/router.d.ts +18 -0
  80. package/dist/channels/router.js +68 -0
  81. package/dist/channels/serve.d.ts +5 -70
  82. package/dist/channels/serve.js +105 -287
  83. package/dist/channels/slack/history.d.ts +42 -0
  84. package/dist/channels/slack/history.js +282 -0
  85. package/dist/channels/slack/invoke-turn.d.ts +12 -5
  86. package/dist/channels/slack/invoke-turn.js +7 -5
  87. package/dist/channels/slack/manifest.d.ts +1 -4
  88. package/dist/channels/slack/manifest.js +22 -10
  89. package/dist/channels/slack/model.d.ts +2 -1
  90. package/dist/channels/slack/onboard.d.ts +0 -2
  91. package/dist/channels/slack/onboard.js +2 -4
  92. package/dist/channels/slack/onboarding-state.d.ts +0 -2
  93. package/dist/channels/slack/onboarding-state.js +0 -1
  94. package/dist/channels/slack/parse.d.ts +0 -5
  95. package/dist/channels/slack/parse.js +1 -16
  96. package/dist/channels/slack/preview.d.ts +2 -3
  97. package/dist/channels/slack/preview.js +78 -52
  98. package/dist/channels/slack/register-webhook.d.ts +1 -0
  99. package/dist/channels/slack/register-webhook.js +6 -5
  100. package/dist/channels/slack/scaffold/channel.ts +23 -24
  101. package/dist/channels/slack/scaffold/slack-send.ts +50 -43
  102. package/dist/channels/slack/scaffold/slack-threads.ts +29 -0
  103. package/dist/channels/slack/setup-server.d.ts +19 -0
  104. package/dist/channels/slack/setup-server.js +113 -11
  105. package/dist/channels/slack/shared-api.d.ts +15 -4
  106. package/dist/channels/slack/shared-api.js +19 -14
  107. package/dist/channels/slack/slack-api.d.ts +61 -2
  108. package/dist/channels/slack/slack-api.js +28 -5
  109. package/dist/channels/slack/slack.js +97 -52
  110. package/dist/channels/slack/welcomed.js +1 -1
  111. package/dist/channels/telegram/context-buffer.d.ts +6 -1
  112. package/dist/channels/telegram/context-buffer.js +19 -0
  113. package/dist/channels/telegram/invoke-turn.d.ts +8 -4
  114. package/dist/channels/telegram/invoke-turn.js +7 -7
  115. package/dist/channels/telegram/parse.d.ts +7 -2
  116. package/dist/channels/telegram/parse.js +7 -0
  117. package/dist/channels/telegram/preview.d.ts +2 -2
  118. package/dist/channels/telegram/register-webhook.d.ts +1 -0
  119. package/dist/channels/telegram/register-webhook.js +3 -2
  120. package/dist/channels/telegram/scaffold/channel.ts +27 -28
  121. package/dist/channels/telegram/scaffold/telegram-send.ts +61 -108
  122. package/dist/channels/telegram/shared-api.d.ts +34 -0
  123. package/dist/channels/telegram/shared-api.js +79 -0
  124. package/dist/channels/telegram/telegram-api.d.ts +15 -5
  125. package/dist/channels/telegram/telegram-api.js +10 -6
  126. package/dist/channels/telegram/telegram.js +27 -21
  127. package/dist/channels/telegram/turn-store.js +1 -1
  128. package/dist/channels/{kit/turn-queue.d.ts → turn-queue.d.ts} +2 -2
  129. package/dist/channels/{kit/turn-queue.js → turn-queue.js} +10 -10
  130. package/dist/cli/add-feishu.d.ts +42 -12
  131. package/dist/cli/add-feishu.js +177 -124
  132. package/dist/cli/add-slack.d.ts +8 -3
  133. package/dist/cli/add-slack.js +115 -63
  134. package/dist/cli/auth-view.d.ts +10 -3
  135. package/dist/cli/auth-view.js +8 -6
  136. package/dist/cli/box-login.d.ts +17 -7
  137. package/dist/cli/box-login.js +46 -12
  138. package/dist/cli/commands/add.d.ts +15 -4
  139. package/dist/cli/commands/add.js +252 -143
  140. package/dist/cli/commands/auth.d.ts +6 -0
  141. package/dist/cli/commands/auth.js +50 -0
  142. package/dist/cli/commands/content.d.ts +6 -0
  143. package/dist/cli/commands/content.js +45 -0
  144. package/dist/cli/commands/deploy/agentcore.js +50 -77
  145. package/dist/cli/commands/deploy/docker.js +38 -39
  146. package/dist/cli/commands/deploy/fly.js +21 -18
  147. package/dist/cli/commands/deploy/host-login.d.ts +26 -0
  148. package/dist/cli/commands/deploy/host-login.js +90 -0
  149. package/dist/cli/commands/deploy/railway.js +21 -26
  150. package/dist/cli/commands/deploy/shared.d.ts +45 -17
  151. package/dist/cli/commands/deploy/shared.js +34 -15
  152. package/dist/cli/commands/deploy.d.ts +0 -4
  153. package/dist/cli/commands/deploy.js +237 -50
  154. package/dist/cli/commands/destroy.js +8 -6
  155. package/dist/cli/commands/dev.d.ts +0 -2
  156. package/dist/cli/commands/dev.js +20 -19
  157. package/dist/cli/commands/env.d.ts +12 -0
  158. package/dist/cli/commands/env.js +190 -0
  159. package/dist/cli/commands/info.js +95 -156
  160. package/dist/cli/commands/init.d.ts +2 -2
  161. package/dist/cli/commands/init.js +35 -33
  162. package/dist/cli/commands/invoke.d.ts +6 -0
  163. package/dist/cli/commands/invoke.js +131 -16
  164. package/dist/cli/commands/login.d.ts +31 -2
  165. package/dist/cli/commands/login.js +413 -64
  166. package/dist/cli/commands/logout.d.ts +4 -0
  167. package/dist/cli/commands/logout.js +53 -0
  168. package/dist/cli/commands/logs.js +8 -6
  169. package/dist/cli/commands/models.d.ts +2 -1
  170. package/dist/cli/commands/models.js +39 -37
  171. package/dist/cli/commands/schedules.d.ts +21 -0
  172. package/dist/cli/commands/schedules.js +143 -0
  173. package/dist/cli/commands/start.d.ts +11 -10
  174. package/dist/cli/commands/start.js +35 -62
  175. package/dist/cli/content-view.d.ts +9 -0
  176. package/dist/cli/content-view.js +26 -0
  177. package/dist/cli/fail.d.ts +9 -12
  178. package/dist/cli/fail.js +17 -14
  179. package/dist/cli/invoke-stream.d.ts +12 -3
  180. package/dist/cli/invoke-stream.js +85 -23
  181. package/dist/cli/kernel.d.ts +9 -2
  182. package/dist/cli/kernel.js +15 -6
  183. package/dist/cli/login-relay.d.ts +3 -2
  184. package/dist/cli/login-relay.js +18 -6
  185. package/dist/cli/models-view.d.ts +5 -1
  186. package/dist/cli/models-view.js +1 -1
  187. package/dist/cli/person.d.ts +10 -0
  188. package/dist/cli/person.js +23 -0
  189. package/dist/cli/program.js +421 -203
  190. package/dist/cli/serve.d.ts +13 -63
  191. package/dist/cli/serve.js +31 -121
  192. package/dist/cli/shared.d.ts +21 -48
  193. package/dist/cli/shared.js +68 -89
  194. package/dist/cli/ticket.d.ts +136 -0
  195. package/dist/cli/ticket.js +476 -0
  196. package/dist/client.d.ts +7 -0
  197. package/dist/client.js +2 -0
  198. package/dist/content/declare.d.ts +70 -0
  199. package/dist/content/declare.js +163 -0
  200. package/dist/content/file.d.ts +32 -0
  201. package/dist/content/file.js +119 -0
  202. package/dist/content/git.d.ts +41 -0
  203. package/dist/content/git.js +217 -0
  204. package/dist/content/mount.d.ts +2 -0
  205. package/dist/content/mount.js +15 -0
  206. package/dist/content/resolve.d.ts +52 -0
  207. package/dist/content/resolve.js +151 -0
  208. package/dist/content/source.d.ts +27 -0
  209. package/dist/content/source.js +53 -0
  210. package/dist/conversation-sessions.d.ts +26 -0
  211. package/dist/conversation-sessions.js +99 -0
  212. package/dist/declared-secrets.d.ts +19 -37
  213. package/dist/declared-secrets.js +16 -58
  214. package/dist/deploy/agentcore/destroy.d.ts +3 -3
  215. package/dist/deploy/agentcore/destroy.js +46 -28
  216. package/dist/deploy/agentcore/forwarder.js +105 -22
  217. package/dist/deploy/agentcore/logs.js +1 -1
  218. package/dist/deploy/agentcore/plan.d.ts +10 -24
  219. package/dist/deploy/agentcore/plan.js +31 -135
  220. package/dist/deploy/agentcore/run.d.ts +1 -1
  221. package/dist/deploy/agentcore/run.js +8 -5
  222. package/dist/deploy/agentcore/shell.d.ts +1 -1
  223. package/dist/deploy/agentcore/shell.js +5 -5
  224. package/dist/deploy/api.d.ts +34 -0
  225. package/dist/deploy/api.js +50 -0
  226. package/dist/deploy/box-shell.d.ts +6 -0
  227. package/dist/deploy/box-shell.js +7 -0
  228. package/dist/deploy/build-context.d.ts +7 -5
  229. package/dist/deploy/build-context.js +41 -35
  230. package/dist/deploy/channel-ingress.d.ts +6 -1
  231. package/dist/deploy/channel-ingress.js +18 -5
  232. package/dist/deploy/container.d.ts +22 -9
  233. package/dist/deploy/container.js +96 -55
  234. package/dist/deploy/docker/plan.d.ts +3 -2
  235. package/dist/deploy/docker/plan.js +13 -14
  236. package/dist/deploy/docker/run.d.ts +2 -2
  237. package/dist/deploy/docker/run.js +8 -4
  238. package/dist/deploy/fly/plan.d.ts +1 -1
  239. package/dist/deploy/fly/plan.js +12 -21
  240. package/dist/deploy/fly/run.d.ts +10 -6
  241. package/dist/deploy/fly/run.js +11 -6
  242. package/dist/deploy/hosts.d.ts +2 -1
  243. package/dist/deploy/hosts.js +2 -1
  244. package/dist/deploy/preflight.d.ts +21 -23
  245. package/dist/deploy/preflight.js +222 -201
  246. package/dist/deploy/railway/plan.d.ts +8 -6
  247. package/dist/deploy/railway/plan.js +15 -58
  248. package/dist/deploy/railway/run.d.ts +8 -3
  249. package/dist/deploy/railway/run.js +12 -8
  250. package/dist/deploy/residency.d.ts +5 -11
  251. package/dist/deploy/residency.js +0 -6
  252. package/dist/deploy/runner.d.ts +1 -1
  253. package/dist/deploy/runner.js +1 -1
  254. package/dist/deploy/secrets.d.ts +16 -2
  255. package/dist/deploy/secrets.js +33 -6
  256. package/dist/deploy/workspace.d.ts +11 -6
  257. package/dist/deploy/workspace.js +55 -38
  258. package/dist/dev-supervisor.d.ts +11 -2
  259. package/dist/dev-supervisor.js +51 -26
  260. package/dist/effect-port.d.ts +4 -4
  261. package/dist/effect-port.js +4 -4
  262. package/dist/environment/apt.d.ts +4 -0
  263. package/dist/environment/apt.js +32 -0
  264. package/dist/environment/declare.d.ts +59 -0
  265. package/dist/environment/declare.js +210 -0
  266. package/dist/environment/mise.d.ts +50 -0
  267. package/dist/environment/mise.js +268 -0
  268. package/dist/feishu.d.ts +2 -2
  269. package/dist/feishu.js +2 -2
  270. package/dist/git.d.ts +13 -0
  271. package/dist/git.js +55 -0
  272. package/dist/harnesses/models/catalog.d.ts +20 -0
  273. package/dist/harnesses/models/catalog.js +254 -0
  274. package/dist/{engines/pi/auth.d.ts → harnesses/models/credentials.d.ts} +3 -3
  275. package/dist/{engines/pi/auth.js → harnesses/models/credentials.js} +3 -3
  276. package/dist/harnesses/models/environment.d.ts +43 -0
  277. package/dist/harnesses/models/environment.js +82 -0
  278. package/dist/{engines/pi → harnesses/models}/login.d.ts +57 -26
  279. package/dist/{engines/pi → harnesses/models}/login.js +187 -79
  280. package/dist/harnesses/models/models.d.ts +83 -0
  281. package/dist/harnesses/models/models.js +158 -0
  282. package/dist/harnesses/pi-durable/agent.d.ts +42 -0
  283. package/dist/harnesses/pi-durable/agent.js +579 -0
  284. package/dist/harnesses/pi-durable/defaults.d.ts +28 -0
  285. package/dist/harnesses/pi-durable/defaults.js +26 -0
  286. package/dist/harnesses/pi-durable/define.d.ts +77 -0
  287. package/dist/harnesses/pi-durable/define.js +28 -0
  288. package/dist/harnesses/pi-durable/entries.d.ts +32 -0
  289. package/dist/harnesses/pi-durable/entries.js +126 -0
  290. package/dist/harnesses/pi-durable/events.d.ts +25 -0
  291. package/dist/harnesses/pi-durable/events.js +238 -0
  292. package/dist/harnesses/pi-durable/ids.d.ts +16 -0
  293. package/dist/harnesses/pi-durable/ids.js +55 -0
  294. package/dist/harnesses/pi-durable/messages.d.ts +41 -0
  295. package/dist/harnesses/pi-durable/messages.js +116 -0
  296. package/dist/harnesses/pi-durable/open.d.ts +88 -0
  297. package/dist/harnesses/pi-durable/open.js +329 -0
  298. package/dist/harnesses/pi-durable/reads.d.ts +30 -0
  299. package/dist/harnesses/pi-durable/reads.js +94 -0
  300. package/dist/harnesses/pi-durable/registry.d.ts +18 -0
  301. package/dist/harnesses/pi-durable/registry.js +56 -0
  302. package/dist/harnesses/pi-durable/release.d.ts +13 -0
  303. package/dist/harnesses/pi-durable/release.js +40 -0
  304. package/dist/harnesses/pi-durable/serve.d.ts +80 -0
  305. package/dist/harnesses/pi-durable/serve.js +156 -0
  306. package/dist/harnesses/pi-durable/sessions.d.ts +27 -0
  307. package/dist/harnesses/pi-durable/sessions.js +47 -0
  308. package/dist/harnesses/pi-durable/storage.d.ts +4 -0
  309. package/dist/harnesses/pi-durable/storage.js +49 -0
  310. package/dist/harnesses/pi-durable/stream.d.ts +20 -0
  311. package/dist/harnesses/pi-durable/stream.js +221 -0
  312. package/dist/harnesses/pi-durable/text.d.ts +30 -0
  313. package/dist/harnesses/pi-durable/text.js +292 -0
  314. package/dist/harnesses/pi-durable/wake.d.ts +4 -0
  315. package/dist/harnesses/pi-durable/wake.js +81 -0
  316. package/dist/http/client.d.ts +6 -0
  317. package/dist/http/client.js +256 -0
  318. package/dist/http/contract.d.ts +541 -0
  319. package/dist/http/contract.js +179 -0
  320. package/dist/http/server.d.ts +34 -0
  321. package/dist/http/server.js +194 -0
  322. package/dist/index.d.ts +17 -4
  323. package/dist/index.js +10 -5
  324. package/dist/lark.d.ts +2 -2
  325. package/dist/lark.js +2 -2
  326. package/dist/locked-file.d.ts +17 -0
  327. package/dist/{engines/pi/locked-file.js → locked-file.js} +16 -10
  328. package/dist/once.d.ts +6 -0
  329. package/dist/once.js +23 -0
  330. package/dist/paths.d.ts +36 -52
  331. package/dist/paths.js +66 -182
  332. package/dist/runtime.d.ts +25 -2
  333. package/dist/runtime.js +27 -0
  334. package/dist/scaffold/add-channel.d.ts +14 -27
  335. package/dist/scaffold/add-channel.js +82 -180
  336. package/dist/scaffold/init.d.ts +17 -18
  337. package/dist/scaffold/init.js +87 -99
  338. package/dist/scaffold/templates/SYSTEM.md +12 -0
  339. package/dist/scaffold/templates/env.example +4 -10
  340. package/dist/scaffold/templates/fastagent.config.ts +21 -33
  341. package/dist/scaffold/templates/gitignore +8 -1
  342. package/dist/scaffold/templates/skills/writing-great-skills/SKILL.md +1 -2
  343. package/dist/scaffold/templates.d.ts +9 -4
  344. package/dist/scaffold/templates.js +7 -5
  345. package/dist/scaffold/vendor-skill.d.ts +1 -4
  346. package/dist/scaffold/vendor-skill.js +5 -6
  347. package/dist/schedule/cron.d.ts +13 -0
  348. package/dist/schedule/cron.js +52 -0
  349. package/dist/schedule/eventbridge-cron.d.ts +17 -0
  350. package/dist/schedule/eventbridge-cron.js +88 -0
  351. package/dist/schedule/schedule.d.ts +23 -0
  352. package/dist/schedule/schedule.js +34 -0
  353. package/dist/schedule/scheduler.d.ts +34 -33
  354. package/dist/schedule/scheduler.js +251 -137
  355. package/dist/schedule/state.d.ts +4 -14
  356. package/dist/schedule/state.js +7 -7
  357. package/dist/schedule/wake-alarm.d.ts +22 -2
  358. package/dist/schedule/wake-alarm.js +66 -20
  359. package/dist/schedule/wakeups.d.ts +16 -15
  360. package/dist/schedule/wakeups.js +47 -45
  361. package/dist/service.d.ts +56 -169
  362. package/dist/service.js +91 -187
  363. package/dist/serving-command.d.ts +10 -0
  364. package/dist/serving-command.js +16 -0
  365. package/dist/slack.d.ts +1 -1
  366. package/dist/slack.js +1 -1
  367. package/dist/{channels/kit/state.js → state-file.js} +6 -6
  368. package/dist/telegram.d.ts +1 -0
  369. package/dist/telegram.js +1 -0
  370. package/dist/tunnel.d.ts +3 -0
  371. package/dist/tunnel.js +12 -4
  372. package/dist/value-file.d.ts +21 -0
  373. package/dist/value-file.js +108 -0
  374. package/dist/version.d.ts +5 -0
  375. package/dist/version.js +14 -1
  376. package/package.json +18 -28
  377. package/dist/agent.d.ts +0 -106
  378. package/dist/agent.js +0 -21
  379. package/dist/channels/control.d.ts +0 -31
  380. package/dist/channels/control.js +0 -329
  381. package/dist/channels/define-channel.d.ts +0 -39
  382. package/dist/channels/define-channel.js +0 -9
  383. package/dist/channels/feishu/context-buffer.d.ts +0 -35
  384. package/dist/channels/feishu/context-buffer.js +0 -65
  385. package/dist/channels/feishu/scaffold/channel.websocket.ts +0 -31
  386. package/dist/channels/http.d.ts +0 -23
  387. package/dist/channels/http.js +0 -57
  388. package/dist/channels/lark/scaffold/channel.websocket.ts +0 -31
  389. package/dist/channels/slack/context-buffer.d.ts +0 -23
  390. package/dist/channels/slack/context-buffer.js +0 -37
  391. package/dist/channels/sse.d.ts +0 -4
  392. package/dist/channels/sse.js +0 -66
  393. package/dist/cli/commands/chat.d.ts +0 -3
  394. package/dist/cli/commands/chat.js +0 -14
  395. package/dist/cli/commands/routine-run.d.ts +0 -6
  396. package/dist/cli/commands/routine-run.js +0 -46
  397. package/dist/cli/commands/routine.d.ts +0 -25
  398. package/dist/cli/commands/routine.js +0 -122
  399. package/dist/cli/commands/tool.d.ts +0 -1
  400. package/dist/cli/commands/tool.js +0 -57
  401. package/dist/collect.d.ts +0 -63
  402. package/dist/collect.js +0 -90
  403. package/dist/core.d.ts +0 -13
  404. package/dist/core.js +0 -15
  405. package/dist/engines/pi/agent-models.d.ts +0 -83
  406. package/dist/engines/pi/agent-models.js +0 -120
  407. package/dist/engines/pi/agent-session-factory.d.ts +0 -121
  408. package/dist/engines/pi/agent-session-factory.js +0 -517
  409. package/dist/engines/pi/chat.d.ts +0 -3
  410. package/dist/engines/pi/chat.js +0 -8
  411. package/dist/engines/pi/config.d.ts +0 -71
  412. package/dist/engines/pi/config.js +0 -199
  413. package/dist/engines/pi/create.d.ts +0 -170
  414. package/dist/engines/pi/create.js +0 -343
  415. package/dist/engines/pi/definition.d.ts +0 -74
  416. package/dist/engines/pi/definition.js +0 -236
  417. package/dist/engines/pi/entry-images.d.ts +0 -14
  418. package/dist/engines/pi/entry-images.js +0 -23
  419. package/dist/engines/pi/invoke-session.d.ts +0 -18
  420. package/dist/engines/pi/invoke-session.js +0 -403
  421. package/dist/engines/pi/locked-file.d.ts +0 -9
  422. package/dist/engines/pi/machine.d.ts +0 -53
  423. package/dist/engines/pi/machine.js +0 -137
  424. package/dist/engines/pi/models.d.ts +0 -200
  425. package/dist/engines/pi/models.js +0 -533
  426. package/dist/engines/pi/open.d.ts +0 -180
  427. package/dist/engines/pi/open.js +0 -270
  428. package/dist/engines/pi/openai-account-models.d.ts +0 -35
  429. package/dist/engines/pi/openai-account-models.js +0 -116
  430. package/dist/engines/pi/report.d.ts +0 -18
  431. package/dist/engines/pi/report.js +0 -59
  432. package/dist/engines/pi/retry-event.d.ts +0 -6
  433. package/dist/engines/pi/retry-event.js +0 -15
  434. package/dist/engines/pi/service.d.ts +0 -9
  435. package/dist/engines/pi/service.js +0 -14
  436. package/dist/engines/pi/session-builder.d.ts +0 -13
  437. package/dist/engines/pi/session-builder.js +0 -117
  438. package/dist/engines/pi/session-control.d.ts +0 -77
  439. package/dist/engines/pi/session-control.js +0 -908
  440. package/dist/engines/pi/session-effects.d.ts +0 -14
  441. package/dist/engines/pi/session-effects.js +0 -30
  442. package/dist/engines/pi/session-inheritance.d.ts +0 -21
  443. package/dist/engines/pi/session-inheritance.js +0 -204
  444. package/dist/engines/pi/session-markers.d.ts +0 -49
  445. package/dist/engines/pi/session-markers.js +0 -60
  446. package/dist/engines/pi/session-settings.d.ts +0 -67
  447. package/dist/engines/pi/session-settings.js +0 -119
  448. package/dist/engines/pi/session-store.d.ts +0 -69
  449. package/dist/engines/pi/session-store.js +0 -430
  450. package/dist/engines/pi/tool-context.d.ts +0 -45
  451. package/dist/engines/pi/tool-context.js +0 -31
  452. package/dist/engines/pi/tool.d.ts +0 -99
  453. package/dist/engines/pi/tool.js +0 -118
  454. package/dist/engines/pi/turn-kit.d.ts +0 -83
  455. package/dist/engines/pi/turn-kit.js +0 -275
  456. package/dist/engines/pi/wake-tool.d.ts +0 -9
  457. package/dist/engines/pi/wake-tool.js +0 -92
  458. package/dist/loader.d.ts +0 -41
  459. package/dist/loader.js +0 -116
  460. package/dist/node.d.ts +0 -3
  461. package/dist/node.js +0 -4
  462. package/dist/observe.d.ts +0 -6
  463. package/dist/observe.js +0 -47
  464. package/dist/pi.d.ts +0 -25
  465. package/dist/pi.js +0 -15
  466. package/dist/scaffold/templates/APPEND_SYSTEM.md +0 -12
  467. package/dist/scaffold/templates/tools/fetch-url.ts +0 -25
  468. package/dist/schedule/discover.d.ts +0 -17
  469. package/dist/schedule/discover.js +0 -61
  470. package/dist/schedule/routine.d.ts +0 -43
  471. package/dist/schedule/routine.js +0 -32
  472. package/dist/schedule/run.d.ts +0 -55
  473. package/dist/schedule/run.js +0 -146
  474. package/dist/secrets-gate.d.ts +0 -32
  475. package/dist/secrets-gate.js +0 -33
  476. package/dist/session-remote.d.ts +0 -47
  477. package/dist/session-remote.js +0 -460
  478. package/dist/session.d.ts +0 -466
  479. package/dist/session.js +0 -57
  480. /package/dist/{channels/kit/state.d.ts → state-file.d.ts} +0 -0
@@ -1,44 +1,18 @@
1
1
  /** Resident cron and wake polling. */
2
2
  import * as Cause from "effect/Cause";
3
3
  import * as Clock from "effect/Clock";
4
+ import * as Deferred from "effect/Deferred";
5
+ import * as Result from "effect/Result";
4
6
  import * as Effect from "effect/Effect";
5
- import { SESSION_BUSY_CODE } from "../agent.js";
7
+ import { join } from "node:path";
8
+ import { AgentError } from "../agent-api.js";
9
+ import { conversationSessions } from "../conversation-sessions.js";
6
10
  import { PortFailure } from "../effect-port.js";
7
11
  import { beginWork } from "../channels/busy.js";
8
12
  import { log } from "../log.js";
9
13
  import { nextRun } from "./cron.js";
10
- import { routineSession } from "./routine.js";
11
14
  import { claimSlot, latestFire, settleClaim } from "./state.js";
12
- import { deferWakeup, takeFirstDueWakeup } from "./wakeups.js";
13
- /**
14
- * Account for a claim whose turn never reported. The slot is claimed BEFORE the turn runs and shutdown does not wait
15
- * for that turn (see `AgentService.close`), so a restart landing mid-fire leaves a claim that the next boot skips —
16
- * the exact silence the history exists to prevent. Settled once, at the next boot, which also makes it idempotent.
17
- *
18
- * The claim is the ONE record here: it IS the decision, and the outcome is written back into it.
19
- *
20
- * NOT re-fired: a turn that kills its own process would then replay on every boot. Cron only: a killed wake-up
21
- * leaves no claim behind to reconcile (`takeFirstDueWakeup` removes it before the turn starts).
22
- *
23
- * THE NEWEST CLAIM ONLY. A schedule's loop runs one turn at a time, so a killed process leaves exactly one claim
24
- * unsettled — the one it was in. Walking the whole retained window would add nothing a resident process can
25
- * produce, and would instead rewrite every claim an OLDER FORMAT left without an outcome, turning a window of runs
26
- * that actually succeeded into `interrupted`.
27
- *
28
- * KNOWN FALSE POSITIVE: a second scheduler booting while the first is mid-turn sees a claim nothing accounts for
29
- * YET, and reports it; the first process then overwrites the outcome when its turn settles. Telling them apart needs
30
- * the claimer's liveness, which is a lease, not a claim — and the whole point of the claim is that it needs none.
31
- */
32
- function markInterruptedFire(stateRoot, name, fire) {
33
- if (fire === undefined || fire.outcome !== undefined)
34
- return;
35
- log.warn(`[schedule] ${name}: the fire claimed at ${fire.firedAt} never finished — the process stopped mid-turn ` +
36
- `and that slot stays skipped (see \`fastagent routine history ${name}\`)`);
37
- // `fire.slot` came from a claim file name, which `listClaims` admits only when it round-trips through this same
38
- // conversion — so the Date is valid and `settleClaim` writes back the file it was read from. No duration: this
39
- // turn was never timed, and the process that could have timed it is gone.
40
- settleClaim(stateRoot, name, new Date(fire.slot), "interrupted");
41
- }
15
+ import { requeueWakeup, takeFirstDueWakeup } from "./wakeups.js";
42
16
  /**
43
17
  * The longest a single armed wait sleeps before looking at the wall clock again.
44
18
  *
@@ -66,67 +40,94 @@ function oneLine(text) {
66
40
  const folded = text.replace(/\s+/g, " ").trim();
67
41
  return folded === "" ? "" : `: ${folded}`;
68
42
  }
43
+ /** What a wait for a message's end gave instead of its end, as the one line this log says it with. */
44
+ async function untilEnded(agent, key, signal) {
45
+ return agent.messages.get(key, { wait: Number.POSITIVE_INFINITY, signal }).then((message) => ({ ended: message }), (error) => ({
46
+ unfinished: `waiting for its end failed: ${error instanceof Error ? error.message : String(error)}`,
47
+ }));
48
+ }
69
49
  /**
70
- * The iterator is a Promise port inside an uninterruptible claimed occurrence, including its cleanup.
71
- *
72
- * WHAT THE TURN SAID IS NOT LOGGED. It is already durable: every fire runs in the session named in the line below
73
- * (`routine:<name>` for a cron, the asking conversation for a wake-up), and a session is persisted under
74
- * EXPORTED for the API path (`POST /run`), which runs the same turn without a claim: it has no occurrence to
75
- * claim, because nobody's grid produced it (schedule/run.ts). The resident clock's own claim/run/settle is
76
- * {@link fireRoutineOnce}.
50
+ * Send one message and wait for its end; `onAccepted` hears that it was recorded. Interrupted, it stops waiting, and
51
+ * the agent goes on: a Caller that stops waiting only lets go (SPEC.md §4).
52
+ */
53
+ async function sendAndWait(agent, request, onAccepted, signal) {
54
+ let message;
55
+ try {
56
+ message = await agent.messages.send(request);
57
+ }
58
+ catch (error) {
59
+ if (!(error instanceof AgentError))
60
+ throw error;
61
+ if (error.code === "unconfirmed")
62
+ return { unfinished: `whether it was recorded is not known: ${error.message}` };
63
+ return { refused: error.message, busy: error.code === "busy" };
64
+ }
65
+ onAccepted();
66
+ return untilEnded(agent, { sessionId: message.sessionId, messageId: message.id }, signal);
67
+ }
68
+ /** How a message ended, as the one word a claim keeps, and the detail worth a log line. */
69
+ function settled(message) {
70
+ switch (message.status) {
71
+ case "completed":
72
+ case "incomplete":
73
+ return { fire: "completed" };
74
+ case "canceled":
75
+ return { fire: "failed", detail: "canceled" };
76
+ default:
77
+ return { fire: "failed", detail: message.error?.message ?? `the message ended ${message.status}` };
78
+ }
79
+ }
80
+ /**
81
+ * One invoke, read to its message's end, with what became of it said in the log.
77
82
  *
78
- * `<stateRoot>/sessions/` like any other. Copying the reply into the log would be a second store of the same text,
79
- * unstructured, in a stream with a wider audience and a bound that differs per host.
83
+ * WHAT THE AGENT SAID IS NOT LOGGED. It is already durable: every fire runs in the session named in the line below
84
+ * (the schedule's own, the asking conversation's for a wake-up), kept in the instance's storage like any other.
85
+ * Copying the reply into the log would be a second store of the same text, unstructured, in a stream with a wider
86
+ * audience and a bound that differs per host.
80
87
  */
81
- export function runTurn(agent, label, session, prompt) {
88
+ function runTurn(agent, label, request, onAccepted = () => { }) {
82
89
  return Effect.gen(function* () {
83
90
  const clock = yield* Clock.Clock;
84
91
  const startedAt = clock.currentTimeMillisUnsafe();
85
92
  const elapsed = () => clock.currentTimeMillisUnsafe() - startedAt;
86
- log.info(`[schedule] ${label} firing (session=${session})`);
87
- return yield* Effect.tryPromise({
88
- try: async () => {
89
- let failed;
90
- let busy = false;
91
- // The text deltas are iterated and dropped. Accumulating them would be a second copy of what the session
92
- // already holds, and no caller here reads it — the outcome is the whole record (docs/design/core.md §8).
93
- for await (const e of agent.invoke({ session }, { text: prompt })) {
94
- if (e.type === "failed") {
95
- failed = e.details;
96
- busy = e.code === SESSION_BUSY_CODE;
97
- }
98
- }
99
- return { busy: failed !== undefined && busy, failed };
100
- },
93
+ log.info(`[schedule] ${label} firing (session=${request.sessionId})`);
94
+ const read = yield* Effect.tryPromise({
95
+ try: (signal) => sendAndWait(agent, request, onAccepted, signal),
101
96
  catch: (cause) => new PortFailure(cause),
102
- }).pipe(Effect.match({
103
- onSuccess: (result) => {
104
- // BUSY IS NOT A FAILURE, and this line is where that distinction has to land: the claim file and the
105
- // route's reply both say `skipped`, but `docker logs` / CloudWatch is the first place anyone looks, and
106
- // it kept saying `ERROR … failed: busy`. "The previous turn is still running" and "the model call died"
107
- // need different reactions — a session-busy rejection carries `failed` only because that is the SPEC's
108
- // one terminal event shape, which is a wire detail, not a verdict.
109
- if (result.busy)
110
- log.info(`[schedule] ${label}: occurrence skipped (session busy) (${elapsed()}ms)`);
111
- else if (result.failed)
112
- log.error(`[schedule] ${label} failed (${elapsed()}ms)${oneLine(result.failed)}`);
113
- else
114
- log.info(`[schedule] ${label} completed (${elapsed()}ms)`);
115
- return { ...result, ms: elapsed() };
116
- },
117
- onFailure: (error) => {
118
- // An iterator throw violates SPEC MUST 2 and is never a replay-safe busy rejection.
119
- log.error(`[schedule] ${label} errored (${elapsed()}ms)${oneLine(String(error.cause))}`);
120
- return { busy: false, failed: String(error.cause), ms: elapsed() };
121
- },
122
- }));
97
+ }).pipe(Effect.result);
98
+ const ms = elapsed();
99
+ if (Result.isFailure(read)) {
100
+ // Not an AgentError: a fault of the agent's, so this fire's failure, never a refusal.
101
+ const failed = String(read.failure.cause);
102
+ log.error(`[schedule] ${label} errored (${ms}ms)${oneLine(failed)}`);
103
+ return { fire: "failed", busy: false, failed, ms };
104
+ }
105
+ const end = read.success;
106
+ if ("unfinished" in end) {
107
+ log.error(`[schedule] ${label} failed (${ms}ms)${oneLine(end.unfinished)}`);
108
+ return { fire: "failed", busy: false, failed: end.unfinished, ms };
109
+ }
110
+ if ("refused" in end) {
111
+ // BUSY IS NOT A FAILURE: "the previous run is still going" and "the model call died" need different reactions.
112
+ if (end.busy)
113
+ log.info(`[schedule] ${label}: occurrence skipped (session busy) (${ms}ms)`);
114
+ else
115
+ log.error(`[schedule] ${label} refused (${ms}ms)${oneLine(end.refused)}`);
116
+ return { fire: "failed", busy: end.busy, failed: end.refused, ms };
117
+ }
118
+ const { fire, detail } = settled(end.ended);
119
+ if (fire === "completed")
120
+ log.info(`[schedule] ${label} completed (${ms}ms)`);
121
+ else
122
+ log.error(`[schedule] ${label} ${fire} (${ms}ms)${oneLine(detail ?? "")}`);
123
+ return { fire, busy: false, failed: fire === "completed" ? undefined : detail, ms };
123
124
  });
124
125
  }
125
126
  /** Shared resident/external claim → run → settle. */
126
- export function fireRoutineOnce(opts) {
127
+ export function fireScheduleOnce(opts) {
127
128
  return Effect.gen(function* () {
128
129
  const clock = yield* Clock.Clock;
129
- const { agent, stateRoot, routine: s, slot, now = () => new Date(clock.currentTimeMillisUnsafe()) } = opts;
130
+ const { agent, stateRoot, schedule: s, slot, now = () => new Date(clock.currentTimeMillisUnsafe()) } = opts;
130
131
  const firedAt = now();
131
132
  const skippedReason = yield* Effect.try({
132
133
  try: () => {
@@ -154,9 +155,29 @@ export function fireRoutineOnce(opts) {
154
155
  });
155
156
  if (skippedReason !== undefined)
156
157
  return { fired: false, skippedReason, ms: 0 };
157
- const r = yield* runTurn(agent, s.name, routineSession(s.name), s.prompt);
158
+ // Before anything is sent: the session it continues (created and recorded the first time), and its prompt.
159
+ const prepared = yield* Effect.tryPromise({
160
+ try: async () => ({
161
+ sessionId: await scheduleSessions(stateRoot).of(s.name, agent),
162
+ text: typeof s.prompt === "string" ? s.prompt : await s.prompt({ at: slot }),
163
+ }),
164
+ catch: (cause) => new PortFailure(cause),
165
+ }).pipe(Effect.result);
166
+ if (Result.isFailure(prepared)) {
167
+ const failed = `could not prepare the fire: ${String(prepared.failure.cause)}`;
168
+ log.error(`[schedule] ${s.name} failed${oneLine(failed)}`);
169
+ settleClaim(stateRoot, s.name, slot, "failed", 0);
170
+ return { fired: true, failed, ms: 0 };
171
+ }
172
+ // An occurrence on a busy session is refused, and so skipped; its key is the instant, so one occurrence is one run.
173
+ const r = yield* runTurn(agent, s.name, {
174
+ sessionId: prepared.success.sessionId,
175
+ content: [{ type: "text", text: prepared.success.text }],
176
+ whenBusy: "reject",
177
+ idempotencyKey: slot.toISOString(),
178
+ });
158
179
  // OVERLAP IS NOT FAILURE. The claim still stands — this occurrence is decided and will not be retried — but
159
- // what decided it was the previous turn still holding `routine:<name>`, which is the policy every scheduler
180
+ // what decided it was the previous turn still holding the schedule's session, which is the policy every scheduler
160
181
  // names (k8s `concurrencyPolicy: Forbid`, Temporal's `Skip`) and none of them reports as an error.
161
182
  if (r.busy) {
162
183
  settleClaim(stateRoot, s.name, slot, "skipped", r.ms);
@@ -167,10 +188,89 @@ export function fireRoutineOnce(opts) {
167
188
  ms: r.ms,
168
189
  };
169
190
  }
170
- settleClaim(stateRoot, s.name, slot, r.failed ? "failed" : "completed", r.ms);
171
- return { fired: true, failed: r.failed, ms: r.ms };
191
+ settleClaim(stateRoot, s.name, slot, r.fire, r.ms);
192
+ return { fired: true, ...(r.failed === undefined ? {} : { failed: r.failed }), ms: r.ms };
172
193
  }).pipe(Effect.uninterruptible);
173
194
  }
195
+ /**
196
+ * Fire `schedule` once, now, outside its clock (`fastagent schedules run`), the way its clock would: a newest claim no
197
+ * fire reported is accounted for first, as a boot does, then this instant is the slot, claimed and settled as any.
198
+ */
199
+ export function fireScheduleNow(opts) {
200
+ return Effect.gen(function* () {
201
+ const { agent, stateRoot, schedule } = opts;
202
+ const fire = yield* Effect.try({
203
+ try: () => latestFire(stateRoot, schedule.name),
204
+ catch: (cause) => new PortFailure(cause),
205
+ });
206
+ if (fire !== undefined && fire.outcome === undefined) {
207
+ yield* accountForUnreportedFire(agent, stateRoot, schedule.name, fire);
208
+ }
209
+ const slot = new Date((yield* Clock.Clock).currentTimeMillisUnsafe());
210
+ return { slot, ...(yield* fireScheduleOnce({ agent, stateRoot, schedule, slot })) };
211
+ });
212
+ }
213
+ /**
214
+ * Account for a claim whose fire never reported: the slot is claimed BEFORE its message is sent, and shutdown does not
215
+ * wait for the message (see `AgentService.close`), so a restart landing mid-fire leaves a claim nothing settled — the
216
+ * exact silence the history exists to prevent. Read once, at the next boot. The claim is the ONE record here: it IS
217
+ * the decision, and the outcome is written back into it.
218
+ *
219
+ * WHAT BECAME OF ITS MESSAGE decides the outcome. One the agent took is durable: it is answered after the restart
220
+ * (or already was), so its end is read through its key and written back, as the fire that ran would have. One that was
221
+ * never taken is `interrupted`, and NOT re-fired: a fire that kills its own process would then replay on every boot.
222
+ * Cron only: a killed wake-up leaves no claim behind (`takeFirstDueWakeup` removes it before its message is sent).
223
+ *
224
+ * THE NEWEST CLAIM ONLY. A schedule's loop runs one fire at a time, so a killed process leaves exactly one claim
225
+ * unsettled — the one it was in. Walking the whole retained window would add nothing a resident process can produce.
226
+ */
227
+ function accountForUnreportedFire(agent, stateRoot, name, fire) {
228
+ return Effect.gen(function* () {
229
+ // `fire.slot` came from a claim file name, which `listClaims` admits only when it round-trips through this same
230
+ // conversion — so the Date is valid and `settleClaim` writes back the file it was read from.
231
+ const slot = new Date(fire.slot);
232
+ const key = slot.toISOString();
233
+ const sessionId = scheduleSessions(stateRoot).get(name);
234
+ const taken = sessionId === undefined
235
+ ? undefined
236
+ : yield* Effect.tryPromise({
237
+ try: () => agent.messages.get({ sessionId, idempotencyKey: key }).catch((error) => {
238
+ if (error instanceof AgentError && error.code === "not_found")
239
+ return undefined;
240
+ throw error;
241
+ }),
242
+ catch: (cause) => new PortFailure(cause),
243
+ });
244
+ if (sessionId === undefined || taken === undefined) {
245
+ log.warn(`[schedule] ${name}: the fire claimed at ${fire.firedAt} never sent its message — the process stopped ` +
246
+ `before it, and that slot stays skipped (see \`fastagent schedules list\`)`);
247
+ // No duration: this fire was never timed, and the process that could have timed it is gone.
248
+ settleClaim(stateRoot, name, slot, "interrupted");
249
+ return;
250
+ }
251
+ log.info(`[schedule] ${name}: the fire claimed at ${fire.firedAt} was taken before a restart — reading its end`);
252
+ const end = yield* Effect.tryPromise({
253
+ try: (signal) => untilEnded(agent, { sessionId, idempotencyKey: key }, signal),
254
+ catch: (cause) => new PortFailure(cause),
255
+ });
256
+ if (!("ended" in end)) {
257
+ // Unsettled still, so the next boot reads it again.
258
+ log.warn(`[schedule] ${name}: the end of the fire claimed at ${fire.firedAt} is not known yet${oneLine("refused" in end ? end.refused : end.unfinished)}`);
259
+ return;
260
+ }
261
+ const { fire: outcome, detail } = settled(end.ended);
262
+ if (outcome === "completed")
263
+ log.info(`[schedule] ${name}: the fire claimed at ${fire.firedAt} completed`);
264
+ else
265
+ log.error(`[schedule] ${name}: the fire claimed at ${fire.firedAt} ${outcome}${oneLine(detail ?? "")}`);
266
+ // No duration either: only its end was seen here.
267
+ settleClaim(stateRoot, name, slot, outcome);
268
+ });
269
+ }
270
+ /** Which session each schedule continues: one per schedule, so the agent sees what its earlier runs did. */
271
+ export function scheduleSessions(stateRoot) {
272
+ return conversationSessions(join(stateRoot, "schedule", "sessions.json"));
273
+ }
174
274
  /** Wakes re-enter a chat session; identify the agent's own instruction and its cancellation handle. */
175
275
  function wakeEnvelope(w) {
176
276
  const tag = w.cron
@@ -182,21 +282,19 @@ export function createScheduler(options) {
182
282
  return Effect.gen(function* () {
183
283
  const clock = yield* Clock.Clock;
184
284
  const fork = Effect.runForkWith(yield* Effect.context());
185
- const { agent, stateRoot, routines, now = () => new Date(clock.currentTimeMillisUnsafe()), externalClock = false, } = options;
285
+ const { agent, stateRoot, schedules, localClock = true, now = () => new Date(clock.currentTimeMillisUnsafe()), } = options;
186
286
  const loops = new Set();
187
287
  let stopped = false;
188
- const launch = (label, work) => {
189
- fork(Effect.withFiber((fiber) => {
190
- // Publish ownership before a synchronous invoke callback can re-enter stop().
191
- loops.add(fiber);
192
- return work.pipe(Effect.catchCause((cause) => Effect.sync(() => {
193
- if (!Cause.hasInterruptsOnly(cause))
194
- log.error(`[schedule] ${label}: stopped unexpectedly: ${String(Cause.squash(cause))}`);
195
- })), Effect.ensuring(Effect.sync(() => {
196
- loops.delete(fiber);
197
- })));
198
- }));
199
- };
288
+ const launch = (label, work) => fork(Effect.withFiber((fiber) => {
289
+ // Publish ownership before a synchronous agent callback can re-enter stop().
290
+ loops.add(fiber);
291
+ return work.pipe(Effect.catchCause((cause) => Effect.sync(() => {
292
+ if (!Cause.hasInterruptsOnly(cause))
293
+ log.error(`[schedule] ${label}: stopped unexpectedly: ${String(Cause.squash(cause))}`);
294
+ })), Effect.ensuring(Effect.sync(() => {
295
+ loops.delete(fiber);
296
+ })));
297
+ }));
200
298
  const cronLoop = (s, first) => Effect.gen(function* () {
201
299
  let due = first;
202
300
  while (due) {
@@ -209,14 +307,19 @@ export function createScheduler(options) {
209
307
  }
210
308
  // `slot: due` is what two schedulers have in common: both compute the same cron instant, so both try to
211
309
  // claim the same name and exactly one wins. Claiming `now()` would give them different names.
212
- yield* fireRoutineOnce({ agent, stateRoot, routine: s, slot: due, now }).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() => {
310
+ yield* fireScheduleOnce({ agent, stateRoot, schedule: s, slot: due, now }).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() => {
213
311
  // Nothing to record: this fault happens BEFORE the claim exists (that is what keeps the slot
214
312
  // unburned), so there is no claim to write an outcome into.
215
313
  log.error(`[schedule] ${s.name}: fire failed (skipping this run, schedule stays armed): ${String(error.cause)}`);
216
314
  })), Effect.uninterruptible);
217
- due = s.cron === undefined ? undefined : nextRun(s.cron, s.tz, now());
315
+ due = nextRun(s.cron, s.tz, now());
218
316
  }
219
317
  });
318
+ /**
319
+ * Fire the first due wake-up into its session, queued behind whatever runs there. The wake-up is taken out of the
320
+ * store first, so accepted is delivered: the pump goes on to the next one, and how the message ends is followed
321
+ * apart.
322
+ */
220
323
  const wakeOnce = Effect.gen(function* () {
221
324
  const w = yield* Effect.try({
222
325
  try: () => takeFirstDueWakeup(stateRoot, now()),
@@ -224,27 +327,31 @@ export function createScheduler(options) {
224
327
  });
225
328
  if (!w)
226
329
  return false;
330
+ const label = `wake ${w.id.slice(0, 8)}`;
331
+ const request = {
332
+ sessionId: w.session,
333
+ content: [{ type: "text", text: wakeEnvelope(w) }],
334
+ whenBusy: "queue",
335
+ idempotencyKey: `wake:${w.id}@${w.fireAt}`,
336
+ };
337
+ // Counted until the message is accepted: from then on the message itself is the work in flight.
227
338
  yield* Effect.acquireUseRelease(Effect.sync(beginWork), () => Effect.gen(function* () {
228
- const label = `wake ${w.id.slice(0, 8)}`;
229
- const r = yield* runTurn(agent, label, w.session, wakeEnvelope(w));
230
- let kept = false;
231
- if (r.busy && !w.cron) {
232
- kept = yield* Effect.try({
233
- try: () => deferWakeup(stateRoot, w, new Date(now().getTime() + WAKEUP_POLL_MS)),
339
+ const accepted = yield* Deferred.make();
340
+ const delivered = () => void Deferred.doneUnsafe(accepted, Effect.void);
341
+ const followed = runTurn(agent, label, request, delivered).pipe(Effect.tap((r) =>
342
+ // Queued, it is refused only while the process or its session's queue is full: for now. A recurring
343
+ // wake-up has its next occurrence; a one-shot one goes back in the store, for the next poll.
344
+ r.busy && w.cron === undefined
345
+ ? Effect.try({
346
+ try: () => {
347
+ requeueWakeup(stateRoot, w, new Date(now().getTime() + WAKEUP_POLL_MS));
348
+ log.info(`[schedule] ${label}: refused for now — retrying next poll (${r.failed})`);
349
+ },
234
350
  catch: (cause) => new PortFailure(cause),
235
- });
236
- if (kept)
237
- log.info(`[schedule] ${label}: session busy — retrying next poll`);
238
- else
239
- log.error(`[schedule] ${label}: dropped after too many busy retries`);
240
- }
241
- else if (r.busy && w.cron) {
242
- // `runTurn` already said it was skipped for a busy session; this adds the one thing it cannot know —
243
- // that a recurring wake has a next occurrence, so nothing is deferred and nothing is lost.
244
- log.info(`[schedule] ${label}: next fires per cron`);
245
- }
246
- // A wake-up has no claim to settle (it is removed from the store before the turn starts), so its whole
247
- // record is the log lines above and `runTurn`'s — deferred, dropped, failed, completed.
351
+ }).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() => log.error(`[schedule] ${label}: refused, and could not be kept for a retry, so it is gone: ${String(error.cause)}`))))
352
+ : Effect.void), Effect.ensuring(Effect.sync(delivered)));
353
+ launch(label, Effect.asVoid(followed));
354
+ yield* Deferred.await(accepted);
248
355
  }), (done) => Effect.sync(done));
249
356
  return true;
250
357
  }).pipe(
@@ -261,6 +368,16 @@ export function createScheduler(options) {
261
368
  yield* Effect.sleep(WAKEUP_POLL_MS);
262
369
  }
263
370
  });
371
+ /** Arm `s` to fire next at `due`, or say it never will. */
372
+ const arm = (s, due) => {
373
+ if (!localClock)
374
+ return;
375
+ if (!due) {
376
+ log.warn(`[schedule] ${s.name}: cron "${s.cron}" will never fire again — not armed`);
377
+ return;
378
+ }
379
+ launch(s.name, cronLoop(s, due));
380
+ };
264
381
  return {
265
382
  start() {
266
383
  stopped = false;
@@ -270,34 +387,31 @@ export function createScheduler(options) {
270
387
  // one to run blind, it is a boot failure. The rest of the window is history, and history does not gate a
271
388
  // boot (`latestFire`).
272
389
  const lastFires = new Map();
273
- if (!externalClock) {
274
- for (const s of routines) {
275
- const fire = latestFire(stateRoot, s.name);
276
- if (fire !== undefined)
277
- lastFires.set(s.name, fire.firedAt);
278
- markInterruptedFire(stateRoot, s.name, fire);
279
- }
390
+ for (const s of schedules) {
391
+ const fire = latestFire(stateRoot, s.name);
392
+ if (fire === undefined)
393
+ continue;
394
+ lastFires.set(s.name, fire.firedAt);
395
+ if (fire.outcome !== undefined)
396
+ continue;
397
+ const accounted = accountForUnreportedFire(agent, stateRoot, s.name, fire).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() =>
398
+ // Unsettled still, so the next boot reads it again.
399
+ log.error(`[schedule] ${s.name}: could not account for the fire claimed at ${fire.firedAt}: ${String(error.cause)}`))));
400
+ launch(s.name, accounted);
280
401
  }
281
402
  const current = now();
282
- for (const s of externalClock ? [] : routines) {
403
+ for (const s of schedules) {
283
404
  if (stopped)
284
405
  break;
285
- // NO CRON, NO CLOCK. A routine without one is not unscheduled by accident — it is reached by name
286
- // (`POST /run`, `fastagent routine run`), so there is nothing here to arm and nothing to warn about.
287
- if (s.cron === undefined)
288
- continue;
289
- // FROM THE LAST FIRE, or from now when there was none. A routine that has never fired does not catch
406
+ // FROM THE LAST FIRE, or from now when there was none. A schedule that has never fired does not catch
290
407
  // up the occurrence it missed while the process was down: nothing recorded that it was ever armed then,
291
408
  // so "missed" is not a fact this process has. Every boot after the first resumes from the claim.
292
409
  const last = lastFires.get(s.name);
293
410
  const due = nextRun(s.cron, s.tz, last ? new Date(last) : current);
294
- if (!due) {
295
- log.warn(`[schedule] ${s.name}: cron "${s.cron}" will never fire again — not armed`);
296
- continue;
297
- }
298
- if (due.getTime() <= current.getTime())
411
+ if (localClock && due && due.getTime() <= current.getTime()) {
299
412
  log.info(`[schedule] ${s.name}: catching up a missed run`);
300
- launch(s.name, cronLoop(s, due));
413
+ }
414
+ arm(s, due);
301
415
  }
302
416
  if (!stopped)
303
417
  launch("wake-up poll", wakeLoop);
@@ -10,7 +10,7 @@ export declare function writeScheduleFile(path: string, value: unknown): void;
10
10
  * How a claimed occurrence ended.
11
11
  *
12
12
  * `skipped` is NOT a failure and is the reason this is an enum rather than a boolean: a schedule's turns share one
13
- * `routine:<name>` session, so an occurrence arriving while the previous one is still running is refused by that
13
+ * `schedule:<name>` session, so an occurrence arriving while the previous one is still running is refused by that
14
14
  * session, and recording it as `failed` made "the last run was still going" indistinguishable from "the model call
15
15
  * died" — two things an operator reacts to completely differently. Every scheduler names this: Kubernetes calls it
16
16
  * `concurrencyPolicy: Forbid`, Temporal calls it the `Skip` overlap policy, and neither treats it as an error.
@@ -25,16 +25,6 @@ export interface Fire {
25
25
  outcome?: FireOutcome;
26
26
  ms?: number;
27
27
  }
28
- /**
29
- * A schedule name becomes a path segment (its claims live in `claims/<name>/`), so the rule is exactly the safety
30
- * boundary and nothing more: a name may not leave that directory. The name comes from a filename under `routines/`,
31
- * which already cannot contain a separator — so what a stricter rule would actually reject is legal filenames
32
- * (`每日简报`, `my schedule`), whose only symptom would be a schedule that silently never fires again.
33
- *
34
- * One definition, two enforcers: `discover.ts` refuses an illegal one where the author can see which file is wrong,
35
- * and `claimDir` asserts it again for a programmatic caller.
36
- */
37
- export declare function isSafeScheduleName(name: string): boolean;
38
28
  /**
39
29
  * Why a slot is not ours. `duplicate` is ordinary (the same delivery arrived twice, or another scheduler took it and
40
30
  * is running it now); `stale` means the schedule has moved past this instant and the slot will never run — a turn
@@ -75,9 +65,9 @@ export declare function claimSlot(stateRoot: string, name: string, slot: Date, f
75
65
  * the history. Nothing here grows: the pruning that keeps the claim gate cheap keeps the history's size fixed too,
76
66
  * which is why there is no separate audit file to rotate (what the turn SAID is in its session, not here).
77
67
  *
78
- * THE HISTORY read, and its only caller is the read-only `routine history` — which translates a read fault into a
79
- * one-line refusal. The serving boot reads `latestFire` instead: it asks about ONE claim, and a window of files kept
80
- * for an operator to look at has no business deciding whether a service starts.
68
+ * THE HISTORY read, and its only caller is the read-only `schedules list --json` — which translates a read fault into a
69
+ * one-line refusal. Its text mode and the serving boot read `latestFire` instead: each asks about ONE claim, and a
70
+ * window of files kept for an operator to look at has no business failing either.
81
71
  */
82
72
  export declare function readFires(stateRoot: string, name: string): Fire[];
83
73
  /**
@@ -32,8 +32,8 @@ export function writeScheduleFile(path, value) {
32
32
  }
33
33
  // ── claims/: one file per fired slot — the DECISION to fire, made atomically ──
34
34
  /**
35
- * How many claims to keep per schedule — and therefore how far back `fastagent routine history` can see, since
36
- * the claims ARE the history. The gate below reads only the newest, so this number answers the history's question,
35
+ * How many claims to keep per schedule — and therefore how far back the fire history under
36
+ * `<stateRoot>/schedule/claims/<name>/` reaches, since the claims ARE the history. The gate below reads only the newest, so this number answers the history's question,
37
37
  * "did last night's run silently fail?": at 512 a minute cron keeps ~8.5 hours, an hourly one ~3 weeks.
38
38
  *
39
39
  * Raise it by FILE COUNT, not by byte count: 512 short lines are ~25 KB of content but ~2 MiB of blocks and 512
@@ -151,14 +151,14 @@ function readClaim(dir, name) {
151
151
  }
152
152
  /**
153
153
  * A schedule name becomes a path segment (its claims live in `claims/<name>/`), so the rule is exactly the safety
154
- * boundary and nothing more: a name may not leave that directory. The name comes from a filename under `routines/`,
154
+ * boundary and nothing more: a name may not leave that directory. The name comes from a filename under `schedules/`,
155
155
  * which already cannot contain a separator — so what a stricter rule would actually reject is legal filenames
156
156
  * (`每日简报`, `my schedule`), whose only symptom would be a schedule that silently never fires again.
157
157
  *
158
158
  * One definition, two enforcers: `discover.ts` refuses an illegal one where the author can see which file is wrong,
159
159
  * and `claimDir` asserts it again for a programmatic caller.
160
160
  */
161
- export function isSafeScheduleName(name) {
161
+ function isSafeScheduleName(name) {
162
162
  return name !== "" && !/[/\\]/.test(name) && !name.includes("\u0000") && name !== "." && name !== "..";
163
163
  }
164
164
  /** This function builds a path from it, so a name that can leave `claims/<name>/` is a bug, not an input. */
@@ -226,9 +226,9 @@ export function claimSlot(stateRoot, name, slot, firedAt) {
226
226
  * the history. Nothing here grows: the pruning that keeps the claim gate cheap keeps the history's size fixed too,
227
227
  * which is why there is no separate audit file to rotate (what the turn SAID is in its session, not here).
228
228
  *
229
- * THE HISTORY read, and its only caller is the read-only `routine history` — which translates a read fault into a
230
- * one-line refusal. The serving boot reads `latestFire` instead: it asks about ONE claim, and a window of files kept
231
- * for an operator to look at has no business deciding whether a service starts.
229
+ * THE HISTORY read, and its only caller is the read-only `schedules list --json` — which translates a read fault into a
230
+ * one-line refusal. Its text mode and the serving boot read `latestFire` instead: each asks about ONE claim, and a
231
+ * window of files kept for an operator to look at has no business failing either.
232
232
  */
233
233
  export function readFires(stateRoot, name) {
234
234
  const dir = claimDir(stateRoot, name);
@@ -1,6 +1,7 @@
1
1
  import * as Effect from "effect/Effect";
2
- import { type WakeAlarm } from "../channels/agentcore-protocol.ts";
2
+ import { type RecurringSchedule, type WakeAlarm } from "../channels/agentcore-protocol.ts";
3
3
  import { type Wakeup } from "./wakeups.ts";
4
+ import type { Schedule } from "./schedule.ts";
4
5
  /**
5
6
  * Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on every turn, the
6
7
  * file should not churn).
@@ -13,14 +14,33 @@ export declare function readWakeAlarmUrl(stateRoot: string): string | undefined;
13
14
  * the pending store is the durable desired state — so "give up" means "until the next mirror", never "lost").
14
15
  */
15
16
  export declare const MAX_SYNC_ATTEMPTS = 5;
16
- /** Pending wake-ups → the desired alarm set, minus already-due entries (see {@link DUE_MARGIN_MS}). */
17
+ /**
18
+ * After a whole sync gives up, how long until it is tried again, for as long as this process lives. Outside the busy
19
+ * count, so a container that is only waiting to retry can still be reclaimed; its next start mirrors again.
20
+ */
21
+ export declare const HEAL_MS: number;
22
+ /**
23
+ * Pending wake-ups → the desired one-shot alarms. A wake-up already due is left out (see {@link DUE_MARGIN_MS}): the
24
+ * container is awake for it, and its wake pump fires it.
25
+ *
26
+ * ONE ID PER INSTANT, never per wake-up. An alarm deletes itself once it has fired, and a recurring wake-up's claim
27
+ * mirrors its next instant right after: an id reused for it would update the alarm EventBridge is about to delete,
28
+ * and the next instant would be deleted with it. With one id per instant, mirroring again is idempotent.
29
+ */
17
30
  export declare function toAlarms(pending: Wakeup[], now: Date): WakeAlarm[];
31
+ /**
32
+ * Each armed schedule → the recurring EventBridge schedule that fires it. Discovery admits only a cron that
33
+ * translates (schedule/discover.ts), so one that does not here is a defect, and throws.
34
+ */
35
+ export declare function toRecurring(schedules: readonly Schedule[]): RecurringSchedule[];
18
36
  /**
19
37
  * Build the wakeups sink for an AgentCore deployment (registered via `setWakeupsSink` by `start` when
20
38
  * `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present).
21
39
  */
22
40
  export declare function createWakeAlarmSink(options: {
23
41
  secret: string;
42
+ /** The schedules armed now, mirrored as recurring schedules beside the wake-ups' alarms. */
43
+ schedules?: () => readonly Schedule[];
24
44
  fetchImpl?: typeof fetch;
25
45
  /** Injectable clock (tests); defaults to the wall clock. */
26
46
  now?: () => Date;