@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
@@ -8,13 +8,9 @@ import { join, relative, resolve } from "node:path";
8
8
  import { isCancel, select } from "@clack/prompts";
9
9
  import { onboardFeishuCloudApp } from "../add-feishu.js";
10
10
  import { dotEnvPath, loadDotEnv } from "../../env.js";
11
- import { resolveStateRoot } from "../../paths.js";
12
- import { SECRETS_DIRNAME } from "../../paths.js";
11
+ import { resolveStateRoot, SECRETS_DIRNAME, isUnderDir, displayPath } from "../../paths.js";
13
12
  import { detectRuntime, readPackageJson } from "../../runtime.js";
14
- import { isUnderDir } from "../../paths.js";
15
- import { displayPath } from "../../paths.js";
16
- import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, } from "../../scaffold/add-channel.js";
17
- import { exists } from "../../paths.js";
13
+ import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, scaffoldCompanionTools, } from "../../scaffold/add-channel.js";
18
14
  import { vendorSkill } from "../../scaffold/vendor-skill.js";
19
15
  import { failStartup, failUsage, placementOrExit } from "../fail.js";
20
16
  /** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
@@ -37,25 +33,10 @@ export async function runAddChannel(channelKind, dirArg, opts) {
37
33
  const inAgent = (p) => (agentFromCwd === undefined ? p : join(agentFromCwd, p));
38
34
  const envPath = dotEnvPath(target);
39
35
  const envLabel = isUnderDir(envPath, target) ? inAgent(relative(target, envPath)) : envPath;
40
- // App creation is not a flag — it is what `add feishu` IS (the scan-to-create flow is the default
41
- // and only path there). The retired --create-app spelling gets a pointer, not silence.
42
- if (opts.createApp) {
43
- if (channelKind === "feishu") {
44
- console.error(`[fastagent] note: --create-app is retired — \`add feishu\` creates the app by default`);
45
- }
46
- else if (channelKind === "lark") {
47
- failStartup(new Error("--create-app is retired — `add lark` now opens the developer console and guides credential setup by default"));
48
- }
49
- else {
50
- failStartup(new Error("--create-app is retired — app creation is the default behavior of `add feishu`"));
51
- }
52
- }
53
36
  // Preconditions before the write, so a refusal is side-effect-free. slack/feishu/lark are exceptions:
54
37
  // their add is scaffold + ONBOARD THE APP, so an existing scaffold skips the write and continues (a
55
38
  // failed/cancelled app or OAuth flow must be re-runnable without hand-deleting authored glue).
56
39
  const file = join(target, "channels", `${channelKind}.ts`);
57
- const slackToolFile = join(target, "tools", "slack-send.ts");
58
- const slackToolExisted = channelKind === "slack" ? await exists(slackToolFile) : false;
59
40
  const existsAlready = await channelExists(target, channelKind).catch(failStartup);
60
41
  const ingress = await resolveIngress(channelKind, file, existsAlready, opts.ingress);
61
42
  const groupBehavior = await resolveGroupBehavior(channelKind, opts.groupBehavior);
@@ -67,11 +48,13 @@ export async function runAddChannel(channelKind, dirArg, opts) {
67
48
  }
68
49
  else {
69
50
  await assertChannelReady(target).catch(failStartup);
70
- await scaffoldChannel(target, channelKind, { ingress, groupBehavior: groupBehavior.behavior }).catch(failStartup);
51
+ await scaffoldChannel(target, channelKind, { ingress }).catch(failStartup);
71
52
  console.error(`[fastagent] created ${relative(target, file)}`);
72
- if (channelKind === "slack") {
73
- console.error(`[fastagent] ${slackToolExisted ? "kept existing" : "created"} ${relative(target, slackToolFile)}`);
74
- }
53
+ }
54
+ // Companion tools are the package's, not authored glue: written on every add, so an upgraded
55
+ // package reaches an existing agent by re-running `add <kind>` (`--no-onboard` skips the prompts).
56
+ for (const tool of await scaffoldCompanionTools(target, channelKind).catch(failStartup)) {
57
+ console.error(`[fastagent] wrote ${relative(target, tool)}`);
75
58
  }
76
59
  if (await appendChannelEnv(target, channelKind, ingress).catch(failStartup)) {
77
60
  console.error(`[fastagent] added ${channelKind} env vars to ${inAgent(join(SECRETS_DIRNAME, ".env.example"))}`);
@@ -97,9 +80,9 @@ export async function runAddChannel(channelKind, dirArg, opts) {
97
80
  const env = setup.env;
98
81
  const steps = channelKind === "slack" && opts.onboard !== false
99
82
  ? [
100
- `Slack internal app created/configured/installed through OAuth; runtime credentials are in ${envLabel}`,
101
- "run fastagent dev --tunnel to replace the temporary Events API URL automatically",
102
- "invite the app to every channel it should read",
83
+ // No line about the Request URL: the `dev --tunnel` line below is the whole instruction, and
84
+ // FastAgent sets that URL itself when the agent first runs.
85
+ "invite the app to each channel it should read",
103
86
  "the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
104
87
  ]
105
88
  : setup.steps;
@@ -141,7 +124,7 @@ export async function runAddChannel(channelKind, dirArg, opts) {
141
124
  console.error(` fastagent dev # no public URL or tunnel required`);
142
125
  }
143
126
  else if (channelKind === "slack") {
144
- console.error(` fastagent dev --tunnel # serve locally; ${opts.onboard === false ? "print the Request URL for manual Slack setup" : "auto-update the onboarded Slack Request URL"}`);
127
+ console.error(` fastagent dev --tunnel # ${opts.onboard === false ? "serve locally + print the URL to paste into the Slack app's Event Subscriptions" : "start the agent then message it in Slack"}`);
145
128
  }
146
129
  else if (channelKind === "github") {
147
130
  console.error(` fastagent dev --tunnel # serve locally + print the URL for manual GitHub webhook setup`);
@@ -187,16 +170,20 @@ async function resolveIngress(kind, file, existsAlready, raw) {
187
170
  }
188
171
  const answer = await select({
189
172
  message: `How should ${kind === "feishu" ? "Feishu" : "Lark"} deliver events?`,
173
+ // The default must match the non-interactive branch above. Webhook is the cheaper side to be
174
+ // wrong on: its credentials are a superset of websocket's (App ID/Secret plus the Verification
175
+ // Token), and that token has no read API — a websocket app moving to webhook must re-acquire it.
176
+ initialValue: "webhook",
190
177
  options: [
191
- {
192
- value: "websocket",
193
- label: "WebSocket long connection",
194
- hint: "no public URL; requires an always-on process",
195
- },
196
178
  {
197
179
  value: "webhook",
198
180
  label: "Webhook endpoint",
199
- hint: "supports scale-to-zero; requires a public HTTPS URL",
181
+ hint: "works on every deploy target; supports scale-to-zero; requires a public HTTPS URL",
182
+ },
183
+ {
184
+ value: "websocket",
185
+ label: "WebSocket long connection",
186
+ hint: "no public URL; needs an always-on process (not on AgentCore)",
200
187
  },
201
188
  ],
202
189
  });
@@ -1,4 +1,4 @@
1
- import type { SessionControl, SessionEntry } from "../../session.ts";
1
+ import { type SessionControl, type SessionEntry } from "../../session.ts";
2
2
  export interface AttachOptions {
3
3
  /** Override the control endpoint (skip control.json discovery) — for a remote serve. */
