@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,48 +1,54 @@
1
1
  import type { Agent } from "../agent.ts";
2
- import type { StateSync } from "./agentcore-state.ts";
3
- import type { Routes } from "../channel.ts";
4
- import { type PrefixMount } from "../channels/serve.ts";
5
- import type { ScheduleFireOutcome } from "../schedule/scheduler.ts";
6
- /** What the lazy factory hands back: literal routes plus any prefix-owning mounts (the control
7
- * plane), so the adapter's INNER dispatch is assembled exactly like a direct host's. */
2
+ import type { ChannelHandler, Routes } from "../channel.ts";
3
+ /**
4
+ * What the lazy factory hands back: the CHANNELS' routes, and nothing else.
5
+ *
6
+ * Deliberately not "the whole inner surface". Every request that reaches this dispatcher arrived through the
7
+ * forwarder's public Function URL, which relays an arbitrary path verbatim and supplies the ingress secret itself —
8
+ * so behind THIS door an anonymous caller and an IAM one are the same thing. A channel route survives that because
9
+ * it verifies the platform's signature inside itself; nothing fastagent serves does.
10
+ *
11
+ * The other door is the Runtime's own, IAM-gated: the forwarder emits only `webhook`, `routine-fire`, `wake-poke`
12
+ * and `probe`, so any other KIND can only come from a direct `InvokeAgentRuntime` call. That is what lets
13
+ * `kind: "invoke"` run a turn here with no ingress secret, and it is where anything else that needs a real caller
14
+ * identity belongs.
15
+ */
8
16
  export interface RouteSurface {
9
17
  routes: Routes;
10
- mounts?: readonly PrefixMount[];
11
18
  }
12
19
  export interface AgentcoreAdapterOptions {
13
- /** The serving routes a direct deployment would mount (channels or the builtin invoke + health),
14
- * built LAZILY: channel construction loads channel state and replays durable turn intent, so on
15
- * AgentCore it must not run until the state root is authoritative — which happens at the first
16
- * envelope's `stateSync.ready()` (the restore URLs only an envelope carries), never at boot, where
17
- * the mount is pre-restore (empty after every version update). A factory, not a value: there is no
18
- * moment during construction at which the right answer is knowable. May answer synchronously — the
19
- * resolution chain normalizes it either way. */
20
+ /** The channel surface. */
20
21
  channels: () => Promise<RouteSurface> | RouteSurface;
21
22
  agent: Agent;
22
23
  /** Where the forwarder URL from envelopes is persisted for the wake-alarm sink (the state root). */
23
24
  stateRoot: string;
24
25
  /** Process-wide background-work signal (busy.ts `activeWork() > 0`) — injected for tests. */
25
26
  isBusy: () => boolean;
26
- /** Slot-idempotent schedule fire ({@link fireScheduleOnce} bound to this workspace's schedules);
27
- * undefined when the workspace has none — a schedule-fire envelope then 404s (deploy drift: an
28
- * external clock still firing for a schedule this definition no longer has). */
29
- fire?: (name: string, slot: Date) => Promise<ScheduleFireOutcome>;
30
- /** Cross-deploy state durability (agentcore-state.ts). Absent = the state root is local-only,
31
- * which on AgentCore means it is erased by the next deploy — the serving path always wires it. */
32
- stateSync?: StateSync;
33
- /** FASTAGENT_INGRESS_SECRET: what makes an envelope the FORWARDER's rather than any IAM principal's.
34
- * Undefined = nothing can be trusted, so only the public `invoke` kind is served. */
27
+ /**
28
+ * Fire ONE declared schedule for the occurrence EventBridge named — the claim/run/settle the resident clock uses,
29
+ * not the `POST /run` API.
30
+ *
31
+ * THIS IS WHERE OCCURRENCE SEMANTICS LIVE ON THIS HOST, and it is separate from that route on purpose. `deploy`
32
+ * wrote the rule, injected `<aws.scheduler.scheduled-time>` and the forwarder relays it behind the ingress
33
+ * secret, so this caller is one we produced and authenticated: the instant it names really is a grid point of
34
+ * that schedule, which is what makes a claim (and therefore dedup across EventBridge's redeliveries, a fire
35
+ * history and the overlap policy) mean anything. `POST /run` has none of that — it is an unauthenticated API
36
+ * whose caller cannot be told which occurrence it means, so it does not pretend to (schedule/run.ts).
37
+ *
38
+ * Undefined when the definition declares no schedules.
39
+ */
40
+ fireRoutine?: (name: string, occurrence: Date) => Promise<Response>;
41
+ /**
42
+ * Run a declared routine BY NAME — the `POST /run` contract, for the host that publishes no routes.
43
+ *
44
+ * Undefined when the definition declares none. Note what this does NOT do: no occurrence, no claim, no fire
45
+ * history. Those belong to {@link fireRoutine}, whose caller is a clock we wrote.
46
+ */
47
+ runRoutine?: (name: string) => Promise<Response>;
48
+ /** FASTAGENT_INGRESS_SECRET: what makes an envelope the FORWARDER's rather than any IAM principal's. */
35
49
  ingressSecret?: string;
36
- /** Cancels the adapter's process-global registrations. Without it a closed adapter keeps saving
37
- * state on every later idle edge — including work belonging to whatever mounted after it. */
38
- signal?: AbortSignal;
39
- /** Runs ONCE, after the state root is authoritative (post-restore) — the wake-alarm reconcile, which
40
- * at boot would see the mount the platform just wiped and conclude there is nothing pending. */
50
+ /** Runs once on activation, after accepting the current forwarder callback URL. */
41
51
  onStateReady?: () => void;
42
52
  }
