@fastagent-sh/fastagent 0.19.0 → 0.21.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 (212) hide show
  1. package/README.md +5 -1
  2. package/dist/atomic-write.d.ts +11 -7
  3. package/dist/atomic-write.js +16 -11
  4. package/dist/channels/agentcore-limits.d.ts +9 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-protocol.d.ts +112 -0
  7. package/dist/channels/agentcore-protocol.js +22 -0
  8. package/dist/channels/agentcore-service.d.ts +12 -15
  9. package/dist/channels/agentcore-service.js +15 -24
  10. package/dist/channels/agentcore-state.d.ts +5 -11
  11. package/dist/channels/agentcore-state.js +4 -1
  12. package/dist/channels/agentcore.d.ts +9 -83
  13. package/dist/channels/agentcore.js +101 -93
  14. package/dist/channels/control.d.ts +50 -12
  15. package/dist/channels/control.js +251 -157
  16. package/dist/channels/discover.d.ts +26 -5
  17. package/dist/channels/discover.js +13 -26
  18. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  19. package/dist/channels/feishu/context-buffer.js +0 -38
  20. package/dist/channels/feishu/crypto.d.ts +0 -2
  21. package/dist/channels/feishu/crypto.js +3 -8
  22. package/dist/channels/feishu/feishu-api.js +7 -12
  23. package/dist/channels/feishu/feishu.js +86 -128
  24. package/dist/channels/feishu/parse.d.ts +4 -3
  25. package/dist/channels/feishu/parse.js +5 -4
  26. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  27. package/dist/channels/feishu/register-webhook.js +43 -57
  28. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  29. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  30. package/dist/channels/feishu/setup-mode.js +26 -0
  31. package/dist/channels/feishu/shared-api.d.ts +10 -0
  32. package/dist/channels/feishu/shared-api.js +38 -0
  33. package/dist/channels/http.d.ts +0 -8
  34. package/dist/channels/http.js +4 -56
  35. package/dist/channels/kit/attachment-path.d.ts +12 -0
  36. package/dist/channels/kit/attachment-path.js +43 -0
  37. package/dist/channels/kit/context-buffer.d.ts +9 -0
  38. package/dist/channels/kit/context-buffer.js +11 -0
  39. package/dist/channels/kit/signature.d.ts +12 -0
  40. package/dist/channels/kit/signature.js +17 -0
  41. package/dist/channels/kit/stop-command.js +3 -3
  42. package/dist/channels/kit/tasks.d.ts +1 -1
  43. package/dist/channels/kit/tasks.js +10 -4
  44. package/dist/channels/kit/turn-runner.d.ts +59 -0
  45. package/dist/channels/kit/turn-runner.js +84 -0
  46. package/dist/channels/kit/turn-store.d.ts +19 -3
  47. package/dist/channels/kit/turn-store.js +29 -2
  48. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  49. package/dist/channels/registration.d.ts +36 -1
  50. package/dist/channels/registration.js +57 -1
  51. package/dist/channels/secret.d.ts +1 -0
  52. package/dist/channels/secret.js +16 -0
  53. package/dist/channels/slack/config-api.d.ts +35 -3
  54. package/dist/channels/slack/config-api.js +51 -9
  55. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  56. package/dist/channels/slack/invoke-turn.js +5 -1
  57. package/dist/channels/slack/manifest.js +5 -1
  58. package/dist/channels/slack/onboard.d.ts +5 -5
  59. package/dist/channels/slack/onboard.js +44 -23
  60. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  61. package/dist/channels/slack/onboarding-state.js +13 -20
  62. package/dist/channels/slack/register-webhook.d.ts +3 -2
  63. package/dist/channels/slack/register-webhook.js +35 -15
  64. package/dist/channels/slack/scaffold/channel.ts +3 -10
  65. package/dist/channels/slack/scaffold/slack-send.ts +18 -141
  66. package/dist/channels/slack/shared-api.d.ts +10 -0
  67. package/dist/channels/slack/shared-api.js +34 -0
  68. package/dist/channels/slack/slack-api.d.ts +20 -2
  69. package/dist/channels/slack/slack-api.js +100 -73
  70. package/dist/channels/slack/slack.d.ts +0 -10
  71. package/dist/channels/slack/slack.js +68 -107
  72. package/dist/channels/sse.d.ts +4 -0
  73. package/dist/channels/sse.js +66 -0
  74. package/dist/channels/telegram/parse.d.ts +21 -1
  75. package/dist/channels/telegram/parse.js +65 -11
  76. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  77. package/dist/channels/telegram/register-webhook.js +44 -42
  78. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  79. package/dist/channels/telegram/telegram-api.js +5 -6
  80. package/dist/channels/telegram/telegram.d.ts +2 -2
  81. package/dist/channels/telegram/telegram.js +97 -204
  82. package/dist/channels/wait-health.js +7 -4
  83. package/dist/cli/add-feishu.js +3 -10
  84. package/dist/cli/add-slack.js +13 -25
  85. package/dist/cli/commands/add.d.ts +0 -1
  86. package/dist/cli/commands/add.js +22 -35
  87. package/dist/cli/commands/attach.d.ts +2 -4
  88. package/dist/cli/commands/attach.js +17 -15
  89. package/dist/cli/commands/chat.js +6 -12
  90. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  91. package/dist/cli/commands/deploy/agentcore.js +178 -0
  92. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  93. package/dist/cli/commands/deploy/docker.js +119 -0
  94. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  95. package/dist/cli/commands/deploy/fly.js +131 -0
  96. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  97. package/dist/cli/commands/deploy/railway.js +71 -0
  98. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  99. package/dist/cli/commands/deploy/shared.js +124 -0
  100. package/dist/cli/commands/deploy.d.ts +38 -25
  101. package/dist/cli/commands/deploy.js +101 -631
  102. package/dist/cli/commands/dev.js +20 -76
  103. package/dist/cli/commands/fire.js +6 -12
  104. package/dist/cli/commands/info.js +1 -1
  105. package/dist/cli/commands/init.js +1 -2
  106. package/dist/cli/commands/invoke.js +4 -11
  107. package/dist/cli/commands/schedule.js +1 -1
  108. package/dist/cli/commands/start.js +41 -80
  109. package/dist/cli/commands/tool.js +3 -8
  110. package/dist/cli/kernel.d.ts +0 -2
  111. package/dist/cli/kernel.js +0 -2
  112. package/dist/cli/program.js +7 -12
  113. package/dist/cli/serve.d.ts +42 -34
  114. package/dist/cli/serve.js +107 -28
  115. package/dist/cli/shared.d.ts +52 -27
  116. package/dist/cli/shared.js +69 -6
  117. package/dist/deploy/agentcore/forwarder.js +250 -0
  118. package/dist/deploy/agentcore/logs.d.ts +2 -2
  119. package/dist/deploy/agentcore/logs.js +2 -2
  120. package/dist/deploy/agentcore/plan.d.ts +34 -11
  121. package/dist/deploy/agentcore/plan.js +52 -259
  122. package/dist/deploy/agentcore/run.d.ts +9 -24
  123. package/dist/deploy/agentcore/run.js +35 -43
  124. package/dist/deploy/channel-ingress.d.ts +73 -0
  125. package/dist/deploy/channel-ingress.js +101 -0
  126. package/dist/deploy/docker/plan.d.ts +3 -7
  127. package/dist/deploy/docker/plan.js +4 -15
  128. package/dist/deploy/docker/run.d.ts +35 -4
  129. package/dist/deploy/docker/run.js +40 -8
  130. package/dist/deploy/fly/plan.d.ts +4 -5
  131. package/dist/deploy/fly/plan.js +14 -22
  132. package/dist/deploy/fly/run.d.ts +36 -23
  133. package/dist/deploy/fly/run.js +129 -83
  134. package/dist/deploy/hosts.d.ts +5 -0
  135. package/dist/deploy/hosts.js +4 -0
  136. package/dist/deploy/preflight.d.ts +6 -7
  137. package/dist/deploy/preflight.js +27 -23
  138. package/dist/deploy/railway/plan.d.ts +9 -5
  139. package/dist/deploy/railway/plan.js +18 -26
  140. package/dist/deploy/railway/run.d.ts +11 -10
  141. package/dist/deploy/railway/run.js +44 -43
  142. package/dist/deploy/runner.js +22 -2
  143. package/dist/deploy/secrets.d.ts +18 -9
  144. package/dist/deploy/secrets.js +63 -28
  145. package/dist/dev-supervisor.js +3 -4
  146. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  147. package/dist/engines/pi/agent-session-factory.js +111 -106
  148. package/dist/engines/pi/auth.js +43 -43
  149. package/dist/engines/pi/config.d.ts +2 -2
  150. package/dist/engines/pi/create.d.ts +22 -27
  151. package/dist/engines/pi/create.js +59 -74
  152. package/dist/engines/pi/definition.d.ts +1 -1
  153. package/dist/engines/pi/definition.js +11 -12
  154. package/dist/engines/pi/invoke-session.js +13 -30
  155. package/dist/engines/pi/login.js +32 -16
  156. package/dist/engines/pi/open.d.ts +3 -4
  157. package/dist/engines/pi/open.js +32 -42
  158. package/dist/engines/pi/retry-event.d.ts +6 -0
  159. package/dist/engines/pi/retry-event.js +15 -0
  160. package/dist/engines/pi/search-tools.js +1 -1
  161. package/dist/engines/pi/service.d.ts +1 -1
  162. package/dist/engines/pi/service.js +8 -0
  163. package/dist/engines/pi/session-builder.js +26 -140
  164. package/dist/engines/pi/session-control.d.ts +27 -22
  165. package/dist/engines/pi/session-control.js +551 -490
  166. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  167. package/dist/engines/pi/session-inheritance.js +95 -76
  168. package/dist/engines/pi/session-markers.d.ts +48 -0
  169. package/dist/engines/pi/session-markers.js +59 -0
  170. package/dist/engines/pi/session-settings.d.ts +5 -5
  171. package/dist/engines/pi/session-settings.js +8 -5
  172. package/dist/engines/pi/session-store.d.ts +91 -26
  173. package/dist/engines/pi/session-store.js +413 -82
  174. package/dist/engines/pi/tool-context.d.ts +24 -11
  175. package/dist/engines/pi/tool-context.js +29 -4
  176. package/dist/engines/pi/tool.d.ts +6 -9
  177. package/dist/engines/pi/tool.js +3 -2
  178. package/dist/env.js +1 -2
  179. package/dist/feishu.d.ts +1 -0
  180. package/dist/feishu.js +1 -0
  181. package/dist/lark.d.ts +1 -0
  182. package/dist/lark.js +1 -0
  183. package/dist/loader.d.ts +51 -7
  184. package/dist/loader.js +84 -18
  185. package/dist/log.d.ts +9 -17
  186. package/dist/log.js +25 -30
  187. package/dist/paths.d.ts +26 -3
  188. package/dist/paths.js +43 -5
  189. package/dist/scaffold/add-channel.d.ts +6 -1
  190. package/dist/scaffold/add-channel.js +50 -67
  191. package/dist/scaffold/init.js +7 -2
  192. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  193. package/dist/schedule/discover.js +3 -15
  194. package/dist/schedule/wake-alarm.d.ts +14 -19
  195. package/dist/schedule/wake-alarm.js +89 -48
  196. package/dist/schedule/wakeups.d.ts +1 -1
  197. package/dist/schedule/wakeups.js +10 -7
  198. package/dist/service.d.ts +24 -24
  199. package/dist/service.js +33 -77
  200. package/dist/session-remote.d.ts +12 -7
  201. package/dist/session-remote.js +185 -118
  202. package/dist/session.d.ts +227 -93
  203. package/dist/session.js +61 -23
  204. package/dist/slack.d.ts +2 -0
  205. package/dist/slack.js +1 -0
  206. package/dist/telegram.d.ts +1 -1
  207. package/dist/telegram.js +1 -1
  208. package/dist/tunnel.d.ts +34 -9
  209. package/dist/tunnel.js +83 -50
  210. package/package.json +5 -4
  211. package/dist/channels/slack/bot-auth.d.ts +0 -15
  212. package/dist/channels/slack/bot-auth.js +0 -135
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  [![node](https://img.shields.io/node/v/@fastagent-sh/fastagent.svg)](https://nodejs.org)
9
9
  [![built with pi](https://img.shields.io/badge/built%20with-pi-0b7285.svg)](https://pi.dev)
10
10
  [![GitHub stars](https://img.shields.io/github/stars/fastagent-sh/fastagent?style=social)](https://github.com/fastagent-sh/fastagent/stargazers)
11
+ [![follow @kid7st](https://img.shields.io/badge/follow-%40kid7st-000?logo=x)](https://x.com/kid7st)
11
12
 
12
13
  <p align="center">
13
14
  <sub>Built on</sub>
@@ -64,7 +65,9 @@ FastAgent stays a small serving layer, so it never dictates your stack. Capabili
64
65
 
65
66
  ## Install
66
67
 
67
- For agents paste this into Claude Code, Codex, Cursor, or any coding agent that reads the web:
68
+ Start with the [agent development guide](https://github.com/fastagent-sh/fastagent/blob/main/docs/ai-start.md), the authoring entry point for both humans and coding agents. It covers responsibilities, TypeScript tools, verification, channels, scheduling, and deployment. FastAgent's repository `AGENTS.md` is for maintainers.
69
+
70
+ To work with Claude Code, Codex, Cursor, or another coding agent, paste:
68
71
 
69
72
  > Read https://fastagent.sh/start.md and build an agent in this project.
70
73
 
@@ -138,6 +141,7 @@ const agent = createPiAgent({
138
141
 
139
142
  | Document | Purpose |
140
143
  |---|---|
144
+ | [Agent development guide](https://github.com/fastagent-sh/fastagent/blob/main/docs/ai-start.md) | Canonical authoring path for humans and coding agents |
141
145
  | [Documentation index](https://fastagent.sh/docs/) | Documentation map |
142
146
  | [Quickstart](https://fastagent.sh/docs/quickstart/) | Scaffold, run, add a tool, and start |
143
147
  | [Configuration](https://fastagent.sh/docs/configuration/) | Configure model, auth, ports, sessions, tools, and channels |
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Write a file so a reader sees the whole thing or nothing: same-directory temp, then rename.
3
3
  *
4
- * The temp name is fixed (`<path>.tmp`). That is safe here because no two processes write one state
5
- * root: a deployment runs one container, and `dev`'s supervisor respawns its worker only after the
6
- * old one has EXITED (dev-supervisor.ts) and these writes are synchronous, so an exited process
7
- * has none in flight. The fixed name is also the seam several channel tests use to inject a write
8
- * failure, by occupying that path with a directory. A unique name would trade that seam for a race
9
- * this codebase does not have; revisit it if a second writer ever becomes real.
4
+ * The temp name is fixed (`<path>.tmp`). It holds wherever one process writes one state root: a
5
+ * deployment runs one container, `dev`'s supervisor respawns its worker only after the old one has
6
+ * EXITED (dev-supervisor.ts), and these writes are synchronous, so an exited process has none in
7
+ * flight. Slack's onboarding state is the one file with two writers `add slack`, and the
8
+ * config-token rotation inside `--tunnel` webhook registration so a second terminal running
9
+ * `add slack --replace-config` can overlap a live `dev --tunnel`. Kept fixed anyway: that window is
10
+ * a single write at tunnel startup, its repair is the `add slack --replace-config` the registration
11
+ * failure already prints, and the fixed name is the seam several channel tests use to inject a write
12
+ * failure by occupying that path with a directory. Revisit for a writer that is neither rare nor
13
+ * self-repairing.
10
14
  *
11
15
  * `mode` is applied to the temp first, so the content is never briefly world-readable.
12
16
  *
@@ -16,4 +20,4 @@
16
20
  * It runs on the temp, before the rename — the final path is then never observable with the wrong
17
21
  * permissions, which a chmod after the rename cannot promise.
18
22
  */
19
- export declare function writeFileAtomic(path: string, data: string, mode?: number): void;
23
+ export declare function writeFileAtomic(path: string, data: string | Buffer, mode?: number): void;
@@ -1,22 +1,27 @@
1
1
  /**
2
- * One spelling of "a reader sees the whole file or none of it" for SYNCHRONOUS writes, after four
3
- * copies of it drifted apart: two identical, two with different temp names and different permission
4
- * handling.
2
+ * One spelling of "a reader sees the whole file or none of it", after five copies of it drifted
3
+ * apart: two identical, three with different temp names and different permission handling.
5
4
  *
6
- * Slack's onboarding state stays on its own async path this is deliberately not an async API, and
7
- * converting that caller is a separate question from de-duplicating these four.
5
+ * Synchronous, and there is no async sibling: every caller either sits on a path where a KB-sized
6
+ * write must complete BEFORE a transport ACK (channel state an ACKed delivery is not redelivered)
7
+ * or on a CLI/startup path where the cost is not observable. An async spelling would buy one of them
8
+ * nothing and cost this module a second set of rules to keep true.
8
9
  */
9
10
  import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
11
  import { dirname } from "node:path";
11
12
  /**
12
13
  * Write a file so a reader sees the whole thing or nothing: same-directory temp, then rename.
13
14
  *
14
- * The temp name is fixed (`<path>.tmp`). That is safe here because no two processes write one state
15
- * root: a deployment runs one container, and `dev`'s supervisor respawns its worker only after the
16
- * old one has EXITED (dev-supervisor.ts) and these writes are synchronous, so an exited process
17
- * has none in flight. The fixed name is also the seam several channel tests use to inject a write
18
- * failure, by occupying that path with a directory. A unique name would trade that seam for a race
19
- * this codebase does not have; revisit it if a second writer ever becomes real.
15
+ * The temp name is fixed (`<path>.tmp`). It holds wherever one process writes one state root: a
16
+ * deployment runs one container, `dev`'s supervisor respawns its worker only after the old one has
17
+ * EXITED (dev-supervisor.ts), and these writes are synchronous, so an exited process has none in
18
+ * flight. Slack's onboarding state is the one file with two writers `add slack`, and the
19
+ * config-token rotation inside `--tunnel` webhook registration so a second terminal running
20
+ * `add slack --replace-config` can overlap a live `dev --tunnel`. Kept fixed anyway: that window is
21
+ * a single write at tunnel startup, its repair is the `add slack --replace-config` the registration
22
+ * failure already prints, and the fixed name is the seam several channel tests use to inject a write
23
+ * failure by occupying that path with a directory. Revisit for a writer that is neither rare nor
24
+ * self-repairing.
20
25
  *
21
26
  * `mode` is applied to the temp first, so the content is never briefly world-readable.
22
27
  *
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The HOST's body ceilings, and the one place they are computed.
3
+ *
4
+ * Lambda Function URLs cap a request at 6 MB, so the forwarder cannot deliver more than that no
5
+ * matter what the adapter accepts; the body arrives base64-encoded (×4/3) inside a JSON envelope, so
6
+ * the ORIGINAL body ceiling is smaller still. This is a real capability difference from a resident
7
+ * host — the GitHub channel's own contract is 25 MiB — which is why `deploy agentcore` states it at
8
+ * plan time rather than letting an oversized payload surface as an opaque 502.
9
+ */
1
10
  /**
2
11
  * Maximum original webhook body after reserving JSON-envelope overhead and base64 expansion.
3
12
  * Enforced by both the public forwarder and the runtime adapter.
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The HOST's body ceilings, and the one place they are computed.
3
+ *
4
+ * Lambda Function URLs cap a request at 6 MB, so the forwarder cannot deliver more than that no
5
+ * matter what the adapter accepts; the body arrives base64-encoded (×4/3) inside a JSON envelope, so
6
+ * the ORIGINAL body ceiling is smaller still. This is a real capability difference from a resident
7
+ * host — the GitHub channel's own contract is 25 MiB — which is why `deploy agentcore` states it at
8
+ * plan time rather than letting an oversized payload surface as an opaque 502.
9
+ */
1
10
  /** AWS Lambda Function URLs accept request payloads up to 6 MB. */
2
11
  const FUNCTION_URL_REQUEST_LIMIT = 6 * 1000 * 1000;
3
12
  /**
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The wire between the forwarder Lambda and the container, in ONE place: the envelope every
3
+ * trigger arrives as, the reply a webhook rides back in, the presigned-URL pair the state snapshot
4
+ * uses, the wake-alarm request, and the forwarder's reserved paths. The forwarder itself is JavaScript
5
+ * (`deploy/agentcore/forwarder.js`) and cannot import this, so `agentcore-forwarder.test.ts` pins its
6
+ * literals to these — a rename here fails there, not on a live box.
7
+ *
8
+ * Pure types and constants: the adapter, the state sync, the wake sink and the deploy driver all
9
+ * read it, and none of them may pull the others in for it.
10
+ */
11
+ /** Paths the forwarder answers ITSELF — never forwarded to a channel route. */
12
+ export declare const RESERVED_PATHS: {
13
+ /** The deploy driver's post-deploy verification (ingress secret). */
14
+ readonly probe: "/__fastagent/probe";
15
+ /** The container's wake-alarm mirror callback (wake secret). */
16
+ readonly wakeAlarm: "/__fastagent/wake-alarm";
17
+ /** Re-mint the state snapshot's presigned URLs with current Lambda credentials (ingress secret). */
18
+ readonly stateUrls: "/__fastagent/state-urls";
19
+ };
20
+ /** Presigned S3 URLs for the one snapshot object, minted per envelope by the forwarder. */
21
+ export interface StateUrls {
22
+ getUrl: string;
23
+ putUrl: string;
24
+ /** Authenticated forwarder callback that re-mints URLs with current Lambda credentials. */
25
+ refresh?: {
26
+ url: string;
27
+ auth: string;
28
+ };
29
+ }
30
+ /** Every kind the container's `POST /invocations` dispatches on. Listed as a value so the forwarder
31
+ * pin test can check each one is spelled the same on the other side. */
32
+ export declare const ENVELOPE_KINDS: readonly ["webhook", "schedule-fire", "invoke", "wake-poke", "checkpoint", "probe"];
33
+ /** What the forwarder Lambda / EventBridge deliver in the `/invocations` payload. Every kind may
34
+ * carry `wake` — the forwarder's self-resolved public URL, which the adapter persists so the wake
35
+ * ALARM sink (schedule/wake-alarm.ts) can call back without the URL being baked anywhere. */
36
+ export type AgentcoreEnvelope = {
37
+ /** Shared secret proving this envelope came from the forwarder (FASTAGENT_INGRESS_SECRET). The
38
+ * public `invoke` data plane neither has nor needs it — and may not carry the fields below. */
39
+ auth?: string;
40
+ wake?: {
41
+ url: string;
42
+ };
43
+ state?: StateUrls;
44
+ } & ({
45
+ kind: "webhook";
46
+ /** Original webhook request line, verbatim. `path` must be absolute ("/telegram"). */
47
+ method: string;
48
+ path: string;
49
+ /** Original raw query string (no leading `?`) — "verbatim" includes it; a channel reading
50
+ * `request.url.searchParams` must see what the webhook sender sent. */
51
+ query?: string;
52
+ /** Original headers — signature material (secret tokens, Feishu signatures) rides here. */
53
+ headers?: Record<string, string>;
54
+ /** Original body, base64 (webhook bodies are JSON but the tunnel must be byte-exact). */
55
+ bodyB64?: string;
56
+ } | {
57
+ kind: "schedule-fire";
58
+ name: string;
59
+ /** The cron instant this fire is FOR (ISO) — the slot-idempotency key. */
60
+ slot: string;
61
+ } | {
62
+ kind: "invoke";
63
+ session: string;
64
+ text: string;
65
+ }
66
+ /** An EventBridge wake-up poke: the invocation ITSELF is the payload — it wakes the container,
67
+ * whose boot drain / 30s wake pump then fires whatever is due. The handler only acks. */
68
+ | {
69
+ kind: "wake-poke";
70
+ }
71
+ /** Pre-stop checkpoint (`--run`, right before stop-runtime-session): push the state snapshot NOW.
72
+ * A stop cuts an in-flight turn, and its durable turn intent — written pre-ACK by every replaying
73
+ * channel — lives on a mount the version update is about to erase. Flushing first is what makes
74
+ * "channels with replay re-run it" true rather than aspirational. */
75
+ | {
76
+ kind: "checkpoint";
77
+ }
78
+ /** The deploy driver's post-deploy verification (relayed by the forwarder's reserved probe path,
79
+ * which answers on EVERY forwarder topology — schedule-only URLs refuse ordinary public traffic).
80
+ * Runs restore + channel construction end to end and answers a TRANSPORT-200 structured verdict
81
+ * `{ ok, error? }`: the ordinary webhook path folds a non-200 transport into an opaque 502 at the
82
+ * forwarder, which would strip exactly the diagnostics this probe exists to carry. */
83
+ | {
84
+ kind: "probe";
85
+ });
86
+ /** The webhook envelope's reply: the channel's real HTTP response, ridden inside a transport-200
87
+ * body so the forwarder can re-emit it verbatim (AgentCore folds a container non-2xx into its own
88
+ * 424 RuntimeClientError). */
89
+ export interface WebhookReply {
90
+ status: number;
91
+ headers: Record<string, string>;
92
+ bodyB64: string;
93
+ }
94
+ /** One desired alarm: mirror of a pending wake-up (id names the EventBridge schedule; at = fireAt). */
95
+ export interface WakeAlarm {
96
+ id: string;
97
+ at: string;
98
+ }
99
+ /** The wire shape the wake sink POSTs to {@link RESERVED_PATHS.wakeAlarm} (the forwarder validates `secret`). */
100
+ export interface WakeAlarmRequest {
101
+ secret: string;
102
+ alarms: WakeAlarm[];
103
+ }
104
+ /** What EventBridge hands the forwarder for a cron slot; `slot` is `<aws.scheduler.scheduled-time>`.
105
+ * Its sibling, the wake-alarm poke `{ wakePoke: true }`, is minted and read inside the forwarder
106
+ * alone (`syncAlarms` writes it, the handler reads it), so it has no type here. */
107
+ export interface ScheduleFireEvent {
108
+ scheduleFire: {
109
+ name: string;
110
+ slot: string;
111
+ };
112
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The wire between the forwarder Lambda and the container, in ONE place: the envelope every
3
+ * trigger arrives as, the reply a webhook rides back in, the presigned-URL pair the state snapshot
4
+ * uses, the wake-alarm request, and the forwarder's reserved paths. The forwarder itself is JavaScript
5
+ * (`deploy/agentcore/forwarder.js`) and cannot import this, so `agentcore-forwarder.test.ts` pins its
6
+ * literals to these — a rename here fails there, not on a live box.
7
+ *
8
+ * Pure types and constants: the adapter, the state sync, the wake sink and the deploy driver all
9
+ * read it, and none of them may pull the others in for it.
10
+ */
11
+ /** Paths the forwarder answers ITSELF — never forwarded to a channel route. */
12
+ export const RESERVED_PATHS = {
13
+ /** The deploy driver's post-deploy verification (ingress secret). */
14
+ probe: "/__fastagent/probe",
15
+ /** The container's wake-alarm mirror callback (wake secret). */
16
+ wakeAlarm: "/__fastagent/wake-alarm",
17
+ /** Re-mint the state snapshot's presigned URLs with current Lambda credentials (ingress secret). */
18
+ stateUrls: "/__fastagent/state-urls",
19
+ };
20
+ /** Every kind the container's `POST /invocations` dispatches on. Listed as a value so the forwarder
21
+ * pin test can check each one is spelled the same on the other side. */
22
+ export const ENVELOPE_KINDS = ["webhook", "schedule-fire", "invoke", "wake-poke", "checkpoint", "probe"];
@@ -18,37 +18,34 @@ import type { Agent } from "../agent.ts";
18
18
  import type { Routes } from "../channel.ts";
19
19
  import type { LoadedSchedule } from "../schedule/schedule.ts";
20
20
  import { type AgentService, type MountableAgent } from "../service.ts";
21
- import { type RouteSurface } from "./agentcore.ts";
21
+ import { type AgentcoreAdapterOptions } from "./agentcore.ts";
22
22
  export interface MountAgentcoreServiceOptions {
23
23
  /** Wrap the opened agent before anything binds to it (the CLI's turn trace). */
24
24
  wrapAgent?: (agent: Agent) => Agent;
25
25
  /** Runs once the state snapshot is restored. The wake-alarm reconcile passes through here because
26
26
  * its sink is a PROCESS-global: the process entry owns that, not a service that can be closed. */
27
27
  onStateReady?: () => void;
28
- control?: {
29
- tunnel?: boolean;
30
- host?: string;
31
- };
32
28
  }
33
29
  /** Is this process running inside the AgentCore Runtime? Set by the generated deploy artifacts. */
34
30
  export declare function isAgentcoreRuntime(): boolean;
35
31
  export declare function mountAgentcoreService(opened: MountableAgent, options?: MountAgentcoreServiceOptions): Promise<AgentService>;
36
32
  /**
37
- * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
38
- * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
39
- * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
40
- * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
41
- * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
33
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) the deployed container's
34
+ * ONLY reachable surface (channels/agentcore.ts). Wired by `start` when `FASTAGENT_AGENTCORE=1` (set
35
+ * by the generated deploy artifacts, never by hand).
36
+ *
37
+ * The adapter IS the surface: the agent's channels live in a table INSIDE the envelope dispatch, a
38
+ * separate namespace from these two paths, so a channel route named `/invocations` is reached
39
+ * through the Function URL as itself and cannot shadow anything.
42
40
  */
43
- export declare function mountAgentcore(routes: Routes, options: {
41
+ export declare function mountAgentcore(options: {
44
42
  agent: Agent;
45
43
  stateRoot: string;
46
44
  schedules: readonly LoadedSchedule[];
47
45
  onStateReady?: () => void;
48
46
  /** Cancels the adapter's process-global registrations on close. */
49
47
  signal?: AbortSignal;
50
- /** The serving path's LAZY channel surface: constructed by the adapter on the first envelope
51
- * AFTER the state-snapshot restore, never at boot (channels/agentcore.ts). When absent,
52
- * `routes` is the dispatch target — for wirings whose state root is already authoritative. */
53
- lazyChannels?: () => Promise<RouteSurface>;
48
+ /** The channel surface, constructed on the first envelope AFTER the state-snapshot restore — never
49
+ * at boot, where the mount is pre-restore (channels/agentcore.ts). */
50
+ channels: AgentcoreAdapterOptions["channels"];
54
51
  }): Routes;
@@ -4,7 +4,7 @@ import { assertNoControlPlaneCollision, mountSessionControl, routesFor, startSch
4
4
  import { UnknownScheduleError, agentcoreRoutes } from "./agentcore.js";
5
5
  import { createStateSync } from "./agentcore-state.js";
6
6
  import { activeWork } from "./busy.js";
7
- import { routeKeysConflict, router } from "./serve.js";
7
+ import { router } from "./serve.js";
8
8
  /** Is this process running inside the AgentCore Runtime? Set by the generated deploy artifacts. */
9
9
  export function isAgentcoreRuntime() {
10
10
  return process.env.FASTAGENT_AGENTCORE === "1";
@@ -14,7 +14,7 @@ export async function mountAgentcoreService(opened, options = {}) {
14
14
  const agent = options.wrapAgent?.(opened.agent) ?? opened.agent;
15
15
  // The control plane mounts over an EMPTY route surface: the lazy channels join it later, and the
16
16
  // collision rule runs again then (below) against what they actually brought.
17
- const withControl = mountSessionControl({}, sessionControl, stateRoot, { ...options.control, agent });
17
+ const withControl = mountSessionControl({}, sessionControl, { agent });
18
18
  const scheduled = await startSchedules(agentDir, agent, stateRoot, opened.selfSchedule, {
19
19
  externalClock: true,
20
20
  });
@@ -32,17 +32,16 @@ export async function mountAgentcoreService(opened, options = {}) {
32
32
  };
33
33
  // The adapter registers process-global listeners; this is what takes them down on close.
34
34
  const closed = new AbortController();
35
- const adapterRoutes = mountAgentcore({}, {
35
+ const adapterRoutes = mountAgentcore({
36
36
  signal: closed.signal,
37
37
  agent,
38
38
  stateRoot,
39
39
  schedules: scheduled.schedules,
40
40
  onStateReady: options.onStateReady,
41
- lazyChannels,
41
+ channels: lazyChannels,
42
42
  });
43
43
  const handler = router(adapterRoutes, withControl.mounts);
44
44
  log.info(`[fastagent] agentcore: serving POST /invocations + GET /ping (FASTAGENT_AGENTCORE=1)`);
45
- let unannounce;
46
45
  return {
47
46
  handler,
48
47
  agent,
@@ -56,30 +55,28 @@ export async function mountAgentcoreService(opened, options = {}) {
56
55
  schedules: scheduled.schedules,
57
56
  ready: Promise.resolve(), // nothing to open: no port of our own, no resident connections
58
57
  ...(withControl.control ? { control: withControl.control } : {}),
59
- announce(boundPort) {
60
- unannounce = withControl.announce(boundPort);
61
- },
62
58
  async close() {
63
59
  // UNTESTED, deliberately noted: no test observes these timers being cleared. Installing fake
64
60
  // timers early enough to count them deadlocks the assembly's own IO. What IS tested is that
65
61
  // close() runs and is idempotent; the stop itself rides on scheduler.stop()'s own tests.
66
62
  scheduled.stop();
67
63
  closed.abort();
68
- unannounce?.(); // a stale discovery file would point `attach` at a stopped service
69
64
  },
70
65
  };
71
66
  }
72
67
  /**
73
- * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
74
- * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
75
- * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
76
- * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
77
- * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
68
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) the deployed container's
69
+ * ONLY reachable surface (channels/agentcore.ts). Wired by `start` when `FASTAGENT_AGENTCORE=1` (set
70
+ * by the generated deploy artifacts, never by hand).
71
+ *
72
+ * The adapter IS the surface: the agent's channels live in a table INSIDE the envelope dispatch, a
73
+ * separate namespace from these two paths, so a channel route named `/invocations` is reached
74
+ * through the Function URL as itself and cannot shadow anything.
78
75
  */
79
- export function mountAgentcore(routes, options) {
80
- const { agent, stateRoot, schedules, onStateReady, lazyChannels, signal } = options;
81
- const mounted = agentcoreRoutes({
82
- routes: lazyChannels ?? { routes },
76
+ export function mountAgentcore(options) {
77
+ const { agent, stateRoot, schedules, onStateReady, channels, signal } = options;
78
+ return agentcoreRoutes({
79
+ channels,
83
80
  agent,
84
81
  stateRoot,
85
82
  isBusy: () => activeWork() > 0,
@@ -102,10 +99,4 @@ export function mountAgentcore(routes, options) {
102
99
  return fireScheduleOnce({ agent, stateRoot, schedule, slot });
103
100
  },
104
101
  });
105
- const collisions = Object.keys(routes).filter((key) => Object.keys(mounted).some((adapterKey) => routeKeysConflict(key, adapterKey)));
106
- if (collisions.length > 0) {
107
- throw new Error(`channel route(s) ${collisions.map((key) => `"${key}"`).join(", ")} collide with the AgentCore adapter ` +
108
- `(/invocations, /ping) — rename the channel route`);
109
- }
110
- return { ...routes, ...mounted };
111
102
  }
@@ -19,21 +19,15 @@
19
19
  * half-applied state root is far worse than a slightly stale one.
20
20
  */
21
21
  import { Buffer } from "node:buffer";
22
+ import type { StateUrls } from "./agentcore-protocol.ts";
22
23
  /** Snapshot envelope version — an unknown version fails the restore loudly (never a silent skip). */
23
24
  export declare const SNAPSHOT_VERSION = 1;
24
25
  /** Refuse to pack beyond this (before gzip): a runaway state root would OOM the microVM silently. */
25
26
  export declare const MAX_SNAPSHOT_BYTES: number;
26
- /** Presigned S3 URLs for the one snapshot object, minted per envelope by the forwarder. */
27
- export interface StateUrls {
28
- getUrl: string;
29
- putUrl: string;
30
- /** Authenticated forwarder callback that re-mints URLs with current Lambda credentials. */
31
- refresh?: {
32
- url: string;
33
- auth: string;
34
- };
35
- }
36
- /** Pack the whole state root into one gzipped snapshot object. */
27
+ /** Pack the whole state root into one gzipped snapshot object.
28
+ * ponytail: the WHOLE root, every idle edge — O(state size) per settled turn, and session jsonl
29
+ * grows without bound. Upgrade path when the 16 MiB warning is real for someone: a per-file
30
+ * manifest with content hashes, uploading only what changed. */
37
31
  export declare function packStateRoot(stateRoot: string, maxBytes?: number): Promise<Buffer>;
38
32
  /** Apply a snapshot over the state root. Returns how many files were written. */
39
33
  export declare function unpackIntoStateRoot(stateRoot: string, packed: Buffer): Promise<number>;
@@ -72,7 +72,10 @@ async function walk(root, dir = root, out = []) {
72
72
  }
73
73
  return out;
74
74
  }
75
- /** Pack the whole state root into one gzipped snapshot object. */
75
+ /** Pack the whole state root into one gzipped snapshot object.
76
+ * ponytail: the WHOLE root, every idle edge — O(state size) per settled turn, and session jsonl
77
+ * grows without bound. Upgrade path when the 16 MiB warning is real for someone: a per-file
78
+ * manifest with content hashes, uploading only what changed. */
76
79
  export async function packStateRoot(stateRoot, maxBytes = MAX_SNAPSHOT_BYTES) {
77
80
  const files = {};
78
81
  let raw = 0;
@@ -1,77 +1,8 @@
1
1
  import type { Agent } from "../agent.ts";
2
- import type { StateSync, StateUrls } from "./agentcore-state.ts";
2
+ import type { StateSync } from "./agentcore-state.ts";
3
3
  import type { Routes } from "../channel.ts";
4
4
  import { type PrefixMount } from "../channels/serve.ts";
5
5
  import type { ScheduleFireOutcome } from "../schedule/scheduler.ts";
6
- /**
7
- * The HOST's webhook body limit, and the one place it is computed. Lambda Function URLs cap a request
8
- * at 6 MB, so the forwarder cannot deliver more than that no matter what the adapter accepts; the
9
- * body arrives base64-encoded (×4/3) inside a JSON envelope, so the ORIGINAL body ceiling is smaller
10
- * still. This is a real capability difference from a resident host — the GitHub channel's own
11
- * contract is 25 MiB — so `deploy agentcore` says so at plan time rather than letting an oversized
12
- * payload surface as an opaque 502.
13
- */
14
- /** What the forwarder Lambda / EventBridge deliver in the `/invocations` payload. Every kind may
15
- * carry `wake` — the forwarder's self-resolved public URL, which the adapter persists so the wake
16
- * ALARM sink (schedule/wake-alarm.ts) can call back without the URL being baked anywhere. */
17
- export type AgentcoreEnvelope = {
18
- /** Shared secret proving this envelope came from the forwarder (FASTAGENT_INGRESS_SECRET). The
19
- * public `invoke` data plane neither has nor needs it — and may not carry the fields below. */
20
- auth?: string;
21
- wake?: {
22
- url: string;
23
- };
24
- state?: StateUrls;
25
- } & ({
26
- kind: "webhook";
27
- /** Original webhook request line, verbatim. `path` must be absolute ("/telegram"). */
28
- method: string;
29
- path: string;
30
- /** Original raw query string (no leading `?`) — "verbatim" includes it; a channel reading
31
- * `request.url.searchParams` must see what the webhook sender sent. */
32
- query?: string;
33
- /** Original headers — signature material (secret tokens, Feishu signatures) rides here. */
34
- headers?: Record<string, string>;
35
- /** Original body, base64 (webhook bodies are JSON but the tunnel must be byte-exact). */
36
- bodyB64?: string;
37
- } | {
38
- kind: "schedule-fire";
39
- name: string;
40
- /** The cron instant this fire is FOR (ISO) — the slot-idempotency key. */
41
- slot: string;
42
- } | {
43
- kind: "invoke";
44
- session: string;
45
- text: string;
46
- }
47
- /** An EventBridge wake-up poke: the invocation ITSELF is the payload — it wakes the container,
48
- * whose boot drain / 30s wake pump then fires whatever is due. The handler only acks. */
49
- | {
50
- kind: "wake-poke";
51
- }
52
- /** Pre-stop checkpoint (`--run`, right before stop-runtime-session): push the state snapshot NOW.
53
- * A stop cuts an in-flight turn, and its durable turn intent — written pre-ACK by every replaying
54
- * channel — lives on a mount the version update is about to erase. Flushing first is what makes
55
- * "channels with replay re-run it" true rather than aspirational. */
56
- | {
57
- kind: "checkpoint";
58
- }
59
- /** The deploy driver's post-deploy verification (relayed by the forwarder's reserved
60
- * `/__fastagent/probe` path, which answers on EVERY forwarder topology — schedule-only URLs
61
- * refuse ordinary public traffic). Runs restore + channel construction end to end and answers a
62
- * TRANSPORT-200 structured verdict `{ ok, error? }`: the ordinary webhook path folds a non-200
63
- * transport into an opaque 502 at the forwarder, which would strip exactly the diagnostics this
64
- * probe exists to carry. */
65
- | {
66
- kind: "probe";
67
- });
68
- /** The webhook envelope's reply: the channel's real HTTP response, ridden inside a transport-200
69
- * body so the forwarder can re-emit it verbatim (see the module header on AgentCore's 424 folding). */
70
- export interface WebhookReply {
71
- status: number;
72
- headers: Record<string, string>;
73
- bodyB64: string;
74
- }
75
6
  /** What the lazy factory hands back: literal routes plus any prefix-owning mounts (the control
76
7
  * plane), so the adapter's INNER dispatch is assembled exactly like a direct host's. */
77
8
  export interface RouteSurface {
@@ -79,13 +10,14 @@ export interface RouteSurface {
79
10
  mounts?: readonly PrefixMount[];
80
11
  }
81
12
  export interface AgentcoreAdapterOptions {
82
- /** The serving routes a direct deployment would mount (channels or the builtin invoke + health).
83
- * The serving path passes a LAZY factory: channel construction loads channel state and replays
84
- * durable turn intent, so on AgentCore it must not run until the state root is authoritative —
85
- * which happens at the first envelope's `stateSync.ready()` (the restore URLs only an envelope
86
- * carries), never at boot, where the mount is pre-restore (empty after every version update).
87
- * An eager `Routes` value remains supported for wirings whose state root is already durable. */
88
- routes: RouteSurface | (() => Promise<RouteSurface> | RouteSurface);
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
+ channels: () => Promise<RouteSurface> | RouteSurface;
89
21
  agent: Agent;
90
22
  /** Where the forwarder URL from envelopes is persisted for the wake-alarm sink (the state root). */
91
23
  stateRoot: string;
@@ -108,12 +40,6 @@ export interface AgentcoreAdapterOptions {
108
40
  * at boot would see the mount the platform just wiped and conclude there is nothing pending. */
109
41
  onStateReady?: () => void;
110
42
  }
111
- /**
112
- * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }`. The caller merges it
113
- * over its routes (collision-checked at the mount site, serve.ts) — the inner routes stay mounted
114
- * too, which is harmless (AgentCore routes only /invocations and /ping into the container) and keeps
115
- * a local `curl` debug surface.
116
- */
117
43
  export declare function agentcoreRoutes(options: AgentcoreAdapterOptions): Routes;
118
44
  /** Thrown by the mount-site `fire` binding when the envelope names a schedule this workspace does
119
45
  * not have — the adapter maps it to 404 (deploy drift stays visible in the external clock's logs). */