@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
@@ -3,38 +3,23 @@
3
3
  * assemble + serve, restarting it on agent edits. A fresh process per reload means what is served
4
4
  * is always the latest code, including modules a tool/config imports.
5
5
  */
6
- import { resolve } from "node:path";
7
6
  import { runDevSupervisor } from "../../dev-supervisor.js";
8
- import { loadDotEnv } from "../../env.js";
9
- import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
10
- import { reportModuleLoadFailures } from "../../log.js";
11
- import { CODING_TOOL_NAMES } from "../../engines/pi/create.js";
7
+ import { setLogLevel } from "../../log.js";
12
8
  import { createPiAgentFromDir } from "../../engines/pi/open.js";
13
9
  import { mountAgentService } from "../../service.js";
14
- import { setLogLevel } from "../../log.js";
15
10
  import { logAgentLoop } from "../../observe.js";
16
- import { installProxyFetch } from "../../proxy.js";
17
- import { workspaceHint } from "../../paths.js";
18
- import { bindAddress } from "../../bind.js";
19
- import { failStartup, placementOrExit } from "../fail.js";
20
- import { SHUTDOWN_GRACE_MS, assertTunnelBindable, maybeTunnel, reportServing, serve } from "../serve.js";
21
- import { parseBind, parsePort, reportAuth, reportLine, resolveFirstRunModel, reportWorkspaceHint } from "../shared.js";
11
+ import { failStartup } from "../fail.js";
12
+ import { assertTunnelBindable, cliMountOptions, resolveBindHost, serveService } from "../serve.js";
13
+ import { enterAgentCommand, parseBind, parsePort, reportAssembly } from "../shared.js";
22
14
  export async function runDev(dirArg, opts) {
23
- const dir = resolve(dirArg);
24
- const placement = placementOrExit(dir);
25
15
  setLogLevel("debug"); // dev posture: verbose, includes the debug turn trace (content) — supervisor and worker both
26
16
  const isWorker = process.env.FASTAGENT_DEV_WORKER === "1";
27
- // Pick a model interactively once, in the parent (both watch and --no-watch have a TTY); a spawned
28
- // watch worker inherits the choice via FASTAGENT_MODEL, so it must not prompt again. Load .env and
29
- // the proxy FIRST (as invoke/start do): the picker reads FASTAGENT_MODEL and provider keys from
30
- // .env, and getAuth's OAuth refresh must go through HTTPS_PROXY. The worker re-loads both in serveOnce.
31
- if (!isWorker) {
32
- loadDotEnv(placement.agentDir);
33
- installProxyFetch();
34
- await resolveFirstRunModel(placement.agentDir, opts);
35
- }
17
+ // The model is picked ONCE, in the parent process (a TTY; watch and --no-watch both); a spawned
18
+ // worker inherits the choice through FASTAGENT_MODEL and must not prompt even when the pick was
19
+ // cancelled.
20
+ const placement = await enterAgentCommand(dirArg, { ...opts, input: isWorker ? false : opts.input });
36
21
  if (isWorker || opts.watch === false) {
37
- await serveOnce(dir, opts);
22
+ await serveOnce(placement, opts);
38
23
  return;
39
24
  }
40
25
  parsePort(opts.port, "--port", "flag"); // flag-shape checks before spawning
@@ -44,64 +29,23 @@ export async function runDev(dirArg, opts) {
44
29
  await runDevSupervisor(placement, { tunnel: opts.tunnel ?? false });
45
30
  }
46
31
  /** Assemble the agent and serve it once (the dev worker; also the --no-watch path). */
47
- async function serveOnce(dir, opts) {
32
+ async function serveOnce(placement, opts) {
48
33
  const portFlag = parsePort(opts.port, "--port", "flag");
49
34
  const bindFlag = parseBind(opts.bind);
50
- loadDotEnv(placementOrExit(dir).agentDir);
51
- installProxyFetch();
52
- const a = await createPiAgentFromDir(dir, {
35
+ const tunnel = opts.tunnel ?? false;
36
+ const a = await createPiAgentFromDir(placement.workspace, {
53
37
  model: opts.model,
54
38
  authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
55
39
  serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
56
40
  }).catch(failStartup);
57
- reportLine("agent", a.agentDir);
58
- reportLine("workspace", a.workspace);
59
- reportWorkspaceHint(workspaceHint(a));
60
- reportLine("config", a.configPath ?? "(none)");
61
- reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
62
- await reportAuth(a.agentDir, a.modelSpec, a.authPath);
63
- reportAgentsSkillsTools(a);
64
- // Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev, gated
65
- // out in start (level info), keeping end-user content out of production logs. Wired in both postures.
66
- // `http.host` enters here the way the flag enters `parseBind` — through `bindAddress`, so a
67
- // configured `localhost` is an ADDRESS by the time anything binds, renders or dials it.
68
- const configured = a.config.http?.host;
69
- const host = bindFlag ?? (configured === undefined ? undefined : bindAddress(configured));
70
- assertTunnelBindable(host, opts.tunnel ?? false, bindFlag ? "flag" : "config");
41
+ // The same report `start` prints; `config:` is dev's own extra (see reportAssembly on the asymmetry).
42
+ await reportAssembly(a, { beforeModel: [["config", a.configPath ?? "(none)"]] });
43
+ const host = resolveBindHost(bindFlag, a.config.http?.host, tunnel);
71
44
  // The SAME assembly an embedder gets from `createAgentService` — channels, control plane,
72
45
  // schedules, long connections. `dev` opens the directory itself only because its startup report
73
- // prints the opened values before anything mounts.
74
- const service = await mountAgentService(a, {
75
- // Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev,
76
- // gated out in start (level info), keeping end-user content out of production logs.
77
- wrapAgent: logAgentLoop,
78
- closeTimeoutMs: SHUTDOWN_GRACE_MS,
79
- control: { tunnel: opts.tunnel ?? false, ...(host !== undefined ? { host } : {}) },
80
- onChannelClosed: (name, error) => failStartup(new Error(`${name} ${error === undefined ? "closed unexpectedly" : `failed: ${String(error)}`}`)),
81
- }).catch(failStartup);
82
- serve(service.handler, { port: portFlag ?? a.config.http?.port ?? 8787, host }, {
83
- ready: service.ready,
84
- onListening: (p) => {
85
- reportServing(service, host, p);
86
- service.announce(p);
87
- maybeTunnel(a.agentDir, service.channels.routes, p, opts.tunnel ?? false, a.stateRoot);
88
- },
89
- onShutdown: () => service.close(),
90
- });
91
- }
92
- /** The agents/skills/tools/collisions report lines. */
93
- function reportAgentsSkillsTools(a) {
94
- reportLine("context", a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
95
- if (a.definition.persona)
96
- reportLine("persona", "persona.md");
97
- reportLine("skills", a.definition.skills.map((s) => s.name).join(", ") || "(none)");
98
- reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
99
- if (a.toolNames.length > 0)
100
- reportLine("tools", a.toolNames.join(", "));
101
- if (a.deferredToolNames.length > 0) {
102
- reportLine("deferred", `${a.deferredToolNames.join(", ")} (activated via search_tools)`);
103
- }
104
- reportToolCollisions(a.toolCollisions);
105
- reportModuleLoadFailures(a.toolFailures);
106
- reportFindingsIfChanged(a.definition.dir, a.definition);
46
+ // prints the opened values before anything mounts. The turn trace (tool calls + reply) logs at
47
+ // debug level: shown here, gated out in start (level info), keeping end-user content out of
48
+ // production logs.
49
+ const service = await mountAgentService(a, cliMountOptions(logAgentLoop)).catch(failStartup);
50
+ serveService(service, { port: portFlag ?? a.config.http?.port ?? 8787, host }, { tunnel, agentDir: a.agentDir, stateRoot: a.stateRoot });
107
51
  }
@@ -4,23 +4,17 @@
4
4
  * the schedule's stable session (faithful to the served behavior). Does NOT advance the schedule's fire
5
5
  * state — a test run must never make the scheduler skip the real next run.
6
6
  */
7
- import { join, resolve } from "node:path";
8
- import { loadDotEnv } from "../../env.js";
7
+ import { join } from "node:path";
9
8
  import { displayPath } from "../../paths.js";
10
- import { reportModuleLoadFailures } from "../../log.js";
9
+ import { reportModuleLoadFailures } from "../../loader.js";
11
10
  import { createPiAgentFromDir } from "../../engines/pi/open.js";
12
11
  import { runInvokeStream } from "../invoke-stream.js";
13
- import { installProxyFetch } from "../../proxy.js";
14
12
  import { loadSchedules } from "../../schedule/discover.js";
15
13
  import { scheduleSession } from "../../schedule/scheduler.js";
16
- import { failStartup, placementOrExit } from "../fail.js";
17
- import { reportAuth, resolveFirstRunModel } from "../shared.js";
14
+ import { failStartup } from "../fail.js";
15
+ import { enterAgentCommand, reportAuth } from "../shared.js";
18
16
  export async function runFire(name, dirArg, opts) {
19
- const fireDir = resolve(dirArg);
20
- const placement = placementOrExit(fireDir);
21
- loadDotEnv(placement.agentDir);
22
- installProxyFetch();
23
- await resolveFirstRunModel(placement.agentDir, opts);
17
+ const placement = await enterAgentCommand(dirArg, opts);
24
18
  // Schedules are agent surface — discover them where dev/start/`schedule list` do (the agent
25
19
  // dir), so `fire` sees the same set the scheduler serves.
26
20
  const { schedules, failures } = await loadSchedules(placement.agentDir).catch(failStartup);
@@ -32,7 +26,7 @@ export async function runFire(name, dirArg, opts) {
32
26
  failStartup(new Error(`unknown schedule "${name}" (looked in ${displayPath(process.cwd(), join(placement.agentDir, "schedules")) ?? "schedules"}). ` +
33
27
  `available: ${schedules.map((s) => s.name).join(", ") || "(none)"}`));
34
28
  }
35
- const { agent, modelSpec, authPath } = await createPiAgentFromDir(fireDir, {
29
+ const { agent, modelSpec, authPath } = await createPiAgentFromDir(placement.workspace, {
36
30
  model: opts.model,
37
31
  authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
38
32
  }).catch(failStartup);
@@ -8,8 +8,8 @@ import { resolveStateRoot, workspaceHint } from "../../paths.js";
8
8
  import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
9
9
  import { loadAgentDefinition } from "../../engines/pi/definition.js";
10
10
  import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
11
- import { reportModuleLoadFailures } from "../../log.js";
12
11
  import { log } from "../../log.js";
12
+ import { reportModuleLoadFailures } from "../../loader.js";
13
13
  import { nextRun } from "../../schedule/cron.js";
14
14
  import { loadSchedules } from "../../schedule/discover.js";
15
15
  import { failStartup, placementOrExit } from "../fail.js";
@@ -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);
@@ -1,19 +1,12 @@
1
1
  /** `fastagent invoke <message> [dir]`: run ONE turn against the assembled agent, then exit. */
2
2
  import { randomUUID } from "node:crypto";
3
- import { resolve } from "node:path";
4
- import { loadDotEnv } from "../../env.js";
5
3
  import { createPiAgentFromDir } from "../../engines/pi/open.js";
6
4
  import { runInvokeStream } from "../invoke-stream.js";
7
- import { installProxyFetch } from "../../proxy.js";
8
- import { failStartup, placementOrExit } from "../fail.js";
9
- import { reportAuth, resolveFirstRunModel } from "../shared.js";
5
+ import { failStartup } from "../fail.js";
6
+ import { enterAgentCommand, reportAuth } from "../shared.js";
10
7
  export async function runInvoke(message, dirArg, opts) {
11
- const invokeDir = resolve(dirArg);
12
- const placement = placementOrExit(invokeDir);
13
- loadDotEnv(placement.agentDir);
14
- installProxyFetch();
15
- await resolveFirstRunModel(placement.agentDir, opts);
16
- const { agent, modelSpec, authPath } = await createPiAgentFromDir(invokeDir, {
8
+ const placement = await enterAgentCommand(dirArg, opts);
9
+ const { agent, modelSpec, authPath } = await createPiAgentFromDir(placement.workspace, {
17
10
  model: opts.model,
18
11
  authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
19
12
  }).catch(failStartup);
@@ -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 "../../log.js";
9
+ import { reportModuleLoadFailures } from "../../loader.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,67 +2,49 @@
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
- import { dirname, resolve } from "node:path";
7
- import { authSeedBytes, collectAuthSeed } from "../../deploy/fly/run.js";
8
- import { loadDotEnv } from "../../env.js";
5
+ import { dirname } from "node:path";
6
+ import { writeFileAtomic } from "../../atomic-write.js";
7
+ import { authSeedBytes, collectAuthSeed } from "../../deploy/secrets.js";
9
8
  import { resolveAuthPath, resolveSessionsDirOverride } from "../../engines/pi/config.js";
10
- import { resolveSecretsDir, workspaceHint } from "../../paths.js";
11
- import { isUnderDir } from "../../paths.js";
12
- import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
13
- import { reportModuleLoadFailures } from "../../log.js";
14
- import { CODING_TOOL_NAMES } from "../../engines/pi/create.js";
9
+ import { SECRET_FILE_MODE, ensureSecretsDir, resolveSecretsDir, isUnderDir, exists } from "../../paths.js";
10
+ import { log, setLogLevel } from "../../log.js";
15
11
  import { createPiAgentFromDir } from "../../engines/pi/open.js";
16
12
  import { mountAgentService } from "../../service.js";
17
- import { log, setLogLevel } from "../../log.js";
18
13
  import { logAgentLoop } from "../../observe.js";
19
- import { installProxyFetch } from "../../proxy.js";
20
- import { exists } from "../../paths.js";
21
- import { bindAddress } from "../../bind.js";
22
14
  import { isAgentcoreRuntime, mountAgentcoreService } from "../../channels/agentcore-service.js";
23
- import { createWakeAlarmSink, reconcileWakeAlarms } from "../../schedule/wake-alarm.js";
15
+ import { createWakeAlarmSink } from "../../schedule/wake-alarm.js";
24
16
  import { setWakeupsSink } from "../../schedule/wakeups.js";
25
- import { failStartup, placementOrExit } from "../fail.js";
26
- import { SHUTDOWN_GRACE_MS, assertTunnelBindable, maybeTunnel, reportServing, serve } from "../serve.js";
27
- import { parseBind, parsePort, reportAuth, reportLine, resolveFirstRunModel, reportWorkspaceHint } from "../shared.js";
17
+ import { failStartup } from "../fail.js";
18
+ import { cliMountOptions, resolveBindHost, serveService } from "../serve.js";
19
+ import { enterAgentCommand, parseBind, parsePort, reportAssembly } from "../shared.js";
28
20
  export async function runStart(dirArg, opts) {
29
- const dir = resolve(dirArg);
30
21
  // Flag validation first: a bad --port is a USAGE error (exit 2), and reporting it must not depend on
31
22
  // the directory being an agent (which is a runtime/environment failure, exit 1).
32
23
  const portFlag = parsePort(opts.port, "--port", "flag");
33
24
  const bindFlag = parseBind(opts.bind);
34
- const placement = placementOrExit(dir);
25
+ const tunnel = opts.tunnel ?? false;
35
26
  setLogLevel("info"); // production posture: info+, the debug turn trace (and its end-user content) gated out
36
- loadDotEnv(placement.agentDir);
37
- installProxyFetch();
38
- await resolveFirstRunModel(placement.agentDir, opts);
27
+ const placement = await enterAgentCommand(dirArg, opts);
39
28
  // A `deploy --run` may carry the operator's local credential as FASTAGENT_AUTH_SEED —
40
29
  // materialize it into the writable secrets dir BEFORE the opener resolves auth (once, absent-only).
41
30
  // Same resolveAuthPath the opener uses — ONE owner of the flag > env > default chain.
42
31
  await maybeSeedAuth(resolveAuthPath(placement.agentDir, opts.authPath));
43
32
  // The same opener dev uses (single assembly source), just no watch.
44
- const sessionsDirOverride = resolveSessionsDirOverride(opts.sessionsDir);
45
- const opened = await openStartDir(dir, opts, sessionsDirOverride);
46
- const { agent, definition, agentDir, workspace, config, modelSpec, stateRoot, sessionsDir, authPath, toolNames, deferredToolNames, toolCollisions, toolFailures, } = opened;
47
- reportLine("agent", agentDir);
48
- reportLine("workspace", workspace);
49
- reportWorkspaceHint(workspaceHint({ agentDir, workspace }));
50
- reportLine("model", `${modelSpec}${config.thinkingLevel ? ` (thinking: ${config.thinkingLevel})` : ""}`);
51
- await reportAuth(agentDir, modelSpec, authPath);
52
- reportLine("context", definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
53
- if (definition.persona)
54
- reportLine("persona", "persona.md");
55
- reportLine("skills", definition.skills.map((s) => s.name).join(", ") || "(none)");
56
- reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
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);
33
+ const opened = await createPiAgentFromDir(placement.workspace, {
34
+ model: opts.model,
35
+ sessionsDir: resolveSessionsDirOverride(opts.sessionsDir),
36
+ authPath: opts.authPath,
37
+ serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
38
+ }).catch(failStartup);
39
+ const { agent, agentDir, config, stateRoot, sessionsDir } = opened;
40
+ // The same report `dev` prints; `state:`/`sessions:` are start's own extras, and the persistence
41
+ // notes below are why (see reportAssembly on the asymmetry).
42
+ await reportAssembly(opened, {
43
+ afterTools: [
44
+ ["state", stateRoot],
45
+ ["sessions", sessionsDir],
46
+ ],
47
+ });
66
48
  // State defaults under the agent dir, which a redeploy may replace wholesale. Gate on where the
67
49
  // state root ACTUALLY resolved (inside the agent dir?), not on the raw env var: an empty
68
50
  // `FASTAGENT_STATE_DIR=""` reads as unset (resolveStateRoot) and still lands in-agent, so a raw
@@ -81,15 +63,9 @@ export async function runStart(dirArg, opts) {
81
63
  log.info(`[fastagent] note: secrets (.env, rotated auth.json) live under the definition dir; point ` +
82
64
  `FASTAGENT_SECRETS_DIR at a persistent volume so a redeploy that replaces the dir does not wipe them.`);
83
65
  }
84
- reportFindingsIfChanged(definition.dir, definition);
85
- // Same debug turn trace as dev; gated out here by the info level (see dev.ts serveOnce).
66
+ // Same debug turn trace as dev; gated out here by the info level.
86
67
  const traced = logAgentLoop(agent);
87
- // `http.host` enters here the way the flag enters `parseBind` — through `bindAddress`, so a
88
- // configured `localhost` is an ADDRESS by the time anything binds, renders or dials it.
89
- const configured = config.http?.host;
90
- const host = bindFlag ?? (configured === undefined ? undefined : bindAddress(configured));
91
- assertTunnelBindable(host, opts.tunnel ?? false, bindFlag ? "flag" : "config");
92
- const control = { tunnel: opts.tunnel ?? false, ...(host !== undefined ? { host } : {}) };
68
+ const host = resolveBindHost(bindFlag, config.http?.host, tunnel);
93
69
  // ONE branch for the whole posture. AgentCore assembles differently — lazy channels over a
94
70
  // pre-restore state mount, an external clock, no resident connections — but it yields the same
95
71
  // AgentService, so everything below this point is common.
@@ -98,22 +74,9 @@ export async function runStart(dirArg, opts) {
98
74
  // handed to something that can, without inventing an ownership the singleton does not have.
99
75
  const onStateReady = isAgentcoreRuntime() && config.selfSchedule ? armWakeAlarms(stateRoot) : undefined;
100
76
  const service = await (isAgentcoreRuntime()
101
- ? mountAgentcoreService(opened, { wrapAgent: () => traced, control, onStateReady })
102
- : mountAgentService(opened, {
103
- wrapAgent: () => traced,
104
- closeTimeoutMs: SHUTDOWN_GRACE_MS,
105
- control,
106
- onChannelClosed: (name, error) => failStartup(new Error(`${name} ${error === undefined ? "closed unexpectedly" : `failed: ${String(error)}`}`)),
107
- })).catch(failStartup);
108
- serve(service.handler, { port: portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, host }, {
109
- ready: service.ready,
110
- onListening: (p) => {
111
- reportServing(service, host, p);
112
- service.announce(p);
113
- maybeTunnel(agentDir, service.channels.routes, p, opts.tunnel ?? false, stateRoot);
114
- },
115
- onShutdown: () => service.close(),
116
- });
77
+ ? mountAgentcoreService(opened, { wrapAgent: () => traced, onStateReady })
78
+ : mountAgentService(opened, cliMountOptions(() => traced))).catch(failStartup);
79
+ serveService(service, { port: portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, host }, { tunnel, agentDir, stateRoot });
117
80
  // No graceful drain: webhook turns run fire-and-forget; SIGTERM just exits mid-turn. Whether an
118
81
  // in-flight turn is LOST depends on the channel: the Telegram channel persists turn intent pre-ACK
119
82
  // and replays it next start (turn-store.ts, L1 durable execution, at-least-once); HTTP and other
@@ -125,23 +88,19 @@ export async function runStart(dirArg, opts) {
125
88
  * volume copy is never clobbered by the stale seed. Lets a deploy carry the operator's local
126
89
  * OAuth/API credential so the box runs on the SAME subscription. No-op locally (the seed is unset).
127
90
  */
128
- /** The opener, kept as its own call so `opened` can also feed the shared assembly below. */
129
- function openStartDir(dir, opts, sessionsDir) {
130
- return createPiAgentFromDir(dir, {
131
- model: opts.model,
132
- sessionsDir,
133
- authPath: opts.authPath,
134
- serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
135
- }).catch(failStartup);
136
- }
137
91
  async function maybeSeedAuth(authPath) {
138
92
  // collectAuthSeed: the seed may arrive CHUNKED (FASTAGENT_AUTH_SEED + _2…) on hosts with a small
139
93
  // env-value max length (AgentCore); single-var hosts are unchanged.
140
94
  const bytes = authSeedBytes(collectAuthSeed(process.env), await exists(authPath));
141
95
  if (!bytes)
142
96
  return;
143
- await mkdir(dirname(authPath), { recursive: true });
144
- await writeFile(authPath, bytes);
97
+ await ensureSecretsDir(dirname(authPath));
98
+ // Same spelling as the credential store: the mode is applied before the content is reachable, so
99
+ // the seed can never be published at some other file's looser permissions. It does NOT close the
100
+ // gap between the `exists` check and the write — the rename would overwrite a file created in
101
+ // between, against this function's absent-only promise. That window needs two `start` processes on
102
+ // one secrets dir, which no supported deployment runs (one container, one volume).
103
+ writeFileAtomic(authPath, bytes, SECRET_FILE_MODE);
145
104
  log.info(`[fastagent] seeded ${authPath} from FASTAGENT_AUTH_SEED (first boot)`);
146
105
  }
147
106
  /**
@@ -160,5 +119,7 @@ function armWakeAlarms(stateRoot) {
160
119
  const sink = createWakeAlarmSink({ secret });
161
120
  setWakeupsSink(sink);
162
121
  log.info(`[fastagent] wake alarms: EventBridge-backed via the forwarder`);
163
- return () => reconcileWakeAlarms(stateRoot, sink);
122
+ // Boot reconcile: pending wake-ups may exist while their alarms were lost (a deploy replaced the
123
+ // forwarder, a sink call failed). The sink re-reads the store, so a bare notification is enough.
124
+ return () => sink(stateRoot);
164
125
  }
@@ -1,10 +1,9 @@
1
1
  /** `fastagent tool <name> '<json>' [dir]`: run one tool's body directly with JSON args — no model. */
2
2
  import { resolve } from "node:path";
3
- import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
4
3
  import { loadDotEnv } from "../../env.js";
5
4
  import { loadConfig } from "../../engines/pi/config.js";
6
5
  import { resolveAgentTools } from "../../engines/pi/create.js";
7
- import { reportModuleLoadFailures } from "../../log.js";
6
+ import { reportModuleLoadFailures } from "../../loader.js";
8
7
  import { turnContext } from "../../engines/pi/tool-context.js";
9
8
  import { failStartup, failUsage, placementOrExit } from "../fail.js";
10
9
  export async function runTool(name, argsJson, dirArg) {
@@ -26,12 +25,8 @@ export async function runTool(name, argsJson, dirArg) {
26
25
  if (!tool) {
27
26
  failStartup(new Error(`unknown tool "${name}". available: ${tools.map((t) => t.name).join(", ") || "(none)"}`));
28
27
  }
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.
31
- const env = new NodeExecutionEnv({ cwd: workspace });
32
- const result = await turnContext
33
- .run({ cwd: workspace }, () => tool.execute(`cli-${name}`, args, undefined, undefined, { env }))
34
- .catch(failStartup);
28
+ // Authored tools read cwd from turnContext; coding tools are already rooted at the workspace.
29
+ const result = await turnContext.run({ cwd: workspace }, () => tool.execute(`cli-${name}`, args)).catch(failStartup);
35
30
  const out = result?.details !== undefined
36
31
  ? result.details
37
32
  : (result?.content ?? []).map((c) => ("text" in c ? c.text : "")).join("");
@@ -24,8 +24,6 @@ interface ArgSpec {
24
24
  export interface FlagSpec {
25
25
  flags: string;
26
26
  description: string;
27
- /** Parses but does not appear in help — for retired flags that should still explain themselves. */
28
- hidden?: boolean;
29
27
  /** Mutually exclusive with these {@link optionKey} values — validated at build time. */
30
28
  conflicts?: string[];
31
29
  }
@@ -114,8 +114,6 @@ function register(parent, spec) {
114
114
  }
115
115
  for (const f of spec.flags ?? []) {
116
116
  const opt = new Option(f.flags, f.description);
117
- if (f.hidden)
118
- opt.hideHelp();
119
117
  if (f.conflicts)
120
118
  opt.conflicts(f.conflicts);
121
119
  cmd.addOption(opt);
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { fastagentVersion } from "../version.js";
8
8
  import { buildProgram } from "./kernel.js";
9
+ import { DEPLOY_HOSTS } from "../deploy/hosts.js";
9
10
  // Help groups (clig: most common commands first) — the authoring loop leads, operations close.
10
11
  // Shared flags — same name, same meaning, on every command that supports them (clig: consistency).
11
12
  const DIR_ARG = {
@@ -256,8 +257,6 @@ const start = {
256
257
  input: f.input !== false,
257
258
  }),
258
259
  };
259
- /** The retired app-creation flag — parsed so it can explain itself, hidden from help. */
260
- const CREATE_APP = { flags: "--create-app", description: "(retired)", hidden: true };
261
260
  const INGRESS = {
262
261
  flags: "--ingress <mode>",
263
262
  description: "Feishu/Lark ingress: websocket or webhook (interactive when omitted)",
@@ -280,18 +279,14 @@ const channelSub = (kind, summary, description, notes) => ({
280
279
  summary,
281
280
  description,
282
281
  args: [DIR_ARG],
283
- flags: [
284
- CREATE_APP,
285
- ...(kind === "feishu" || kind === "lark"
286
- ? [INGRESS, GROUP_BEHAVIOR]
287
- : kind === "slack"
288
- ? [GROUP_BEHAVIOR, NO_ONBOARD, REPLACE_CONFIG]
289
- : []),
290
- ],
282
+ flags: kind === "feishu" || kind === "lark"
283
+ ? [INGRESS, GROUP_BEHAVIOR]
284
+ : kind === "slack"
285
+ ? [GROUP_BEHAVIOR, NO_ONBOARD, REPLACE_CONFIG]
286
+ : [],
291
287
  examples: [{ cmd: `fastagent add ${kind}` }],
292
288
  ...(notes ? { notes } : {}),
293
289
  run: async (args, f) => (await import("./commands/add.js")).runAddChannel(kind, args[0], {
294
- createApp: f.createApp === true,
295
290
  ingress: f.ingress,
296
291
  groupBehavior: f.groupBehavior,
297
292
  onboard: f.onboard !== false,
@@ -357,7 +352,7 @@ const deploy = {
357
352
  "CloudFormation stack (AWS Bedrock AgentCore Runtime + forwarder Lambda for webhooks + " +
358
353
  "EventBridge rules for schedules; linux/arm64 image built locally). Durable ingress " +
359
354
  "remains operator-owned (agentcore's forwarder URL is the exception — the stack owns it).",
360
- args: [{ name: "<host>", description: "deploy target", choices: ["docker", "fly", "railway", "agentcore"] }, DIR_ARG],
355
+ args: [{ name: "<host>", description: "deploy target", choices: [...DEPLOY_HOSTS] }, DIR_ARG],
361
356
  flags: [
362
357
  {
363
358
  flags: "--run",
@@ -1,15 +1,5 @@
1
- import type { ChannelHandler } from "../channel.ts";
2
- import type { AgentService } from "../service.ts";
3
- /**
4
- * Mount the session control plane (`/control/*`) when the agent enabled it
5
- * (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
6
- * writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
7
- * the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
8
- * its trust boundary, and each boot overwrites it with a fresh per-boot token. A user channel
9
- * colliding on `/control/*` fails startup — the same disposition as a channel-channel collision
10
- * (routesFor): `sessionControl` is an explicit opt-in, so declaring both is a configuration error,
11
- * and silently shadowing either side would serve a surface the author didn't write.
12
- */
1
+ import type { Agent } from "../agent.ts";
2
+ import type { AgentService, MountAgentServiceOptions } from "../service.ts";
13
3
  /**
14
4
  * Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
15
5
  * (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
@@ -20,6 +10,32 @@ import type { AgentService } from "../service.ts";
20
10
  * came from config is broken runtime configuration (1).
21
11
  */
22
12
  export declare function assertTunnelBindable(host: string | undefined, tunnel: boolean, source: "flag" | "config"): void;
13
+ /**
14
+ * The bind address a serve uses: the flag, else `http.host` from the config — through `bindAddress`,
15
+ * so a configured `localhost` is an ADDRESS by the time anything binds, renders or dials it — checked
16
+ * against `--tunnel` with the exit code the SOURCE of the value earns (serve.ts assertTunnelBindable).
17
+ */
18
+ export declare function resolveBindHost(bindFlag: string | undefined, configured: string | undefined, tunnel: boolean): string | undefined;
19
+ /** What the CLI adds to the assembly: its shutdown grace, and exit on a connection that drops. */
20
+ export declare function cliMountOptions(wrapAgent: (agent: Agent) => Agent): MountAgentServiceOptions;
21
+ /**
22
+ * Bind, report, announce the control plane, open the tunnel, and close in order on a signal — the
23
+ * tail dev's worker and start share once the service is assembled.
24
+ */
25
+ export declare function serveService(service: AgentService, bind: {
26
+ port: number;
27
+ host?: string;
28
+ }, posture: {
29
+ tunnel: boolean;
30
+ agentDir: string;
31
+ stateRoot: string;
32
+ }): void;
33
+ /**
34
+ * The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
35
+ * what mounted. One function because both commands must say the same thing at the same moment —
36
+ * after readiness, never at socket bind.
37
+ */
38
+ export declare function reportServing(service: AgentService, host: string | undefined, boundPort: number): void;
23
39
  /**
24
40
  * The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
25
41
  * ONE function because they are one message — they were two, and `--bind` updated the first while the
@@ -29,29 +45,21 @@ export declare function assertTunnelBindable(host: string | undefined, tunnel: b
29
45
  * A wildcard bind is every interface, and naming one address there would understate it — but the curl
30
46
  * still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
31
47
  */
32
- /**
33
- * The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
34
- * what mounted. One function because both commands must say the same thing at the same moment —
35
- * after readiness, never at socket bind.
36
- */
37
- export declare function reportServing(service: AgentService, host: string | undefined, boundPort: number): void;
38
48
  export declare function readyAddressLines(host: string | undefined, boundPort: number, builtinInvoke: boolean): string[];
39
49
  /**
40
- * Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
41
- * adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
42
- * the sole clean-shutdown command. `host` unset binds all interfaces.
50
+ * Write `<stateRoot>/control.json` so a LOCAL client (`fastagent attach`) can find the control plane
51
+ * once the port is known, and say what reaches it. Returns the file's removal wired into the
52
+ * shutdown so a stale file cannot point a client at a dead port (`attach` then fails with "cannot
53
+ * read", accurate, instead of a stale token's misleading 401/ECONNREFUSED). A hard exit leaves the
54
+ * file behind: advisory, overwritten by the next boot.
55
+ *
56
+ * Here and not in the assembly: the file, its permissions and the warnings are how THIS process's
57
+ * operator finds and protects the plane. An embedder distributes `service.control` itself.
43
58
  */
44
- /** What the CLI gives a service to stop in, and the hard exit that follows it. The order matters:
45
- * a forced exit before the service answers would report a clean shutdown over a stuck channel. */
46
- export declare const SHUTDOWN_GRACE_MS = 800;
47
- export declare function serve(handler: ChannelHandler, bind: {
48
- port: number;
59
+ export declare function announceControl(control: {
60
+ token: string;
61
+ prefix: string;
62
+ } | undefined, stateRoot: string, bind: {
49
63
  host?: string;
50
- }, hooks?: {
51
- /** Awaited before anything is reported ready — see the listening handler. */
52
- ready?: Promise<void>;
53
- onListening?: (boundPort: number) => void;
54
- onShutdown?: () => Promise<void> | void;
55
- }): void;
56
- /** Start a Cloudflare tunnel for route channels only. */
57
- export declare function maybeTunnel(agentDir: string, routeChannels: string[], boundPort: number, tunnel: boolean, stateRoot?: string): void;
64
+ tunnel: boolean;
65
+ }, boundPort: number): () => void;