43
53
  export declare function agentcoreRoutes(options: AgentcoreAdapterOptions): Routes;
44
- /** Thrown by the mount-site `fire` binding when the envelope names a schedule this workspace does
45
- * not have — the adapter maps it to 404 (deploy drift stays visible in the external clock's logs). */
46
- export declare class UnknownScheduleError extends Error {
47
- constructor(name: string);
48
- }
54
+ export declare function agentcorePing(isBusy: () => boolean): ChannelHandler;
@@ -1,124 +1,54 @@
1
- /**
2
- * AWS AgentCore Runtime adapter: serve fastagent's whole HTTP surface through the Runtime's service
3
- * contract. AgentCore gives a container exactly TWO paths — `POST /invocations` (the only ingress,
4
- * reached via the SigV4 `InvokeAgentRuntime` API) and `GET /ping` (health) — and no public URL, so
5
- * the deployment fronts webhooks with a thin forwarder Lambda and delivers cron slots from
6
- * EventBridge Scheduler; both arrive here as an ENVELOPE in the /invocations payload:
7
- *
8
- * - `{ kind: "webhook", method, path, headers?, bodyB64? }` — a verbatim webhook request captured
9
- * by the forwarder. Reconstructed into a real `Request` and dispatched to the SAME channel routes
10
- * a direct deployment serves — signature verification (Telegram secret token, Feishu signatures)
11
- * runs unchanged inside the channel. The channel's HTTP response travels back INSIDE the
12
- * transport reply (`{ status, headers, bodyB64 }`, transport always 200): AgentCore folds a
13
- * container non-2xx into its own 424 RuntimeClientError, so riding the real status inside the
14
- * envelope is the only way the forwarder can re-emit it verbatim (a Feishu URL-verification
15
- * challenge needs the exact body + content-type back).
16
- * - `{ kind: "schedule-fire", name, slot }` — one cron instant from the external clock. Dispatched
17
- * to {@link fireScheduleOnce}-shaped `fire` with the slot as the idempotency key (EventBridge
18
- * delivery is at-least-once; a duplicate slot must not double-fire).
19
- * - `{ kind: "invoke", session, text }` — the programmatic data plane; streams the invoke back as
20
- * SSE (AgentCore's streaming response form), reusing the HTTP channel's handler wholesale.
21
- *
22
- * `/ping` reports `HealthyBusy` (+ `time_of_last_update`, required — see the handler) while
23
- * process-wide background work is in flight (busy.ts) — webhook
24
- * channels ACK fast and run turns fire-and-forget, and AgentCore ends an idle session, so without
25
- * this signal a long turn would be killed mid-flight right after its ACK. `Healthy` when idle lets
26
- * the platform reclaim the microVM (that idle-to-zero IS the point of this deployment).
27
- */
1
+ /** AWS AgentCore Runtime adapter: serve fastagent's whole HTTP surface through the Runtime's service contract. */
28
2
  import { Buffer } from "node:buffer";
29
- import { ENVELOPE_KINDS } from "./agentcore-protocol.js";
30
- import { beginWork, onIdle } from "./busy.js";
3
+ import * as Effect from "effect/Effect";
4
+ import { PortFailure, portJoin } from "../effect-port.js";
5
+ import { ENVELOPE_KINDS, fromForwarder } from "./agentcore-protocol.js";
6
+ import { beginWork } from "./busy.js";
31
7
  import { router } from "../channels/serve.js";
32
8
  import { log } from "../log.js";