4
4
  url?: string;
@@ -63,7 +63,7 @@ export interface AttachIo {
63
63
  warn: (line: string) => void;
64
64
  }
65
65
  /**
66
- * The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a `navigate`, or a
66
+ * The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a leaf move, or a
67
67
  * compaction, leaves sibling branches behind), and printing the slice raw renders the abandoned
68
68
  * branch interleaved with the live one as if it were one conversation. Reachability is computed
69
69
  * backwards from the leaf; an entry whose parent lies BEFORE the slice is path-connected by
@@ -106,5 +106,3 @@ settleMs?: number): Promise<{
106
106
  cursor: string | undefined;
107
107
  sawProgress: boolean;
108
108
  }>;
109
- /** The cursor a failed round reached, if it recorded one. */
110
- export declare function roundCursor(error: unknown): string | undefined;
@@ -16,7 +16,7 @@ import { loadDotEnv } from "../../env.js";
16
16
  import { resolveStateRoot } from "../../paths.js";
17
17
  import { log, setLogLevel } from "../../log.js";
18
18
  import { ABORTED_CODE, SESSION_BUSY_CODE } from "../../agent.js";
19
- import { NO_ACTIVE_RUN_CODE } from "../../session.js";
19
+ import { NO_ACTIVE_RUN_CODE, } from "../../session.js";
20
20
  import { ControlRequestError, connectAgent, connectSessionControl } from "../../session-remote.js";
