@fastagent-sh/fastagent 0.14.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 (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +258 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. package/dist/workspace.js +0 -45
@@ -1,15 +1,20 @@
1
1
  /**
2
- * The per-turn context a tool's `execute` can read (beyond its abort signal): the SESSION the current
3
- * turn runs in. A `defineTool` tool is built ONCE and reused across sessions, so the session can't be a
4
- * closure it rides an AsyncLocalStorage set around the harness turn (invoke.ts) and read inside
5
- * `execute` (tool.ts). Undefined outside a turn (e.g. `fastagent tool`, which runs a tool with no
6
- * session). This is what lets a tool know which conversation it is in — the mechanism the agent's
7
- * self-scheduling `wake` tool needs to fire a later turn back into the SAME session.
8
- *
9
- * Only `session` lives here (a per-turn runtime value). Deploy-time ambients a tool closes over at
10
- * build time (e.g. a stateRoot) do NOT belong here — pass them via the tool's own closure.
2
+ * Per-turn capabilities shared by every FastAgent-defined tool. A tool is built once and reused across
3
+ * turns, so current cwd/session/activation bindings ride AsyncLocalStorage rather than definition
4
+ * closures. Deploy-time ambients a tool closes over at build time do NOT belong here.
11
5
  */
12
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
+ import type { SessionTreeEntry } from "@earendil-works/pi-agent-core";
8
+ /** FastAgent's read-only port over the current conversation manager. Serving and chat adapt their
9
+ * different concrete session implementations to this one tool-runtime contract. */
10
+ export interface ReadonlySessionManager {
11
+ getSessionId(): string;
12
+ getHeader(): Promise<{
13
+ id: string;
14
+ timestamp: string;
15
+ }>;
16
+ getBranch(): Promise<SessionTreeEntry[]>;
17
+ }
13
18
  /**
14
19
  * The turn's tool-activation bridge — narrow closures over the CURRENT harness (invoke.ts builds it
15
20
  * per turn), so a loader tool can activate deferred tools mid-turn without tool.ts importing the
@@ -31,8 +36,10 @@ export interface ToolActivation {
31
36
  activate(names: string[]): Promise<string[]>;
32
37
  }
33
38
  export interface TurnContext {
34
- /** The session id of the current turn. */
35
- session: string;
39
+ /** Working directory for this execution. Falls back to process.cwd() only for an unbound direct call. */
40
+ cwd?: string;
41
+ /** Current conversation manager. Absent outside a FastAgent-managed agent turn. */
42
+ sessionManager?: ReadonlySessionManager;
36
43
  /** Tool activation for the current turn. Two producers, one consumer surface: invoke.ts bridges the
37
44
  * serving harness; chat.ts bridges pi's AgentSession (chat emulates deferral — same loader, same
38
45
  * semantics). Absent only outside any turn (a bare `fastagent tool` run). */
@@ -1,13 +1,7 @@
1
1
  /**
2
- * The per-turn context a tool's `execute` can read (beyond its abort signal): the SESSION the current
3
- * turn runs in. A `defineTool` tool is built ONCE and reused across sessions, so the session can't be a
4
- * closure it rides an AsyncLocalStorage set around the harness turn (invoke.ts) and read inside
5
- * `execute` (tool.ts). Undefined outside a turn (e.g. `fastagent tool`, which runs a tool with no
6
- * session). This is what lets a tool know which conversation it is in — the mechanism the agent's
7
- * self-scheduling `wake` tool needs to fire a later turn back into the SAME session.
8
- *
9
- * Only `session` lives here (a per-turn runtime value). Deploy-time ambients a tool closes over at
10
- * build time (e.g. a stateRoot) do NOT belong here — pass them via the tool's own closure.
2
+ * Per-turn capabilities shared by every FastAgent-defined tool. A tool is built once and reused across
3
+ * turns, so current cwd/session/activation bindings ride AsyncLocalStorage rather than definition
4
+ * closures. Deploy-time ambients a tool closes over at build time do NOT belong here.
11
5
  */
12
6
  import { AsyncLocalStorage } from "node:async_hooks";
13
7
  export const turnContext = new AsyncLocalStorage();
@@ -1,16 +1,14 @@
1
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
2
2
  import { z } from "zod";
3
3
  import { type ModuleLoadFailure } from "../../loader.ts";
4
- import { type ToolActivation } from "./tool-context.ts";
4
+ import { type ReadonlySessionManager, type ToolActivation } from "./tool-context.ts";
5
5
  export interface ToolContext {
6
+ /** Working directory for this execution. */
7
+ cwd: string;
6
8
  /** Abort signal for the current turn — honor it to cancel in-flight work on cancellation. */
7
9
  signal?: AbortSignal;
8
- /** The session id of the current turn which conversation this tool is running in. A general tool
9
- * capability: partition per-conversation data, tag logs, scope state. Undefined outside a turn (a bare
10
- * `fastagent tool` run, or any call with no session). (The built-in `wake` tool is one consumer — it
11
- * fires a later turn back into this same session.) In a `fastagent chat` turn this is pi's LOCAL
12
- * chat session id, not a served session — serving-coupled consumers like wake are not mounted there. */
13
- session?: string;
10
+ /** Current conversation manager. Present during serving/chat; absent for sessionless direct calls. */
11
+ sessionManager?: ReadonlySessionManager;
14
12
  /** Tool activation for the current turn (a loader tool activates {@link DefineToolOptions.deferred}
15
13
  * tools with it — the built-in `search_tools` is one consumer). Provided by both the serving path
16
14
  * (invoke.ts, over the harness) and chat (over pi's AgentSession); undefined only outside any turn
@@ -48,7 +46,7 @@ export declare function isDeferredTool(tool: AgentTool): boolean;
48
46
  /** The same tool without the deferred marker — for a loader that must stay active (a deferred loader
49
47
  * could never be activated and would strand every deferred tool). */
50
48
  export declare function stripDeferredMarker(tool: AgentTool): AgentTool;
51
- export declare function defineTool<I extends z.ZodType>(options: DefineToolOptions<I>): AgentTool;
49
+ export declare function defineTool<I extends z.ZodType>(options: DefineToolOptions<I>): FastagentTool;
52
50
  /** A discarded same-name tool (within `tools/`, or against an existing tool). Surfaced, never silent. */
53
51
  export interface ToolCollision {
54
52
  name: string;
@@ -11,6 +11,7 @@
11
11
  * });
12
12
  */
13
13
  import { join } from "node:path";
14
+ import { assertInsideAgentDir } from "../../paths.js";
14
15
  import { z } from "zod";
15
16
  import { loadModuleDir } from "../../loader.js";
16
17
  import { turnContext } from "./tool-context.js";
@@ -69,7 +70,12 @@ export function defineTool(options) {
69
70
  },
70
71
  }
