@fastagent-sh/fastagent 0.18.0 → 0.20.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 (222) hide show
  1. package/README.md +25 -7
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/atomic-write.d.ts +23 -0
  5. package/dist/atomic-write.js +53 -0
  6. package/dist/{host/node.d.ts → channel.d.ts} +24 -28
  7. package/dist/channel.js +1 -0
  8. package/dist/channels/agentcore-limits.d.ts +9 -0
  9. package/dist/channels/agentcore-limits.js +9 -0
  10. package/dist/channels/agentcore-service.d.ts +55 -0
  11. package/dist/channels/agentcore-service.js +107 -0
  12. package/dist/channels/agentcore.d.ts +22 -20
  13. package/dist/channels/agentcore.js +20 -11
  14. package/dist/channels/busy.d.ts +1 -1
  15. package/dist/channels/busy.js +1 -1
  16. package/dist/channels/control.d.ts +55 -7
  17. package/dist/channels/control.js +312 -67
  18. package/dist/channels/discover.d.ts +59 -0
  19. package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
  20. package/dist/channels/feishu/card.js +1 -1
  21. package/dist/channels/feishu/context-buffer.d.ts +1 -1
  22. package/dist/channels/feishu/context-buffer.js +4 -4
  23. package/dist/channels/feishu/feishu-api.js +6 -9
  24. package/dist/channels/feishu/feishu.d.ts +1 -1
  25. package/dist/channels/feishu/feishu.js +41 -24
  26. package/dist/channels/feishu/invoke-turn.d.ts +1 -1
  27. package/dist/channels/feishu/invoke-turn.js +3 -4
  28. package/dist/channels/feishu/preview.d.ts +1 -1
  29. package/dist/channels/feishu/preview.js +7 -3
  30. package/dist/channels/feishu/ws-ingress.d.ts +1 -1
  31. package/dist/channels/github/github.d.ts +1 -1
  32. package/dist/channels/http.d.ts +11 -7
  33. package/dist/channels/http.js +1 -109
  34. package/dist/channels/kit/attachment-path.d.ts +12 -0
  35. package/dist/channels/kit/attachment-path.js +43 -0
  36. package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
  37. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
  38. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
  39. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
  40. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  41. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  42. package/dist/channels/kit/signature.d.ts +12 -0
  43. package/dist/channels/kit/signature.js +17 -0
  44. package/dist/channels/{state.js → kit/state.js} +4 -7
  45. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  46. package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
  47. package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
  48. package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
  49. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
  50. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  51. package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
  52. package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
  53. package/dist/channels/lark/lark.d.ts +1 -1
  54. package/dist/channels/serve.d.ts +73 -0
  55. package/dist/channels/serve.js +243 -0
  56. package/dist/channels/slack/bot-auth.js +3 -14
  57. package/dist/channels/slack/context-buffer.d.ts +2 -2
  58. package/dist/channels/slack/context-buffer.js +2 -2
  59. package/dist/channels/slack/invoke-turn.d.ts +2 -2
  60. package/dist/channels/slack/invoke-turn.js +13 -6
  61. package/dist/channels/slack/onboard.js +5 -5
  62. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  63. package/dist/channels/slack/onboarding-state.js +13 -20
  64. package/dist/channels/slack/parse.js +2 -2
  65. package/dist/channels/slack/preview.d.ts +1 -1
  66. package/dist/channels/slack/preview.js +11 -3
  67. package/dist/channels/slack/register-webhook.js +1 -1
  68. package/dist/channels/slack/scaffold/slack-send.ts +2 -17
  69. package/dist/channels/slack/slack-api.js +23 -55
  70. package/dist/channels/slack/slack.d.ts +1 -1
  71. package/dist/channels/slack/slack.js +20 -20
  72. package/dist/channels/slack/welcomed.js +1 -1
  73. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  74. package/dist/channels/telegram/context-buffer.js +2 -2
  75. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  76. package/dist/channels/telegram/invoke-turn.js +1 -1
  77. package/dist/channels/telegram/parse.d.ts +21 -1
  78. package/dist/channels/telegram/parse.js +67 -13
  79. package/dist/channels/telegram/preview.d.ts +1 -1
  80. package/dist/channels/telegram/preview.js +6 -2
  81. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  82. package/dist/channels/telegram/telegram-api.js +5 -6
  83. package/dist/channels/telegram/telegram.d.ts +3 -3
  84. package/dist/channels/telegram/telegram.js +60 -72
  85. package/dist/channels/telegram/turn-store.d.ts +2 -2
  86. package/dist/channels/telegram/turn-store.js +2 -2
  87. package/dist/cli/add-slack.js +6 -5
  88. package/dist/cli/commands/add.js +1 -5
  89. package/dist/cli/commands/attach.d.ts +2 -4
  90. package/dist/cli/commands/attach.js +17 -15
  91. package/dist/cli/commands/deploy.d.ts +50 -5
  92. package/dist/cli/commands/deploy.js +183 -134
  93. package/dist/cli/commands/dev.js +25 -40
  94. package/dist/cli/commands/fire.js +1 -1
  95. package/dist/cli/commands/info.js +7 -5
  96. package/dist/cli/commands/init.js +1 -2
  97. package/dist/cli/commands/schedule.js +1 -1
  98. package/dist/cli/commands/start.js +80 -121
  99. package/dist/cli/commands/tool.js +5 -7
  100. package/dist/cli/program.js +1 -1
  101. package/dist/cli/serve.d.ts +22 -70
  102. package/dist/cli/serve.js +66 -257
  103. package/dist/cli/shared.d.ts +40 -10
  104. package/dist/cli/shared.js +53 -6
  105. package/dist/core.d.ts +3 -5
  106. package/dist/core.js +7 -5
  107. package/dist/deploy/agentcore/logs.d.ts +2 -2
  108. package/dist/deploy/agentcore/logs.js +2 -2
  109. package/dist/deploy/agentcore/plan.d.ts +12 -10
  110. package/dist/deploy/agentcore/plan.js +39 -31
  111. package/dist/deploy/agentcore/run.d.ts +5 -4
  112. package/dist/deploy/agentcore/run.js +13 -36
  113. package/dist/deploy/channel-ingress.d.ts +73 -0
  114. package/dist/deploy/channel-ingress.js +101 -0
  115. package/dist/deploy/docker/plan.d.ts +3 -7
  116. package/dist/deploy/docker/plan.js +4 -15
  117. package/dist/deploy/docker/run.d.ts +18 -1
  118. package/dist/deploy/docker/run.js +10 -0
  119. package/dist/deploy/fly/plan.d.ts +4 -5
  120. package/dist/deploy/fly/plan.js +7 -22
  121. package/dist/deploy/fly/run.d.ts +9 -23
  122. package/dist/deploy/fly/run.js +32 -68
  123. package/dist/deploy/preflight.d.ts +6 -7
  124. package/dist/deploy/preflight.js +27 -23
  125. package/dist/deploy/railway/plan.d.ts +4 -5
  126. package/dist/deploy/railway/plan.js +11 -26
  127. package/dist/deploy/railway/run.d.ts +9 -9
  128. package/dist/deploy/railway/run.js +40 -40
  129. package/dist/deploy/runner.js +22 -2
  130. package/dist/deploy/secrets.d.ts +18 -9
  131. package/dist/deploy/secrets.js +64 -12
  132. package/dist/dev-supervisor.js +11 -6
  133. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  134. package/dist/engines/pi/agent-session-factory.js +295 -0
  135. package/dist/engines/pi/auth.js +43 -43
  136. package/dist/engines/pi/config.d.ts +5 -5
  137. package/dist/engines/pi/config.js +1 -1
  138. package/dist/engines/pi/create.d.ts +67 -42
  139. package/dist/engines/pi/create.js +142 -72
  140. package/dist/engines/pi/definition.d.ts +42 -8
  141. package/dist/engines/pi/definition.js +114 -11
  142. package/dist/engines/pi/invoke-session.d.ts +22 -19
  143. package/dist/engines/pi/invoke-session.js +286 -100
  144. package/dist/engines/pi/login.js +39 -20
  145. package/dist/engines/pi/models.d.ts +14 -0
  146. package/dist/engines/pi/models.js +9 -1
  147. package/dist/engines/pi/open.d.ts +10 -8
  148. package/dist/engines/pi/open.js +21 -11
  149. package/dist/engines/pi/report.d.ts +0 -4
  150. package/dist/engines/pi/report.js +0 -7
  151. package/dist/engines/pi/search-tools.js +1 -1
  152. package/dist/engines/pi/service.d.ts +23 -0
  153. package/dist/engines/pi/service.js +27 -0
  154. package/dist/engines/pi/session-builder.js +87 -118
  155. package/dist/engines/pi/session-control.d.ts +28 -14
  156. package/dist/engines/pi/session-control.js +570 -438
  157. package/dist/engines/pi/session-inheritance.d.ts +30 -0
  158. package/dist/engines/pi/session-inheritance.js +261 -0
  159. package/dist/engines/pi/session-markers.d.ts +48 -0
  160. package/dist/engines/pi/session-markers.js +59 -0
  161. package/dist/engines/pi/session-settings.d.ts +15 -3
  162. package/dist/engines/pi/session-settings.js +22 -1
  163. package/dist/engines/pi/session-store.d.ts +140 -0
  164. package/dist/engines/pi/session-store.js +666 -0
  165. package/dist/engines/pi/tool-context.d.ts +36 -13
  166. package/dist/engines/pi/tool-context.js +51 -4
  167. package/dist/engines/pi/tool.d.ts +5 -5
  168. package/dist/engines/pi/tool.js +3 -3
  169. package/dist/engines/pi/turn-kit.d.ts +32 -6
  170. package/dist/engines/pi/turn-kit.js +28 -1
  171. package/dist/engines/pi/wake-tool.js +1 -1
  172. package/dist/env.js +1 -2
  173. package/dist/index.d.ts +1 -0
  174. package/dist/index.js +1 -0
  175. package/dist/loader.d.ts +49 -7
  176. package/dist/loader.js +79 -18
  177. package/dist/log.d.ts +3 -0
  178. package/dist/log.js +5 -1
  179. package/dist/node.d.ts +16 -0
  180. package/dist/node.js +20 -0
  181. package/dist/paths.d.ts +26 -0
  182. package/dist/paths.js +37 -2
  183. package/dist/pi.d.ts +14 -8
  184. package/dist/pi.js +6 -5
  185. package/dist/scaffold/add-channel.js +2 -2
  186. package/dist/scaffold/init.js +7 -2
  187. package/dist/scaffold/templates/persona.md +2 -2
  188. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  189. package/dist/schedule/discover.js +3 -15
  190. package/dist/schedule/state.js +4 -9
  191. package/dist/schedule/wake-alarm.d.ts +13 -7
  192. package/dist/schedule/wake-alarm.js +88 -46
  193. package/dist/schedule/wakeups.d.ts +2 -2
  194. package/dist/schedule/wakeups.js +11 -8
  195. package/dist/service.d.ts +157 -0
  196. package/dist/service.js +376 -0
  197. package/dist/session-remote.d.ts +8 -3
  198. package/dist/session-remote.js +162 -92
  199. package/dist/session.d.ts +229 -93
  200. package/dist/session.js +61 -23
  201. package/dist/telegram.d.ts +1 -1
  202. package/dist/telegram.js +1 -1
  203. package/dist/tunnel.d.ts +18 -3
  204. package/dist/tunnel.js +32 -40
  205. package/package.json +14 -5
  206. package/dist/engines/pi/channel.d.ts +0 -37
  207. package/dist/engines/pi/harness.d.ts +0 -115
  208. package/dist/engines/pi/harness.js +0 -154
  209. package/dist/engines/pi/invoke.d.ts +0 -60
  210. package/dist/engines/pi/invoke.js +0 -459
  211. package/dist/engines/pi/read-image.d.ts +0 -4
  212. package/dist/engines/pi/read-image.js +0 -62
  213. package/dist/engines/pi/sessions.d.ts +0 -60
  214. package/dist/engines/pi/sessions.js +0 -464
  215. package/dist/host/node.js +0 -52
  216. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  217. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  218. /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
  219. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  220. /package/dist/channels/{text.js → kit/text.js} +0 -0
  221. /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
  222. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