21
21
  import { failStartup, placementOrExit } from "../fail.js";
22
22
  /** Read the serving process's local discovery file. */
@@ -157,7 +157,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
157
157
  if (!endpoint)
158
158
  endpoint = discover(dir); // discovered: the first read shares the budget
159
159
  const connected = await connectSessionControl(endpoint);
160
- return { control: connected, state: await connected.state(sessionArg) };
160
+ return { control: connected, state: await connected.sessions.get(sessionArg).state() };
161
161
  }
162
162
  catch (error) {
163
163
  const decision = decideRound(errorFacts(error), {
@@ -208,7 +208,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
208
208
  // ABORTED_CODE: a deliberate stop from ANY client (this attach's /abort, another attach, a
209
209
  // Web panel) — the settled line reports it either way; no source discrimination here.
210
210
  }
211
- // session_busy = the LEASE is held — by another run OR a boundary mutation (compact/set_model
211
+ // session_busy = the LEASE is held — by another run OR a boundary mutation (compact/update
212
212
  // contend on the same lease); "a run started" would be a guess, and "steer it" bad advice
213
213
  // against a compact. State the lease fact, promise nothing.
214
214
  if (sawBusy)
@@ -225,12 +225,14 @@ export async function runAttach(sessionArg, dirArg, opts) {
225
225
  void answerSlashInput(trimmed, control, (l) => console.log(l));
226
226
  return;
227
227
  }
228
- const command = trimmed === "/abort" ? { type: "abort" } : { type: "steer", prompt: { text: trimmed } };
229
- void control.dispatch(sessionArg, command).then((result) => {
228
+ const stopping = trimmed === "/abort";
229
+ const label = stopping ? "abort" : "steer";
230
+ const session = control.sessions.get(sessionArg);
231
+ void (stopping ? session.abort() : session.steer({ text: trimmed })).then((result) => {
230
232
  if (result.ok) {
231
- console.log(`[${command.type} accepted]`);
233
+ console.log(`[${label} accepted]`);
232
234
  }
233
- else if (command.type === "steer" && result.error.code === NO_ACTIVE_RUN_CODE) {
235
+ else if (!stopping && result.error.code === NO_ACTIVE_RUN_CODE) {
234
236
  // The invoke stream attach holds IS this run's driver (design: disconnect = cancel), so
235
237
  // unlike channel-started runs, this one dies with the attach. Say so up front.
236
238
  // Intent, not fact: admission may still fail (session_busy — the lease could be held
@@ -240,9 +242,9 @@ export async function runAttach(sessionArg, dirArg, opts) {
240
242
  void startRun(trimmed).catch((error) => console.log(`[prompt failed: ${String(error)}]`));
241
243
  }
242
244
  else {
243
- console.log(`[${command.type} rejected: ${result.error.code} — ${result.error.message}]`);
245
+ console.log(`[${label} rejected: ${result.error.code} — ${result.error.message}]`);
244
246
  }
245
- }, (error) => console.log(`[${command.type} failed: ${String(error)}]`));
247
+ }, (error) => console.log(`[${label} failed: ${String(error)}]`));
246
248
  });
247
249
  // Every round has ONE shape: subscribe → backfill (render the durable record since the cursor)
248
250
  // → drain live until the stream drops. Subscribing first + the server's eager registration
@@ -424,7 +426,7 @@ const isAuthError = (error) => error instanceof ControlRequestError && error.sta
424
426
  * startup (a serve restart hours in), so the local name must not imply "startup only". */
425
427
  const exitWith = failStartup;
426
428
  /**
427
- * The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a `navigate`, or a
429
+ * The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a leaf move, or a
428
430
  * compaction, leaves sibling branches behind), and printing the slice raw renders the abandoned
429
431
  * branch interleaved with the live one as if it were one conversation. Reachability is computed
430
432
  * backwards from the leaf; an entry whose parent lies BEFORE the slice is path-connected by
@@ -437,7 +439,7 @@ export function activePathSlice(entries, leafEntryId) {
437
439
  return entries;
438
440
  const byId = new Map(entries.map((e) => [e.id, e]));
439
441
  // An append always moves the leaf, so a leaf BEHIND the slice means every entry in it was
440
- // appended and then abandoned — a navigate backwards with no new turn since. Nothing here is on
442
+ // appended and then abandoned — a leaf move backwards with no new turn since. Nothing here is on
441
443
  // the active path.
442
444
  if (!byId.has(leafEntryId))
443
445
  return [];
@@ -509,7 +511,7 @@ settleMs = 300) {
509
511
  // The round HOLDS its subscription's iterator: one round = one stream, on every path — a
510
512
  // backfill failure must close it before propagating, or the caller's retry round would stack a
511
513
  // second concurrent stream interleaving the same session's output.
512
- const iterator = control.events(session)[Symbol.asyncIterator]();
514
+ const iterator = control.sessions.get(session).events()[Symbol.asyncIterator]();
513
515
  // Live output is BUFFERED while the replay block prints, then flushed — the drain runs
514
516
  // concurrently with the backfill, and interleaving raw deltas into the labeled replay would make
515
517
  // both unreadable; the [end of replay] label is only honest if the block is contiguous.
@@ -554,7 +556,7 @@ settleMs = 300) {
554
556
  let next = cursor;
555
557
  let sawBackfill = false;
556
558
  try {
557
- const backfill = await control.entries(session, cursor !== undefined ? { since: cursor } : undefined);
559
+ const backfill = await control.sessions.get(session).entries(cursor !== undefined ? { since: cursor } : undefined);
558
560
  sawBackfill = backfill.entries.length > 0;
559
561
  // Advance by APPEND ORDER (the last returned record), never by leafEntryId: `since` is an
560
562
  // append-position cursor (design §7), and a leaf that sits before later appends (abandoned
@@ -579,7 +581,7 @@ settleMs = 300) {
579
581
  }
580
582
  // The protocol's reconnect step the replay cannot cover: status changes while away are
581
583
  // LIVE-only events (state_changed before a restart is neither replayed nor re-emitted).
582
- const now = await control.state(session);
584
+ const now = await control.sessions.get(session).state();
583
585
  io.println(`[live — ${now.status}${now.activeRunId ? ` (run ${now.activeRunId})` : ""}]`);
584
586
  }
585
587
  catch (error) {
@@ -612,7 +614,7 @@ function withCursor(error, cursor) {
612
614
  return Object.assign(e, { attachCursor: cursor });
613
615
  }
614
616
  /** The cursor a failed round reached, if it recorded one. */
615
- export function roundCursor(error) {
617
+ function roundCursor(error) {
616
618
  return error.attachCursor;
617
619
  }
618
620
  /** Render one durable record on replay — the guaranteed kind vocabulary; other kinds are skipped. */
@@ -1,17 +1,11 @@
1
1
  /** `fastagent chat [dir]`: open the SAME assembled agent in pi's interactive TUI. */
2
- import { resolve } from "node:path";
3
- import { loadDotEnv } from "../../env.js";
4
- import { installProxyFetch } from "../../proxy.js";
5
- import { failStartup, placementOrExit } from "../fail.js";
6
- import { resolveFirstRunModel } from "../shared.js";
2
+ import { failStartup } from "../fail.js";
3
+ import { enterAgentCommand } from "../shared.js";
7
4
  export async function runChat(dirArg, opts) {
8
- const placement = placementOrExit(resolve(dirArg));
9
- loadDotEnv(placement.agentDir);
10
- installProxyFetch(); // model calls (and the login dialog) must go through the proxy too
11
- // First-run funnel, FULL picker: chat authenticates through fastagent's credential store like every
12
- // other command (the shared session builder injects it — see engines/pi/session-builder.ts), so the
13
- // credential-annotated catalog and inline login apply here too.
14
- await resolveFirstRunModel(placement.agentDir, { model: opts.model, authPath: opts.authPath });
5
+ // Chat authenticates through fastagent's credential store like every other command (the shared
6
+ // session builder injects it — see engines/pi/session-builder.ts), so the first-run picker and its
7
+ // inline login apply here too.
8
+ const placement = await enterAgentCommand(dirArg, opts);
15
9
  // Run the chat process AT the workspace: pi resolves a session's cwd as `header.cwd ?? process.cwd()`,
16
10
  // so aligning process.cwd() with the workspace keeps a cwd-less session on it. Paths are absolute.
17
11
  process.chdir(placement.workspace);
@@ -0,0 +1,2 @@
1
+ import { type HostDeploy } from "./shared.ts";
2
+ export declare const agentcoreHost: HostDeploy;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `deploy agentcore`: one CloudFormation stack (runtime + forwarder Lambda + EventBridge schedules);
3
+ * no public URL and no resident process — see deploy/agentcore/plan.ts for the topology decisions.
4
+ */
5
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { basename, join } from "node:path";
8
+ import { MAX_WEBHOOK_BODY_BYTES } from "../../../channels/agentcore-limits.js";
9
+ import { FORWARDER_FILE, TEMPLATE_FILE, agentcoreName, isGeneratedAgentcoreTemplate, planAgentcoreDeploy, } from "../../../deploy/agentcore/plan.js";
10
+ import { deployAgentcoreRun } from "../../../deploy/agentcore/run.js";
11
+ import { spawnRunner } from "../../../deploy/runner.js";
12
+ import { exists } from "../../../paths.js";
13
+ import { loadSchedules } from "../../../schedule/discover.js";
14
+ import { failStartup } from "../../fail.js";
15
+ import { carryCredentials, gateOnModelCredential, registrarsFor } from "./shared.js";
16
+ /** A copy/paste-safe POSIX shell argument for the command hints deploy prints. */
17
+ function shellArg(value) {
18
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
19
+ }
20
+ export const agentcoreHost = {
21
+ isOurs: (path, content) => path.endsWith(TEMPLATE_FILE) && isGeneratedAgentcoreTemplate(content),
22
+ async deploy(ctx) {
23
+ const { opts, agentDir, workspace, config, channels, longConnectionChannels, pre, write } = ctx;
24
+ const { modelAuth, modelKeyInDefinition, authPath, container, extraSecrets } = pre;
25
+ // Long-connection channels are STRUCTURALLY unsupported: the connection is the ingress, and a
26
+ // reclaimed session has nothing to wake it — events in the gap are silently lost. `--run` gates
27
+ // (deploying a channel that can't connect); generate-only warns and prints the runbook.
28
+ if (longConnectionChannels.length > 0) {
29
+ const msg = `long-connection channel (${longConnectionChannels.map((c) => c.name).join(", ")}) cannot run on AgentCore — there is no ` +
30
+ `resident process to hold the connection, and nothing wakes a reclaimed session. Switch the channel ` +
31
+ `to webhook mode (its events then ride the forwarder like every other channel).`;
32
+ if (opts.run)
33
+ failStartup(new Error(`deploy stopped: ${msg}`));
34
+ console.error(`[fastagent] warn: ${msg}`);
35
+ }
36
+ // selfSchedule is fully supported: pending wake-ups are mirrored into one-shot EventBridge
37
+ // schedules via the forwarder (the wake-alarm mechanism — see deploy/agentcore/plan.ts).
38
+ // Schedules feed EventBridge rules — parsed facts (cron/tz), not just file names, so a bad file
39
+ // must surface here (a schedule silently missing its rule would never fire).
40
+ const loaded = await loadSchedules(agentDir).catch(failStartup);
41
+ if (loaded.failures.length > 0) {
42
+ failStartup(new Error(`deploy stopped: cannot load schedules: ${loaded.failures.map((x) => `${x.label}: ${x.message}`).join("; ")}`));
43
+ }
44
+ const acName = agentcoreName(basename(workspace));
45
+ // Every derived AWS name embeds acName; the tightest ceiling is the Lambda function name
46
+ // (`fastagent-<name>-forwarder` ≤ 64 chars). Gate the base instead of silently truncating —
47
+ // truncation would break the redeploy identity (a renamed stack starts blank state).
48
+ if (acName.length > 40) {
49
+ failStartup(new Error(`deploy stopped: the directory name maps to "${acName}" (${acName.length} chars) — AWS resource ` +
50
+ `names derived from it exceed their limits past 40 chars. Deploy from a shorter directory name.`));
51
+ }
52
+ const plan = planAgentcoreDeploy({
53
+ name: acName,
54
+ modelAuth,
55
+ channels,
56
+ extraSecrets,
57
+ schedules: loaded.schedules.map((s) => ({ name: s.name, cron: s.cron, tz: s.tz })),
58
+ selfSchedule: !!config.selfSchedule,
59
+ ...container,
60
+ });
61
+ for (const u of plan.untranslatableSchedules) {
62
+ // Same discipline as Fly's kept-toml time-trigger gate: a deploy whose schedule silently never
63
+ // fires is worse than a stopped deploy — nothing fails visibly when the instant passes.
64
+ const msg = `schedule "${u.name}" cannot be expressed as an EventBridge rule — ${u.reason}`;
65
+ if (opts.run)
66
+ failStartup(new Error(`deploy stopped: ${msg}`));
67
+ console.error(`[fastagent] warn: ${msg} — it will NOT fire on this deployment`);
68
+ }
69
+ // Host capability limit, stated at plan time. GitHub's own webhook contract is 25 MiB, but a
70
+ // Lambda Function URL request caps at 6 MB — so on this host a large payload cannot arrive at
71
+ // all. Better a sentence here than an opaque 502 the first time someone pushes a big diff.
72
+ if (channels.some((channel) => channel.name === "github")) {
73
+ console.error(`[fastagent] note: on AgentCore a webhook body is capped at ~${Math.round(MAX_WEBHOOK_BODY_BYTES / (1 << 20))} MiB ` +
74
+ `(the forwarder's Function URL limit); the GitHub channel accepts 25 MiB on a resident host, so the largest ` +
75
+ `payloads are rejected here rather than delivered`);
76
+ }
77
+ // The template IS the topology (EventBridge rules, wake wiring, secrets) — a kept generated
78
+ // template that no longer matches the definition would deploy a stack silently missing the
79
+ // difference (a new schedule with no rule never fires: the exact miss the gate above stops).
80
+ // A hand-written template (marker removed) is the operator's own — kept, never gated.
81
+ const templateArtifact = plan.artifacts.find((a) => a.path.endsWith(TEMPLATE_FILE));
82
+ const templateHome = join(workspace, templateArtifact?.path ?? TEMPLATE_FILE);
83
+ if (!opts.force && templateArtifact && (await exists(templateHome))) {
84
+ const existing = await readFile(templateHome, "utf8");
85
+ if (isGeneratedAgentcoreTemplate(existing) && existing !== templateArtifact.content) {
86
+ const msg = `${templateArtifact.path} no longer matches this definition (channels/schedules/selfSchedule changed) — ` +
87
+ `the kept template would silently drop the difference. Pass --force to regenerate (hand edits are lost), ` +
88
+ `or delete the file.`;
89
+ if (opts.run)
90
+ failStartup(new Error(`deploy stopped: ${msg}`));
91
+ console.error(`[fastagent] warn: ${msg}`);
92
+ }
93
+ }
94
+ await write(plan.artifacts, {
95
+ force: !!opts.force,
96
+ alwaysWrite: [`${container.agentPrefix}${FORWARDER_FILE}`],
97
+ });
98
+ if (opts.run) {
99
+ return runDeployAgentcore({
100
+ agentDir,
101
+ workspace,
102
+ agentPrefix: container.agentPrefix,
103
+ name: acName,
104
+ modelAuth,
105
+ modelKeyInDefinition,
106
+ authPath,
107
+ channels,
108
+ extraSecrets,
109
+ topology: plan.topology,
110
+ });
111
+ }
112
+ console.log(plan.runbook.join("\n"));
113
+ return;
114
+ },
115
+ };
116
+ /**
117
+ * `deploy agentcore --run`: drive aws + docker to completion. Mirrors the fly driver — same
118
+ * credential carry via {@link carryCredentials}, same runner seam (spawned `aws` + `docker`, cwd = the
119
+ * workspace so the build context is the agent). The AgentCore-specific sequence (identity → buildx →
120
+ * ECR → CloudFormation → outputs → webhooks) lives in {@link deployAgentcoreRun}; the params temp
121
+ * file (secret values off argv) is created here — 0600, removed after the run either way.
122
+ */
123
+ async function runDeployAgentcore(params) {
124
+ const { agentDir, workspace, agentPrefix, name, channels, topology } = params;
125
+ const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
126
+ // The wake-alarm shared secret (container ↔ forwarder). Minted fresh each run — both sides receive
127
+ // the SAME parameter, so rotation is atomic; it never needs to be remembered locally.
128
+ if (topology.wakeAlarms)
129
+ secrets.FASTAGENT_WAKE_SECRET = crypto.randomUUID();
130
+ // The forwarder→runtime ingress secret: what makes an envelope the forwarder's rather than any IAM
131
+ // principal's. Minted fresh each run — both sides receive the SAME parameter, so rotation is atomic.
132
+ if (topology.forwarder)
133
+ secrets.FASTAGENT_INGRESS_SECRET = crypto.randomUUID();
134
+ gateOnModelCredential(needsModelCredential);
135
+ // The params temp dir holds the ONE file carrying secret values (file:// parameter-overrides —
136
+ // never argv); 0700/0600 and removed after the run, success or gate.
137
+ const paramsDir = await mkdtemp(join(tmpdir(), "fastagent-agentcore-"));
138
+ try {
139
+ const outcome = await deployAgentcoreRun({
140
+ name,
141
+ templatePath: `${agentPrefix}${TEMPLATE_FILE}`,
142
+ dockerfilePath: agentPrefix ? `${agentPrefix}Dockerfile` : undefined,
143
+ tag: new Date()
144
+ .toISOString()
145
+ .replace(/[-:.TZ]/g, "")
146
+ .slice(0, 14),
147
+ region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION,
148
+ secrets,
149
+ missingSecrets,
150
+ channels,
151
+ topology,
152
+ }, spawnRunner("aws", workspace), spawnRunner("docker", workspace), (m) => console.error(`[fastagent] ${m}`), async (content) => {
153
+ const path = join(paramsDir, "params.json");
154
+ await writeFile(path, content, { mode: 0o600 });
155
+ return path;
156
+ }, async (bytes) => {
157
+ const path = join(paramsDir, "forwarder.zip");
158
+ await writeFile(path, bytes);
159
+ return path;
160
+ }, registrarsFor(agentDir));
161
+ if (!outcome.ok)
162
+ failStartup(new Error(`deploy stopped: ${outcome.gate}`));
163
+ console.error(`[fastagent] deployed → ${outcome.runtimeArn}`);
164
+ if (outcome.url)
165
+ console.error(`[fastagent] webhook ingress → ${outcome.url}`);
166
+ const logsDir = shellArg(workspace);
167
+ console.error(`[fastagent] runtime logs → fastagent logs agentcore ${logsDir} --follow`);
168
+ if (topology.forwarder) {
169
+ console.error(`[fastagent] forwarder logs → fastagent logs agentcore ${logsDir} --source forwarder --follow`);
170
+ }
171
+ console.error(`[fastagent] invoke: aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn ${outcome.runtimeArn} \\\n` +
172
+ ` --runtime-session-id "my-conversation-000000000000000000" \\\n` +
173
+ ` --payload '{"kind":"invoke","session":"cli","text":"hello"}' --cli-binary-format raw-in-base64-out /dev/stdout`);
174
+ }
175
+ finally {
176
+ await rm(paramsDir, { recursive: true, force: true });
177
+ }
178
+ }
@@ -0,0 +1,2 @@
1
+ import { type HostDeploy } from "./shared.ts";
2
+ export declare const dockerHost: HostDeploy;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `deploy docker`: one app service + loopback port + state volume, as a user-owned Compose file.
3
+ * `--tunnel` shapes the generated topology with an optional Quick Tunnel service; `--run` alone
4
+ * decides whether Docker receives side effects.
5
+ */
6
+ import { basename, join } from "node:path";
7
+ import { webhookPaths } from "../../../deploy/channel-ingress.js";
8
+ import { composeHasTunnelService, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../../deploy/docker/plan.js";
9
+ import { deployDockerRun } from "../../../deploy/docker/run.js";
10
+ import { spawnRunner } from "../../../deploy/runner.js";
11
+ import { openExternalUrl } from "../../../open-url.js";
12
+ import { readTextIfExists, resolveStateRoot } from "../../../paths.js";
13
+ import { announceWebhooks } from "../../../tunnel.js";
14
+ import { failStartup } from "../../fail.js";
15
+ import { carryCredentials } from "./shared.js";
16
+ export const dockerHost = {
17
+ isOurs: (path, content) => path.endsWith("fastagent.compose.yml") && isGeneratedCompose(content),
18
+ async deploy(ctx) {
19
+ const { opts, agentDir, workspace, channels, webhookChannels, pre, write } = ctx;
20
+ const { modelAuth, modelKeyInDefinition, authPath, container, port, extraSecrets } = pre;
21
+ const hasDeclaredChannels = channels.length > 0;
22
+ const projectName = toDockerProjectName(basename(workspace));
23
+ const dockerPlan = (tunnel) => planDockerDeploy({
24
+ projectName,
25
+ port,
26
+ modelAuth,
27
+ channels,
28
+ tunnel,
29
+ extraSecrets,
30
+ ...container,
31
+ });
32
+ const requestedTunnel = !!opts.tunnel && (!hasDeclaredChannels || webhookChannels.length > 0);
33
+ if (opts.tunnel && hasDeclaredChannels && webhookChannels.length === 0) {
34
+ console.error(`[fastagent] note: --tunnel skipped — every channel uses a long connection`);
35
+ }
36
+ let plan = dockerPlan(requestedTunnel);
37
+ // An existing Compose file is authoritative: shape its comparison/runbook from the topology on disk,
38
+ // regardless of the current flag. `--force` is the explicit reset to the requested generated shape.
39
+ const composeFile = join(workspace, plan.composePath);
40
+ let keptWithoutRequestedTunnel = false;
41
+ // Same ownership question as fly.toml: a hand-owned compose file survives --force, so the plan must
42
+ // describe the topology that will actually be there.
43
+ const composeText = await readTextIfExists(composeFile).catch(failStartup);
44
+ if (composeText !== undefined && (!opts.force || !isGeneratedCompose(composeText))) {
45
+ const existingHasTunnel = composeHasTunnelService(composeText);
46
+ plan = dockerPlan(existingHasTunnel);
47
+ keptWithoutRequestedTunnel = requestedTunnel && !existingHasTunnel;
48
+ }
49
+ await write(plan.artifacts, { force: !!opts.force });
50
+ if (opts.run) {
51
+ return runDeployDocker({
52
+ agentDir,
53
+ workspace,
54
+ composeFile: plan.composePath,
55
+ port,
56
+ requireTunnel: requestedTunnel,
57
+ modelAuth,
58
+ modelKeyInDefinition,
59
+ authPath,
60
+ channels,
61
+ extraSecrets,
62
+ });
63
+ }
64
+ if (keptWithoutRequestedTunnel) {
65
+ console.error(`[fastagent] warn: --tunnel was requested but kept ${plan.composePath} has no "tunnel" service — ` +
66
+ `edit it, delete it and regenerate, or pass --force`);
67
+ }
68
+ console.log(plan.runbook.join("\n"));
69
+ return;
70
+ },
71
+ };
72
+ /**
73
+ * `deploy docker --run`: carry local credentials into Compose's child environment, then reconcile the
74
+ * user-owned local topology. Docker owns container/network/volume lifecycle. A Compose tunnel service,
75
+ * when present, yields an ephemeral URL that reuses the same webhook announcer as `dev --tunnel`.
76
+ */
77
+ async function runDeployDocker(params) {
78
+ const { agentDir, workspace, composeFile, port, requireTunnel, channels } = params;
79
+ const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
80
+ const outcome = await deployDockerRun({
81
+ composeFile,
82
+ port,
83
+ secrets,
84
+ missingSecrets,
85
+ needsModelCredential,
86
+ requireTunnel,
87
+ announce: (tunnelUrl) => announceWebhooks(agentDir, tunnelUrl, channels, {
88
+ openUrl: openExternalUrl,
89
+ stateRoot: resolveStateRoot(agentDir),
90
+ }),
91
+ }, spawnRunner("docker", workspace), (message) => console.error(`[fastagent] ${message}`));
92
+ const compose = `docker compose -f ${composeFile}`;
93
+ // Compose reached "up" iff the driver could report where it is. The gates before that point
94
+ // (no Docker CLI, no daemon, missing secrets) must not be preceded by `docker compose logs`
95
+ // instructions for containers that do not exist; the health-check gate names its own logs command.
96
+ const isUp = outcome.ok || outcome.url !== undefined || outcome.tunnelUrl !== undefined;
97
+ if (outcome.url)
98
+ console.error(`[fastagent] running → ${outcome.url}`);
99
+ if (isUp) {
100
+ console.error(`[fastagent] logs: ${compose} logs -f agent`);
101
+ console.error(`[fastagent] stop: ${compose} down (state volume is kept)`);
102
+ }
103
+ // BEFORE failStartup: a registration gate says "re-run this deploy", and a re-run rebuilds the
104
+ // tunnel service — the operator needs to know the URL will be a different one, or they will read
105
+ // the retry as re-registering the same address.
106
+ if (outcome.tunnelUrl) {
107
+ console.error(`[fastagent] note: Quick Tunnel URLs are ephemeral — after the tunnel container/Docker daemon ` +
108
+ `restarts, re-run this deploy so webhooks receive the new URL`);
109
+ }
110
+ if (!outcome.ok)
111
+ failStartup(new Error(`deploy stopped: ${outcome.gate}`));
112
+ if (outcome.tunnelUrl)
113
+ return;
114
+ const paths = webhookPaths(channels);
115
+ if (paths.length > 0) {
116
+ console.error(`[fastagent] note: public ingress is operator-owned — configure your tunnel/proxy, then wire the ` +
117
+ `default webhook path(s): ${paths.join(", ")} (or your remapped channel routes)`);
118
+ }
119
+ }
@@ -0,0 +1,2 @@
1
+ import { type HostDeploy } from "./shared.ts";
2
+ export declare const flyHost: HostDeploy;