@fastagent-sh/fastagent 0.21.0 → 0.22.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 (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 -43
  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 +209 -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 +153 -228
  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
@@ -2,9 +2,7 @@
2
2
  export interface TelegramMessage {
3
3
  message_id: number;
4
4
  text?: string;
5
- /** Entities Telegram's server parsed out of `text` (mentions, commands, URLs, code…). Offsets are
6
- * UTF-16 code units — exactly JS string indexing, so `text.slice(offset, offset + length)` is the
7
- * entity's text verbatim. */
5
+ /** Entities Telegram's server parsed out of `text` (mentions, commands, URLs, code…). */
8
6
  entities?: {
9
7
  type: string;
10
8
  offset: number;
@@ -20,7 +18,7 @@ export interface TelegramMessage {
20
18
  length: number;
21
19
  [k: string]: unknown;
22
20
  }[];
23
- /** Photo sizes, smallest → largest. The channel sends the largest to the model as a vision image. */
21
+ /** Photo sizes, smallest → largest. */
24
22
  photo?: {
25
23
  file_id: string;
26
24
  file_unique_id: string;
@@ -85,16 +83,20 @@ export interface TelegramMessage {
85
83
  };
86
84
  [k: string]: unknown;
87
85
  }
88
- /** A Telegram update (the common subset the channel ACTS on — an update kind not listed here is ACKed
89
- * and dropped before `route` sees it, so listing it would be a false promise; `[k]` keeps the raw
90
- * payload reachable). Narrow for what you route on, e.g. `update.message?.text`. */
86
+ /**
87
+ * A Telegram update (the common subset the channel ACTS on — an update kind not listed here is ACKed and dropped
88
+ * before `route` sees it, so listing it would be a false promise; `[k]` keeps the raw payload reachable).
89
+ */
91
90
  export interface TelegramUpdate {
92
91
  update_id: number;
93
92
  message?: TelegramMessage;
94
93
  channel_post?: TelegramMessage;
95
94
  [k: string]: unknown;
96
95
  }
97
- /** What `route` returns: act with these (every field optional — omitted ones default from the message), or null to ignore. */
96
+ /**
97
+ * What `route` returns: act with these (every field optional — omitted ones default from the message), or null to
98
+ * ignore.
99
+ */
98
100
  export interface TelegramRoute {
99
101
  /** Conversation identity (default: `chat` or `chat:thread`). */
100
102
  session?: string;
@@ -105,68 +107,39 @@ export interface TelegramRoute {
105
107
  /** Base prompt (default: {@link telegramEnvelope}); the channel still appends attachments + the HTML hint. */
106
108
  text?: string;
107
109
  }
108
- /** The actionable message in an update (a fresh message or channel post). Edits (`edited_message` /
109
- * `edited_channel_post`) are deliberately NOT actionable: answering them re-answers every typo fix (a
110
- * duplicate reply per edit), so an edited message changes nothing — the standard bot behavior. The
111
- * trade-off: editing a mention INTO an old message does not summon either; send a new message. */
110
+ /** The actionable message in an update (a fresh message or channel post). */
112
111
  export declare function pickMessage(update: TelegramUpdate): TelegramMessage | undefined;
113
112
  /** file_ids to send the model as vision images: this message's largest photo + a replied-to photo. */
114
113
  export declare function extractImages(m: TelegramMessage): string[];
115
- /** The message's OWN photo (largest size), without the replied-to message's — the buffer path uses
116
- * this: each message is its own entry there, so counting the replied-to attachment again would
117
- * duplicate it and squeeze the attachment cap (the reply relation is expressed by `replyTo` instead). */
114
+ /** The message's OWN photo (largest size), without the replied-to message's. */
118
115
  export declare function ownImages(m: TelegramMessage): string[];
119
- /** file_ids to download to disk for the agent's tools: this message's files + a replied-to message's
120
- * (so "summarize this file" works when the user replies to a document with the mention). */
116
+ /** file_ids to download to disk: this message's files plus a replied-to message's ("summarize this file"). */
121
117
  export declare function extractFiles(m: TelegramMessage): string[];
122
- /** The message's OWN files, without the replied-to message's (see {@link ownImages} for why). */
118
+ /** The message's OWN files, without the replied-to message's. */
123
119
  export declare function ownFiles(m: TelegramMessage): string[];
124
- /** A stable sender label for attribution. In a shared (multi-user) session the model must tell who is
125
- * who across turns; a username-less user still gets a name + id rather than vanishing. */
120
+ /** A stable sender label for attribution. */
126
121
  export declare function fromLabel(from: TelegramMessage["from"]): string | undefined;
127
- /** A one-line description of a message's attachment, so the envelope names what was sent even before
128
- * the agent opens it (and so a media-only message isn't blank). */
122
+ /**
123
+ * A one-line description of a message's attachment, so the envelope names what was sent even before the agent opens it
124
+ * (and so a media-only message isn't blank).
125
+ */
129
126
  export declare function attachmentSummary(m: TelegramMessage): string | undefined;
130
127
  /** A one-line, length-capped rendering of a message's content for the context buffer. */
131
128
  export declare function messageText(m: TelegramMessage): string;
132
129
  /**
133
- * The default base prompt: a context envelope (chat/thread/sender + a group note + reply) then the
134
- * user's text/caption and a compact rendering of structured payloads (location/contact/poll). The
135
- * sender is named on every message and a group chat is flagged — in a shared multi-user session that is
136
- * how the model tells participants apart and knows it is not a 1:1. The reply
137
- * block carries the replied-to sender, message id, and text/caption or an attachment summary (and the
138
- * channel downloads a replied-to file/photo too). Exported so a custom `route` can reuse it, e.g.
139
- * `text: `${telegramEnvelope(m)}\n\n[extra]``. The channel still appends downloaded attachments.
130
+ * The default base prompt: a context envelope (chat/thread/sender + a group note + reply) then the user's text/caption
131
+ * and a compact rendering of structured payloads (location/contact/poll).
140
132
  */
141
133
  export declare function telegramEnvelope(m: TelegramMessage): string;
142
134
  /**
143
- * Whether the update is a `/stop` ADDRESSED TO THIS BOT — the one question the channel asks about a
144
- * stop, because an unaddressed one is not a command it may act on. Four ways to address it, all
145
- * equivalent: a bare `/stop` in a private chat (no one else is there), `/stop@thisbot`, an `@thisbot`
146
- * mention beside the command, and a bare `/stop` replying to one of this bot's messages.
147
- *
148
- * A BARE `/stop` in a group is deliberately NOT one of them. Telegram hands it to every bot in the
149
- * chat — and only promises to deliver it at all when this bot spoke last — so it names no one: acting
150
- * on it lets a bystander's ask for a different bot abort this bot's run, and answering it lets that
151
- * bystander make this bot talk. It stays ordinary discussion, which is what the group sees anyway.
152
- *
153
- * Takes the UPDATE, like `route` itself, so a custom route can ask it directly — that route is the
154
- * gate a stop must pass, and one that refuses this update silences the command.
135
+ * Whether the update is a `/stop` ADDRESSED TO THIS BOT — the one question the channel asks about a stop, because an
136
+ * unaddressed one is not a command it may act on.
155
137
  */
156
138
  export declare function telegramStop(update: TelegramUpdate, options?: {
157
139
  botUsername?: string;
158
140
  botId?: number;
159
141
  }): boolean;
160
- /**
161
- * The default routing policy (used when `route` is omitted; exported so a custom route can reuse it):
162
- * answer private chats always; a group only on a reply to THIS bot (by `botId`) or a `mention` entity
163
- * naming it (when `botUsername` is supplied — telegramChannel parses the id from the token and resolves
164
- * the username via getMe). A bare or directed slash command does NOT summon in a group (that was noisy;
165
- * a bot author who wants commands adds a custom route) — except a `/stop` ADDRESSED to this bot, which
166
- * must reach the run it stops rather than sit in the buffer behind it (see {@link telegramStop}; a
167
- * bare group `/stop` addresses no one and stays ordinary discussion). Returns `{}` (act; the channel fills
168
- * session/target/prompt from the message) or `null` (ignore).
169
- */
142
+ /** The default routing policy (used when `route` is omitted; exported so a custom route can reuse it). */
170
143
  export declare function defaultTelegramRoute(update: TelegramUpdate, options?: {
171
144
  botUsername?: string;
172
145
  botId?: number;
@@ -1,16 +1,7 @@
1
- /**
2
- * Telegram protocol parsing — PURE: message field extraction, the prompt envelope, and the summon/route
3
- * policy. The defining invariant is purity: no state, no IO, no Bot API calls — plain data-in → data-out.
4
- * In telegram.ts's pipeline (verify → decide via `route` → run the turn → stream reply), this is the
5
- * "decide" and prompt-building half; telegram.ts wires it in and owns the stateful lifecycle. Kept
6
- * separate so this layer tests as plain functions and reads without the factory's noise.
7
- */
1
+ /** Telegram protocol parsing — PURE: message field extraction, the prompt envelope, and the summon/route policy. */
8
2
  import { BUFFER_LINE_MAX_CHARS } from "../kit/context-buffer.js";
9
3
  import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } from "../kit/text.js";
10
- /** The actionable message in an update (a fresh message or channel post). Edits (`edited_message` /
11
- * `edited_channel_post`) are deliberately NOT actionable: answering them re-answers every typo fix (a
12
- * duplicate reply per edit), so an edited message changes nothing — the standard bot behavior. The
13
- * trade-off: editing a mention INTO an old message does not summon either; send a new message. */
4
+ /** The actionable message in an update (a fresh message or channel post). */
14
5
  export function pickMessage(update) {
15
6
  return update.message ?? update.channel_post;
16
7
  }
@@ -18,30 +9,28 @@ export function pickMessage(update) {
18
9
  export function extractImages(m) {
19
10
  return [...ownImages(m), ...(m.reply_to_message ? ownImages(m.reply_to_message) : [])];
20
11
  }
21
- /** The message's OWN photo (largest size), without the replied-to message's — the buffer path uses
22
- * this: each message is its own entry there, so counting the replied-to attachment again would
23
- * duplicate it and squeeze the attachment cap (the reply relation is expressed by `replyTo` instead). */
12
+ /** The message's OWN photo (largest size), without the replied-to message's. */
24
13
  export function ownImages(m) {
25
14
  return [m.photo?.at(-1)?.file_id].filter((id) => Boolean(id));
26
15
  }
27
- /** file_ids to download to disk for the agent's tools: this message's files + a replied-to message's
28
- * (so "summarize this file" works when the user replies to a document with the mention). */
16
+ /** file_ids to download to disk: this message's files plus a replied-to message's ("summarize this file"). */
29
17
  export function extractFiles(m) {
30
18
  return [...ownFiles(m), ...(m.reply_to_message ? ownFiles(m.reply_to_message) : [])];
31
19
  }
32
- /** The message's OWN files, without the replied-to message's (see {@link ownImages} for why). */
20
+ /** The message's OWN files, without the replied-to message's. */
33
21
  export function ownFiles(m) {
34
22
  return [m.document?.file_id, m.voice?.file_id, m.video?.file_id, m.audio?.file_id].filter((id) => Boolean(id));
35
23
  }
36
- /** A stable sender label for attribution. In a shared (multi-user) session the model must tell who is
37
- * who across turns; a username-less user still gets a name + id rather than vanishing. */
24
+ /** A stable sender label for attribution. */
38
25
  export function fromLabel(from) {
39
26
  if (!from)
40
27
  return undefined;
41
28
  return from.username ? `@${from.username}` : `${from.first_name ?? "user"} (id ${from.id})`;
42
29
  }
43
- /** A one-line description of a message's attachment, so the envelope names what was sent even before
44
- * the agent opens it (and so a media-only message isn't blank). */
30
+ /**
31
+ * A one-line description of a message's attachment, so the envelope names what was sent even before the agent opens it
32
+ * (and so a media-only message isn't blank).
33
+ */
45
34
  export function attachmentSummary(m) {
46
35
  if (m.photo?.length)
47
36
  return "[photo]";
@@ -56,9 +45,10 @@ export function attachmentSummary(m) {
56
45
  return "[audio]";
57
46
  return undefined;
58
47
  }
59
- /** A message's readable body: its text, else its caption, else a one-line attachment summary; undefined
60
- * for an empty/service message. The single source for "what did this message say" — envelope, reply-
61
- * quote, and the context-buffer line all read through it so their fallbacks cannot drift apart. */
48
+ /**
49
+ * A message's readable body: its text, else its caption, else a one-line attachment summary; undefined for an
50
+ * empty/service message.
51
+ */
62
52
  function bodyOf(m) {
63
53
  return m.text ?? m.caption ?? attachmentSummary(m);
64
54
  }
@@ -67,13 +57,8 @@ export function messageText(m) {
67
57
  return truncateCodePointPrefix((bodyOf(m) ?? "").replace(/\s+/g, " ").trim(), BUFFER_LINE_MAX_CHARS);
68
58
  }
69
59
  /**
70
- * The default base prompt: a context envelope (chat/thread/sender + a group note + reply) then the
71
- * user's text/caption and a compact rendering of structured payloads (location/contact/poll). The
72
- * sender is named on every message and a group chat is flagged — in a shared multi-user session that is
73
- * how the model tells participants apart and knows it is not a 1:1. The reply
74
- * block carries the replied-to sender, message id, and text/caption or an attachment summary (and the
75
- * channel downloads a replied-to file/photo too). Exported so a custom `route` can reuse it, e.g.
76
- * `text: `${telegramEnvelope(m)}\n\n[extra]``. The channel still appends downloaded attachments.
60
+ * The default base prompt: a context envelope (chat/thread/sender + a group note + reply) then the user's text/caption
61
+ * and a compact rendering of structured payloads (location/contact/poll).
77
62
  */
78
63
  export function telegramEnvelope(m) {
79
64
  const r = m.reply_to_message;
@@ -84,9 +69,7 @@ export function telegramEnvelope(m) {
84
69
  ]
85
70
  .filter(Boolean)
86
71
  .join(", ");
87
- // In a shared group session the model sees turns from different people (each `from`-tagged); tell it
88
- // so it addresses participants by name and does not assume one continuous interlocutor. A 1:1 DM is
89
- // self-evident, so no note there.
72
+ // In a shared group session the model sees turns from different people (each `from`-tagged).
90
73
  const isGroup = m.chat.type === "group" || m.chat.type === "supergroup";
91
74
  const scope = isGroup ? "\n[group chat — multiple people; each message is prefixed with its sender]" : "";
92
75
  const replyTo = r
@@ -101,20 +84,14 @@ export function telegramEnvelope(m) {
101
84
  parts.push(`[poll: ${m.poll.question} — ${(m.poll.options ?? []).map((o) => o.text).join(" / ")}]`);
102
85
  return `[telegram: ${meta}]${scope}${replyTo}\n${parts.filter(Boolean).join("\n")}`;
103
86
  }
104
- /** Normalize a configured bot username: drop a leading `@`, trim, lowercase (usernames are case-
105
- * insensitive). Undefined when unknown. */
87
+ /** Normalize a configured bot username: drop a leading `@`, trim, lowercase (usernames are case-insensitive). */
106
88
  function botName(botUsername) {
107
89
  const s = botUsername?.replace(/^@/, "").trim().toLowerCase();
108
90
  return s || undefined;
109
91
  }
110
92
  /**
111
- * The `mention` ENTITIES naming THIS bot — read from what Telegram's server already parsed, not a
112
- * regex over the raw text. The entity type excludes by construction what a text scan false-matches:
113
- * `@bot` inside a code block or a URL is not a `mention` entity, a glued `/cmd@bot` is a `bot_command`
114
- * — and slicing the exact offset/length range makes `@fast` vs `@fastagent` confusion impossible.
115
- * (Offsets are UTF-16 code units = native JS string indexing.) No text fallback: mention entities are
116
- * produced server-side, so their absence means there is no mention. Empty when the bot does not know
117
- * its own username — "which of these names me?" has no answer then, and guessing would mis-summon.
93
+ * The `mention` ENTITIES naming THIS bot — read from what Telegram's server already parsed, not a regex over the raw
94
+ * text.
118
95
  */
119
96
  function botMentions(m, botUsername) {
120
97
  const name = botName(botUsername);
@@ -124,14 +101,10 @@ function botMentions(m, botUsername) {
124
101
  const entities = (m.text !== undefined ? m.entities : m.caption_entities) ?? [];
125
102
  return entities.filter((e) => e.type === "mention" && text.slice(e.offset, e.offset + e.length).toLowerCase() === `@${name}`);
126
103
  }
127
- /** Whether the message @mentions the bot. */
128
104
  function mentionsBot(m, botUsername) {
129
105
  return botMentions(m, botUsername).length > 0;
130
106
  }
131
- /** The message text with THIS bot's mentions cut out, so a command addressed to it reads as the bare
132
- * command. Only its own: a message that names someone else (`@otherbot /stop`) must keep that name,
133
- * or cutting it would turn an ask aimed elsewhere into this bot's command. Cut from the end so the
134
- * earlier offsets stay valid (they are UTF-16 code units = native JS indexing). */
107
+ /** The message text with THIS bot's mentions cut out, so a command addressed to it reads as the bare command. */
135
108
  function textWithoutBotMentions(m, botUsername) {
136
109
  const text = m.text ?? m.caption ?? "";
137
110
  return botMentions(m, botUsername)
@@ -140,41 +113,27 @@ function textWithoutBotMentions(m, botUsername) {
140
113
  .trim();
141
114
  }
142
115
  /**
143
- * Whether the update is a `/stop` ADDRESSED TO THIS BOT — the one question the channel asks about a
144
- * stop, because an unaddressed one is not a command it may act on. Four ways to address it, all
145
- * equivalent: a bare `/stop` in a private chat (no one else is there), `/stop@thisbot`, an `@thisbot`
146
- * mention beside the command, and a bare `/stop` replying to one of this bot's messages.
147
- *
148
- * A BARE `/stop` in a group is deliberately NOT one of them. Telegram hands it to every bot in the
149
- * chat — and only promises to deliver it at all when this bot spoke last — so it names no one: acting
150
- * on it lets a bystander's ask for a different bot abort this bot's run, and answering it lets that
151
- * bystander make this bot talk. It stays ordinary discussion, which is what the group sees anyway.
152
- *
153
- * Takes the UPDATE, like `route` itself, so a custom route can ask it directly — that route is the
154
- * gate a stop must pass, and one that refuses this update silences the command.
116
+ * Whether the update is a `/stop` ADDRESSED TO THIS BOT — the one question the channel asks about a stop, because an
117
+ * unaddressed one is not a command it may act on.
155
118
  */
156
119
  export function telegramStop(update, options) {
157
120
  const m = pickMessage(update);
158
121
  if (!m)
159
122
  return false;
160
- // This bot's own mentions are cut out before matching: `@thisbot /stop` is the same command as
161
- // `/stop@thisbot`. Anyone else's stays in the text, so `@otherbot /stop` never reduces to a command
162
- // here — not even when it also replies to this bot.
123
+ // This bot's own mentions are cut out before matching: `@thisbot /stop` is the same command as `/stop@thisbot`.
163
124
  const match = /^\/stop(?:@([A-Za-z0-9_]+))?$/i.exec(textWithoutBotMentions(m, options?.botUsername));
164
125
  if (!match)
165
126
  return false;
166
- // `/stop@name` states its addressee: ours only when the name is ours, and never when this bot does
167
- // not know its own username (fail closed — the same rule reply/mention summon follows).
127
+ // `/stop@name` states its addressee: ours only when the name is ours, and never when this bot does not know its own
128
+ // username (fail closed — the same rule reply/mention summon follows).
168
129
  if (match[1] !== undefined)
169
130
  return match[1].toLowerCase() === botName(options?.botUsername);
170
131
  return m.chat.type === "private" || mentionsBot(m, options?.botUsername) || repliesToBot(m, options);
171
132
  }
172
- /** Whether the message replies to THIS bot — not just any bot: in a multi-bot group, replying to
173
- * another bot must not summon ours. Identity is the bot's numeric id (stable; a username is a mutable
174
- * handle) — telegramChannel parses it synchronously from the token, so there is no resolution race.
175
- * Without an id, fall back to username; with NEITHER, fail closed (false): answering "is this a reply
176
- * to me?" with "I don't know who I am, so yes" would mis-summon in every multi-bot group — a caller
177
- * reusing the route bare must supply an identity to get reply summon. */
133
+ /**
134
+ * Whether the message replies to THIS bot — not just any bot: in a multi-bot group, replying to another bot must not
135
+ * summon ours.
136
+ */
178
137
  function repliesToBot(m, options) {
179
138
  const r = m.reply_to_message?.from;
180
139
  if (r?.is_bot !== true)
@@ -184,16 +143,7 @@ function repliesToBot(m, options) {
184
143
  const name = botName(options?.botUsername);
185
144
  return name !== undefined && r.username?.toLowerCase() === name;
186
145
  }
187
- /**
188
- * The default routing policy (used when `route` is omitted; exported so a custom route can reuse it):
189
- * answer private chats always; a group only on a reply to THIS bot (by `botId`) or a `mention` entity
190
- * naming it (when `botUsername` is supplied — telegramChannel parses the id from the token and resolves
191
- * the username via getMe). A bare or directed slash command does NOT summon in a group (that was noisy;
192
- * a bot author who wants commands adds a custom route) — except a `/stop` ADDRESSED to this bot, which
193
- * must reach the run it stops rather than sit in the buffer behind it (see {@link telegramStop}; a
194
- * bare group `/stop` addresses no one and stays ordinary discussion). Returns `{}` (act; the channel fills
195
- * session/target/prompt from the message) or `null` (ignore).
196
- */
146
+ /** The default routing policy (used when `route` is omitted; exported so a custom route can reuse it). */
197
147
  export function defaultTelegramRoute(update, options) {
198
148
  const m = pickMessage(update);
199
149
  if (!m)
@@ -201,9 +151,7 @@ export function defaultTelegramRoute(update, options) {
201
151
  const summoned = m.chat.type === "private" ||
202
152
  repliesToBot(m, options) ||
203
153
  mentionsBot(m, options?.botUsername) ||
204
- // Adds exactly ONE case to the three above: the `/stop@thisbot` suffix form. Every other way to
205
- // address a stop (private, mention, reply) is already a summon on its own — telegramStop repeats
206
- // those three by design, since it must answer "addressed to me?" without a route to lean on.
154
+ // Adds exactly ONE case to the three above: the `/stop@thisbot` suffix form.
207
155
  telegramStop(update, options);
208
156
  return summoned ? {} : null;
209
157
  }
@@ -6,14 +6,23 @@
6
6
  * message, works in groups and private (unlike sendMessageDraft, which is private/forum-topic only).
7
7
  */
8
8
  import type { AgentEvent } from "../../agent.ts";
9
+ import * as Effect from "effect/Effect";
10
+ import type * as Stream from "effect/Stream";
11
+ import { type PortFailure } from "../../effect-port.ts";
9
12
  import { type ChannelFailure, defaultErrorMessage } from "../kit/preview-kit.ts";
10
13
  import { type Target } from "./telegram-api.ts";
11
14
  /** A terminal failure, as the channel hands it to `onError` — the shared channel shape. */
12
15
  export type TelegramFailure = ChannelFailure;
13
16
  export { defaultErrorMessage };
17
+ /**
18
+ * Deliver a reply this process did not generate: a recovered answer. The run that produced it is gone with its
19
+ * preview, but a "⏳ Queued" notice THIS process put up while the record waited its turn is still there — so the
20
+ * same terminal-write policy applies, taking that message over rather than leaving it pinned above the answer.
21
+ */
22
+ export declare function deliverTelegramAnswer(api: string, botToken: string, target: Target, text: string, previewId?: number): Promise<void>;
14
23
  /**
15
24
  * Consume one turn's event stream into a Telegram chat, live (see the module header for the preview
16
25
  * model). Preview edits are best-effort (logged once if they fail); the final write is authoritative
17
26
  * and surfaces a real failure (bad token, etc.).
18
27
  */
19
- export declare function streamReply(events: AsyncIterable<AgentEvent>, api: string, botToken: string, target: Target, formatError: (failed: TelegramFailure) => string | undefined, previewId?: number): Promise<void>;
28
+ export declare function telegramReply(events: Stream.Stream<AgentEvent, PortFailure>, api: string, botToken: string, target: Target, formatError: (failed: TelegramFailure) => string | undefined, previewId?: number, onAnswered?: (answer: string) => void): Effect.Effect<void, PortFailure, import("effect/Scope").Scope>;
@@ -1,4 +1,9 @@
1
- import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../kit/preview-kit.js";
1
+ import * as Effect from "effect/Effect";
2
+ import * as Clock from "effect/Clock";
3
+ import { previewPump, renderReply } from "../kit/delivery.js";
4
+ import { DROPPABLE_FRAME } from "../kit/transport.js";
5
+ import { portJoin } from "../../effect-port.js";
6
+ import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../kit/preview-kit.js";
2
7
  import { log } from "../../log.js";
3
8
  import { TELEGRAM_MAX_TEXT, callApi, editMessageText, sendMessage } from "./telegram-api.js";
4
9
  export { defaultErrorMessage };
@@ -10,7 +15,7 @@ const EDIT_THROTTLE_MS = 1500;
10
15
  /** How much of the (growing) reasoning to peek at in the live view — the most recent tail. */
11
16
  const THINKING_PREVIEW = 280;
12
17
  /**
13
- * The terminal-write POLICY: resolve the single preview message into `text`. streamReply owns the
18
+ * The terminal-write POLICY: resolve the single preview message into `text`. telegramReply owns the
14
19
  * preview lifecycle, so this composition of transport primitives lives here, not in telegram-api. One
15
20
  * message → edit the preview in place; if the edit fails (preview gone, or a persistent 429/5xx) fall
16
21
  * back to deleting the placeholder and sending fresh, so no "Thinking…" is left pinned above the answer.
@@ -44,109 +49,83 @@ async function finalize(api, botToken, target, messageId, text, opts = {}) {
44
49
  }
45
50
  await sendMessage(api, botToken, target, text, { html });
46
51
  }
52
+ /**
53
+ * Deliver a reply this process did not generate: a recovered answer. The run that produced it is gone with its
54
+ * preview, but a "⏳ Queued" notice THIS process put up while the record waited its turn is still there — so the
55
+ * same terminal-write policy applies, taking that message over rather than leaving it pinned above the answer.
56
+ */
57
+ export function deliverTelegramAnswer(api, botToken, target, text, previewId) {
58
+ return finalize(api, botToken, target, previewId, text);
59
+ }
47
60
  /**
48
61
  * Consume one turn's event stream into a Telegram chat, live (see the module header for the preview
49
62
  * model). Preview edits are best-effort (logged once if they fail); the final write is authoritative
50
63
  * and surfaces a real failure (bad token, etc.).
51
64
  */
52
- export async function streamReply(events, api, botToken, target, formatError, previewId) {
53
- // Event → view-state reduction is the shared machine (preview-kit); this renderer owns the reveal
54
- // policy, formatting, and delivery below.
55
- const turn = createTurnView();
56
- const view = () => {
57
- const v = composeTurnBody([
58
- thinkingLine(turn, THINKING_PREVIEW),
59
- toolLines(turn),
60
- turn.retrying ? RETRY_NOTICE : "",
61
- revealedAnswer(turn, EDIT_THROTTLE_MS),
62
- ]);
63
- // Before any reasoning/tool/text arrives, show an explicit placeholder rather than an empty edit.
64
- return v === "" ? THINKING_PLACEHOLDER : v;
65
- };
66
- // The live preview is ONE real message: sent once (capturing its id + threading under the asker),
67
- // then edited in place. messageId/lastSent are shared with the final write on completion.
68
- // `previewId`: an already-sent message to take over as the preview (the "⏳ queued" notice) — the
69
- // pump edits it in place, so the queue notice morphs into the live view instead of leaving an orphan.
70
- let messageId = previewId;
71
- let previewSent = messageId !== undefined; // a placeholder send was attempted — guards against re-sending when no id came back
72
- let finalized = false; // a terminal write (completed/failed) ran — the finally skips its orphan cleanup
73
- let lastSent = "";
74
- const flushPreview = async () => {
75
- const text = view();
76
- if (text === lastSent)
77
- return; // skip an unchanged edit (Telegram rejects "message is not modified")
78
- lastSent = text;
79
- if (messageId !== undefined) {
80
- await editMessageText(api, botToken, target, messageId, text); // plain — a partial answer may carry unbalanced HTML
81
- return;
82
- }
83
- // No preview message yet. Send the placeholder ONCE; never re-send (that would spam a new message per
84
- // frame). If Telegram returns ok WITHOUT a message_id (proxy / odd API base / unparseable body) we
85
- // cannot edit — fail visibly and stop previewing (the final write still lands via finalize).
86
- if (previewSent)
87
- return;
88
- previewSent = true;
89
- messageId = await sendMessage(api, botToken, target, text, { html: false });
90
- if (messageId === undefined)
91
- throw new Error("telegram sendMessage returned ok without a message_id — live preview disabled for this turn");
92
- };
93
- // The shared single-writer pump (preview-kit) serializes edits to the one preview message. (No
94
- // keepalive: a real message does not expire, unlike a Bot API `sendMessageDraft` (30s window).)
95
- const { touch, finish } = createPreviewPump({
96
- flush: flushPreview,
97
- throttleMs: EDIT_THROTTLE_MS,
98
- onError: (e) => log.warn(`[telegram] live preview failed (final reply still sends): ${String(e)}`),
99
- });
100
- touch(); // send the "💭 Thinking…" placeholder immediately
101
- try {
102
- for await (const e of events) {
103
- if (e.type === "completed") {
104
- await finish();
105
- // Edit the preview into the final answer (HTML, plain fallback); the persisted message is the
106
- // answer alone — the process (thinking/tools) was preview-only. Mark finalized BEFORE delivering:
107
- // the terminal was reached, so a delivery failure here is a plain failure, not an "abnormal exit"
108
- // (which would wrongly fire the finally's neutral-notice fallback = double delivery + wrong text).
109
- finalized = true;
110
- await finalize(api, botToken, target, messageId, turn.answer.trim() !== "" ? turn.answer : "(no reply)");
65
+ export function telegramReply(events, api, botToken, target, formatError, previewId, onAnswered) {
66
+ return Effect.gen(function* () {
67
+ const clock = yield* Clock.Clock;
68
+ const now = () => clock.currentTimeMillisUnsafe();
69
+ // Event → view-state reduction is the shared machine (preview-kit); this renderer owns the reveal
70
+ // policy, formatting, and delivery below.
71
+ const turn = createTurnView();
72
+ const view = () => {
73
+ const v = composeTurnBody([
74
+ thinkingLine(turn, THINKING_PREVIEW),
75
+ toolLines(turn),
76
+ turn.retrying ? RETRY_NOTICE : "",
77
+ revealedAnswer(turn, EDIT_THROTTLE_MS, now()),
78
+ ]);
79
+ // Before any reasoning/tool/text arrives, show an explicit placeholder rather than an empty edit.
80
+ return v === "" ? THINKING_PLACEHOLDER : v;
81
+ };
82
+ // The live preview is ONE real message: sent once (capturing its id + threading under the asker),
83
+ // then edited in place. messageId/lastSent are shared with the final write on completion.
84
+ // `previewId`: an already-sent message to take over as the preview (the "⏳ queued" notice) — the
85
+ // pump edits it in place, so the queue notice morphs into the live view instead of leaving an orphan.
86
+ let messageId = previewId;
87
+ let previewSent = messageId !== undefined; // a placeholder send was attempted — guards against re-sending when no id came back
88
+ let lastSent = "";
89
+ const flushPreview = async () => {
90
+ const text = view();
91
+ if (text === lastSent)
92
+ return; // skip an unchanged edit (Telegram rejects "message is not modified")
93
+ lastSent = text;
94
+ if (messageId !== undefined) {
95
+ // plain — a partial answer may carry unbalanced HTML; droppable — see DROPPABLE_FRAME.
96
+ await editMessageText(api, botToken, target, messageId, text, DROPPABLE_FRAME);
111
97
  return;
112
98
  }
113
- if (e.type === "failed") {
114
- await finish();
115
- // Two audiences: the chat (customer-facing — formatError, neutral by default) and the operator
116
- // log (dev-facing — the full details, via the throw below + the handler's catch). Same terminal
117
- // write as completed: edit → fresh-send if the preview is gone; an empty notice deletes the
118
- // placeholder (suppress = no residue). HTML like the answer — symmetric, and finalize already
119
- // falls back to plain if a custom onError returns markup Telegram rejects. Best-effort — we throw
120
- // below regardless.
121
- finalized = true;
122
- {
123
- const msg = formatError({
124
- details: e.details,
125
- retryable: e.retryable,
126
- ...(e.code !== undefined ? { code: e.code } : {}),
127
- }) ?? "";
128
- await finalize(api, botToken, target, messageId, msg).catch(() => { });
129
- }
130
- throw new Error(`agent failed: ${e.details} (retryable=${e.retryable})`);
131
- }
132
- if (applyTurnEvent(turn, e))
133
- touch();
134
- }
135
- throw new Error("stream ended without a terminal event"); // violates SPEC MUST 1
136
- }
137
- finally {
138
- await finish();
139
- // Abnormal exit (stream ended without a terminal, the generator threw, or the consumer abandoned): no
140
- // terminal write ran. Show the SAME neutral notice a `failed` event would — the preview may show real
141
- // partial work, so don't delete it silently, and don't leave the user in silence. A suppressing
142
- // onError still collapses to a delete (finalize on empty text).
143
- if (!finalized) {
144
- // retryable:false — an abnormal end (no terminal / a throw) is of UNKNOWN retryability, so use the
145
- // neutral "something went wrong" default rather than promising "try again" that may not help.
146
- const notice = formatError({ details: "the turn ended without completing", retryable: false }) ?? "";
147
- // finalize handles messageId===undefined (no preview reached) with a fresh send — so the user is
148
- // told even when the turn died before any message.
149
- await finalize(api, botToken, target, messageId, notice).catch(() => { });
150
- }
151
- }
99
+ // No preview message yet. Send the placeholder ONCE; never re-send (that would spam a new message per
100
+ // frame). If Telegram returns ok WITHOUT a message_id (proxy / odd API base / unparseable body) we
101
+ // cannot edit — fail visibly and stop previewing (the final write still lands via finalize).
102
+ //
103
+ // NOT droppable, unlike the frames above: this send happens once and every later frame depends on
104
+ // its id, so dropping it costs the whole turn's live preview rather than one redrawable view.
105
+ if (previewSent)
106
+ return;
107
+ previewSent = true;
108
+ messageId = await sendMessage(api, botToken, target, text, { html: false });
109
+ if (messageId === undefined)
110
+ throw new Error("telegram sendMessage returned ok without a message_id — live preview disabled for this turn");
111
+ };
112
+ const { touch, finish } = yield* previewPump({
113
+ flush: flushPreview,
114
+ throttleMs: EDIT_THROTTLE_MS,
115
+ onError: (e) => log.warn(`[telegram] live preview failed (final reply still sends): ${String(e)}`),
116
+ });
117
+ touch(); // send the "💭 Thinking…" placeholder immediately
118
+ yield* renderReply(events, {
119
+ label: "[telegram]",
120
+ finish,
121
+ formatError,
122
+ onEvent: (event) => {
123
+ if (applyTurnEvent(turn, event, now()))
124
+ touch();
125
+ },
126
+ answer: () => (turn.answer.trim() !== "" ? turn.answer : "(no reply)"),
127
+ ...(onAnswered ? { onAnswered } : {}),
128
+ settle: (text) => portJoin(() => finalize(api, botToken, target, messageId, text)),
129
+ });
130
+ });
152
131
  }