71
72
  : undefined;
72
- const result = wrapResult(await options.execute(parsed.data, { signal, session: store?.session, tools }));
73
+ const result = wrapResult(await options.execute(parsed.data, {
74
+ cwd: store?.cwd ?? process.cwd(),
75
+ signal,
76
+ sessionManager: store?.sessionManager,
77
+ tools,
78
+ }));
73
79
  if (added.length > 0) {
74
80
  // A copy, not a mutation: wrapResult passes a full AgentToolResult through by REFERENCE, and an
75
81
  // author may legally return a shared/frozen result object — stamping in place would corrupt it
@@ -89,6 +95,9 @@ export function defineTool(options) {
89
95
  * `tools/` dir of its OWN scripts, which is exactly this case.
90
96
  */
91
97
  export async function loadTools(dir) {
98
+ // The same containment guard channels/schedules/skills get — and `tools/` is the one that gets
99
+ // IMPORTED AND EXECUTED, so a symlink escaping the agent dir is exactly what it must refuse.
100
+ await assertInsideAgentDir(dir, "tools");
92
101
  const { modules, failures } = await loadModuleDir(join(dir, "tools"));
93
102
  const byName = new Map();
94
103
  const collisions = [];
@@ -15,6 +15,3 @@ export declare function parseDelayMs(input: string | number): number | undefined
15
15
  export declare function withWakeTool(tools: AgentTool[], stateRoot: string, enabled: boolean): AgentTool[];
16
16
  /** Build the `wake` tool bound to `stateRoot` (where wake-ups persist). */
17
17
  export declare function makeWakeTool(stateRoot: string, now?: () => Date): AgentTool;
18
- /** Build the `unwake` tool: cancel one of THIS conversation's pending wake-ups by id (a wake/recurring
19
- * that is no longer needed). Session-scoped — a conversation can never cancel another's. */
20
- export declare function makeUnwakeTool(stateRoot: string): AgentTool;
@@ -2,7 +2,7 @@
2
2
  * The built-in `wake` tool: the agent's self-scheduling surface. Calling it records a one-shot wake-up
3
3
  * (wakeups.ts); the scheduler fires it back into the SAME session, so the agent resumes THIS
4
4
  * conversation after a delay ("check the deploy in 10 minutes"). The session comes from the turn
5
- * context (ToolContext.session, set around the harness turn); the state root is closed over at build
5
+ * context (ToolContext.sessionManager, set around the harness turn); the state root is closed over at build
6
6
  * time (where it is known — the workspace opener), never read from the turn.
7
7
  *
8
8
  * Mounted by the opener ONLY when `config.selfSchedule` is on AND on the serving path (`dev`/`start`, where
@@ -67,14 +67,15 @@ export function makeWakeTool(stateRoot, now = () => new Date()) {
67
67
  prompt: z.string().min(1).describe("the instruction for the woken turn (runs in this same conversation)"),
68
68
  }),
69
69
  execute(input, ctx) {
70
- if (!ctx.session)
70
+ const session = ctx.sessionManager?.getSessionId();
71
+ if (!session)
71
72
  return "wake is only available inside a conversation (there is no session to resume).";
72
73
  if ((input.in === undefined) === (input.cron === undefined)) {
73
74
  return "pass exactly one of `in` (one-shot) or `cron` (recurring).";
74
75
  }
75
76
  if (input.cron !== undefined) {
76
77
  // addWakeup validates the cron and DERIVES the first instant itself — one computation, one truth.
77
- const r = addWakeup(stateRoot, { session: ctx.session, prompt: input.prompt, cron: input.cron, tz: input.tz }, now());
78
+ const r = addWakeup(stateRoot, { session, prompt: input.prompt, cron: input.cron, tz: input.tz }, now());
78
79
  if (!r.ok)
79
80
  return r.error; // guardrail message the model can act on
80
81
  return `OK — recurring wake ${r.id} (cron "${input.cron}"${input.tz ? ` ${input.tz}` : ""}), first at ${r.fireAt}: ${input.prompt}. Use unwake({ id: "${r.id}" }) to stop it.`;
@@ -84,7 +85,7 @@ export function makeWakeTool(stateRoot, now = () => new Date()) {
84
85
  return `couldn't parse "in" (${JSON.stringify(input.in)}) — use a unit like "30m" / "2h" / "1d", or a number of seconds (a bare number as text like "120" is rejected).`;
85
86
  }
86
87
  const at = new Date(now().getTime() + ms);
87
- const r = addWakeup(stateRoot, { session: ctx.session, prompt: input.prompt, fireAt: at }, now());
88
+ const r = addWakeup(stateRoot, { session, prompt: input.prompt, fireAt: at }, now());
88
89
  if (!r.ok)
89
90
  return r.error; // guardrail message the model can act on
90
91
  return `OK — I'll wake up at ${r.fireAt} (id ${r.id}) to: ${input.prompt}. Use unwake({ id: "${r.id}" }) if it becomes unnecessary.`;
@@ -93,16 +94,17 @@ export function makeWakeTool(stateRoot, now = () => new Date()) {
93
94
  }
94
95
  /** Build the `unwake` tool: cancel one of THIS conversation's pending wake-ups by id (a wake/recurring
95
96
  * that is no longer needed). Session-scoped — a conversation can never cancel another's. */
96
- export function makeUnwakeTool(stateRoot) {
97
+ function makeUnwakeTool(stateRoot) {
97
98
  return defineTool({
98
99
  name: "unwake",
99
100
  description: "Cancel one of YOUR pending wake-ups (one-shot or recurring) by the id `wake` returned. Use it when " +
100
101
  "a scheduled follow-up is no longer needed — especially to stop a recurring wake once its job is done.",
101
102
  input: z.object({ id: z.string().min(1).describe("the wake-up id (returned by `wake`)") }),
102
103
  execute(input, ctx) {
103
- if (!ctx.session)
104
+ const session = ctx.sessionManager?.getSessionId();
105
+ if (!session)
104
106
  return "unwake is only available inside a conversation.";
105
- return removeWakeup(stateRoot, input.id, ctx.session)
107
+ return removeWakeup(stateRoot, input.id, session)
106
108
  ? `OK — wake-up ${input.id} cancelled.`
107
109
  : `no pending wake-up ${input.id} in this conversation (already fired, or not yours).`;
108
110
  },
package/dist/env.d.ts CHANGED
@@ -16,9 +16,21 @@ export declare function loadEnvFile(file: string): void;
16
16
  * anything else reading/deciding on .env content (e.g. `add`'s secret pre-fill) must use this, never a
17
17
  * private re-implementation: two parsers of one dialect diverge silently. */
18
18
  export declare function parseEnvContent(content: string): Map<string, string>;
19
+ /** The agent's `.env` file: `<resolved secrets dir>/.env` — default `<agentDir>/.secrets/.env`,
20
+ * moved together with auth.json by `FASTAGENT_SECRETS_DIR` ({@link resolveSecretsDir} in the neutral
21
+ * paths.ts). THE path every reader/writer of the agent's .env must use, so "where do secrets
22
+ * live" cannot diverge across commands. The file's OWN location resolves from the REAL environment:
23
+ * commands locate + load `.env` first, so a `FASTAGENT_SECRETS_DIR` set INSIDE it still relocates
24
+ * auth.json but cannot move the file it is read from. */
25
+ export declare function dotEnvPath(agentDir: string, env?: NodeJS.ProcessEnv): string;
26
+ /** The committable template: `<agentDir>/.secrets/.env.example` — deliberately NOT moved by
27
+ * `FASTAGENT_SECRETS_DIR`: it is authored agent surface that travels with the directory (the
28
+ * scaffolded `.secrets/.gitignore` un-ignores exactly it), while the real values follow the override. */
29
+ export declare function envExamplePath(agentDir: string): string;
19
30
  /**
20
- * Load `<dir>/.env` into `process.env` ({@link loadEnvFile}), treating a MISSING file as normal (no .env)
21
- * — the workspace-facing entry every command + the tunnel use. Only ENOENT is swallowed; any other read
22
- * error (a corrupt/unreadable file) propagates, so a real problem surfaces instead of silently skipping.
31
+ * Load the agent's `.env` ({@link dotEnvPath}) into `process.env` ({@link loadEnvFile}), treating a
32
+ * MISSING file as normal (no .env) — the agent-facing entry every command + the tunnel use. `agentDir`
33
+ * is the AGENT DIR (resolvePlacement().agentDir). Only ENOENT is swallowed; any other read error (a
34
+ * corrupt/unreadable file) propagates, so a real problem surfaces instead of silently skipping.
23
35
  */
24
- export declare function loadDotEnv(dir: string): void;
36
+ export declare function loadDotEnv(agentDir: string): void;
package/dist/env.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { log } from "./log.js";
5
+ import { SECRETS_DIRNAME, resolveSecretsDir } from "./paths.js";
3
6
  /**
4
7
  * Load a `.env` file into `process.env`, matching Node's `--env-file` / `process.loadEnvFile` precedence
5
8
  * on BOTH axes (verified against Node): a real env var wins over the file (an already-set key is kept),
@@ -41,17 +44,52 @@ export function parseEnvContent(content) {
41
44
  }
42
45
  return parsed;
43
46
  }
47
+ /** The agent's `.env` file: `<resolved secrets dir>/.env` — default `<agentDir>/.secrets/.env`,
48
+ * moved together with auth.json by `FASTAGENT_SECRETS_DIR` ({@link resolveSecretsDir} in the neutral
49
+ * paths.ts). THE path every reader/writer of the agent's .env must use, so "where do secrets
50
+ * live" cannot diverge across commands. The file's OWN location resolves from the REAL environment:
51
+ * commands locate + load `.env` first, so a `FASTAGENT_SECRETS_DIR` set INSIDE it still relocates
52
+ * auth.json but cannot move the file it is read from. */
53
+ export function dotEnvPath(agentDir, env = process.env) {
54
+ return join(resolveSecretsDir(agentDir, env), ".env");
55
+ }
56
+ /** The committable template: `<agentDir>/.secrets/.env.example` — deliberately NOT moved by
57
+ * `FASTAGENT_SECRETS_DIR`: it is authored agent surface that travels with the directory (the
58
+ * scaffolded `.secrets/.gitignore` un-ignores exactly it), while the real values follow the override. */
59
+ export function envExamplePath(agentDir) {
60
+ return join(agentDir, SECRETS_DIRNAME, ".env.example");
61
+ }
44
62
  /**
45
- * Load `<dir>/.env` into `process.env` ({@link loadEnvFile}), treating a MISSING file as normal (no .env)
46
- * — the workspace-facing entry every command + the tunnel use. Only ENOENT is swallowed; any other read
47
- * error (a corrupt/unreadable file) propagates, so a real problem surfaces instead of silently skipping.
63
+ * Load the agent's `.env` ({@link dotEnvPath}) into `process.env` ({@link loadEnvFile}), treating a
64
+ * MISSING file as normal (no .env) — the agent-facing entry every command + the tunnel use. `agentDir`
65
+ * is the AGENT DIR (resolvePlacement().agentDir). Only ENOENT is swallowed; any other read error (a
66
+ * corrupt/unreadable file) propagates, so a real problem surfaces instead of silently skipping.
48
67
  */
49
- export function loadDotEnv(dir) {
68
+ export function loadDotEnv(agentDir) {
69
+ const path = dotEnvPath(agentDir);
50
70
  try {
51
- loadEnvFile(join(dir, ".env"));
71
+ loadEnvFile(path);
52
72
  }
53
73
  catch (error) {
54
74
  if (error.code !== "ENOENT")
55
75
  throw error;
56
76
  }
77
+ // A `.env` at the agent's root is the file habit puts there, and nothing reads it. Left silent, the
78
+ // symptom is a setting that appears configured and is not.
79
+ //
80
+ // Scoped to keys FASTAGENT itself reads, because that is the only case we can be sure about: an agent
81
+ // directory can be the author's repository too (the shape `--agent-dir .` exists for), where a root
82
+ // `.env` is their APPLICATION's — and "move the values" would break it. Nothing here can tell those
83
+ // apart, so the broader guess (warn whenever the agent has no env of its own) was a warning on every
84
+ // boot, with destructive advice, aimed at the population that never had the problem. Channel
85
+ // credentials are deliberately out of scope: a channel that cannot find its token reports that itself,
86
+ // and it is the one that knows the name.
87
+ const stray = join(agentDir, ".env");
88
+ if (stray === path || !existsSync(stray))
89
+ return;
90
+ const misplaced = [...parseEnvContent(readFileSync(stray, "utf8")).keys()].filter((k) => k.startsWith("FASTAGENT_"));
91
+ if (misplaced.length > 0) {
92
+ log.warn(`[fastagent] ${stray} is NOT read — it sets ${misplaced.join(", ")}, and this agent's env lives at ` +
93
+ `${path}; move those values there`);
94
+ }
57
95
  }
package/dist/feishu.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** `@fastagent-sh/fastagent/feishu` — the canonical Feishu (open.feishu.cn) bot-channel surface. */
2
- export { feishuChannel, defaultFeishuRoute, feishuEnvelope, type FeishuChannelOptions, type FeishuMessageEvent, type FeishuMessage, type FeishuRoute, type FeishuFailure, } from "./channels/feishu/feishu.ts";
2
+ export { feishuChannel, feishuWebSocketChannel, defaultFeishuRoute, feishuEnvelope, type FeishuChannelOptions, type FeishuWebSocketChannelOptions, type FeishuMessageEvent, type FeishuMessage, type FeishuRoute, type FeishuFailure, } from "./channels/feishu/feishu.ts";
package/dist/feishu.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** `@fastagent-sh/fastagent/feishu` — the canonical Feishu (open.feishu.cn) bot-channel surface. */
2
- export { feishuChannel, defaultFeishuRoute, feishuEnvelope, } from "./channels/feishu/feishu.js";
2
+ export { feishuChannel, feishuWebSocketChannel, defaultFeishuRoute, feishuEnvelope, } from "./channels/feishu/feishu.js";
@@ -1,23 +1,39 @@
1
1
  import type { Agent } from "../agent.ts";
2
+ import type { SessionControl } from "../session.ts";
2
3
  /** A mounted request handler (a channel's fetch, or a plain route like health). */
3
4
  export type ChannelHandler = (req: Request) => Response | Promise<Response>;
4
5
  /** This deployment's HTTP surface: route key → handler. Key is `"/path"` or `"METHOD /path"`. */
5
6
  export type Routes = Record<string, ChannelHandler>;
6
7
  /**
7
8
  * What the framework hands a channel at mount time: the assembled agent plus the resolved state ROOT
8
- * (absolute; `FASTAGENT_STATE_DIR` > `<dir>/.fastagent`). Channels derive their OWN durable home from
9
+ * (absolute; `FASTAGENT_STATE_DIR` > `<root>/.state`). Channels derive their OWN durable home from
9
10
  * it (`<stateRoot>/channels/<kind>/`) — they never anchor on `process.cwd()`. env is the OPERATOR
10
- * input plane; this context is how the resolved result reaches code (embedders without the workspace
11
+ * input plane; this context is how the resolved result reaches code (embedders without the agent
11
12
  * opener construct it explicitly).
12
13
  */
13
14
  export interface ChannelContext {
14
15
  agent: Agent;
15
16
  stateRoot: string;
17
+ /** The serving session-control hub, when the serve wires one (`config.sessionControl`). Channels
18
+ * use it for DISPATCH only (the user-facing stop command); observation stays on the data plane. */
19
+ control?: SessionControl;
16
20
  }
17
- /** A `channels/<name>.ts` default export: receives the mount context, returns the routes it mounts.
18
- * Adapters (`telegramChannel(opts)`, `githubChannel(opts)`) RETURN one of these, so user glue holds
19
- * only policy — the framework pipes `agent`/`stateRoot` to the adapter without transiting user code. */
21
+ /** A `channels/<name>.ts` route channel: receives mount context and returns its HTTP routes. */
20
22
  export type ChannelModule = (ctx: ChannelContext) => Routes;
23
+ /** One logical long connection's lifecycle. `ready` settles after its first usable connection — and
24
+ * when `signal` aborts before one exists it must still settle: resolution then means cancellation, not
25
+ * readiness (the server skips ready-side effects once the signal is aborted; it must never hang).
26
+ * `closed` resolves after abort-driven shutdown and rejects on a terminal connection failure. */
27
+ export interface LongConnection {
28
+ ready: Promise<void>;
29
+ closed: Promise<void>;
30
+ }
31
+ /** A long-connection channel is an explicit module object rather than an HTTP-route factory.
32
+ * The adapter owns reconnects and treats `signal` as its sole shutdown command. */
33
+ export interface LongConnectionChannelModule {
34
+ name: string;
35
+ connect(ctx: ChannelContext, signal: AbortSignal): LongConnection;
36
+ }
21
37
  /** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method). */
22
38
  export declare function parseRouteKey(key: string): {
23
39
  method?: string;
@@ -29,13 +45,14 @@ export declare function parseRouteKey(key: string): {
29
45
  */
30
46
  export declare function router(routes: Routes): ChannelHandler;
31
47
  /**
32
- * Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller close
33
- * it — no logging/signals/exit (the CLI owns those). `listening` resolves with the bound port (useful
34
- * for port 0) or rejects on a bind error.
48
+ * Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
49
+ * accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
50
+ * `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
35
51
  */
36
52
  export declare function serveNode(handler: ChannelHandler, options: {
37
53
  port: number;
38
54
  }): {
39
55
  listening: Promise<number>;
40
56
  close: () => Promise<void>;
57
+ closeAllConnections: () => void;
41
58
  };
package/dist/host/node.js CHANGED
@@ -32,9 +32,9 @@ export function router(routes) {
32
32
  };
33
33
  }
34
34
  /**
35
- * Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller close
36
- * it — no logging/signals/exit (the CLI owns those). `listening` resolves with the bound port (useful
37
- * for port 0) or rejects on a bind error.
35
+ * Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
36
+ * accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
37
+ * `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
38
38
  */
39
39
  export function serveNode(handler, options) {
40
40
  const server = createServer(nodeListener(async (req) => handler(req)));
@@ -46,5 +46,6 @@ export function serveNode(handler, options) {
46
46
  });
47
47
  });
48
48
  const close = () => new Promise((resolve, reject) => server.close((e) => (e ? reject(e) : resolve())));
49
- return { listening, close };
49
+ const closeAllConnections = () => server.closeAllConnections();
50
+ return { listening, close, closeAllConnections };
50
51
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./core.ts";
2
+ export * from "./session.ts";
2
3
  export * from "./pi.ts";
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@
2
2
  // Contract/channel-only consumers should prefer `@fastagent-sh/fastagent/core`; pi-specific consumers
3
3
  // may use `@fastagent-sh/fastagent/pi`. The root remains the supported all-in-one surface.
4
4
  export * from "./core.js";
5
+ export * from "./session.js";
5
6
  export * from "./pi.js";
package/dist/lark.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  /** `@fastagent-sh/fastagent/lark` — the Lark-international compatibility surface over the canonical
2
2
  * Feishu engine. Feishu tenants use `@fastagent-sh/fastagent/feishu`. */
3
- export { larkChannel, defaultLarkRoute, larkEnvelope, type LarkChannelOptions, type LarkMessageEvent, type LarkMessage, type LarkRoute, type LarkFailure, } from "./channels/lark/lark.ts";
3
+ export { larkChannel, larkWebSocketChannel, defaultLarkRoute, larkEnvelope, type LarkChannelOptions, type LarkWebSocketChannelOptions, type LarkMessageEvent, type LarkMessage, type LarkRoute, type LarkFailure, } from "./channels/lark/lark.ts";
package/dist/lark.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** `@fastagent-sh/fastagent/lark` — the Lark-international compatibility surface over the canonical
2
2
  * Feishu engine. Feishu tenants use `@fastagent-sh/fastagent/feishu`. */
3
- export { larkChannel, defaultLarkRoute, larkEnvelope, } from "./channels/lark/lark.js";
3
+ export { larkChannel, larkWebSocketChannel, defaultLarkRoute, larkEnvelope, } from "./channels/lark/lark.js";
package/dist/loader.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** Whether `name` is an importable workspace module (a discovery candidate, not a type declaration). */
1
+ /** Whether `name` is an importable agent module (a discovery candidate, not a type declaration). */
2
2
  export declare function isModuleFile(name: string): boolean;
3
3
  export interface DiscoveredModule {
4
4
  /** Basename without extension — the authoritative name for tools/channels. */
@@ -10,7 +10,7 @@ export interface DiscoveredModule {
10
10
  default?: unknown;
11
11
  };
12
12
  }
13
- /** A workspace module that failed to load, surfaced as data so its caller can report the exact file.
13
+ /** An agent module that failed to load, surfaced as data so its caller can report the exact file.
14
14
  * `loadModuleDir` fills it for import failures; domain loaders add validation failures. The caller owns
15
15
  * policy: tools/schedules may skip one bad file, while serving treats a broken declared channel as fatal. */
16
16
  export interface ModuleLoadFailure {
package/dist/loader.js CHANGED
@@ -10,7 +10,7 @@ import { readdir } from "node:fs/promises";
10
10
  import { basename, extname, join } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
12
  const MODULE_EXTS = new Set([".ts", ".js", ".mjs"]);
13
- /** Whether `name` is an importable workspace module (a discovery candidate, not a type declaration). */
13
+ /** Whether `name` is an importable agent module (a discovery candidate, not a type declaration). */
14
14
  export function isModuleFile(name) {
15
15
  return MODULE_EXTS.has(extname(name)) && !name.endsWith(".d.ts");
16
16
  }
@@ -60,10 +60,10 @@ export async function loadModuleDir(subDir) {
60
60
  */
61
61
  export function moduleLoadHint(error) {
62
62
  if (error.code === "ERR_MODULE_NOT_FOUND" || /Cannot find (package|module)/.test(error.message)) {
63
- return "\n (a dependency is not installed — run `npm install` in the workspace)";
63
+ return "\n (a dependency is not installed — run `npm install` in the agent dir)";
64
64
  }
65
65
  if (/import statement outside a module|Unexpected token 'export'|ERR_REQUIRE_ESM/.test(error.message)) {
66
- return '\n (this workspace must be ESM — set "type": "module" in package.json)';
66
+ return '\n (the agent dir must be ESM — set "type": "module" in package.json)';
67
67
  }
68
68
  return "";
69
69
  }
package/dist/log.d.ts CHANGED
@@ -15,7 +15,7 @@ export interface Logger {
15
15
  warn(msg: string): void;
16
16
  error(msg: string): void;
17
17
  }
18
- /** A standalone logger over an explicit sink — used in tests to assert level gating without the singleton. */
18
+ /** An embedded logger over an explicit sink — used in tests to assert level gating without the singleton. */
19
19
  export declare function createLogger(opts: {
20
20
  level: LogLevel;
21
21
  sink?: (line: string) => void;
package/dist/log.js CHANGED
@@ -11,7 +11,7 @@
11
11
  const ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
12
12
  const isLevel = (s) => s in ORDER;
13
13
  const format = (level, msg) => `${level.toUpperCase().padEnd(5)} ${msg}`;
14
- /** A standalone logger over an explicit sink — used in tests to assert level gating without the singleton. */
14
+ /** An embedded logger over an explicit sink — used in tests to assert level gating without the singleton. */
15
15
  export function createLogger(opts) {
16
16
  const sink = opts.sink ?? ((line) => console.error(line));
17
17
  const make = (level) => (msg) => {
package/dist/observe.js CHANGED
@@ -28,6 +28,9 @@ export function logAgentLoop(agent, sink = (line) => log.debug(line)) {
28
28
  else if (e.type === "tool_ended") {
29
29
  sink(`[agent] tool ${e.isError ? "✗" : "✓"} ${toolName.get(e.id) ?? e.id} → ${preview(e.content)}`);
30
30
  }
31
+ else if (e.type === "retrying") {
32
+ sink(`[agent] retrying (${e.attempt}/${e.maxAttempts} in ${e.delayMs}ms): ${oneLine(e.reason)}`);
33
+ }
31
34
  else if (e.type === "completed") {
32
35
  if (thinking.trim() !== "")
33
36
  sink(`[agent] thinking: ${oneLine(thinking)}`);
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The directory name `init` gives a nested agent (`<workspace>/fastagent/`) unless `--agent-dir` names
3
+ * another. A DEFAULT, not a rule: resolution reads the config marker and never a name, so renaming an
4
+ * agent directory changes nothing about how it resolves. Visible on purpose — the agent directory holds
5
+ * the AUTHOR's content (persona, skills, tool code: code, not tool configuration), so it follows the
6
+ * repo convention for code (a plain directory), while fastagent's own machinery inside it (`.secrets/`,
7
+ * `.state/`) keeps the dot prefix.
8
+ */
9
+ export declare const DEFAULT_AGENT_DIRNAME = "fastagent";
10
+ /** The user-global machinery home under `$HOME` — hidden, per the dotfile convention for per-user
11
+ * tool homes (`~/.cargo`, `~/.docker`); unrelated to {@link DEFAULT_AGENT_DIRNAME}, which only names
12
+ * what `init` creates.
13
+ * It carries the same shape inside it as an agent dir does (`~/.fastagent/.secrets/auth.json`), so the
14
+ * resolvers below need no special case: `login` outside any agent simply hands them this directory. */
15
+ export declare const GLOBAL_HOME_DIR = ".fastagent";
16
+ /** The secrets segment inside an agent dir (or the global home): every PATH fastagent resolves —
17
+ * `.env`, `.env.example`, auth.json, the scaffold's write — derives from it, so they cannot drift
18
+ * apart. `FASTAGENT_SECRETS_DIR` relocates the RESOLVED dir ({@link resolveSecretsDir}), never this
19
+ * name. The scaffold's ignore templates are real files the author owns from `init` on, so they spell
20
+ * their rules out as literal text — renaming this constant means editing them too. */
21
+ export declare const SECRETS_DIRNAME = ".secrets";
22
+ /** The state segment inside an agent dir — same rule and same template caveat as {@link SECRETS_DIRNAME}. */
23
+ export declare const STATE_DIRNAME = ".state";
24
+ /** The config filenames, in load precedence. ONE source: the loader (below) and `scaffoldAgent`'s
25
+ * already-an-agent refusal both read this, so "is there a config?" can't diverge between them. */
26
+ export declare const AGENT_CONFIG_NAMES: readonly ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
27
+ export interface ResolvedPlacement {
28
+ /** The AGENT directory — where the definition (persona.md/skills/tools/channels/schedules), the
29
+ * config, and the machinery dirs (`.secrets/`, `.state/`) live. Absolute. */
30
+ agentDir: string;
31
+ /** The WORKSPACE — what the agent works ON: its cwd, and the start of the ② context walk. ALWAYS the
32
+ * directory fastagent was pointed at, which makes it the agent dir's PARENT when the agent was found
33
+ * one level down, and the agent dir ITSELF when you aimed straight at it. Absolute.
34
+ * `agentDir === workspace` is the only discriminant — there is no mode field, because there are no
35
+ * two placements to distinguish: there is one lookup and the directory you gave it. The naming
36
+ * follows git: the repository sits at the root of its working tree, and WORK belongs to the tree. */
37
+ workspace: string;
38
+ }
39
+ /**
40
+ * The agents `dir` resolves over: ITSELF when it holds a config, else the ones directly inside it —
41
+ * never both, because aiming at an agent can only mean that agent. Exported for `init`, which asks the
42
+ * same question this lookup asks: which agents would `dir` resolve over, before and after scaffolding.
43
+ */
44
+ export declare function agentsAt(dir: string): string[];
45
+ /**
46
+ * The one-line hint for "you pointed at the agent, but the project around it is what you meant" — or
47
+ * undefined. The workspace being whatever you aimed at is deliberate (a deployed box may hold nothing
48
+ * but the agent), and the cost is that `cd my-agent && fastagent dev` legitimately narrows the agent's
49
+ * WORKSPACE to its own directory: its cwd, its coding tools' root, and deploy's build context. (②
50
+ * context is not affected — that walk climbs ancestors either way.) Resolution must not guess which you
51
+ * wanted, so this is a HINT — and a hint may use the heuristic ("the parent carries an AGENTS.md or a
52
+ * .git") that a rule may not.
53
+ *
54
+ * It suggests a command only after RUNNING the lookup that command would run, because a hint that dead-ends
55
+ * is worse than none: with several agents beside this one, `..` refuses and names them — sending the
56
+ * reader to a refusal whose own advice points back here.
57
+ */
58
+ export declare function workspaceHint({ agentDir, workspace }: ResolvedPlacement, env?: NodeJS.ProcessEnv): string | undefined;
59
+ /** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair,
60
+ * and without the throw. `login` is the caller: it is the one command allowed to run outside an agent,
61
+ * so it needs the answer as a value rather than as a refusal. */
62
+ export declare function findAgentDir(dir: string): string | undefined;
63
+ /**
64
+ * The agent whose DEFINITION contains `dir` — scaffolding there would make the new agent part of the
65
+ * outer one's loaded surface rather than an agent of its own. Narrower than {@link enclosingAgentDir} on
66
+ * purpose: an agent owns only what it LOADS ({@link LOADED_SURFACE}); the rest of its directory is the
67
+ * author's tree, where a second agent (a monorepo package, say) is a legitimate thing to create.
68
+ * `enclosingAgentDir` answers a different question ("where do I `cd` to?"), and for that an agent's
69
+ * `src/` genuinely IS inside it.
70
+ */
71
+ export declare function agentDefinitionOwner(dir: string): string | undefined;
72
+ /**
73
+ * Why `dir` is not an agent, when it has its OWN way out — or undefined when it is simply not near one.
74
+ * Two positions qualify: standing INSIDE an agent (its `tools/`, its `src/`), and standing on a
75
+ * directory whose several agents nothing selects between. Both matter because the generic advice ("run
76
+ * `fastagent init`") would not help — the agent already exists, one step away.
77
+ *
78
+ * Exported because `login` is the one command allowed to run outside an agent, and it must tell "truly
79
+ * outside" (→ the global credential) from "a dead end" (→ refuse, like every other command).
80
+ */
81
+ export declare function placementDeadEnd(dir: string, env?: NodeJS.ProcessEnv): string | undefined;
82
+ /**
83
+ * Resolve a directory into its placement — the ONE owner of the rule ({@link findPlacement}): `dir` is
84
+ * the workspace, and the agent is the single `fastagent.config.*` holder at it or one level inside.
85
+ * Placement is never configured and never detected from surroundings; it is that lookup and the
86
+ * directory you pointed at.
87
+ *
88
+ * Anything else throws (fail visibly). Resolution never walks UP — an agent must not be claimed from
89
+ * arbitrarily deep inside it — but the MESSAGE reads the path, so each dead end gets the exit that fits
90
+ * it ({@link placementDeadEnd}).
91
+ */
92
+ export declare function resolvePlacement(dir: string, env?: NodeJS.ProcessEnv): ResolvedPlacement;
93
+ /** How to WRITE a path for someone standing in `cwd`: relative when it is inside `cwd`, absolute when
94
+ * it climbs out (a `../../..` is noise), and undefined when it IS `cwd` (nothing to say). ONE policy,
95
+ * shared by `init`'s `cd` step, `add`'s next-steps paths and `fire`'s "looked in" hint — they all answer
96
+ * the same question, which is a placement-PRESENTATION question, not a scaffolding one. */
97
+ export declare function displayPath(cwd: string, dir: string): string | undefined;
98
+ /** Does a path exist? Plain fs, no placement in it — it lives here because `paths.ts` is where the
99
+ * neutral path helpers are, and the scaffolder is not a utility home for the CLI and deploy. */
100
+ export declare function exists(p: string): Promise<boolean>;
101
+ /**
102
+ * Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
103
+ * leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
104
+ * never gets the shell's `~` expansion, so a bare `resolve("~/x")` would silently create a literal `~`
105
+ * directory — a fail-silently footgun for a secret/state path. Expanding here makes `~` mean home
106
+ * everywhere these knobs are read.
107
+ */
108
+ export declare function resolveOverridePath(raw: string | undefined): string | undefined;
109
+ /**
110
+ * The resolved state root — the durable machine-state home (sessions/, channels/<kind>/, schedule/,
111
+ * control.json): `FASTAGENT_STATE_DIR` env > `<agentDir>/.state`. Absolute, so channels and the
112
+ * startup report agree regardless of cwd. Definition: mutable runtime state — single lifecycle
113
+ * (precious, survives redeploy), single process; a container points this at its mounted volume.
114
+ * Secrets are NOT here — they live under {@link resolveSecretsDir} (a different deploy lifecycle:
115
+ * secret store vs volume). The finer knob (`FASTAGENT_SESSIONS_DIR`) still overrides its path on top.
116
+ *
117
+ * `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
118
+ * — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
119
+ * DEFAULT (`<root>/.state`) is dir-anchored.
120
+ */
121
+ export declare function resolveStateRoot(dir: string, env?: NodeJS.ProcessEnv): string;
122
+ /**
123
+ * The resolved secrets dir — everything fastagent manages that must NEVER leave the machine (the
124
+ * agent's `.env` + auth.json): `FASTAGENT_SECRETS_DIR` env > `<agentDir>/.secrets`. Split from
125
+ * the state root on deploy lifecycle: secrets travel through the host's secret store (env vars / the
126
+ * auth seed), state through a volume. A deployed box sets both env knobs at its volume (e.g.
127
+ * `/data/.secrets`, `/data/.state`) so a seeded-then-ROTATED OAuth credential persists across
128
+ * restarts. The `.env`'s OWN location resolves from the REAL environment — commands locate and load
129
+ * `.env` before anything else, so a `FASTAGENT_SECRETS_DIR` set INSIDE `.env` still relocates
130
+ * auth.json but cannot move the file it is read from (env.ts dotEnvPath).
131
+ */
132
+ export declare function resolveSecretsDir(dir: string, env?: NodeJS.ProcessEnv): string;
133
+ /**
134
+ * Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
135
+ * absolute target) is rejected, so discovery/scaffolding never reaches out of the definition directory.
136
+ * A missing target is fine (nothing to guard yet).
137
+ */
138
+ export declare function assertInsideAgentDir(agentDir: string, name: string): Promise<void>;