33
9
  import { rememberWakeAlarmUrl } from "../schedule/wake-alarm.js";
34
10
  import { readBodyCapped } from "./body.js";
35
11
  import { createInvokeHandler } from "./http.js";
36
12
  import { text } from "./respond.js";
37
- import { secretEquals } from "./secret.js";
38
13
  import { MAX_ENVELOPE_BYTES, MAX_WEBHOOK_BODY_BYTES } from "./agentcore-limits.js";
39
- const unsnapshottedWarning = "[agentcore] this envelope carried no state-snapshot URLs — the state root is LOCAL ONLY and the " +
40
- "platform erases it on the next deploy (redeploy with a current fastagent to restore durability)";
41
14
  const jsonHeaders = { "content-type": "application/json" };
42
15
  const json = (body, status) => new Response(`${JSON.stringify(body)}\n`, { status, headers: jsonHeaders });
43
16
  /**
44
- * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }` — the whole of what the
45
- * platform routes into the container. The agent's own channels are not beside these: they are a table
46
- * inside the envelope dispatch below, reached only by unwrapping a forwarder envelope.
47
- */
48
- /**
49
- * The process's BOOT, deferred to ingress. A resident host restores its state, wires its sinks and
50
- * constructs its channels at start-up; here nothing before the first trusted envelope is
51
- * authoritative (the mount is pre-restore, and the snapshot URLs only an envelope carries), so those
52
- * steps run against the first one and are asserted again on every later one. Two things happen ONCE
53
- * per process, in this order: the post-restore hook (the wake-alarm reconcile, which at boot would
54
- * read the mount the platform just wiped), and channel construction — whose outcome is cached EITHER
55
- * WAY. Success: the same resident channels a direct host keeps. Failure too: construction is an
56
- * ACTIVATION with side effects — loadChannels builds every healthy channel (starting its queues and
57
- * replaying durable turn intent) before reporting another module's failure — and there is no cleanup
58
- * contract to unwind it, so re-running it per envelope could replay the same recovered turn
59
- * concurrently. The first rejection is the process's answer: every later envelope fails with the same
60
- * message (visible each time), the retry boundary is a fresh session (which scale-to-zero provides
61
- * naturally), and the deploy driver's probe catches deterministic failures at deploy time.
17
+ * Channel construction may already have replayed work before failing; cache either outcome to avoid replaying twice.
62
18
  */
63
19
  function createActivation(deps) {
64
- const { stateSync, stateRoot, onStateReady } = deps;
65
- let warnedUnsnapshotted = false;
66
- let stateReadyFired = false;
67
- let dispatchP;
20
+ const { stateRoot, onStateReady } = deps;
21
+ // UNINTERRUPTIBLE, because the outcome is CACHED: `Effect.cached` memoizes whatever exit it sees,
22
+ // interruption included. A once-per-process activation that remembered "interrupted" would answer
23
+ // every later envelope with an empty cause instead of the success or the diagnosable failure this
24
+ // block promises. Both are already unreachable (no caller passes a signal), which is exactly why
25
+ // the invariant has to be written down rather than relied on.
26
+ const stateReady = Effect.runSync(Effect.cached(portJoin(async () => {
27
+ onStateReady?.();
28
+ }).pipe(Effect.uninterruptible)));
29
+ const channels = Effect.runSync(Effect.cached(portJoin(async () => {
30
+ const surface = await deps.channels();
31
+ return router({ selfVerifying: surface.routes });
32
+ }).pipe(Effect.uninterruptible)));
68
33
  return {
69
- async restore(envelope) {
70
- if (stateSync) {
71
- if (envelope.state && typeof envelope.state.getUrl === "string" && typeof envelope.state.putUrl === "string") {
72
- stateSync.use(envelope.state);
73
- }
74
- else if (envelope.kind !== "invoke" && !stateSync.configured() && !warnedUnsnapshotted) {
75
- // webhook/schedule-fire/wake-poke/probe reach us ONLY through the forwarder, which mints the
76
- // pair. Missing = a broken/stale topology whose state dies at the next deploy: say so, loudly,
77
- // once per process (a direct `invoke` legitimately has none — its session storage is its own).
78
- warnedUnsnapshotted = true;
79
- log.warn(unsnapshottedWarning);
80
- }
81
- await stateSync.ready();
82
- }
83
- // The forwarder rides its public URL along on every envelope — persist it (write-if-changed) so
84
- // the wake-alarm sink can call back. AFTER the restore: a stale snapshot copy must not win over
85
- // the URL this deployment is actually reachable at. A bad persist must not fail the turn.
34
+ prepare: (envelope) => Effect.gen(function* () {
35
+ // Use the current callback URL rather than the one persisted by an earlier deployment.
86
36
  if (typeof envelope.wake?.url === "string") {
87
- try {
88
- rememberWakeAlarmUrl(stateRoot, envelope.wake.url);
89
- }
90
- catch (e) {
91
- log.error(`[agentcore] could not persist the wake-alarm URL: ${String(e)}`);
92
- }
93
- }
94
- if (onStateReady && !stateReadyFired) {
95
- stateReadyFired = true;
96
- onStateReady();
97
- }
98
- },
99
- channels() {
100
- if (!dispatchP) {
101
- // The factory runs INSIDE the chain: a synchronous throw must land in the cached rejection,
102
- // not escape before `dispatchP` is assigned (which would silently re-run the activation).
103
- dispatchP = Promise.resolve()
104
- .then(deps.channels)
105
- .then((surface) => router(surface.routes, surface.mounts));
106
- dispatchP.catch(() => { }); // observed here so the CACHED rejection is never "unhandled"
37
+ const url = envelope.wake.url;
38
+ yield* Effect.try({
39
+ try: () => rememberWakeAlarmUrl(stateRoot, url),
40
+ catch: (cause) => new PortFailure(cause),
41
+ });
107
42
  }
108
- return dispatchP;
109
- },
43
+ yield* stateReady;
44
+ }),
45
+ channels,
110
46
  };
111
47
  }
