@fastagent-sh/fastagent 0.15.0 → 0.16.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 (168) hide show
  1. package/README.md +13 -3
  2. package/dist/channels/agentcore-limits.d.ts +7 -0
  3. package/dist/channels/agentcore-limits.js +9 -0
  4. package/dist/channels/agentcore-state.d.ts +83 -0
  5. package/dist/channels/agentcore-state.js +258 -0
  6. package/dist/channels/agentcore.d.ts +98 -0
  7. package/dist/channels/agentcore.js +260 -0
  8. package/dist/channels/busy.d.ts +23 -0
  9. package/dist/channels/busy.js +53 -0
  10. package/dist/channels/context-buffer.d.ts +5 -0
  11. package/dist/channels/context-buffer.js +5 -0
  12. package/dist/channels/feishu/context-buffer.d.ts +13 -8
  13. package/dist/channels/feishu/context-buffer.js +60 -6
  14. package/dist/channels/feishu/feishu-api.js +4 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -12
  16. package/dist/channels/feishu/feishu.js +143 -54
  17. package/dist/channels/feishu/invoke-turn.js +35 -18
  18. package/dist/channels/feishu/model.d.ts +0 -1
  19. package/dist/channels/feishu/normalize.js +0 -1
  20. package/dist/channels/feishu/parse.d.ts +21 -7
  21. package/dist/channels/feishu/parse.js +24 -7
  22. package/dist/channels/feishu/preview.js +3 -2
  23. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  24. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  25. package/dist/channels/feishu/setup-mode.d.ts +30 -1
  26. package/dist/channels/feishu/setup-mode.js +27 -1
  27. package/dist/channels/github/github.js +8 -1
  28. package/dist/channels/http.js +1 -1
  29. package/dist/channels/lark/scaffold/channel.ts +9 -8
  30. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  31. package/dist/channels/preview-kit.d.ts +7 -1
  32. package/dist/channels/preview-kit.js +3 -2
  33. package/dist/channels/slack/parse.d.ts +16 -1
  34. package/dist/channels/slack/parse.js +46 -3
  35. package/dist/channels/slack/preview.d.ts +1 -2
  36. package/dist/channels/slack/preview.js +68 -24
  37. package/dist/channels/slack/scaffold/channel.ts +5 -5
  38. package/dist/channels/slack/slack-api.d.ts +3 -23
  39. package/dist/channels/slack/slack-api.js +6 -22
  40. package/dist/channels/slack/slack.d.ts +13 -20
  41. package/dist/channels/slack/slack.js +95 -50
  42. package/dist/channels/state.d.ts +11 -4
  43. package/dist/channels/state.js +19 -12
  44. package/dist/channels/tasks.d.ts +0 -6
  45. package/dist/channels/tasks.js +16 -1
  46. package/dist/channels/telegram/parse.d.ts +0 -7
  47. package/dist/channels/telegram/parse.js +4 -2
  48. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  49. package/dist/channels/telegram/telegram.js +1 -1
  50. package/dist/channels/text.d.ts +14 -0
  51. package/dist/channels/text.js +14 -0
  52. package/dist/channels/thread-participants.d.ts +21 -0
  53. package/dist/channels/thread-participants.js +132 -0
  54. package/dist/channels/turn-queue.js +7 -0
  55. package/dist/cli/add-feishu.d.ts +7 -4
  56. package/dist/cli/add-feishu.js +57 -37
  57. package/dist/cli/add-slack.d.ts +2 -1
  58. package/dist/cli/add-slack.js +6 -11
  59. package/dist/cli/commands/add.js +50 -51
  60. package/dist/cli/commands/attach.js +8 -4
  61. package/dist/cli/commands/chat.js +8 -8
  62. package/dist/cli/commands/deploy.d.ts +1 -1
  63. package/dist/cli/commands/deploy.js +323 -77
  64. package/dist/cli/commands/dev.js +24 -22
  65. package/dist/cli/commands/fire.js +15 -16
  66. package/dist/cli/commands/info.js +36 -29
  67. package/dist/cli/commands/init.d.ts +1 -1
  68. package/dist/cli/commands/init.js +65 -53
  69. package/dist/cli/commands/invoke.js +9 -6
  70. package/dist/cli/commands/login.js +35 -21
  71. package/dist/cli/commands/schedule.js +6 -8
  72. package/dist/cli/commands/start.js +93 -37
  73. package/dist/cli/commands/tool.js +22 -18
  74. package/dist/cli/fail.d.ts +17 -0
  75. package/dist/cli/fail.js +24 -0
  76. package/dist/cli/program.js +57 -36
  77. package/dist/cli/serve.d.ts +26 -6
  78. package/dist/cli/serve.js +62 -15
  79. package/dist/cli/shared.d.ts +15 -2
  80. package/dist/cli/shared.js +30 -18
  81. package/dist/deploy/agentcore/plan.d.ts +117 -0
  82. package/dist/deploy/agentcore/plan.js +721 -0
  83. package/dist/deploy/agentcore/run.d.ts +73 -0
  84. package/dist/deploy/agentcore/run.js +412 -0
  85. package/dist/deploy/agentcore/zip.d.ts +17 -0
  86. package/dist/deploy/agentcore/zip.js +68 -0
  87. package/dist/deploy/container.d.ts +26 -25
  88. package/dist/deploy/container.js +93 -89
  89. package/dist/deploy/docker/plan.d.ts +1 -1
  90. package/dist/deploy/docker/plan.js +12 -17
  91. package/dist/deploy/fly/plan.d.ts +2 -0
  92. package/dist/deploy/fly/plan.js +27 -19
  93. package/dist/deploy/fly/run.d.ts +12 -1
  94. package/dist/deploy/fly/run.js +36 -2
  95. package/dist/deploy/preflight.d.ts +11 -5
  96. package/dist/deploy/preflight.js +217 -65
  97. package/dist/deploy/railway/plan.d.ts +7 -0
  98. package/dist/deploy/railway/plan.js +41 -16
  99. package/dist/deploy/railway/run.d.ts +8 -1
  100. package/dist/deploy/railway/run.js +7 -2
  101. package/dist/deploy/runner.d.ts +5 -2
  102. package/dist/deploy/runner.js +9 -3
  103. package/dist/dev-supervisor.d.ts +11 -8
  104. package/dist/dev-supervisor.js +53 -51
  105. package/dist/engines/pi/auth.d.ts +8 -7
  106. package/dist/engines/pi/auth.js +12 -10
  107. package/dist/engines/pi/channel.d.ts +1 -1
  108. package/dist/engines/pi/channel.js +5 -5
  109. package/dist/engines/pi/chat.js +2 -2
  110. package/dist/engines/pi/config.d.ts +6 -46
  111. package/dist/engines/pi/config.js +21 -108
  112. package/dist/engines/pi/create.d.ts +8 -8
  113. package/dist/engines/pi/create.js +13 -12
  114. package/dist/engines/pi/definition.d.ts +7 -26
  115. package/dist/engines/pi/definition.js +8 -54
  116. package/dist/engines/pi/login.d.ts +1 -1
  117. package/dist/engines/pi/models.d.ts +3 -3
  118. package/dist/engines/pi/models.js +1 -1
  119. package/dist/engines/pi/{workspace.d.ts → open.d.ts} +29 -23
  120. package/dist/engines/pi/{workspace.js → open.js} +27 -29
  121. package/dist/engines/pi/session-builder.d.ts +2 -2
  122. package/dist/engines/pi/session-builder.js +11 -11
  123. package/dist/engines/pi/tool.js +4 -0
  124. package/dist/env.d.ts +16 -4
  125. package/dist/env.js +43 -5
  126. package/dist/host/node.d.ts +2 -2
  127. package/dist/loader.d.ts +2 -2
  128. package/dist/loader.js +3 -3
  129. package/dist/log.d.ts +1 -1
  130. package/dist/log.js +1 -1
  131. package/dist/paths.d.ts +138 -0
  132. package/dist/paths.js +326 -0
  133. package/dist/pi.d.ts +1 -1
  134. package/dist/pi.js +2 -2
  135. package/dist/runtime.d.ts +7 -5
  136. package/dist/runtime.js +2 -2
  137. package/dist/scaffold/add-channel.d.ts +7 -3
  138. package/dist/scaffold/add-channel.js +55 -29
  139. package/dist/scaffold/init.d.ts +32 -41
  140. package/dist/scaffold/init.js +161 -185
  141. package/dist/scaffold/templates/env.example +15 -6
  142. package/dist/scaffold/templates/fastagent.config.mjs +1 -1
  143. package/dist/scaffold/templates/gitignore +14 -6
  144. package/dist/scaffold/templates/persona.md +4 -2
  145. package/dist/scaffold/templates/secrets.gitignore +5 -0
  146. package/dist/scaffold/templates.d.ts +1 -7
  147. package/dist/scaffold/templates.js +3 -25
  148. package/dist/scaffold/vendor-skill.d.ts +2 -2
  149. package/dist/scaffold/vendor-skill.js +13 -13
  150. package/dist/schedule/discover.js +4 -4
  151. package/dist/schedule/scheduler.d.ts +40 -1
  152. package/dist/schedule/scheduler.js +89 -56
  153. package/dist/schedule/state.js +1 -1
  154. package/dist/schedule/wake-alarm.d.ts +47 -0
  155. package/dist/schedule/wake-alarm.js +136 -0
  156. package/dist/schedule/wakeups.d.ts +1 -0
  157. package/dist/schedule/wakeups.js +18 -0
  158. package/dist/tunnel.d.ts +3 -3
  159. package/dist/tunnel.js +7 -7
  160. package/package.json +4 -1
  161. package/dist/channels/feishu/owned-threads.d.ts +0 -7
  162. package/dist/channels/feishu/owned-threads.js +0 -47
  163. package/dist/channels/slack/owned-threads.d.ts +0 -6
  164. package/dist/channels/slack/owned-threads.js +0 -43
  165. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  166. package/dist/scaffold/templates/gitignore.kit +0 -2
  167. package/dist/workspace.d.ts +0 -9
  168. package/dist/workspace.js +0 -45