@@ -1,14 +1,14 @@
1
1
  /** `fastagent info [dir] [--json]`: print what the directory ASSEMBLES into, WITHOUT booting a server. Read-only. */
2
2
  import { resolve } from "node:path";
3
3
  import { loadDotEnv } from "../../env.js";
4
- import { discoverChannelFiles } from "../../engines/pi/channel.js";
4
+ import { discoverChannelFiles } from "../../channels/discover.js";
5
5
  import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModel, resolveModelSpec, resolveSessionsDirOverride, } from "../../engines/pi/config.js";
6
6
  import { createPiModelRuntime } from "../../engines/pi/models.js";
7
7
  import { resolveStateRoot, workspaceHint } from "../../paths.js";
8
- import { resolveAgentTools } from "../../engines/pi/create.js";
8
+ import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
9
9
  import { loadAgentDefinition } from "../../engines/pi/definition.js";
10
- import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
11
- import { log } from "../../log.js";
10
+ import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
11
+ import { reportModuleLoadFailures, log } from "../../log.js";
12
12
  import { nextRun } from "../../schedule/cron.js";
13
13
  import { loadSchedules } from "../../schedule/discover.js";
14
14
  import { failStartup, placementOrExit } from "../fail.js";
@@ -25,7 +25,7 @@ export async function runInfo(dirArg, opts) {
25
25
  // tool), is isolated the same way everywhere (G2): info, dev, AND start report it and keep going with
26
26
  // the tools that loaded. The `error`/`.catch` below only fires for a whole-load fault (an unreadable
27
27
  // tools/ dir), not a single bad file.
28
- const tools = await resolveAgentTools(config, agentDir)
28
+ const tools = await resolveAgentTools(config, agentDir, workspace)
29
29
  .then((r) => ({
30
30
  names: r.toolNames,
31
31
  deferred: r.deferredToolNames,
@@ -78,6 +78,7 @@ export async function runInfo(dirArg, opts) {
78
78
  model: modelSpec ?? null,
79
79
  modelError: modelError ?? null,
80
80
  thinkingLevel: config.thinkingLevel ?? null,
81
+ codingTools: [...CODING_TOOL_NAMES],
81
82
  context: definition.contextFiles.map((f) => f.path),
82
83
  persona: definition.persona !== undefined,
83
84
  skills: definition.skills.map((skill) => ({ name: skill.name, description: skill.description })),
@@ -114,6 +115,7 @@ export async function runInfo(dirArg, opts) {
114
115
  cont(`⚠ does not resolve: ${modelError}`);
115
116
  if (config.thinkingLevel)
116
117
  line("thinking", config.thinkingLevel);
118
+ line("codingTools", CODING_TOOL_NAMES.join(", "));
117
119
  line("context", definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
118
120
  line("persona", definition.persona ? "persona.md" : "(none)");
119
121
  line("skills", definition.skills.map((skill) => skill.name).join(", ") || "(none)");
@@ -7,10 +7,9 @@
7
7
  */
8
8
  import { spawn } from "node:child_process";
9
9
  import { basename, join, resolve } from "node:path";
10
- import { DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentsAt } from "../../paths.js";
10
+ import { DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentsAt, displayPath } from "../../paths.js";
11
11
  import { detectRuntime, readPackageJson } from "../../runtime.js";
12
12
  import { agentDirName, agentDirNameError, scaffoldAgent } from "../../scaffold/init.js";
13
- import { displayPath } from "../../paths.js";
14
13
  import { failStartup, failUsage } from "../fail.js";
15
14
  export async function runInit(dirArg, opts) {
16
15
  const dir = resolve(dirArg);
@@ -6,7 +6,7 @@
6
6
  import { resolve } from "node:path";
7
7
  import { loadDotEnv } from "../../env.js";
8
8
  import { resolveStateRoot } from "../../paths.js";
9
- import { reportModuleLoadFailures } from "../../engines/pi/report.js";
9
+ import { reportModuleLoadFailures } from "../../log.js";
10
10
  import { readRuns } from "../../schedule/audit.js";
11
11
  import { nextRun } from "../../schedule/cron.js";
12
12
  import { loadSchedules } from "../../schedule/discover.js";
@@ -2,26 +2,24 @@
2
2
  * `fastagent start [dir]`: run the agent in production posture — the SAME assembly as dev (your
3
3
  * directory is the agent), just no file-watching. No build step: start reads the definition directly.
4
4
  */
5
- import { mkdir, writeFile } from "node:fs/promises";
6
5
  import { dirname, resolve } from "node:path";
7
- import { authSeedBytes, collectAuthSeed } from "../../deploy/fly/run.js";
6
+ import { writeFileAtomic } from "../../atomic-write.js";
7
+ import { authSeedBytes, collectAuthSeed } from "../../deploy/secrets.js";
8
8
  import { loadDotEnv } from "../../env.js";
9
9
  import { resolveAuthPath, resolveSessionsDirOverride } from "../../engines/pi/config.js";
10
- import { resolveSecretsDir, workspaceHint } from "../../paths.js";
11
- import { isUnderDir } from "../../engines/pi/definition.js";
12
- import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
13
- import { createPiAgentFromDir } from "../../engines/pi/open.js";
10
+ import { SECRET_FILE_MODE, ensureSecretsDir, resolveSecretsDir, isUnderDir, exists } from "../../paths.js";
14
11
  import { log, setLogLevel } from "../../log.js";
15
- import { createWakeAlarmSink, reconcileWakeAlarms } from "../../schedule/wake-alarm.js";
16
- import { setWakeupsSink } from "../../schedule/wakeups.js";
12
+ import { createPiAgentFromDir } from "../../engines/pi/open.js";
13
+ import { mountAgentService } from "../../service.js";
17
14
  import { logAgentLoop } from "../../observe.js";
18
15
  import { installProxyFetch } from "../../proxy.js";
19
- import { exists } from "../../paths.js";
20
16
  import { bindAddress } from "../../bind.js";
21
- import { parseRouteKey } from "../../host/node.js";
17
+ import { isAgentcoreRuntime, mountAgentcoreService } from "../../channels/agentcore-service.js";
18
+ import { createWakeAlarmSink } from "../../schedule/wake-alarm.js";
19
+ import { setWakeupsSink } from "../../schedule/wakeups.js";
22
20
  import { failStartup, placementOrExit } from "../fail.js";
23
- import { assertTunnelBindable, maybeTunnel, mountAgentcore, mountSessionControl, routesFor, serve, startSchedules, } from "../serve.js";
24
- import { parseBind, parsePort, reportAuth, reportLine, resolveFirstRunModel, reportWorkspaceHint } from "../shared.js";
21
+ import { SHUTDOWN_GRACE_MS, assertTunnelBindable, maybeTunnel, reportServing, serve } from "../serve.js";
22
+ import { parseBind, parsePort, reportAssembly, resolveFirstRunModel } from "../shared.js";
25
23
  export async function runStart(dirArg, opts) {
26
24
  const dir = resolve(dirArg);
27
25
  // Flag validation first: a bad --port is a USAGE error (exit 2), and reporting it must not depend on
@@ -39,30 +37,16 @@ export async function runStart(dirArg, opts) {
39
37
  await maybeSeedAuth(resolveAuthPath(placement.agentDir, opts.authPath));
40
38
  // The same opener dev uses (single assembly source), just no watch.
41
39
  const sessionsDirOverride = resolveSessionsDirOverride(opts.sessionsDir);
42
- const { agent, definition, agentDir, workspace, config, modelSpec, stateRoot, sessionsDir, authPath, toolNames, deferredToolNames, toolCollisions, toolFailures, sessionControl, } = await createPiAgentFromDir(dir, {
43
- model: opts.model,
44
- sessionsDir: sessionsDirOverride,
45
- authPath: opts.authPath,
46
- serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
47
- }).catch(failStartup);
48
- reportLine("agent", agentDir);
49
- reportLine("workspace", workspace);
50
- reportWorkspaceHint(workspaceHint({ agentDir, workspace }));
51
- reportLine("model", `${modelSpec}${config.thinkingLevel ? ` (thinking: ${config.thinkingLevel})` : ""}`);
52
- await reportAuth(agentDir, modelSpec, authPath);
53
- reportLine("context", definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
54
- if (definition.persona)
55
- reportLine("persona", "persona.md");
56
- reportLine("skills", definition.skills.map((s) => s.name).join(", ") || "(none)");
57
- if (toolNames.length > 0)
58
- reportLine("tools", toolNames.join(", "));
59
- if (deferredToolNames.length > 0) {
60
- reportLine("deferred", `${deferredToolNames.join(", ")} (activated via search_tools)`);
61
- }
62
- reportToolCollisions(toolCollisions);
63
- reportModuleLoadFailures(toolFailures);
64
- reportLine("state", stateRoot);
65
- reportLine("sessions", sessionsDir);
40
+ const opened = await openStartDir(dir, opts, sessionsDirOverride);
41
+ const { agent, agentDir, config, stateRoot, sessionsDir } = opened;
42
+ // The same report `dev` prints; `state:`/`sessions:` are start's own extras, and the persistence
43
+ // notes below are why (see reportAssembly on the asymmetry).
44
+ await reportAssembly(opened, {
45
+ afterTools: [
46
+ ["state", stateRoot],
47
+ ["sessions", sessionsDir],
48
+ ],
49
+ });
66
50
  // State defaults under the agent dir, which a redeploy may replace wholesale. Gate on where the
67
51
  // state root ACTUALLY resolved (inside the agent dir?), not on the raw env var: an empty
68
52
  // `FASTAGENT_STATE_DIR=""` reads as unset (resolveStateRoot) and still lands in-agent, so a raw
@@ -81,102 +65,52 @@ export async function runStart(dirArg, opts) {
81
65
  log.info(`[fastagent] note: secrets (.env, rotated auth.json) live under the definition dir; point ` +
82
66
  `FASTAGENT_SECRETS_DIR at a persistent volume so a redeploy that replaces the dir does not wipe them.`);
83
67
  }
84
- reportFindingsIfChanged(definition.dir, definition);
85
- // AgentCore Runtime posture (FASTAGENT_AGENTCORE=1, set by the generated deploy artifacts): the
86
- // adapter (POST /invocations + GET /ping) is the container's only reachable surface, and cron
87
- // slots arrive from the external clock through it — so no resident cron timers. In particular,
88
- // do NOT mount the ordinary unauthenticated POST /invoke fallback: a selfSchedule-only topology
89
- // needs a public Function URL for wake callbacks, and its forwarder can relay arbitrary paths.
90
- const agentcore = process.env.FASTAGENT_AGENTCORE === "1";
91
68
  // Same debug turn trace as dev; gated out here by the info level (see dev.ts serveOnce).
92
69
  const traced = logAgentLoop(agent);
93
- // On AgentCore the channels are constructed LAZILY (mountAgentcore's lazyChannels, resolved on the
94
- // first envelope after the state-snapshot restore): construction loads channel state and replays
95
- // turn intent, and at boot the state mount is PRE-RESTORE — empty after every version update — so
96
- // an eager build would cache that emptiness (thread participation, delivery dedup, pending turns)
97
- // and then clobber the restored files with it. Everywhere else the state root is durable at boot,
98
- // so channels mount eagerly and a broken channel fails startup.
99
- const routed = agentcore
100
- ? undefined
101
- : await routesFor(agentDir, traced, stateRoot, sessionControl, { builtinInvoke: true }).catch(failStartup);
102
70
  // `http.host` enters here the way the flag enters `parseBind` — through `bindAddress`, so a
103
71
  // configured `localhost` is an ADDRESS by the time anything binds, renders or dials it.
104
72
  const configured = config.http?.host;
105
73
  const host = bindFlag ?? (configured === undefined ? undefined : bindAddress(configured));
106
74
  assertTunnelBindable(host, opts.tunnel ?? false, bindFlag ? "flag" : "config");
107
- const withControl = mountSessionControl(routed?.routes ?? {}, sessionControl, stateRoot, {
108
- tunnel: opts.tunnel ?? false,
109
- agent: traced,
110
- host,
111
- });
112
- // AgentCore + selfSchedule: register the wake-ALARM sink BEFORE the scheduler starts the boot
113
- // wake pump may advance a recurring entry (a store save) and that save must already re-arm its
114
- // alarm. The secret arrives via the stack (FASTAGENT_WAKE_SECRET); without it the deployment
115
- // degrades to awake-only wakes — warned, never silent.
116
- let onStateReady;
117
- if (agentcore && config.selfSchedule) {
118
- const wakeSecret = process.env.FASTAGENT_WAKE_SECRET;
119
- if (wakeSecret) {
120
- const sink = createWakeAlarmSink({ secret: wakeSecret });
121
- setWakeupsSink(sink);
122
- // NOT here: at boot the state mount is whatever the platform just provisioned — after a runtime
123
- // version update that is EMPTY, so a reconcile now would see no pending wake-ups and conclude
124
- // there is nothing to re-arm. It runs once the snapshot has been restored (mountAgentcore).
125
- onStateReady = () => reconcileWakeAlarms(stateRoot, sink);
126
- log.info(`[fastagent] wake alarms: EventBridge-backed via the forwarder`);
127
- }
128
- else {
129
- log.warn(`[fastagent] FASTAGENT_WAKE_SECRET is not set — wake-ups fire only while a session is awake ` +
130
- `(redeploy with the current template to fix)`);
131
- }
132
- }
133
- const schedules = await startSchedules(agentDir, traced, stateRoot, config.selfSchedule ?? false, {
134
- externalClock: agentcore,
135
- });
136
- let routes = withControl.routes;
137
- if (agentcore) {
138
- // The lazy channel surface the adapter resolves post-restore. Control routes ride along so a
139
- // forwarder-relayed /control/* request dispatches the same as on a direct host. Long-connection
140
- // channels cannot serve here — scale-to-zero severs a resident connection and nothing
141
- // re-establishes it — so their presence is a configuration error, surfaced per envelope and by
142
- // the deploy driver's health probe (there is no boot to fail on this host).
143
- const lazyChannels = async () => {
144
- const surface = await routesFor(agentDir, traced, stateRoot, sessionControl, { builtinInvoke: false });
145
- if (surface.longConnections.length > 0) {
146
- throw new Error(`long-connection channel(s) ${surface.longConnections.map((c) => c.name).join(", ")} cannot serve on ` +
147
- `AgentCore (scale-to-zero severs resident connections) — use the channel's webhook form`);
148
- }
149
- // mountSessionControl's PATH-level collision rule, re-asserted here: with no channels at boot
150
- // its own check ran against an empty base, and a spread merge would silently let control win —
151
- // but a channel on /control/* is the same configuration error it is on every other host.
152
- const controlPaths = new Set(Object.keys(withControl.routes).map((key) => parseRouteKey(key).path));
153
- const collisions = Object.keys(surface.routes).filter((key) => controlPaths.has(parseRouteKey(key).path));
154
- if (collisions.length > 0) {
155
- throw new Error(`channel route(s) ${collisions.map((key) => `"${key}"`).join(", ")} collide with the session control ` +
156
- `plane — rename the channel route or disable sessionControl in fastagent.config`);
157
- }
158
- return { ...surface.routes, ...withControl.routes };
159
- };
160
- try {
161
- routes = mountAgentcore(routes, { agent: traced, stateRoot, schedules, onStateReady, lazyChannels });
162
- }
163
- catch (e) {
164
- failStartup(e);
165
- }
166
- log.info(`[fastagent] agentcore: serving POST /invocations + GET /ping (FASTAGENT_AGENTCORE=1)`);
167
- }
168
- serve({
169
- ...(routed ?? { longConnections: [], routeChannels: [], builtinInvoke: false, markReady() { } }),
170
- routes,
171
- }, { port: portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, host }, (p) => {
172
- withControl.announce(p);
173
- maybeTunnel(agentDir, routed?.routeChannels ?? [], p, opts.tunnel ?? false, stateRoot);
75
+ const control = { tunnel: opts.tunnel ?? false, ...(host !== undefined ? { host } : {}) };
76
+ // ONE branch for the whole posture. AgentCore assembles differently — lazy channels over a
77
+ // pre-restore state mount, an external clock, no resident connections — but it yields the same
78
+ // AgentService, so everything below this point is common.
79
+ // The wake-alarm sink is a PROCESS-global (schedule/wakeups.ts) — this process IS the AgentCore
80
+ // deployment, so the process entry installs it. A service can be closed; a global cannot be
81
+ // handed to something that can, without inventing an ownership the singleton does not have.
82
+ const onStateReady = isAgentcoreRuntime() && config.selfSchedule ? armWakeAlarms(stateRoot) : undefined;
83
+ const service = await (isAgentcoreRuntime()
84
+ ? mountAgentcoreService(opened, { wrapAgent: () => traced, control, onStateReady })
85
+ : mountAgentService(opened, {
86
+ wrapAgent: () => traced,
87
+ closeTimeoutMs: SHUTDOWN_GRACE_MS,
88
+ control,
89
+ onChannelClosed: (name, error) => failStartup(new Error(`${name} ${error === undefined ? "closed unexpectedly" : `failed: ${String(error)}`}`)),
90
+ })).catch(failStartup);
91
+ serve(service.handler, { port: portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, host }, {
92
+ ready: service.ready,
93
+ onListening: (p) => {
94
+ reportServing(service, host, p);
95
+ service.announce(p);
96
+ maybeTunnel(agentDir, service.channels.routes, p, opts.tunnel ?? false, stateRoot);
97
+ },
98
+ onShutdown: () => service.close(),
174
99
  });
175
100
  // No graceful drain: webhook turns run fire-and-forget; SIGTERM just exits mid-turn. Whether an
176
101
  // in-flight turn is LOST depends on the channel: the Telegram channel persists turn intent pre-ACK
177
102
  // and replays it next start (turn-store.ts, L1 durable execution, at-least-once); HTTP and other
178
103
  // channels have no such layer, so their in-flight turns are still lost (the asker re-invokes).
179
104
  }
105
+ /** The opener, kept as its own call so `opened` can also feed the shared assembly below. */
106
+ function openStartDir(dir, opts, sessionsDir) {
107
+ return createPiAgentFromDir(dir, {
108
+ model: opts.model,
109
+ sessionsDir,
110
+ authPath: opts.authPath,
111
+ serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
112
+ }).catch(failStartup);
113
+ }
180
114
  /**
181
115
  * Materialize `FASTAGENT_AUTH_SEED` (base64 of an auth.json, set by `deploy --run`) into the
182
116
  * writable secrets dir ONCE — only when the seed is set AND the auth file is absent, so a refreshed
@@ -189,7 +123,32 @@ async function maybeSeedAuth(authPath) {
189
123
  const bytes = authSeedBytes(collectAuthSeed(process.env), await exists(authPath));
190
124
  if (!bytes)
191
125
  return;
192
- await mkdir(dirname(authPath), { recursive: true });
193
- await writeFile(authPath, bytes);
126
+ await ensureSecretsDir(dirname(authPath));
127
+ // Same spelling as the credential store: the mode is applied before the content is reachable, so
128
+ // the seed can never be published at some other file's looser permissions. It does NOT close the
129
+ // gap between the `exists` check and the write — the rename would overwrite a file created in
130
+ // between, against this function's absent-only promise. That window needs two `start` processes on
131
+ // one secrets dir, which no supported deployment runs (one container, one volume).
132
+ writeFileAtomic(authPath, bytes, SECRET_FILE_MODE);
194
133
  log.info(`[fastagent] seeded ${authPath} from FASTAGENT_AUTH_SEED (first boot)`);
195
134
  }
135
+ /**
136
+ * Install the wake-ALARM sink before the scheduler starts — the boot wake pump may advance a
137
+ * recurring entry, and that save must already re-arm its alarm. Returns the post-restore reconcile:
138
+ * running it at boot would read the pre-restore state mount, see no pending wake-ups, and conclude
139
+ * there is nothing to re-arm.
140
+ */
141
+ function armWakeAlarms(stateRoot) {
142
+ const secret = process.env.FASTAGENT_WAKE_SECRET;
143
+ if (!secret) {
144
+ log.warn(`[fastagent] FASTAGENT_WAKE_SECRET is not set — wake-ups fire only while a session is awake ` +
145
+ `(redeploy with the current template to fix)`);
146
+ return undefined;
147
+ }
148
+ const sink = createWakeAlarmSink({ secret });
149
+ setWakeupsSink(sink);
150
+ log.info(`[fastagent] wake alarms: EventBridge-backed via the forwarder`);
151
+ // Boot reconcile: pending wake-ups may exist while their alarms were lost (a deploy replaced the
152
+ // forwarder, a sink call failed). The sink re-reads the store, so a bare notification is enough.
153
+ return () => sink(stateRoot);
154
+ }
@@ -4,7 +4,7 @@ import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
4
4
  import { loadDotEnv } from "../../env.js";
5
5
  import { loadConfig } from "../../engines/pi/config.js";
6
6
  import { resolveAgentTools } from "../../engines/pi/create.js";
7
- import { reportModuleLoadFailures } from "../../engines/pi/report.js";
7
+ import { reportModuleLoadFailures } from "../../log.js";
8
8
  import { turnContext } from "../../engines/pi/tool-context.js";
9
9
  import { failStartup, failUsage, placementOrExit } from "../fail.js";
10
10
  export async function runTool(name, argsJson, dirArg) {
@@ -14,10 +14,10 @@ export async function runTool(name, argsJson, dirArg) {
14
14
  const { agentDir, workspace } = placementOrExit(resolve(dirArg));
15
15
  loadDotEnv(agentDir); // a tool may read a key from .env
16
16
  const { config } = await loadConfig(agentDir).catch(failStartup);
17
- // The same tool set dev/start mount (defaults + config.tools + discovered, deduped), so the runner
17
+ // The same tool set dev/start mount (all coding tools + config.tools + discovered, deduped), so the runner
18
18
  // exercises exactly what gets served — a shadowed tool is surfaced, not silently run. Resolve the
19
19
  // placement like the openers, so `fastagent tool` finds the SAME tools/ as dev/start.
20
- const { tools, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir).catch(failStartup);
20
+ const { tools, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir, workspace).catch(failStartup);
21
21
  for (const c of toolCollisions) {
22
22
  console.error(`[fastagent] warn: tool "${c.name}" (${c.source}) is shadowed by a default/config tool — not mounted`);
23
23
  }
@@ -26,10 +26,8 @@ export async function runTool(name, argsJson, dirArg) {
26
26
  if (!tool) {
27
27
  failStartup(new Error(`unknown tool "${name}". available: ${tools.map((t) => t.name).join(", ") || "(none)"}`));
28
28
  }
29
- // Two contexts, because the two tool families read different ones and this command must serve both
30
- // exactly as serving does: fastagent's own tools take cwd/session/activation from `turnContext`
31
- // (AsyncLocalStorage), and pi's default coding tools take the ExecutionEnv from the harness's TOOL
32
- // context — which no harness supplies here, so this stands in for it, rooted at the same workspace.
29
+ // Authored tools read cwd from turnContext; coding tools are already rooted at the workspace. Keep
30
+ // the fifth-argument env for lower-level MountedTools that explicitly consume it.
33
31
  const env = new NodeExecutionEnv({ cwd: workspace });
34
32
  const result = await turnContext
35
33
  .run({ cwd: workspace }, () => tool.execute(`cli-${name}`, args, undefined, undefined, { env }))
@@ -171,7 +171,7 @@ const tool = {
171
171
  DIR_ARG,
172
172
  ],
173
173
  examples: [{ cmd: `fastagent tool add '{"a":2,"b":3}'` }],
174
- notes: "Mounts the same tool set dev/start serve (defaults + config.tools + discovered tools/, " +
174
+ notes: "Mounts the same tool set dev/start serve (all coding tools + config.tools + discovered tools/, " +
175
175
  "deduped), so a shadowed or broken tool is surfaced here exactly as it would be when serving.",
176
176
  run: async (args) => (await import("./commands/tool.js")).runTool(args[0], args[1], args[2]),
177
177
  };
@@ -1,59 +1,5 @@
1
- import type { Agent } from "../agent.ts";
2
- import { type LoadedLongConnectionChannel } from "../engines/pi/channel.ts";
3
- import { type Routes } from "../host/node.ts";
4
- import type { LoadedSchedule } from "../schedule/schedule.ts";
5
- import type { SessionControl } from "../session.ts";
6
- export interface ServingSurface {
7
- routes: Routes;
8
- longConnections: LoadedLongConnectionChannel[];
9
- /** Route-channel basenames; the tunnel registers only this subset. */
10
- routeChannels: string[];
11
- builtinInvoke: boolean;
12
- /** Marks the built-in health route ready after every long-connection channel first connects. */
13
- markReady(): void;
14
- }
15
- /**
16
- * The surface this deployment serves: default `GET /health` plus discovered channels, or the default
17
- * POST `/invoke` only when neither a route nor a long-connection channel was declared.
18
- */
19
- export declare function routesFor(agentDir: string, agent: Agent, stateRoot: string, control?: SessionControl, options?: {
20
- builtinInvoke?: boolean;
21
- }): Promise<ServingSurface>;
22
- /**
23
- * Mount the session control plane (`/control/*`) when the agent enabled it
24
- * (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
25
- * writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
26
- * the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
27
- * its trust boundary, and each boot overwrites it with a fresh per-boot token. A user channel
28
- * colliding on `/control/*` fails startup — the same disposition as a channel-channel collision
29
- * (routesFor): `sessionControl` is an explicit opt-in, so declaring both is a configuration error,
30
- * and silently shadowing either side would serve a surface the author didn't write.
31
- */
32
- export declare function mountSessionControl(routes: Routes, control: SessionControl | undefined, stateRoot: string, options?: {
33
- tunnel?: boolean;
34
- agent?: Agent;
35
- host?: string;
36
- }): {
37
- routes: Routes;
38
- announce: (boundPort: number) => void;
39
- };
40
- /**
41
- * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
42
- * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
43
- * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
44
- * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
45
- * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
46
- */
47
- export declare function mountAgentcore(routes: Routes, options: {
48
- agent: Agent;
49
- stateRoot: string;
50
- schedules: LoadedSchedule[];
51
- onStateReady?: () => void;
52
- /** The serving path's LAZY channel surface: constructed by the adapter on the first envelope
53
- * AFTER the state-snapshot restore, never at boot (channels/agentcore.ts). When absent,
54
- * `routes` is the dispatch target — for wirings whose state root is already authoritative. */
55
- lazyChannels?: () => Promise<Routes>;
56
- }): Routes;
1
+ import type { ChannelHandler } from "../channel.ts";
2
+ import type { AgentService } from "../service.ts";
57
3
  /**
58
4
  * Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
59
5
  * (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
@@ -64,6 +10,12 @@ export declare function mountAgentcore(routes: Routes, options: {
64
10
  * came from config is broken runtime configuration (1).
65
11
  */
66
12
  export declare function assertTunnelBindable(host: string | undefined, tunnel: boolean, source: "flag" | "config"): void;
13
+ /**
14
+ * The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
15
+ * what mounted. One function because both commands must say the same thing at the same moment —
16
+ * after readiness, never at socket bind.
17
+ */
18
+ export declare function reportServing(service: AgentService, host: string | undefined, boundPort: number): void;
67
19
  /**
68
20
  * The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
69
21
  * ONE function because they are one message — they were two, and `--bind` updated the first while the
@@ -74,23 +26,23 @@ export declare function assertTunnelBindable(host: string | undefined, tunnel: b
74
26
  * still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
75
27
  */
76
28
  export declare function readyAddressLines(host: string | undefined, boundPort: number, builtinInvoke: boolean): string[];
29
+ /** What the CLI gives a service to stop in, and the hard exit that follows it. The order matters:
30
+ * a forced exit before the service answers would report a clean shutdown over a stuck channel. */
31
+ export declare const SHUTDOWN_GRACE_MS = 800;
77
32
  /**
78
- * Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
79
- * adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
80
- * the sole clean-shutdown command. `host` unset binds all interfaces.
33
+ * Bind HTTP and report ready — but only once the SERVICE is, which is not the same moment: a bound
34
+ * socket is not a serving agent while a declared long-connection channel is still dialling, so this
35
+ * awaits `hooks.ready` (mountAgentService owns the connections themselves) before announcing
36
+ * anything. Signals are the sole clean-shutdown command; `host` unset binds all interfaces.
81
37
  */
82
- export declare function serve(surface: ServingSurface, bind: {
38
+ export declare function serve(handler: ChannelHandler, bind: {
83
39
  port: number;
84
40
  host?: string;
85
- }, onListening?: (boundPort: number) => void): void;
41
+ }, hooks?: {
42
+ /** Awaited before anything is reported ready — see the listening handler. */
43
+ ready?: Promise<void>;
44
+ onListening?: (boundPort: number) => void;
45
+ onShutdown?: () => Promise<void> | void;
46
+ }): void;
86
47
  /** Start a Cloudflare tunnel for route channels only. */
87
48
  export declare function maybeTunnel(agentDir: string, routeChannels: string[], boundPort: number, tunnel: boolean, stateRoot?: string): void;
88
- /**
89
- * Load and start the agent's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
90
- * there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals. Returns the
91
- * loaded schedules so a serving surface that needs them (the AgentCore adapter's fire binding) shares
92
- * ONE load instead of re-discovering. `externalClock` (AgentCore) arms no resident cron timers.
93
- */
94
- export declare function startSchedules(agentDir: string, agent: Agent, stateRoot: string, selfSchedule: boolean, options?: {
95
- externalClock?: boolean;
96
- }): Promise<LoadedSchedule[]>;