112
48
  export function agentcoreRoutes(options) {
113
- const { channels, agent, stateRoot, isBusy, fire, stateSync, ingressSecret, onStateReady } = options;
114
- const activation = createActivation({ stateSync, stateRoot, onStateReady, channels });
49
+ const { channels, agent, stateRoot, isBusy, fireRoutine, runRoutine, ingressSecret, onStateReady } = options;
50
+ const activation = createActivation({ stateRoot, onStateReady, channels });
115
51
  const invokeHandler = createInvokeHandler(agent);
116
- // Snapshot on the 0-in-flight edge: webhook channels ACK fast and finish the turn in the
117
- // background, so "the request returned" is NOT when the state root settles.
118
- if (stateSync) {
119
- const off = onIdle(() => stateSync.save());
120
- options.signal?.addEventListener("abort", off, { once: true });
121
- }
122
52
  const handleInvocation = async (req) => {
123
53
  const body = await readBodyCapped(req, MAX_ENVELOPE_BYTES);
124
54
  if ("tooLarge" in body)
@@ -133,49 +63,34 @@ export function agentcoreRoutes(options) {
133
63
  if (envelope === null || typeof envelope !== "object" || typeof envelope.kind !== "string") {
134
64
  return text(`need { "kind": ${ENVELOPE_KINDS.map((k) => `"${k}"`).join(" | ")}, ... }\n`, 400);
135
65
  }
136
- // AUTHENTICATION BOUNDARY. `InvokeAgentRuntime` is an ordinary IAM action, so "reached this
137
- // handler" proves nothing about the sender. Only an envelope carrying the shared secret is the
138
- // forwarder's; anything else is the PUBLIC data plane, which may run exactly one kind (`invoke`)
139
- // and may NOT carry internal fields — riding a `state` or `wake` URL on a public invoke would
140
- // redirect the state snapshot (auth.json) or the alarm callback (the wake secret) to the caller.
141
- // Internal fields are DROPPED rather than rejected: a public caller has no business knowing them.
142
- const trusted = secretEquals(envelope.auth, ingressSecret);
66
+ // The forwarder, or another IAM principal? See `fromForwarder` for what that actually decides.
67
+ const trusted = fromForwarder(envelope, ingressSecret);
143
68
  if (!trusted) {
144
- if (envelope.kind !== "invoke") {
69
+ // The IAM door's kinds: AWS authenticated this caller, so no ingress secret is expected or wanted.
70
+ if (envelope.kind !== "invoke" && envelope.kind !== "routine-run") {
145
71
  log.warn(`[agentcore] rejected an unauthenticated "${envelope.kind}" envelope`);
146
72
  return text("forbidden\n", 403);
147
73
  }
148
74
  envelope.wake = undefined;
149
- envelope.state = undefined;
150
75
  }
151
- // Cross-deploy state: the platform wipes /mnt/state on every version update, so the durable copy
152
- // must be pulled back BEFORE anything reads it.
153
76
  try {
154
- await activation.restore(envelope);
77
+ if (trusted)
78
+ await Effect.runPromise(activation.prepare(envelope).pipe(Effect.mapError((e) => e.cause)));
155
79
  }
156
80
  catch (e) {
157
- log.error(`[agentcore] state restore failed: ${String(e)}`);
158
- // The probe is the deploy driver's verification channel: its diagnostics must survive the
159
- // forwarder, which folds a non-200 transport into an opaque 502 — so for it the failure
160
- // rides a transport-200 structured verdict; every other kind keeps the plain 503.
81
+ log.error(`[agentcore] activation failed: ${String(e)}`);
82
+ // The probe is the deploy driver's verification channel: its diagnostics must survive the forwarder, which
83
+ // folds a non-200 transport into an opaque 502.
161
84
  if (envelope.kind === "probe")
162
- return json({ ok: false, error: `state restore failed: ${String(e)}` }, 200);
163
- return text(`state restore failed: ${String(e)}\n`, 503);
85
+ return json({ ok: false, error: `activation failed: ${String(e)}` }, 200);
86
+ return text(`activation failed: ${String(e)}\n`, 503);
164
87
  }
165
- // The channels come up on the first trusted ingress after the state root became authoritative —
166
- // whichever kind carries it, so a cold start woken by a schedule fire or an alarm poke still
167
- // replays checkpointed turn intent. Two deliberate exceptions: `checkpoint` must push state even
168
- // when a channel is broken, and a public `invoke` runs in its own isolated storage — constructing
169
- // against THAT root would cache pre-restore emptiness for the ingress session. Failure policy is
170
- // per kind below: webhook and wake-poke fail their request (503), the probe reports it
171
- // structurally, and a schedule fire proceeds — cron does not consume channels, and letting an
172
- // unrelated channel misconfiguration silence the clock would turn one fault into two (the error
173
- // is logged here either way).
88
+ // Public invokes do not activate channels.
174
89
  let constructionError;
175
90
  let dispatch;
176
- if (trusted && envelope.kind !== "checkpoint" && envelope.kind !== "invoke") {
91
+ if (trusted && envelope.kind !== "invoke") {
177
92
  try {
178
- dispatch = await activation.channels();
93
+ dispatch = await Effect.runPromise(activation.channels.pipe(Effect.mapError((error) => error.cause)));
179
94
  }
180
95
  catch (e) {
181
96
  constructionError = String(e);
@@ -202,13 +117,13 @@ export function agentcoreRoutes(options) {
202
117
  ? Buffer.from(bodyB64, "base64")
203
118
  : undefined,
204
119
  });
205
- // A construction failure is the request's failure (503 through the forwarder, so the
206
- // platform retries and the operator sees the message), never a silently-empty channel.
120
+ // A construction failure is the request's failure (503 through the forwarder, so the platform retries and the
121
+ // operator sees the message), never a silently-empty channel.
207
122
  if (!dispatch)
208
123
  return text(`channel construction failed: ${constructionError ?? "unavailable"}\n`, 503);
209
124
  const response = await dispatch(inner);
210
- // Buffer the channel's ACK (webhook ACKs are small by design — the turn itself runs
211
- // fire-and-forget) and ride it inside the transport reply, byte-exact.
125
+ // Buffer the channel's ACK (webhook ACKs are small by design — the turn itself runs fire-and-forget) and ride
126
+ // it inside the transport reply, byte-exact.
212
127
  const replyBody = Buffer.from(await response.arrayBuffer());
213
128
  const replyHeaders = {};
214
129
  response.headers.forEach((value, key) => {
@@ -221,73 +136,57 @@ export function agentcoreRoutes(options) {
221
136
  };
222
137
  return json(reply, 200);
223
138
  }
224
- case "schedule-fire": {
225
- const { name, slot } = envelope;
226
- if (typeof name !== "string" || typeof slot !== "string" || Number.isNaN(Date.parse(slot))) {
227
- return text('schedule-fire envelope needs { "name": string, "slot": ISO-date }\n', 400);
139
+ case "routine-fire": {
140
+ const { name, occurrence } = envelope;
141
+ if (typeof name !== "string" || typeof occurrence !== "string" || Number.isNaN(Date.parse(occurrence))) {
142
+ return text('routine-fire envelope needs { "name": string, "occurrence": ISO-date }\n', 400);
228
143
  }
229
- // No fire capability (no schedules in this definition) or an unknown name is deploy drift —
230
- // an external clock rule outliving the schedule it fired for. 404 keeps it VISIBLE in the
231
- // clock's logs (a 200 would silently absorb every future fire).
232
- if (!fire)
233
- return text(`no schedules in this deployment (schedule-fire "${name}")\n`, 404);
234
- // The whole agent turn runs inside this request — but the CALLER (the forwarder Lambda) may
235
- // time out and drop the connection while the turn keeps running server-side. Count it as
236
- // in-flight work so /ping holds the session (HealthyBusy) for the remainder.
144
+ // No schedules in this definition: nothing to fire, and no rule should have survived a deploy that removed
145
+ // them either.
146
+ if (!fireRoutine)
147
+ return text(`no routines in this deployment (routine-fire "${name}")\n`, 404);
148
+ // The whole agent turn runs inside this request — but the CALLER (the forwarder Lambda) may time out and drop
149
+ // the connection while the turn keeps running server-side.
237
150
  const workDone = beginWork();
238
151
  try {
239
- const outcome = await fire(name, new Date(slot));
240
- return json(outcome, 200);
241
- }
242
- catch (e) {
243
- if (e instanceof UnknownScheduleError)
244
- return text(`${e.message}\n`, 404);
245
- // A claim-state fault (unreadable/unwritable fires.json) — surface it as the request's
246
- // failure so the external clock's logs carry it (fail visibly, never a silent absorb).
247
- log.error(`[agentcore] schedule-fire ${name} failed: ${String(e)}`);
248
- return text(`schedule-fire failed: ${String(e)}\n`, 500);
152
+ return await fireRoutine(name, new Date(occurrence));
249
153
  }
250
154
  finally {
251
155
  workDone();
252
156
  }
253
157
  }
254
- case "checkpoint": {
255
- // Deliberately does NOT wait for idle: the durable turn intent is persisted BEFORE the
256
- // webhook ACK (turn-store.ts), so a flush right now already carries the interrupted turn —
257
- // and blocking a deploy on a turn that may run for minutes would be the worse trade.
258
- if (!stateSync)
259
- return json({ written: false, reason: "no state sync in this deployment" }, 200);
158
+ case "routine-run": {
159
+ const { name } = envelope;
160
+ if (typeof name !== "string" || name === "") {
161
+ return text('routine-run envelope needs { "name": string }\n', 400);
162
+ }
163
+ if (!runRoutine)
164
+ return text(`no routines in this deployment (routine-run "${name}")\n`, 404);
165
+ // The whole agent turn runs inside this request; the caller may time out while it keeps running.
166
+ const workDone = beginWork();
260
167
  try {
261
- // The reply is the ONLY thing telling the operator whether an in-flight turn was
262
- // protected, so it reports what actually happened — `written: false` (nothing to write)
263
- // reads differently from `written: true`, and a failure is a failure.
264
- return json(await stateSync.checkpoint(), 200);
168
+ return await runRoutine(name);
265
169
  }
266
- catch (e) {
267
- log.error(`[agentcore] checkpoint failed: ${String(e)}`);
268
- return text(`checkpoint failed: ${String(e)}\n`, 500);
170
+ finally {
171
+ workDone();
269
172
  }
270
173
  }
271
174
  case "wake-poke": {
272
- // The poke's job is DONE by arriving: the invocation woke (or kept awake) the container, and
273
- // the wake pump (boot drain + 30s poll) fires whatever is due. Nothing to dispatch — the
274
- // initialization above already resolved construction (replaying checkpointed turn intent),
275
- // and its failure is this request's failure so the alarm's log line names it.
175
+ // The poke's job is DONE by arriving: the invocation woke (or kept awake) the container, and the wake pump
176
+ // (boot drain + 30s poll) fires whatever is due.
276
177
  if (constructionError !== undefined)
277
178
  return text(`channel construction failed: ${constructionError}\n`, 503);
278
179
  return json({ ok: true }, 200);
279
180
  }
280
181
  case "probe": {
281
- // The structured verdict (transport-200 — see the envelope doc): the deploy driver reads it
282
- // through the forwarder's reserved path, so the error text survives the hop that turns any
283
- // non-200 transport into an opaque 502.
182
+ // The structured verdict (transport-200 — see the envelope doc).
284
183
  return json(constructionError === undefined
285
184
  ? { ok: true }
286
185
  : { ok: false, error: `channel construction failed: ${constructionError}` }, 200);
287
186
  }
288
187
  case "invoke": {
289
- // Reuse the HTTP channel's handler wholesale (SSE, cancellation, backpressure) by handing it
290
- // the shape it already validates — one protocol, one implementation.
188
+ // Reuse the HTTP channel's handler wholesale (SSE, cancellation, backpressure) by handing it the shape it
189
+ // already validates — one protocol, one implementation.
291
190
  const inner = new Request("http://agentcore.local/invoke", {
292
191
  method: "POST",
293
192
  headers: jsonHeaders,
@@ -299,53 +198,20 @@ export function agentcoreRoutes(options) {
299
198
  return text(`unknown envelope kind "${envelope.kind}"\n`, 400);
300
199
  }
301
200
  };
302
- // Settle-then-snapshot: when the envelope leaves nothing in flight, its writes are final now (a
303
- // background turn instead reports through the idle edge above).
304
- //
305
- // A public `invoke` is the one kind this MISSES, measurably: it answers with an unconsumed SSE
306
- // stream, so the turn runs after this line and its records wait for the next envelope's snapshot.
307
- // Counting it as in-flight work would close that and cost more than it buys — the stream drains at
308
- // the CLIENT's pace, so one parked reader pins /ping at HealthyBusy and defeats the idle reclaim,
309
- // which is the quota-exhaustion failure the ping contract below warns about. The kinds that carry
310
- // conversations (webhook, schedule-fire, wake-poke) all run their turn inside the request or
311
- // through `beginWork`, so they land on one of the two edges; `invoke` is the direct/debug door.
312
- const invocations = async (req) => {
313
- const response = await handleInvocation(req);
314
- if (stateSync && !isBusy())
315
- stateSync.save();
316
- return response;
317
- };
318
- // The Runtime ping contract: Healthy = reclaimable, HealthyBusy = keep the session alive
319
- // (background turns in flight). `time_of_last_update` is REQUIRED for the keep-alive to work,
320
- // despite the contract documenting it as optional ("If you omit the field, the platform tracks
321
- // status changes on its own"): measured on a live Runtime (us-east-1, 2026-08-04), the platform's
322
- // idle measurement reads ONLY this field — with it omitted, a session polling every ~2s and
323
- // receiving HealthyBusy 200s was still reclaimed at exactly IdleRuntimeSessionTimeout after the
324
- // last InvokeAgentRuntime, mid-turn, 2s after the last HealthyBusy answer; with the field present
325
- // the same turn survived 3.5× the idle timeout with zero invocations and completed. The value
326
- // updates ONLY on a real status change: a timestamp advancing on every ping declares a perpetual
327
- // status change, so the idle timeout never fires and dead-idle sessions live to MaxLifetime
328
- // (quota exhaustion — the failure mode the contract's warning describes).
201
+ return { "POST /invocations": handleInvocation, "GET /ping": agentcorePing(isBusy) };
202
+ }
203
+ export function agentcorePing(isBusy) {
204
+ // The Runtime ping contract: Healthy = reclaimable, HealthyBusy = keep the session alive (background turns in
205
+ // flight).
329
206
  let lastStatus = "Healthy";
330
207
  let lastTransition = Math.floor(Date.now() / 1000);
331
- return {
332
- "POST /invocations": invocations,
333
- "GET /ping": () => {
334
- const status = isBusy() ? "HealthyBusy" : "Healthy";
335
- if (status !== lastStatus) {
336
- lastTransition = Math.floor(Date.now() / 1000);
337
- log.debug(`[agentcore] ping status: ${lastStatus} → ${status}`);
338
- lastStatus = status;
339
- }
340
- return json({ status, time_of_last_update: lastTransition }, 200);
341
- },
208
+ return () => {
209
+ const status = isBusy() ? "HealthyBusy" : "Healthy";
210
+ if (status !== lastStatus) {
211
+ lastTransition = Math.floor(Date.now() / 1000);
212
+ log.debug(`[agentcore] ping status: ${lastStatus} → ${status}`);
213
+ lastStatus = status;
214
+ }
215
+ return json({ status, time_of_last_update: lastTransition }, 200);
342
216
  };
343
217
  }
344
- /** Thrown by the mount-site `fire` binding when the envelope names a schedule this workspace does
345
- * not have — the adapter maps it to 404 (deploy drift stays visible in the external clock's logs). */
346
- export class UnknownScheduleError extends Error {
347
- constructor(name) {
348
- super(`unknown schedule "${name}"`);
349
- this.name = "UnknownScheduleError";
350
- }
351
- }
@@ -1,7 +1,20 @@
1
1
  /**
2
- * Read a request body with a hard byte cap (real bytes). A streaming cap is the only robust guard
3
- * against an unbounded body (Content-Length is bypassable with chunked encoding). Web-streams only.
2
+ * Refuse a request to one of OUR routes that does not declare a JSON body — `undefined` when it does.
3
+ *
4
+ * This is what makes "a foreign origin simply gets no CORS headers" safe. Withholding the headers stops a page from
5
+ * READING the reply; it does not stop the request. A cross-origin POST carrying `text/plain` (or a form's
6
+ * `multipart/form-data`) is a CORS *simple request*: no preflight, sent regardless, and the turn it asks for runs.
7
+ * Demanding `application/json` takes the request out of that class, so the browser must preflight it — and a
8
+ * preflight we do not answer is a request that is never sent.
9
+ *
10
+ * Asked of the ROUTE, never of "does this have a body": the control plane reads an empty body as `{}`, so a
11
+ * body-less POST would otherwise walk straight through the gate.
12
+ *
13
+ * NOT applied to channel routes. Their caller is a platform's server with its own content types (Slack posts
14
+ * `application/x-www-form-urlencoded` for some payloads) and its own signature check, and a page cannot forge that.
4
15
  */
16
+ export declare function refuseNonJsonBody(req: Request): Response | undefined;
17
+ /** Read a request body with a hard byte cap (real bytes). */
5
18
  export declare function readBodyCapped(req: Request, max: number): Promise<{
6
19
  text: string;
7
20
  } | {
@@ -1,7 +1,29 @@
1
+ import { text } from "./respond.js";
1
2
  /**
2
- * Read a request body with a hard byte cap (real bytes). A streaming cap is the only robust guard
3
- * against an unbounded body (Content-Length is bypassable with chunked encoding). Web-streams only.
3
+ * Refuse a request to one of OUR routes that does not declare a JSON body — `undefined` when it does.
4
+ *
5
+ * This is what makes "a foreign origin simply gets no CORS headers" safe. Withholding the headers stops a page from
6
+ * READING the reply; it does not stop the request. A cross-origin POST carrying `text/plain` (or a form's
7
+ * `multipart/form-data`) is a CORS *simple request*: no preflight, sent regardless, and the turn it asks for runs.
8
+ * Demanding `application/json` takes the request out of that class, so the browser must preflight it — and a
9
+ * preflight we do not answer is a request that is never sent.
10
+ *
11
+ * Asked of the ROUTE, never of "does this have a body": the control plane reads an empty body as `{}`, so a
12
+ * body-less POST would otherwise walk straight through the gate.
13
+ *
14
+ * NOT applied to channel routes. Their caller is a platform's server with its own content types (Slack posts
15
+ * `application/x-www-form-urlencoded` for some payloads) and its own signature check, and a page cannot forge that.
4
16
  */
17
+ export function refuseNonJsonBody(req) {
18
+ // Parameters are part of the header (`application/json; charset=utf-8`), so compare the media type alone.
19
+ const mediaType = (req.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
20
+ if (mediaType === "application/json")
21
+ return undefined;
22
+ // Says what is required, and does not echo what arrived: reflecting a request header into a response body is a
23
+ // habit worth not having, and the sender already knows what they sent.
24
+ return text("content-type must be application/json\n", 415);
25
+ }
26
+ /** Read a request body with a hard byte cap (real bytes). */
5
27
  export async function readBodyCapped(req, max) {
6
28
  if (!req.body)
7
29
  return { text: "" };
@@ -1,23 +1,3 @@
1
- /**
2
- * SHARED process-wide in-flight work signal. Channels ACK a webhook fast and run the turn
3
- * fire-and-forget on this process's event loop (serve.ts) — so "is this process busy?" is not
4
- * derivable from open HTTP requests. The two shared execution primitives (turn-queue chains,
5
- * task-tracker side tasks) report here; a serving surface that must stay alive while background
6
- * work runs (the AgentCore adapter's /ping → HealthyBusy) reads it.
7
- *
8
- * Deliberately a counter, not a registry: consumers need only "any work in flight?"; keeping the
9
- * module dependency-free lets both channel primitives import it without cycles.
10
- */
11
- /**
12
- * Mark one unit of background work as started. Returns its completion callback — idempotent, so a
13
- * caller may safely settle it from multiple cleanup paths (finally + catch) without double-counting.
14
- */
1
+ /** Mark background work as started. */
15
2
  export declare function beginWork(): () => void;
16
- /**
17
- * Subscribe to the 0-in-flight edge: the moment the process finishes its last background turn.
18
- * Returns an unsubscribe. Used by the AgentCore adapter to push its state snapshot exactly when the
19
- * state root has settled and before the platform may reclaim the idle microVM.
20
- */
21
- export declare function onIdle(listener: () => void): () => void;
22
- /** How many units of background work are currently in flight (0 = idle). */
23
3
  export declare function activeWork(): number;