package/dist/cli/serve.js CHANGED
@@ -4,6 +4,9 @@
4
4
  */
5
5
  import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
+ import { createStateSync } from "../channels/agentcore-state.js";
8
+ import { agentcoreRoutes, UnknownScheduleError } from "../channels/agentcore.js";
9
+ import { activeWork } from "../channels/busy.js";
7
10
  import { controlRoutes } from "../channels/control.js";
8
11
  import { INVOKE_EXAMPLE_BODY, createInvokeHandler } from "../channels/http.js";
9
12
  import { text } from "../channels/respond.js";
@@ -13,15 +16,15 @@ import { parseRouteKey, router, serveNode } from "../host/node.js";
13
16
  import { log } from "../log.js";
14
17
  import { openExternalUrl } from "../open-url.js";
15
18
  import { loadSchedules } from "../schedule/discover.js";
16
- import { createScheduler } from "../schedule/scheduler.js";
19
+ import { createScheduler, fireScheduleOnce } from "../schedule/scheduler.js";
17
20
  import { announceWebhooks, startCloudflareTunnel } from "../tunnel.js";
18
21
  import { failStartup } from "./fail.js";
19
22
  /**
20
23
  * The surface this deployment serves: default `GET /health` plus discovered channels, or the default
21
24
  * POST `/invoke` only when neither a route nor a long-connection channel was declared.
22
25
  */
