@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,10 +1,7 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { FastagentTool } from "./tool.ts";
3
3
  import type { Models } from "@earendil-works/pi-ai";
4
- import type { AnyModel } from "./harness.ts";
5
- /** pi's thinking levels (types.d.ts `ThinkingLevel`), as a runtime list for config validation — pi
6
- * exports only the type. A level the selected model does not support is clamped by pi per model. */
7
- export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
4
+ import { type AnyModel } from "./harness.ts";
8
5
  export interface FastagentConfig {
9
6
  /** "provider/modelId". Precedence: CLI --model > FASTAGENT_MODEL > config. */
10
7
  model?: string;
@@ -12,14 +9,6 @@ export interface FastagentConfig {
12
9
  * "xhigh" | "max"). Unset = pi's default. Authors tune thinking in the pi TUI while vibing — this
13
10
  * is the serving-side counterpart (fidelity). Levels a model doesn't support are clamped by pi. */
14
11
  thinkingLevel?: ThinkingLevel;
15
- /**
16
- * The agent-definition subdirectory (persona.md, skills/, tools/, channels/), relative to the config
17
- * file's directory. Default: the config directory itself (flat — today's behaviour). Point it at a
18
- * sibling like `"./agent"` to serve an existing repo as a coding agent: the config dir stays the run
19
- * root (cwd, whose AGENTS.md the agent reads as ② context), while the agent's own surface lives in the
20
- * subdir and does not collide with the host's `tools/`/`src/` (core.md scenario grid).
21
- */
22
- agentDir?: string;
23
12
  /** Extra custom tools, appended after pi defaults — never replaces them. `FastagentTool` = AgentTool
24
13
  * plus the optional `deferred` marker (see defineTool). */
25
14
  tools?: FastagentTool[];
@@ -30,6 +19,15 @@ export interface FastagentConfig {
30
19
  * Off by default — self-scheduling is an autonomy capability, opt in when you want it. Only takes
31
20
  * effect on the serving path (`dev`/`start`, where the scheduler poller honors a wake-up). */
32
21
  selfSchedule?: boolean;
22
+ /**
23
+ * Serve the session control plane over HTTP (`/control/*`: state/entries/events + dispatch —
24
+ * steer/abort/compact/set_model…) for remote consumers: a Web panel, a desktop app, `fastagent
25
+ * attach`. Default off (it is a remote-control surface). When on, `dev`/`start` generate a
26
+ * per-boot bearer token and write `<stateRoot>/control.json` for local discovery. The serve
27
+ * binds all interfaces, so the routes are LAN-reachable with the token as the only protection —
28
+ * firewall the port, or wrap it for real exposure (design §14).
29
+ */
30
+ sessionControl?: boolean;
33
31
  /** Deploy-time declarations for what the agent needs on the box, so real agents don't hand-write a
34
32
  * Dockerfile / hand-set variables. */
35
33
  deploy?: {
@@ -47,24 +45,17 @@ export interface FastagentConfig {
47
45
  export declare function defineConfig(config: FastagentConfig): FastagentConfig;
48
46
  export interface LoadedConfig {
49
47
  config: FastagentConfig;
50
- /** Config file path; undefined when running zero-config. */
48
+ /** Config file path; undefined when the loader was pointed at a directory holding none. */
51
49
  path?: string;
52
50
  }
53
51
  /** A valid bindable port. */
54
52
  export declare function isValidPort(n: number): boolean;
55
- /** The config filenames that make a directory a fastagent workspace, in load precedence. ONE source: the
56
- * loader (below) and `scaffoldWorkspace`'s already-a-workspace refusal both read this, so "is there a
57
- * config?" can't diverge between them when the set changes. */
58
- export declare const WORKSPACE_CONFIG_NAMES: readonly ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
59
- /** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = zero-config; a wrong-shape file throws. */
53
+ /** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = defaults; a wrong-shape file throws. */
60
54
  export declare function loadConfig(dir: string): Promise<LoadedConfig>;
61
- /**
62
- * The agent-definition dir from config: `config.agentDir` resolved against `dir`, or `dir` itself when
63
- * unset (flat). The ONE place this is computed every opener (`dev`/`start`/`info`/`tool`/`deploy`/`chat`)
64
- * calls it, so the "relative to the config dir, default `.`" rule can never diverge. loadConfig has
65
- * already validated that agentDir stays under `dir`.
66
- */
67
- export declare function resolveAgentDir(dir: string, config: FastagentConfig): string;
55
+ /** The provider prefix of a "provider/modelId" spec. A spec without "/" returns whole — downstream
56
+ * lookups then miss visibly (an unknown-provider error / a login-required hint), never a mangled id
57
+ * (`slice(0, indexOf("/"))` silently drops the last char when "/" is absent). */
58
+ export declare function providerOf(spec: string): string;
68
59
  /** Resolve "provider/modelId" → a pi Model from `models`, so the harness resolves auth from the same collection. */
69
60
  export declare function resolveModel(models: Models, spec: string): AnyModel;
70
61
  /** All registered "provider/modelId" specs in `models`, sorted — the list behind `fastagent models`. */
@@ -72,7 +63,7 @@ export declare function listModels(models: Models): string[];
72
63
  /**
73
64
  * Rewrite the `model` in a config file's SOURCE TEXT to `spec`, for the first-run picker's write-back.
74
65
  * Handles the scaffold's commented placeholder (`// model: "…"`) and an existing `model:` line; returns
75
- * null when neither is present (zero-config or a hand-shaped config) so the caller falls back to a
66
+ * null when neither is present (no config file, or a hand-shaped one) so the caller falls back to a
76
67
  * printed hint instead of guessing where to insert. Text-level (not AST) on purpose — it only ever
77
68
  * touches a line it recognizes, never reformats the author's file.
78
69
  */
@@ -85,35 +76,9 @@ export declare function resolveModelSpec(flag: string | undefined, config: Fasta
85
76
  * Resolved to absolute so the store and the startup report agree regardless of cwd.
86
77
  */
87
78
  export declare function resolveSessionsDirOverride(flag: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
88
- /**
89
- * The auth-file override: `--auth-path` flag > `FASTAGENT_AUTH_PATH` env > undefined (the opener then
90
- * falls back to {@link defaultAuthPath} under the {@link resolveStateRoot} root). Resolved to absolute
91
- * so the store and the startup report agree regardless of cwd. No implicit project↔global fallback (isolation
92
- * + fail-visibly; see auth.ts); to share one account across projects, point this at the global
93
- * `~/.fastagent/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
94
- */
95
- export declare function resolveAuthPathOverride(flag: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
96
- /**
97
- * The IN-TREE default state root, `<dir>/.fastagent` — what {@link resolveStateRoot} falls back to when
98
- * `FASTAGENT_STATE_DIR` moves state nowhere. THE single definition of that path segment.
99
- */
100
- export declare function projectStateDir(dir: string): string;
101
- /**
102
- * The resolved state root — the ONE durable machine-state home everything derives from (auth.json,
103
- * sessions/, channels/<kind>/): `FASTAGENT_STATE_DIR` env > `<dir>/.fastagent`. Absolute, so channels
104
- * and the startup report agree regardless of cwd. Definition: single lifecycle (precious, survives
105
- * redeploy), single process — a container mounts ONE volume here. The finer knobs
106
- * (`FASTAGENT_SESSIONS_DIR`, `FASTAGENT_AUTH_PATH`) still override their specific path on top.
107
- *
108
- * `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
109
- * — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
110
- * DEFAULT (`<dir>/.fastagent`) is dir-anchored. Deployments set an absolute path (a mounted volume);
111
- * a relative value is in-tree — hence self-ignored — only when run from the definition dir (cwd == dir).
112
- */
113
- export declare function resolveStateRoot(dir: string, env?: NodeJS.ProcessEnv): string;
114
- /** The default credentials file under a resolved state root ({@link resolveStateRoot}). */
115
- export declare function defaultAuthPath(stateRoot: string): string;
116
- /** The effective auth file for a workspace: override if present, else the project-level auth.json. */
79
+ /** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
80
+ export declare function defaultAuthPath(secretsDir: string): string;
81
+ /** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
117
82
  export declare function resolveAuthPath(dir: string, flag: string | undefined, env?: NodeJS.ProcessEnv): string;
118
83
  /** The default sessions dir under a resolved state root ({@link resolveStateRoot}). */
119
84
  export declare function defaultSessionsDir(stateRoot: string): string;
@@ -11,25 +11,18 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
11
11
  * (resolveModel, resolveModelSpec). One concern: everything about fastagent.config.ts.
12
12
  *
13
13
  * Red line: config describes deployment/runtime choices, never authored identity or expertise (those
14
- * live in persona.md + skills, with AGENTS.md as project context). Deleting the config still leaves a
15
- * zero-config agent runnable with a model supplied by --model / FASTAGENT_MODEL.
14
+ * live in persona.md + skills, with AGENTS.md as project context). It has no say in PLACEMENT either —
15
+ * that rule lives in the neutral src/paths.ts — but its FILE is what marks a directory as an agent, so a
16
+ * served agent always has one (its contents may be `export default {}`; a model can still come from
17
+ * --model / FASTAGENT_MODEL). The loader below stays tolerant of a missing file because it is a loader:
18
+ * callers that have not resolved a placement (deploy inspection, tests) may point it anywhere.
16
19
  */
17
- import { existsSync, lstatSync, realpathSync } from "node:fs";
18
- import { homedir } from "node:os";
19
- import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
20
+ import { existsSync, statSync } from "node:fs";
21
+ import { basename, join } from "node:path";
20
22
  import { pathToFileURL } from "node:url";
23
+ import { THINKING_LEVELS } from "./harness.js";
21
24
  import { moduleLoadHint } from "../../loader.js";
22
- /** pi's thinking levels (types.d.ts `ThinkingLevel`), as a runtime list for config validation — pi
23
- * exports only the type. A level the selected model does not support is clamped by pi per model. */
24
- export const THINKING_LEVELS = [
25
- "off",
26
- "minimal",
27
- "low",
28
- "medium",
29
- "high",
30
- "xhigh",
31
- "max",
32
- ];
25
+ import { AGENT_CONFIG_NAMES, resolveOverridePath, resolveSecretsDir } from "../../paths.js";
33
26
  /** Identity function for typing and IDE completion (vite/next-style). */
34
27
  export function defineConfig(config) {
35
28
  return config;
@@ -51,13 +44,9 @@ function validateStringList(value, key, shape, desc, path) {
51
44
  }
52
45
  }
53
46
  }
54
- /** The config filenames that make a directory a fastagent workspace, in load precedence. ONE source: the
55
- * loader (below) and `scaffoldWorkspace`'s already-a-workspace refusal both read this, so "is there a
56
- * config?" can't diverge between them when the set changes. */
57
- export const WORKSPACE_CONFIG_NAMES = ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
58
- /** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = zero-config; a wrong-shape file throws. */
47
+ /** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = defaults; a wrong-shape file throws. */
59
48
  export async function loadConfig(dir) {
60
- const found = WORKSPACE_CONFIG_NAMES.map((name) => join(dir, name)).filter((path) => existsSync(path));
49
+ const found = AGENT_CONFIG_NAMES.map((name) => join(dir, name)).filter((path) => existsSync(path));
61
50
  if (found.length === 0)
62
51
  return { config: {} };
63
52
  if (found.length > 1) {
@@ -67,7 +56,14 @@ export async function loadConfig(dir) {
67
56
  const path = found[0];
68
57
  let mod;
69
58
  try {
70
- mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)));
59
+ // Cache-bust on file change: ESM `import()` caches by URL, so a config REWRITTEN in this process
60
+ // (the first-run picker's write-back) would otherwise read back stale — deploy's model-travel gate
61
+ // then contradicts the "saved model" line it just printed. mtime keeps unchanged files cached;
62
+ // its resolution is the ceiling — a rewrite within the same timestamp tick reads stale (fine for
63
+ // the write-back: sub-tick only on coarse-mtime filesystems, and the next process starts fresh).
64
+ const url = pathToFileURL(path);
65
+ url.searchParams.set("v", String(statSync(path).mtimeMs));
66
+ mod = (await import(__rewriteRelativeImportExtension(url.href)));
71
67
  }
72
68
  catch (error) {
73
69
  throw new Error(`${path}: ${error.message}${moduleLoadHint(error)}`);
@@ -78,64 +74,26 @@ export async function loadConfig(dir) {
78
74
  }
79
75
  const c = config;
80
76
  // Unknown keys throw: defineConfig only type-protects .ts authors; a typo in a .js/.mjs config
81
- // (`modle:`) must not silently degrade to zero-config.
77
+ // (`modle:`) must not silently degrade to defaults.
82
78
  for (const key of Object.keys(c)) {
83
79
  if (key !== "model" &&
84
80
  key !== "thinkingLevel" &&
85
- key !== "agentDir" &&
86
81
  key !== "tools" &&
87
82
  key !== "http" &&
88
83
  key !== "deploy" &&
89
- key !== "selfSchedule") {
90
- throw new Error(`${path}: unknown key "${key}" (valid keys: model, thinkingLevel, agentDir, tools, http, deploy, selfSchedule)`);
84
+ key !== "selfSchedule" &&
85
+ key !== "sessionControl") {
86
+ throw new Error(`${path}: unknown key "${key}" (valid keys: model, thinkingLevel, tools, http, deploy, selfSchedule, sessionControl)`);
91
87
  }
92
88
  }
93
89
  if (c.model !== undefined && typeof c.model !== "string") {
94
90
  throw new Error(`${path}: "model" must be a "provider/modelId" string`);
95
91
  }
96
- if (c.thinkingLevel !== undefined && !THINKING_LEVELS.includes(c.thinkingLevel)) {
97
- throw new Error(`${path}: "thinkingLevel" must be one of ${THINKING_LEVELS.join(", ")}`);
92
+ if (c.sessionControl !== undefined && typeof c.sessionControl !== "boolean") {
93
+ throw new Error(`${path}: "sessionControl" must be a boolean`);
98
94
  }
99
- if (c.agentDir !== undefined && typeof c.agentDir !== "string") {
100
- throw new Error(`${path}: "agentDir" must be a string (a subdirectory relative to the config file)`);
101
- }
102
- if (typeof c.agentDir === "string") {
103
- // Enforce the documented "subdirectory of the config dir" contract: an escaping agentDir (e.g.
104
- // "../shared") would still resolve for tool/channel/persona discovery, but `dev`'s chokidar only
105
- // watches the config dir subtree — edits outside it would silently never trigger a restart. Reject
106
- // it here (fail visibly) rather than let hot-reload break without a signal.
107
- const rel = relative(dir, resolve(dir, c.agentDir));
108
- if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
109
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") must be a subdirectory of the config directory, not escape it`);
110
- }
111
- // An explicitly declared agentDir that doesn't exist is a typo until proven otherwise ("./agnet"):
112
- // without this check every opener would assemble an EMPTY agent (no persona, no skills, no tools)
113
- // with zero errors — the worst silent failure this config can produce. Deliberately NOT auto-created:
114
- // config load is read-only (no implicit operations), and a mkdir would turn the typo into a served
115
- // empty agent plus a junk directory.
116
- // lstat, not stat: a symlink would pass the literal containment check above while its TARGET lives
117
- // outside the config dir — exactly what that check exists to prevent (dev's watch would silently
118
- // never see edits). Same rule as init's parent preflight: reject, don't follow.
119
- const agentDirAbs = resolve(dir, c.agentDir);
120
- const st = lstatSync(agentDirAbs, { throwIfNoEntry: false });
121
- if (!st) {
122
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") does not exist — create it, or fix the path`);
123
- }
124
- if (st.isSymbolicLink()) {
125
- // Separate message: to its user a symlink LOOKS like a working directory — name the reason and the fix.
126
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") is a symlink — not allowed (its target can live outside the ` +
127
- `config directory, where dev's watch would never see edits); use a real directory, or point agentDir at the target's real path`);
128
- }
129
- if (!st.isDirectory()) {
130
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") is not a directory`);
131
- }
132
- // The leaf lstat can't see a symlinked INTERMEDIATE segment (agentDir "./a/b" with `a` → outside):
133
- // realpath equality covers every segment under the config dir in one comparison. dir itself is
134
- // realpath'd on both sides, so a symlinked config-dir path (macOS /tmp) stays legal.
135
- if (realpathSync(agentDirAbs) !== resolve(realpathSync(dir), relative(dir, agentDirAbs))) {
136
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") resolves through a symlink — not allowed (the target can ` +
137
- `live outside the config directory, where dev's watch would never see edits); use the real path`);
138
- }
95
+ if (c.thinkingLevel !== undefined && !THINKING_LEVELS.has(c.thinkingLevel)) {
96
+ throw new Error(`${path}: "thinkingLevel" must be one of ${[...THINKING_LEVELS].join(", ")}`);
139
97
  }
140
98
  if (c.selfSchedule !== undefined && typeof c.selfSchedule !== "boolean") {
141
99
  throw new Error(`${path}: "selfSchedule" must be a boolean`);
@@ -180,14 +138,12 @@ export async function loadConfig(dir) {
180
138
  validateStringList(c.deploy?.apt, "deploy.apt", /^[a-z0-9][a-z0-9.+-]*$/, "a Debian package name", path);
181
139
  return { config: c, path };
182
140
  }
183
- /**
184
- * The agent-definition dir from config: `config.agentDir` resolved against `dir`, or `dir` itself when
185
- * unset (flat). The ONE place this is computed every opener (`dev`/`start`/`info`/`tool`/`deploy`/`chat`)
186
- * calls it, so the "relative to the config dir, default `.`" rule can never diverge. loadConfig has
187
- * already validated that agentDir stays under `dir`.
188
- */
189
- export function resolveAgentDir(dir, config) {
190
- return resolve(dir, config.agentDir ?? ".");
141
+ /** The provider prefix of a "provider/modelId" spec. A spec without "/" returns whole — downstream
142
+ * lookups then miss visibly (an unknown-provider error / a login-required hint), never a mangled id
143
+ * (`slice(0, indexOf("/"))` silently drops the last char when "/" is absent). */
144
+ export function providerOf(spec) {
145
+ const slash = spec.indexOf("/");
146
+ return slash > 0 ? spec.slice(0, slash) : spec;
191
147
  }
192
148
  /** Resolve "provider/modelId" → a pi Model from `models`, so the harness resolves auth from the same collection. */
193
149
  export function resolveModel(models, spec) {
@@ -215,7 +171,7 @@ export function listModels(models) {
215
171
  /**
216
172
  * Rewrite the `model` in a config file's SOURCE TEXT to `spec`, for the first-run picker's write-back.
217
173
  * Handles the scaffold's commented placeholder (`// model: "…"`) and an existing `model:` line; returns
218
- * null when neither is present (zero-config or a hand-shaped config) so the caller falls back to a
174
+ * null when neither is present (no config file, or a hand-shaped one) so the caller falls back to a
219
175
  * printed hint instead of guessing where to insert. Text-level (not AST) on purpose — it only ever
220
176
  * touches a line it recognizes, never reformats the author's file.
221
177
  */
@@ -227,25 +183,19 @@ export function rewriteConfigModel(src, spec) {
227
183
  return src.replace(commented, line);
228
184
  if (active.test(src))
229
185
  return src.replace(active, line);
186
+ // No model line at all — the natural state after "picked once, then hand-deleted the line to reset".
187
+ // Re-INSERT at the top of the default-export object while the config still has the scaffold's block
188
+ // shape; anything else (a wrapper call, a one-liner, a computed export) is hand-shaped — leave it
189
+ // untouched (the caller prints the set-it-yourself hint).
190
+ const opener = /^export default[ \t]*\{[ \t]*$/m;
191
+ if (opener.test(src))
192
+ return src.replace(opener, (open) => `${open}\n${line}`);
230
193
  return null;
231
194
  }
232
195
  /** Model selection precedence: CLI flag > FASTAGENT_MODEL env > config default. */
233
196
  export function resolveModelSpec(flag, config, env = process.env) {
234
197
  return flag ?? env.FASTAGENT_MODEL ?? config.model;
235
198
  }
236
- /**
237
- * Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
238
- * leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
239
- * never gets the shell's `~` expansion, so a bare `resolve("~/x")` would silently create a literal `~`
240
- * directory — a fail-silently footgun for a secret/state path. Expanding here makes `~` mean home
241
- * everywhere these knobs are read.
242
- */
243
- function resolveOverridePath(raw) {
244
- if (!raw)
245
- return undefined;
246
- const expanded = raw === "~" ? homedir() : raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw;
247
- return resolve(expanded);
248
- }
249
199
  /**
250
200
  * `start`'s sessions-dir override: `--sessions-dir` flag > `FASTAGENT_SESSIONS_DIR` env > undefined
251
201
  * (the opener then falls back to {@link defaultSessionsDir} under the {@link resolveStateRoot} root).
@@ -256,43 +206,21 @@ export function resolveSessionsDirOverride(flag, env = process.env) {
256
206
  }
257
207
  /**
258
208
  * The auth-file override: `--auth-path` flag > `FASTAGENT_AUTH_PATH` env > undefined (the opener then
259
- * falls back to {@link defaultAuthPath} under the {@link resolveStateRoot} root). Resolved to absolute
209
+ * falls back to {@link defaultAuthPath} under the {@link resolveSecretsDir} dir). Resolved to absolute
260
210
  * so the store and the startup report agree regardless of cwd. No implicit project↔global fallback (isolation
261
211
  * + fail-visibly; see auth.ts); to share one account across projects, point this at the global
262
- * `~/.fastagent/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
212
+ * `~/.fastagent/.secrets/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
263
213
  */
264
- export function resolveAuthPathOverride(flag, env = process.env) {
214
+ function resolveAuthPathOverride(flag, env = process.env) {
265
215
  return resolveOverridePath(flag ?? env.FASTAGENT_AUTH_PATH);
266
216
  }
267
- /**
268
- * The IN-TREE default state root, `<dir>/.fastagent` — what {@link resolveStateRoot} falls back to when
269
- * `FASTAGENT_STATE_DIR` moves state nowhere. THE single definition of that path segment.
270
- */
271
- export function projectStateDir(dir) {
272
- return join(dir, ".fastagent");
273
- }
274
- /**
275
- * The resolved state root — the ONE durable machine-state home everything derives from (auth.json,
276
- * sessions/, channels/<kind>/): `FASTAGENT_STATE_DIR` env > `<dir>/.fastagent`. Absolute, so channels
277
- * and the startup report agree regardless of cwd. Definition: single lifecycle (precious, survives
278
- * redeploy), single process — a container mounts ONE volume here. The finer knobs
279
- * (`FASTAGENT_SESSIONS_DIR`, `FASTAGENT_AUTH_PATH`) still override their specific path on top.
280
- *
281
- * `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
282
- * — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
283
- * DEFAULT (`<dir>/.fastagent`) is dir-anchored. Deployments set an absolute path (a mounted volume);
284
- * a relative value is in-tree — hence self-ignored — only when run from the definition dir (cwd == dir).
285
- */
286
- export function resolveStateRoot(dir, env = process.env) {
287
- return resolveOverridePath(env.FASTAGENT_STATE_DIR) ?? resolve(projectStateDir(dir));
288
- }
289
- /** The default credentials file under a resolved state root ({@link resolveStateRoot}). */
290
- export function defaultAuthPath(stateRoot) {
291
- return join(stateRoot, "auth.json");
217
+ /** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
218
+ export function defaultAuthPath(secretsDir) {
219
+ return join(secretsDir, "auth.json");
292
220
  }
293
- /** The effective auth file for a workspace: override if present, else the project-level auth.json. */
221
+ /** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
294
222
  export function resolveAuthPath(dir, flag, env = process.env) {
295
- return resolveAuthPathOverride(flag, env) ?? defaultAuthPath(resolveStateRoot(dir, env));
223
+ return resolveAuthPathOverride(flag, env) ?? defaultAuthPath(resolveSecretsDir(dir, env));
296
224
  }
297
225
  /** The default sessions dir under a resolved state root ({@link resolveStateRoot}). */
298
226
  export function defaultSessionsDir(stateRoot) {
@@ -1,22 +1,23 @@
1
1
  import type { AgentTool, ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
- import type { Provider } from "@earendil-works/pi-ai";
2
+ import type { Models, Provider } from "@earendil-works/pi-ai";
3
3
  import type { Agent } from "../../agent.ts";
4
4
  import { type FastagentConfig } from "./config.ts";
5
5
  import { type LoadedDefinition } from "./definition.ts";
6
+ import { piHarnessFactory } from "./harness.ts";
6
7
  import { type PiSessionStore } from "./sessions.ts";
7
8
  import type { ModuleLoadFailure } from "../../loader.ts";
8
9
  import { type FastagentTool, type ToolCollision } from "./tool.ts";
9
- import { type Lease } from "./invoke.ts";
10
+ import { type Lease, type SessionObserver } from "./invoke.ts";
10
11
  /** pi's core default toolset (read/bash/edit/write), rooted at cwd. */
11
12
  export declare function piDefaultTools(cwd: string): AgentTool[];
12
13
  /** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
13
14
  export declare function resolveTools(config: FastagentConfig, cwd: string): AgentTool[];
14
15
  /**
15
- * The full tool set a workspace mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
16
+ * The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
16
17
  * existing win), plus the non-default tool names and collisions to report. One source for the
17
18
  * dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
18
19
  */
19
- export declare function resolveWorkspaceTools(config: FastagentConfig, agentDir: string, cwd?: string): Promise<{
20
+ export declare function resolveAgentTools(config: FastagentConfig, agentDir: string, cwd?: string): Promise<{
20
21
  tools: AgentTool[];
21
22
  toolNames: string[];
22
23
  /** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
@@ -52,6 +53,17 @@ export interface AssembleSystemPromptOptions {
52
53
  cwd?: string;
53
54
  }
54
55
  export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
56
+ /**
57
+ * INTERNAL seam (workspace ↔ assembly): hands the hub-wiring consumer the assembly's live parts —
58
+ * the SAME models collection, harness factory, and lease the agent runs with — so boundary
59
+ * mutations (session-control.ts) contend on the real lease and validate against the real registry.
60
+ * Called synchronously, exactly once, before the agent is returned. Not part of the public surface.
61
+ */
62
+ type OnAssembly = (parts: {
63
+ models: Models;
64
+ harnessFactory: ReturnType<typeof piHarnessFactory>;
65
+ lease: Lease;
66
+ }) => void;
55
67
  /** L1 options. Tier 1: model (spec) + instructions + tools. Tier 2: the injectable ports. */
56
68
  export interface CreatePiAgentOptions {
57
69
  /** Model spec "provider/modelId" (e.g. "openai-codex/gpt-5.5"), resolved against {@link models}. */
@@ -74,8 +86,8 @@ export interface CreatePiAgentOptions {
74
86
  */
75
87
  providers?: Provider[];
76
88
  /**
77
- * Credentials file for stored OAuth/API-key auth. Defaults to `~/.fastagent/auth.json`; the
78
- * directory opener passes the project-level `<dir>/.fastagent/auth.json` instead. Env vars are still
89
+ * Credentials file for stored OAuth/API-key auth. Defaults to `~/.fastagent/.secrets/auth.json`; the
90
+ * directory opener passes the project-level `<root>/.secrets/auth.json` instead. Env vars are still
79
91
  * consulted when a provider is absent from the file (resolution order is upstream-owned).
80
92
  */
81
93
  authPath?: string;
@@ -86,6 +98,9 @@ export interface CreatePiAgentOptions {
86
98
  env?: ExecutionEnv;
87
99
  /** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
88
100
  lease?: Lease;
101
+ /** Observation-plane tap (session control): every rich session event of every run. Wire the one
102
+ * returned by `createPiSessionControl` to serve `state`/`entries`/`events` for this agent. */
103
+ observer?: SessionObserver;
89
104
  }
90
105
  /** L1: assemble from typed parts. */
91
106
  export declare function createPiAgent(options: CreatePiAgentOptions): Agent;
@@ -106,16 +121,16 @@ export interface CreatePiAgentFromDefinitionOptions {
106
121
  tools?: FastagentTool[];
107
122
  /**
108
123
  * The agent's working directory: where the default tools operate AND whose ancestors are walked for
109
- * ② project context (AGENTS.md). Defaults to `dir` (flat: the definition dir is also the run root).
110
- * Set it to the enclosing repo so a coding agent whose definition lives in `dir` operates on — and
111
- * reads the AGENTS.md of that repo (core.md scenario grid).
124
+ * ② project context (AGENTS.md). Defaults to `dir`. Set it to the enclosing repo so a coding agent
125
+ * whose definition lives in `dir` operates on — and reads the AGENTS.md of — that repo (core.md
126
+ * scenario grid); that is what the CLI's opener does with the workspace.
112
127
  */
113
128
  cwd?: string;
114
129
  /** Extra providers registered on top of the built-ins (your own gateway / self-hosted endpoint). */
115
130
  providers?: Provider[];
116
131
  /**
117
132
  * Credentials file (see {@link CreatePiAgentOptions.authPath}). Being dir-aware, this rung defaults
118
- * to the PROJECT-level `<dir>/.fastagent/auth.json` (matching `fastagent dev`/`start` on the same
133
+ * to the PROJECT-level `<dir>/.secrets/auth.json` (matching `fastagent dev`/`start` on the same
119
134
  * dir) — unlike the dir-less {@link createPiAgent}/{@link createPiModels}, which default global.
120
135
  */
121
136
  authPath?: string;
@@ -124,6 +139,10 @@ export interface CreatePiAgentFromDefinitionOptions {
124
139
  * context loader remain local today, so injecting this alone does not sandbox a directory agent. */
125
140
  env?: ExecutionEnv;
126
141
  lease?: Lease;
142
+ /** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
143
+ observer?: SessionObserver;
144
+ /** INTERNAL seam for hub wiring; see {@link OnAssembly}. */
145
+ onAssembly?: OnAssembly;
127
146
  }
128
147
  /**
129
148
  * L2: "point at a directory → agent": load + assemble (base + AGENTS.md + skills + env) + L1 in one
@@ -133,3 +152,4 @@ export declare function createPiAgentFromDefinition(dir: string, options: Create
133
152
  agent: Agent;
134
153
  definition: LoadedDefinition;
135
154
  }>;
155
+ export {};
@@ -6,14 +6,15 @@
6
6
  * L1 createPiAgent(options) — assemble from typed parts (the canonical ctor).
7
7
  * L0 createPiAgentFromHarness({ harnessFactory }) — in invoke.ts (its body is the turn mechanism).
8
8
  *
9
- * Above L2 sits the workspace opener createPiAgentFromWorkspace (workspace.ts), which both `dev` and
9
+ * Above L2 sits the agent opener createPiAgentFromDir (open.ts), which both `dev` and
10
10
  * `start` drive. Each rung calls the one below; options narrow as you go up (L2 owns systemPrompt/skills —
11
11
  * they come from the definition; the openers own model/tools — from config resolution).
12
12
  */
13
13
  import { formatSkillsForSystemPrompt } from "@earendil-works/pi-agent-core";
14
14
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
15
15
  import { createCodingTools } from "@earendil-works/pi-coding-agent";
16
- import { defaultAuthPath, resolveModel, resolveStateRoot } from "./config.js";
16
+ import { defaultAuthPath, resolveModel } from "./config.js";
17
+ import { resolveSecretsDir } from "../../paths.js";
17
18
  import { loadAgentDefinition } from "./definition.js";
18
19
  import { piHarnessFactory } from "./harness.js";
19
20
  import { createPiModels } from "./models.js";
@@ -21,7 +22,7 @@ import { reportDefinitionWarnings } from "./report.js";
21
22
  import { inMemorySessionStore } from "./sessions.js";
22
23
  import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
23
24
  import { withSearchTool } from "./search-tools.js";
24
- import { createPiAgentFromHarness } from "./invoke.js";
25
+ import { createPiAgentFromHarness, inProcessLease } from "./invoke.js";
25
26
  // ── §1 tools ─────────────────────────────────────────────────────────────────
26
27
  //
27
28
  // The full pi toolset is the default for fidelity: authors vibe in local pi with it, so serving with
@@ -37,18 +38,18 @@ export function resolveTools(config, cwd) {
37
38
  return config.tools ? [...defaults, ...config.tools] : defaults;
38
39
  }
39
40
  /**
40
- * The full tool set a workspace mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
41
+ * The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
41
42
  * existing win), plus the non-default tool names and collisions to report. One source for the
42
43
  * dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
43
44
  */
44
- export async function resolveWorkspaceTools(config, agentDir, cwd = agentDir) {
45
- // Default coding tools (read/bash/edit/write) are rooted at `cwd` (the run root the agent operates on);
46
- // discovered `tools/` come from `agentDir` (the agent's own surface). They coincide in the flat case.
45
+ export async function resolveAgentTools(config, agentDir, cwd = agentDir) {
46
+ // Default coding tools (read/bash/edit/write) are rooted at `cwd` (the workspace the agent operates
47
+ // on); discovered `tools/` come from `agentDir` (the agent's own surface).
47
48
  const discovered = await loadTools(agentDir);
48
49
  const merged = mergeDiscoveredTools(resolveTools(config, cwd), discovered.tools);
49
- // The built-in `search_tools` loader mounts here — the one place the workspace's full tool set is
50
- // computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; a
51
- // workspace-defined search_tools wins).
50
+ // The built-in `search_tools` loader mounts here — the one place the agent's full tool set is
51
+ // computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; an
52
+ // agent-defined search_tools wins).
52
53
  const tools = withSearchTool(merged.tools);
53
54
  // Builtin = a search_tools that was ABSENT before withSearchTool (a reference compare would misfire
54
55
  // on the deferred-authored-loader case, where withSearchTool returns a new array without adding one).
@@ -98,7 +99,7 @@ export function piBasePrompt(options = {}) {
98
99
  const deferredCount = mounted.length - tools.length;
99
100
  const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
100
101
  // Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
101
- // (the standalone×code-repo cell's persona; core.md §11), keeping the tools list + guidelines below.
102
+ // (core.md §11), keeping the tools list + guidelines below.
102
103
  const identity = options.persona?.trim() ||
103
104
  "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
104
105
  const deferredNote = deferredCount > 0
@@ -141,20 +142,23 @@ export function assembleSystemPrompt(options) {
141
142
  */
142
143
  function buildPiAgent(opts) {
143
144
  const models = createPiModels({ providers: opts.providers, authPath: opts.authPath });
144
- return createPiAgentFromHarness({
145
- lease: opts.lease,
146
- harnessFactory: piHarnessFactory({
147
- sessions: opts.sessions ?? inMemorySessionStore(),
148
- env: opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() }),
149
- models,
150
- model: resolveModel(models, opts.model),
151
- thinkingLevel: opts.thinkingLevel,
152
- systemPrompt: opts.systemPrompt,
153
- tools: opts.tools,
154
- skills: opts.skills,
155
- live: opts.live,
156
- }),
145
+ const env = opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() });
146
+ // Materialized here (not defaulted inside createPiAgentFromHarness) so the exposed parts carry
147
+ // the SAME lease instance the agent runs under — boundary mutations must contend on it.
148
+ const lease = opts.lease ?? inProcessLease();
149
+ const harnessFactory = piHarnessFactory({
150
+ sessions: opts.sessions ?? inMemorySessionStore(),
151
+ env,
152
+ models,
153
+ model: resolveModel(models, opts.model),
154
+ thinkingLevel: opts.thinkingLevel,
155
+ systemPrompt: opts.systemPrompt,
156
+ tools: opts.tools,
157
+ skills: opts.skills,
158
+ live: opts.live,
157
159
  });
160
+ opts.onAssembly?.({ models, harnessFactory, lease });
161
+ return createPiAgentFromHarness({ lease, observer: opts.observer, cwd: env.cwd, harnessFactory });
158
162
  }
159
163
  /**
160
164
  * L1 system prompt: `instructions` ARE the prompt (no engine base, no wrapping); the skills listing
@@ -185,6 +189,7 @@ export function createPiAgent(options) {
185
189
  sessions: options.sessions,
186
190
  env: options.env,
187
191
  lease: options.lease,
192
+ observer: options.observer,
188
193
  });
189
194
  }
190
195
  /** Stable identity of a definition's non-fatal findings, for change-detection in `live` (dedup only). */
@@ -216,9 +221,9 @@ export async function createPiAgentFromDefinition(dir, options) {
216
221
  model: options.model,
217
222
  thinkingLevel: options.thinkingLevel,
218
223
  providers: options.providers,
219
- // Dir-aware default: the same state-root-derived file the opener uses for this dir (the opener
224
+ // Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
220
225
  // passes an explicit authPath, so this only affects direct L2 callers).
221
- authPath: options.authPath ?? defaultAuthPath(resolveStateRoot(dir)),
226
+ authPath: options.authPath ?? defaultAuthPath(resolveSecretsDir(dir)),
222
227
  // The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills
223
228
  // edits (the author's, or the agent's own self-modification) take effect on the next turn with
224
229
  // no process restart — restarts are reserved for code (tools/channels/config, module cache).
@@ -253,6 +258,8 @@ export async function createPiAgentFromDefinition(dir, options) {
253
258
  sessions: options.sessions,
254
259
  env,
255
260
  lease: options.lease,
261
+ observer: options.observer,
262
+ onAssembly: options.onAssembly,
256
263
  });
257
264
  return { agent, definition };
258
265
  }