23
- export async function routesFor(workspaceDir, agent, stateRoot, control) {
24
- const { routes, longConnections, routeChannels, collisions, failures } = await loadChannels(workspaceDir, {
26
+ export async function routesFor(agentDir, agent, stateRoot, control, options = {}) {
27
+ const { routes, longConnections, routeChannels, collisions, failures } = await loadChannels(agentDir, {
25
28
  agent,
26
29
  stateRoot,
27
30
  control,
@@ -34,7 +37,7 @@ export async function routesFor(workspaceDir, agent, stateRoot, control) {
34
37
  throw new Error(`channel setup is invalid (${failures.length} load failure(s), ${collisions.length} route collision(s)) — ` +
35
38
  `fix it, or rename an intentionally disabled file to *.disabled`);
36
39
  }
37
- const builtinInvoke = Object.keys(routes).length === 0 && longConnections.length === 0;
40
+ const builtinInvoke = options.builtinInvoke !== false && Object.keys(routes).length === 0 && longConnections.length === 0;
38
41
  const channels = builtinInvoke ? { "POST /invoke": createInvokeHandler(agent) } : routes;
39
42
  const healthCovered = Object.keys(channels).some((key) => {
40
43
  const entry = parseRouteKey(key);
@@ -53,7 +56,7 @@ export async function routesFor(workspaceDir, agent, stateRoot, control) {
53
56
  };
54
57
  }
55
58
  /**
56
- * Mount the session control plane (`/control/*`) when the workspace enabled it
59
+ * Mount the session control plane (`/control/*`) when the agent enabled it
57
60
  * (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
58
61
  * writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
59
62
  * the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
@@ -132,6 +135,46 @@ export function mountSessionControl(routes, control, stateRoot, options = {}) {
132
135
  },
133
136
  };
134
137
  }
138
+ /**
139
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
140
+ * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
141
+ * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
142
+ * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
143
+ * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
144
+ */
145
+ export function mountAgentcore(routes, options) {
146
+ const { agent, stateRoot, schedules, onStateReady } = options;
147
+ const mounted = agentcoreRoutes({
148
+ routes,
149
+ agent,
150
+ stateRoot,
151
+ isBusy: () => activeWork() > 0,
152
+ // Cross-deploy durability: AgentCore wipes the state mount on every runtime version update, so
153
+ // the state root is restored from (and pushed to) an S3 snapshot through presigned URLs the
154
+ // forwarder mints per envelope. Always wired on this path — the platform gives no other way to
155
+ // keep an agent's memory across a deploy.
156
+ stateSync: createStateSync({ stateRoot }),
157
+ // What separates a forwarder envelope from any IAM principal's InvokeAgentRuntime call. Absent =
158
+ // no forwarder in this topology, so only the public `invoke` kind is servable.
159
+ ingressSecret: process.env.FASTAGENT_INGRESS_SECRET,
160
+ onStateReady,
161
+ fire: schedules.length === 0
162
+ ? undefined
163
+ : (name, slot) => {
164
+ const schedule = schedules.find((s) => s.name === name);
165
+ if (!schedule)
166
+ throw new UnknownScheduleError(name);
167
+ return fireScheduleOnce({ agent, stateRoot, schedule, slot });
168
+ },
169
+ });
170
+ const mountedPaths = new Set(Object.keys(mounted).map((key) => parseRouteKey(key).path));
171
+ const collisions = Object.keys(routes).filter((key) => mountedPaths.has(parseRouteKey(key).path));
172
+ if (collisions.length > 0) {
173
+ throw new Error(`channel route(s) ${collisions.map((key) => `"${key}"`).join(", ")} collide with the AgentCore adapter ` +
174
+ `(/invocations, /ping) — rename the channel route`);
175
+ }
176
+ return { ...routes, ...mounted };
177
+ }
135
178
  /**
136
179
  * Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
137
180
  * adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
@@ -217,32 +260,36 @@ export function serve(surface, port, onListening) {
217
260
  });
218
261
  }
219
262
  /** Start a Cloudflare tunnel for route channels only. */
220
- export function maybeTunnel(workspaceDir, routeChannels, boundPort, tunnel, stateRoot) {
263
+ export function maybeTunnel(agentDir, routeChannels, boundPort, tunnel, stateRoot) {
221
264
  if (!tunnel || process.env.FASTAGENT_DEV_WORKER === "1")
222
265
  return;
223
266
  void startCloudflareTunnel(boundPort).then((instance) => {
224
267
  if (!instance)
225
268
  return;
226
- void announceWebhooks(workspaceDir, instance.url, { openUrl: openExternalUrl, routeChannels, stateRoot });
269
+ void announceWebhooks(agentDir, instance.url, { openUrl: openExternalUrl, routeChannels, stateRoot });
227
270
  const cleanup = () => instance.close();
228
271
  process.once("SIGINT", cleanup);
229
272
  process.once("SIGTERM", cleanup);
230
273
  });
231
274
  }
232
275
  /**
233
- * Load and start the workspace's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
234
- * there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals.
276
+ * Load and start the agent's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
277
+ * there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals. Returns the
278
+ * loaded schedules so a serving surface that needs them (the AgentCore adapter's fire binding) shares
279
+ * ONE load instead of re-discovering. `externalClock` (AgentCore) arms no resident cron timers.
235
280
  */
236
- export async function startSchedules(workspaceDir, agent, stateRoot, selfSchedule) {
237
- const { schedules, failures } = await loadSchedules(workspaceDir).catch(failStartup);
281
+ export async function startSchedules(agentDir, agent, stateRoot, selfSchedule, options = {}) {
282
+ const { schedules, failures } = await loadSchedules(agentDir).catch(failStartup);
238
283
  reportModuleLoadFailures(failures);
239
284
  if (schedules.length === 0 && !selfSchedule)
240
- return;
241
- const scheduler = createScheduler({ agent, stateRoot, schedules });
285
+ return schedules;
286
+ const scheduler = createScheduler({ agent, stateRoot, schedules, externalClock: options.externalClock });
242
287
  scheduler.start();
243
- if (schedules.length > 0)
244
- log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}`);
288
+ if (schedules.length > 0) {
289
+ log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}${options.externalClock ? " (external clock — no resident cron timers)" : ""}`);
290
+ }
245
291
  const stop = () => scheduler.stop();
246
292
  process.once("SIGINT", stop);
247
293
  process.once("SIGTERM", stop);
294
+ return schedules;
248
295
  }
@@ -1,4 +1,16 @@
1
1
  import { type LoginIO, type LoginMethod, type LoginResult } from "../engines/pi/login.ts";
2
+ /**
3
+ * The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
4
+ * labels drift out of alignment the moment a longer one appears — which is exactly what happened when
5
+ * `workspace:` joined `config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
6
+ * stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
7
+ * policy (pad, never hand-space), not a constant.
8
+ */
9
+ export declare function reportLine(label: string, value: string): void;
10
+ /** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
11
+ * you pointed at the agent, and the project around it is probably what you meant. A hint, so it renders
12
+ * as one and is silent otherwise — `dev` and `start` both print the pair, so both ask for it. */
13
+ export declare function reportWorkspaceHint(hint: string | undefined): void;
2
14
  /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
3
15
  export declare function isInteractive(): boolean;
4
16
  /**
@@ -19,9 +31,10 @@ export declare function reportAuth(modelSpec: string, authPath: string): Promise
19
31
  * already set; on a non-TTY (CI, a piped stdin), with `--no-input`, on cancel, or on a failed login
20
32
  * it stays quiet and lets the caller raise its own clear error (`missing model`, or deploy's
21
33
  * model-travel gate). The pick is exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it,
22
- * and best-effort written back to the config so the next run is quiet.
34
+ * and best-effort written back to the config so the next run is quiet. `agentDir` is the resolved
35
+ * AGENT DIR (resolvePlacement().agentDir) — config and auth both live there.
23
36
  */
24
- export declare function resolveFirstRunModel(workspaceDir: string, options?: {
37
+ export declare function resolveFirstRunModel(agentDir: string, options?: {
25
38
  model?: string;
26
39
  authPath?: string;
27
40
  input?: boolean;
@@ -8,14 +8,30 @@ import { relative } from "node:path";
8
8
  import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
9
9
  import { buildModelPickerOptions } from "./models-view.js";
10
10
  import { fastagentCredentialStore } from "../engines/pi/auth.js";
11
- import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resolveModel, resolveModelSpec, resolveStateRoot, rewriteConfigModel, } from "../engines/pi/config.js";
12
- import { ensureStateRootSelfIgnored, isUnderDir } from "../engines/pi/definition.js";
11
+ import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resolveModel, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
13
12
  import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
14
13
  import { createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses } from "../engines/pi/models.js";
15
14
  import { formatAuthReport } from "./auth-view.js";
16
15
  import { log } from "../log.js";
17
16
  import { openExternalUrl } from "../open-url.js";
18
17
  import { failStartup, failUsage } from "./fail.js";
18
+ /**
19
+ * The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
20
+ * labels drift out of alignment the moment a longer one appears — which is exactly what happened when
21
+ * `workspace:` joined `config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
22
+ * stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
23
+ * policy (pad, never hand-space), not a constant.
24
+ */
25
+ export function reportLine(label, value) {
26
+ log.info(`[fastagent] ${`${label}:`.padEnd(11)}${value}`);
27
+ }
28
+ /** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
29
+ * you pointed at the agent, and the project around it is probably what you meant. A hint, so it renders
30
+ * as one and is silent otherwise — `dev` and `start` both print the pair, so both ask for it. */
31
+ export function reportWorkspaceHint(hint) {
32
+ if (hint)
33
+ reportLine("hint", hint);
34
+ }
19
35
  /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
20
36
  export function isInteractive() {
21
37
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -65,23 +81,24 @@ export async function reportAuth(modelSpec, authPath) {
65
81
  * already set; on a non-TTY (CI, a piped stdin), with `--no-input`, on cancel, or on a failed login
66
82
  * it stays quiet and lets the caller raise its own clear error (`missing model`, or deploy's
67
83
  * model-travel gate). The pick is exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it,
68
- * and best-effort written back to the config so the next run is quiet.
84
+ * and best-effort written back to the config so the next run is quiet. `agentDir` is the resolved
85
+ * AGENT DIR (resolvePlacement().agentDir) — config and auth both live there.
69
86
  */
70
- export async function resolveFirstRunModel(workspaceDir, options = {}) {
71
- const { config, path: configPath } = await loadConfig(workspaceDir).catch(failStartup);
87
+ export async function resolveFirstRunModel(agentDir, options = {}) {
88
+ const { config, path: configPath } = await loadConfig(agentDir).catch(failStartup);
72
89
  if (resolveModelSpec(options.model, config))
73
90
  return; // already set (flag > FASTAGENT_MODEL > config)
74
91
  if (options.input === false)
75
92
  return; // --no-input: never prompt (clig) — the opener raises the clear error
76
93
  if (!isInteractive())
77
94
  return; // CI/deploy: the opener throws the actionable missing-model error
78
- const authPath = resolveAuthPath(workspaceDir, options.authPath);
95
+ const authPath = resolveAuthPath(agentDir, options.authPath);
79
96
  const models = createPiModels({ authPath });
80
- const chosen = await pickWithCredentials(workspaceDir, models, authPath);
97
+ const chosen = await pickWithCredentials(models, authPath);
81
98
  if (chosen === undefined)
82
99
  return; // cancelled (or auth probe failed): the caller raises its clear missing-model error
83
100
  process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
84
- await persistModelChoice(workspaceDir, configPath, chosen);
101
+ await persistModelChoice(agentDir, configPath, chosen);
85
102
  }
86
103
  /**
87
104
  * The credential-aware pick: full catalog annotated per provider, then the post-pick auth policy —
@@ -89,7 +106,7 @@ export async function resolveFirstRunModel(workspaceDir, options = {}) {
89
106
  * independent of credentials), or the inline login for the rest. Returns the chosen spec, or
90
107
  * undefined when the pick should be discarded (picker cancel, login cancel, a failed auth probe).
91
108
  */
92
- async function pickWithCredentials(workspaceDir, models, authPath) {
109
+ async function pickWithCredentials(models, authPath) {
93
110
  let statuses;
94
111
  try {
95
112
  statuses = await providerAuthStatuses(models);
@@ -124,11 +141,6 @@ async function pickWithCredentials(workspaceDir, models, authPath) {
124
141
  }
125
142
  return chosen;
126
143
  }
127
- // Inline login. Same leak guard as `login`: self-ignore the state root BEFORE a credential
128
- // can land in-tree, so the secret is never untracked-but-committable.
129
- const stateRoot = resolveStateRoot(workspaceDir);
130
- if (isUnderDir(authPath, stateRoot))
131
- await ensureStateRootSelfIgnored(workspaceDir, stateRoot);
132
144
  try {
133
145
  // Verified against the CHOSEN model — the exact request the agent is about to make; a rejected
134
146
  // key re-prompts inside the loop, so reaching here means a usable (or at worst unverifiable) key.
@@ -224,10 +236,10 @@ function terminalLoginIO() {
224
236
  /**
225
237
  * Best-effort persist the picked model so the next run does not prompt. Rewrites the commented
226
238
  * `model:` placeholder the scaffold writes / an existing `model:` line, or re-inserts the line into a
227
- * scaffold-shaped config (the hand-deleted-to-reset case); anything else (zero-config, a hand-shaped
228
- * config) is left untouched with a printed hint. Never throws — persistence is a convenience.
239
+ * scaffold-shaped config (the hand-deleted-to-reset case); anything else (a hand-shaped config) is
240
+ * left untouched with a printed hint. Never throws — persistence is a convenience.
229
241
  */
230
- async function persistModelChoice(workspaceDir, configPath, spec) {
242
+ async function persistModelChoice(agentDir, configPath, spec) {
231
243
  const hint = () => console.error(
232
244
  // No "using it for this run" promise: deploy's model-travel gate rightly ignores the un-persisted pick.
233
245
  `[fastagent] picked ${spec} — set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
@@ -238,7 +250,7 @@ async function persistModelChoice(workspaceDir, configPath, spec) {
238
250
  if (!replaced)
239
251
  return hint();
240
252
  await writeFile(configPath, replaced);
241
- console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(workspaceDir, configPath)}`);
253
+ console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(agentDir, configPath)}`);
242
254
  }
243
255
  catch {
244
256
  hint();
@@ -0,0 +1,117 @@
1
+ import type { ChannelKind } from "../../scaffold/add-channel.ts";
2
+ import { type Artifact, type ContainerInput } from "../container.ts";
3
+ /** The one schedule fact the plan needs (from loadSchedules) — name + cron + tz. */
4
+ export interface ScheduleFact {
5
+ name: string;
6
+ cron: string;
7
+ tz?: string;
8
+ }
9
+ export interface AgentcorePlanInput extends ContainerInput {
10
+ /** Base name (dir basename) — shapes the runtime name, stack name, ECR repo, session id. */
11
+ name: string;
12
+ /** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
13
+ modelAuth: string | undefined;
14
+ /** Known first-party channels — each contributes its secret metadata + webhook step. */
15
+ channels: ChannelKind[];
16
+ /** ALL route-channel basenames (customs included) — any of them requires the forwarder. */
17
+ routeChannels: string[];
18
+ /** Extra secret env-var names (fastagent.config deploy.secrets). */
19
+ extraSecrets?: string[];
20
+ /** Static schedules — each becomes an EventBridge Scheduler rule targeting the forwarder. */
21
+ schedules: ScheduleFact[];
22
+ /** Wake tool enabled — DEGRADED here (fires only while a session happens to be awake); warned. */
23
+ selfSchedule: boolean;
24
+ }
25
+ export interface AgentcorePlan {
26
+ /** template + forwarder + Dockerfile/.dockerignore — written by the CLI (kept unless --force). */
27
+ artifacts: Artifact[];
28
+ /** The ordered, values-resolved deploy runbook — printed to stdout. */
29
+ runbook: string[];
30
+ /** Cron expressions EventBridge cannot express — surfaced as runbook warnings, not silent drops. */
31
+ untranslatableSchedules: {
32
+ name: string;
33
+ reason: string;
34
+ }[];
35
+ }
36
+ /** SessionStorage mount = FASTAGENT_STATE_DIR (AgentCore requires exactly `/mnt/<one-level>`). It is
37
+ * a fast LOCAL disk only: the platform wipes it on every runtime version update (= every deploy).
38
+ * Durability across deploys comes from the S3 snapshot (channels/agentcore-state.ts). */
39
+ export declare const MOUNT = "/mnt/state";
40
+ /**
41
+ * How long an idle session keeps its microVM. Memory is billed per second across the WHOLE session
42
+ * — idle included, at the peak level reached — so this tail is the standing cost of every burst of
43
+ * activity, while CPU stops billing the moment the agent stops working. 3 minutes rather than the
44
+ * platform's 15: the tail shrinks 5×, and the cost is a cold start (image + Node + snapshot restore)
45
+ * for anyone who returns after a longer gap. `/ping` reports HealthyBusy while work is in flight, so
46
+ * this timer only ever starts once the agent has genuinely settled — a long turn is never cut short.
47
+ * AWS accepts 60–28800.
48
+ */
49
+ export declare const IDLE_TIMEOUT_SECONDS = 180;
50
+ /** The platform ceiling on one session's compute (8 h). The session ID outlives it: the next invoke
51
+ * simply gets fresh compute with the same storage. */
52
+ export declare const MAX_LIFETIME_SECONDS = 28800;
53
+ /** The state snapshot's object key in the deployment bucket (one object; see agentcore-state.ts). */
54
+ export declare const STATE_KEY = "state/snapshot.json.gz";
55
+ /** The forwarder artifact. Named `index.js` because it IS the Lambda deployment package's entry:
56
+ * zipping it as-is produces a valid package (`Handler: index.handler`), with nothing to rename. */
57
+ export declare const FORWARDER_FILE = "lambda/index.js";
58
+ /** The deployment bucket: forwarder code + the state snapshot. Account-suffixed for S3's GLOBAL
59
+ * namespace, and created OUTSIDE the stack (like the ECR repo) so a `delete-stack` cannot take the
60
+ * agent's memory with it. Bucket names cap at 63 chars; `name` is already gated to 40. */
61
+ export declare function stateBucketName(name: string, account: string): string;
62
+ /** AgentCore env values max 2048 chars — a real OAuth auth.json's base64 exceeds it, so the seed is
63
+ * CHUNKED across FASTAGENT_AUTH_SEED + _2… (collectAuthSeed reassembles at boot). 2000 keeps margin. */
64
+ export declare const AUTH_SEED_CHUNK_SIZE = 2000;
65
+ export declare const AUTH_SEED_MAX_CHUNKS = 4;
66
+ /** The generated template's filename (namespaced under the kit in the agentDir layout). */
67
+ export declare const TEMPLATE_FILE = "agentcore.template.yaml";
68
+ /** The generated template's first-line marker — the ONE source for both the generator and the
69
+ * "did fastagent generate this?" check (deploy's drift gate), so they cannot drift apart. */
70
+ export declare const GENERATED_TEMPLATE_MARKER = "# Generated by `fastagent deploy agentcore`";
71
+ /** Whether an on-disk template is fastagent-generated (vs hand-written — kept, never gated). */
72
+ export declare function isGeneratedAgentcoreTemplate(content: string): boolean;
73
+ /** Runtime name (`[a-zA-Z][a-zA-Z0-9_]{0,47}`) from a dir basename. */
74
+ export declare function toRuntimeName(basename: string): string;
75
+ /** The ONE fixed ingress session id (webhooks + schedule fires) — ≥ 33 chars (the API minimum),
76
+ * deterministic (the Lambda holds it in env), padded so any name clears the floor. */
77
+ export declare function ingressSessionId(name: string): string;
78
+ /** CFN parameter logical id for a secret env-var name: TELEGRAM_BOT_TOKEN → TelegramBotToken
79
+ * (parameter names must be alphanumeric). Deterministic — run.ts builds the same mapping. */
80
+ export declare function cfnParamName(envName: string): string;
81
+ /**
82
+ * Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it
83
+ * can't be. The two dialects disagree exactly where silent translation would misfire:
84
+ * - EventBridge numbers day-of-week 1–7 (1 = Sunday); standard cron uses 0–6 (0/7 = Sunday) —
85
+ * numeric dow values and range endpoints are remapped ({@link mapDowField}); steps and names
86
+ * pass through; a range that wraps under renumbering is refused.
87
+ * - EventBridge requires `?` in dom or dow: a `*` on either side becomes `?`; BOTH restricted is
88
+ * standard cron's OR semantics, which EventBridge cannot express — refused, never approximated.
89
+ * - A 6-field (seconds) expression and L/# day-of-week forms are refused for the same reason.
90
+ */
91
+ export declare function toEventBridgeCron(cron: string): {
92
+ expression: string;
93
+ } | {
94
+ error: string;
95
+ };
96
+ /**
97
+ * The forwarder Lambda source — the ONLY string both the template's inline ZipFile and the readable
98
+ * `lambda/forwarder.js` artifact are generated from (one source, no drift). Zero-dependency: the
99
+ * Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: CloudFormation inline code always
100
+ * lands as `index.js`, where ESM `import` is a syntax error (found by the first real deploy). Two
101
+ * event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
102
+ * channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
103
+ * Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
104
+ * (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key). MUST stay
105
+ * under CloudFormation's 4096-byte inline-code cap.
106
+ */
107
+ export declare function forwarderSource(): string;
108
+ /**
109
+ * The EventBridge physical name for a schedule. A schedule's local name is an arbitrary MODULE FILE
110
+ * NAME (`schedules/晨报.ts`, `schedules/deploy check.ts`), while AWS requires `[0-9A-Za-z-_.]+` within
111
+ * 64 chars — and the `fa-<agent>-` prefix already eats up to 44 of them. So: sanitize, bound the
112
+ * readable part, and end with a hash of the ORIGINAL name, which keeps distinct schedules distinct
113
+ * where sanitizing or truncation would have merged them (one rule silently firing for two).
114
+ */
115
+ export declare function scheduleResourceName(agent: string, schedule: string): string;
116
+ /** Compute the AgentCore deploy plan from the resolved definition. */
117
+ export declare function planAgentcoreDeploy(input: AgentcorePlanInput): AgentcorePlan;