@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
@@ -32,9 +32,9 @@ export interface LoadedDefinition {
32
32
  }
33
33
  export interface LoadAgentDefinitionOptions {
34
34
  /**
35
- * Working directory whose ancestors are walked for context files (segment ②). Default = `agentDir`
36
- * (flat: the agent dir is also the run root). The opener passes the run root so a coding agent that
37
- * lives in `agentDir` picks up the host repo's AGENTS.md up the tree (core.md scenario grid).
35
+ * Working directory whose ancestors are walked for context files (segment ②). Default = `agentDir`.
36
+ * The opener passes the workspace instead, so an agent that lives in `agentDir` picks up the
37
+ * project's AGENTS.md up the tree (core.md scenario grid).
38
38
  */
39
39
  cwd?: string;
40
40
  env?: ExecutionEnv;
@@ -42,31 +42,12 @@ export interface LoadAgentDefinitionOptions {
42
42
  /** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
43
43
  export declare function loadAgentDefinition(agentDir: string, options?: LoadAgentDefinitionOptions): Promise<LoadedDefinition>;
44
44
  /**
45
- * Whether `targetPath` lives inside `baseDir` (same path counts). The self-ignore guard uses it to ask
46
- * "does the resolved state root land inside the workspace tree?" an in-tree root (the default
47
- * `.fastagent`, or a custom `FASTAGENT_STATE_DIR` pointed inside the agent dir) is ours to self-ignore;
48
- * a root on an external volume resolves outside and must not be (we never write a `.gitignore` outside
49
- * the tree). Whether a relative override lands in-tree is a cwd question — see `resolveStateRoot`.
45
+ * Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
46
+ * this OUT of the agent?" the startup report's redeploy notes, `add`'s printed `.env` label, and the
47
+ * dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
48
+ * does not act on where a user's paths point.
50
49
  */
51
50
  export declare function isUnderDir(targetPath: string, baseDir: string): boolean;
52
- /**
53
- * The single owner of the self-ignore MECHANISM: iff the resolved state ROOT lands inside the workspace
54
- * tree, write `<stateRoot>/.gitignore="*"` — which then covers EVERYTHING under it (sessions, auth.json,
55
- * every channel's `channels/<kind>` home). `ensureStateDirSelfIgnored` is private, so a caller cannot
56
- * write a `.gitignore` bypassing this.
57
- *
58
- * ROOT-based, not path-based: everything derives from the state root (config.ts), so protecting the
59
- * root protects all of it — INCLUDING a custom in-tree root (a `FASTAGENT_STATE_DIR` inside the agent
60
- * dir), the case a path-based (`.fastagent`-only) guard would leak. A per-path override
61
- * (`--sessions-dir`/`--auth-path`) is operator-owned: pointed at an external volume it is out-of-tree
62
- * (correctly not ours to ignore); pointed at a custom in-tree dir WE DON'T OWN, we do not write a
63
- * `.gitignore` into it (it may be a directory the operator deliberately tracks).
64
- *
65
- * Excludes the user's HOME-global `~/.fastagent` (e.g. `login`/`dev` run from `$HOME`): self-ignore is
66
- * for protecting state inside an agent PROJECT tree, not for writing a `.gitignore` into the user's
67
- * home, which a dotfiles repo may track. The global credential file there was never self-ignored.
68
- */
69
- export declare function ensureStateRootSelfIgnored(dir: string, stateRoot: string): Promise<void>;
70
51
  /** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
71
52
  * A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
72
53
  export declare function canonicalPath(p: string): string;
@@ -14,12 +14,11 @@
14
14
  * (bad skill files, name collisions) are returned as data. An unreadable ② context file only warns (pi).
15
15
  */
16
16
  import { realpathSync } from "node:fs";
17
- import { mkdir, writeFile } from "node:fs/promises";
18
- import { homedir } from "node:os";
19
17
  import { isAbsolute, join, relative, resolve } from "node:path";
20
18
  import { loadSkills } from "@earendil-works/pi-agent-core";
21
19
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
22
20
  import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
21
+ import { assertInsideAgentDir } from "../../paths.js";
23
22
  /** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
24
23
  export async function loadAgentDefinition(agentDir, options = {}) {
25
24
  // One resolved default for the working directory (env cwd AND the context-walk start), so they can
@@ -44,7 +43,9 @@ export async function loadAgentDefinition(agentDir, options = {}) {
44
43
  }
45
44
  const persona = personaRead.ok ? personaRead.value : undefined;
46
45
  // Skills come ONLY from the definition's own skills/ (no external/global mount), so the same
47
- // definition loads the same skills on every machine.
46
+ // definition loads the same skills on every machine — and, like tools/channels/schedules, a symlink
47
+ // that escapes the agent dir is refused rather than followed (the fourth of four surfaces).
48
+ await assertInsideAgentDir(root, "skills");
48
49
  const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")]);
49
50
  const byName = new Map();
50
51
  const collisions = [];
@@ -60,62 +61,15 @@ export async function loadAgentDefinition(agentDir, options = {}) {
60
61
  return { contextFiles, persona, skills: [...byName.values()], diagnostics, collisions, dir: root };
61
62
  }
62
63
  /**
63
- * Whether `targetPath` lives inside `baseDir` (same path counts). The self-ignore guard uses it to ask
64
- * "does the resolved state root land inside the workspace tree?" an in-tree root (the default
65
- * `.fastagent`, or a custom `FASTAGENT_STATE_DIR` pointed inside the agent dir) is ours to self-ignore;
66
- * a root on an external volume resolves outside and must not be (we never write a `.gitignore` outside
67
- * the tree). Whether a relative override lands in-tree is a cwd question — see `resolveStateRoot`.
64
+ * Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
65
+ * this OUT of the agent?" the startup report's redeploy notes, `add`'s printed `.env` label, and the
66
+ * dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
67
+ * does not act on where a user's paths point.
68
68
  */
69
69
  export function isUnderDir(targetPath, baseDir) {
70
70
  const rel = relative(baseDir, targetPath);
71
71
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
72
72
  }
73
- /**
74
- * Self-ignore a state dir: create it if missing, then write `<stateDir>/.gitignore` = "*" (idempotent
75
- * — an existing one is kept), so a workspace that runs dev/start never shows machine state as
76
- * untracked. Creates the dir because a caller may self-ignore it before anything else populates it
77
- * (e.g. `login` writing auth.json into a not-yet-created root).
78
- *
79
- * Module-PRIVATE on purpose: the only entry to the leak guard is {@link ensureStateRootSelfIgnored}
80
- * (home exclusion + containment). Keeping this unexported makes that single-owner claim hold at the
81
- * type level — a sibling command can't bypass those checks by writing a `.gitignore` directly.
82
- */
83
- async function ensureStateDirSelfIgnored(stateDir) {
84
- await mkdir(stateDir, { recursive: true });
85
- await writeFile(join(stateDir, ".gitignore"), "*\n", { flag: "wx" }).catch((e) => {
86
- if (e.code !== "EEXIST")
87
- throw e;
88
- });
89
- }
90
- /**
91
- * The single owner of the self-ignore MECHANISM: iff the resolved state ROOT lands inside the workspace
92
- * tree, write `<stateRoot>/.gitignore="*"` — which then covers EVERYTHING under it (sessions, auth.json,
93
- * every channel's `channels/<kind>` home). `ensureStateDirSelfIgnored` is private, so a caller cannot
94
- * write a `.gitignore` bypassing this.
95
- *
96
- * ROOT-based, not path-based: everything derives from the state root (config.ts), so protecting the
97
- * root protects all of it — INCLUDING a custom in-tree root (a `FASTAGENT_STATE_DIR` inside the agent
98
- * dir), the case a path-based (`.fastagent`-only) guard would leak. A per-path override
99
- * (`--sessions-dir`/`--auth-path`) is operator-owned: pointed at an external volume it is out-of-tree
100
- * (correctly not ours to ignore); pointed at a custom in-tree dir WE DON'T OWN, we do not write a
101
- * `.gitignore` into it (it may be a directory the operator deliberately tracks).
102
- *
103
- * Excludes the user's HOME-global `~/.fastagent` (e.g. `login`/`dev` run from `$HOME`): self-ignore is
104
- * for protecting state inside an agent PROJECT tree, not for writing a `.gitignore` into the user's
105
- * home, which a dotfiles repo may track. The global credential file there was never self-ignored.
106
- */
107
- export async function ensureStateRootSelfIgnored(dir, stateRoot) {
108
- // Compare CANONICAL paths for the home check: `dir` arrives realpath-resolved (it is `process.cwd()`
109
- // or `resolve(".")`) but `homedir()` returns the raw `$HOME`, so a symlinked home would slip past raw
110
- // equality and we'd write a `.gitignore` into the real `~/.fastagent` — the very thing the doc forbids
111
- // (chat.ts canonicalizes for the same reason).
112
- if (canonicalPath(dir) === canonicalPath(homedir()))
113
- return;
114
- // Containment on RAW paths: stateRoot is resolve()'d (config.ts) and `dir` is absolute, so it is exact
115
- // by construction. An external-volume root resolves outside the tree → skip (not ours to ignore).
116
- if (isUnderDir(stateRoot, dir))
117
- await ensureStateDirSelfIgnored(stateRoot);
118
- }
119
73
  /** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
120
74
  * A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
121
75
  export function canonicalPath(p) {
@@ -60,6 +60,55 @@ export interface PiHarnessFactoryOptions {
60
60
  skills?: Skill[];
61
61
  }>;
62
62
  }
63
+ /**
64
+ * Retry policy for generated compaction/branch-summary model calls (pi ≥0.81.1, #6901). OPT-IN
65
+ * upstream — an undefined policy means no retries — so both compaction paths pass it explicitly:
66
+ * the harness config (auto-compaction inside a run) and the manual `compact()` dispatch in
67
+ * session-control. Values mirror pi's own app defaults (maxRetries 3, base 2s exponential).
68
+ */
69
+ export declare const SUMMARIZATION_RETRY_POLICY: {
70
+ readonly enabled: true;
71
+ readonly maxRetries: 3;
72
+ readonly baseDelayMs: 2000;
73
+ };
74
+ export declare const THINKING_LEVELS: ReadonlySet<ThinkingLevel>;
75
+ /** The shape both override consumers walk — a session entry, structurally. */
76
+ export interface OverrideEntryLike {
77
+ type: string;
78
+ provider?: string;
79
+ modelId?: string;
80
+ thinkingLevel?: string;
81
+ }
82
+ /**
83
+ * The session's durable override FACTS — the ONE walk both surfaces consume (`state()` reports the
84
+ * recorded truth; `resolveHarnessOverrides` below applies registry/scale fallbacks on top). The
85
+ * LAST entry of each kind wins, and a malformed record reads as ABSENT for that kind — never
86
+ * skipped over to an earlier record: the reporting surface and the execution surface must agree on
87
+ * which record is "the" override.
88
+ */
89
+ export declare function lastOverrideEntries(entries: OverrideEntryLike[]): {
90
+ model?: {
91
+ provider: string;
92
+ modelId: string;
93
+ };
94
+ thinkingLevel?: string;
95
+ };
96
+ /**
97
+ * Resolve the session's model/thinking OVERRIDES for a fresh harness — same shape as the
98
+ * active-tools resolve above: pi writes `model_change`/`thinking_level_change` entries on explicit
99
+ * setModel/setThinkingLevel (the control plane's `set_model`/`set_thinking` append them directly)
100
+ * but a fresh harness never reads them back. Override facts come from {@link lastOverrideEntries};
101
+ * this adds the EXECUTION fallbacks: a recorded model no longer in this deployment's registry falls
102
+ * back to the default with a deduped warn (fail visibly without bricking the session — the
103
+ * conversation must survive a registry change across deploys); an unknown thinking level likewise.
104
+ */
105
+ export declare function resolveHarnessOverrides(entries: OverrideEntryLike[], models: Models, defaults: {
106
+ model: AnyModel;
107
+ thinkingLevel: ThinkingLevel;
108
+ }, sessionId: string): {
109
+ model: AnyModel;
110
+ thinkingLevel: ThinkingLevel;
111
+ };
63
112
  export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: AgentTool[], sessionId: string): string[] | undefined;
64
113
  /** Open-or-create the session per invoke: existing → open (history via buildContext); missing → create. */
65
114
  export declare function piHarnessFactory(options: PiHarnessFactoryOptions): PiHarnessFactory;
@@ -38,6 +38,13 @@ export function harnessSession(harness) {
38
38
  * so a mid-stream failure surfaces as a `failed` event.
39
39
  */
40
40
  const PROVIDER_MAX_RETRIES = 2;
41
+ /**
42
+ * Retry policy for generated compaction/branch-summary model calls (pi ≥0.81.1, #6901). OPT-IN
43
+ * upstream — an undefined policy means no retries — so both compaction paths pass it explicitly:
44
+ * the harness config (auto-compaction inside a run) and the manual `compact()` dispatch in
45
+ * session-control. Values mirror pi's own app defaults (maxRetries 3, base 2s exponential).
46
+ */
47
+ export const SUMMARIZATION_RETRY_POLICY = { enabled: true, maxRetries: 3, baseDelayMs: 2000 };
41
48
  /**
42
49
  * The serving default for reasoning effort, pinned to what pi's TUI defaults to (its
43
50
  * DEFAULT_THINKING_LEVEL) — NOT inherited from the bare harness, whose own fallback is "off": an
@@ -69,6 +76,84 @@ const DEFAULT_THINKING_LEVEL = "medium";
69
76
  * (like L2's findings memo), not session state — the resolve stays derived from the session.
70
77
  */
71
78
  const warnedRestores = new Set();
79
+ /** pi's ThinkingLevel scale as a checkable set — THE single source for fastagent (session entries
80
+ * store plain strings; session-control's dispatch validation and capabilities derive from this).
81
+ * The `satisfies Record<ThinkingLevel, …>` anchor makes it EXHAUSTIVE against pi's union: pi
82
+ * adding a level turns this into a type error instead of a silent drift where `set_thinking`
83
+ * rejects a value pi supports. */
84
+ const ALL_THINKING_LEVELS = {
85
+ off: true,
86
+ minimal: true,
87
+ low: true,
88
+ medium: true,
89
+ high: true,
90
+ xhigh: true,
91
+ max: true,
92
+ };
93
+ export const THINKING_LEVELS = new Set(Object.keys(ALL_THINKING_LEVELS));
94
+ /**
95
+ * The session's durable override FACTS — the ONE walk both surfaces consume (`state()` reports the
96
+ * recorded truth; `resolveHarnessOverrides` below applies registry/scale fallbacks on top). The
97
+ * LAST entry of each kind wins, and a malformed record reads as ABSENT for that kind — never
98
+ * skipped over to an earlier record: the reporting surface and the execution surface must agree on
99
+ * which record is "the" override.
100
+ */
101
+ export function lastOverrideEntries(entries) {
102
+ let model;
103
+ let modelSeen = false;
104
+ let thinkingLevel;
105
+ let thinkingSeen = false;
106
+ for (let i = entries.length - 1; i >= 0 && !(modelSeen && thinkingSeen); i--) {
107
+ const e = entries[i];
108
+ if (!modelSeen && e?.type === "model_change") {
109
+ modelSeen = true;
110
+ if (e.provider !== undefined && e.modelId !== undefined)
111
+ model = { provider: e.provider, modelId: e.modelId };
112
+ }
113
+ if (!thinkingSeen && e?.type === "thinking_level_change") {
114
+ thinkingSeen = true;
115
+ if (e.thinkingLevel !== undefined)
116
+ thinkingLevel = e.thinkingLevel;
117
+ }
118
+ }
119
+ return { model, thinkingLevel };
120
+ }
121
+ /**
122
+ * Resolve the session's model/thinking OVERRIDES for a fresh harness — same shape as the
123
+ * active-tools resolve above: pi writes `model_change`/`thinking_level_change` entries on explicit
124
+ * setModel/setThinkingLevel (the control plane's `set_model`/`set_thinking` append them directly)
125
+ * but a fresh harness never reads them back. Override facts come from {@link lastOverrideEntries};
126
+ * this adds the EXECUTION fallbacks: a recorded model no longer in this deployment's registry falls
127
+ * back to the default with a deduped warn (fail visibly without bricking the session — the
128
+ * conversation must survive a registry change across deploys); an unknown thinking level likewise.
129
+ */
130
+ export function resolveHarnessOverrides(entries, models, defaults, sessionId) {
131
+ let model = defaults.model;
132
+ let thinkingLevel = defaults.thinkingLevel;
133
+ const warnOnce = (key, message) => {
134
+ const emit = warnedRestores.has(key) ? log.debug : log.warn;
135
+ warnedRestores.add(key);
136
+ emit(message);
137
+ };
138
+ const recorded = lastOverrideEntries(entries);
139
+ if (recorded.model) {
140
+ const found = models.getModel(recorded.model.provider, recorded.model.modelId);
141
+ if (found)
142
+ model = found;
143
+ else {
144
+ warnOnce(`${sessionId}\u0000model\u0000${recorded.model.provider}/${recorded.model.modelId}`, `[fastagent] session ${sessionId}: recorded model override ${recorded.model.provider}/${recorded.model.modelId} is not in this deployment's registry — using the configured default`);
145
+ }
146
+ }
147
+ if (recorded.thinkingLevel !== undefined) {
148
+ if (THINKING_LEVELS.has(recorded.thinkingLevel)) {
149
+ thinkingLevel = recorded.thinkingLevel;
150
+ }
151
+ else {
152
+ warnOnce(`${sessionId}\u0000thinking\u0000${recorded.thinkingLevel}`, `[fastagent] session ${sessionId}: recorded thinking level "${recorded.thinkingLevel}" is unknown — using the configured default`);
153
+ }
154
+ }
155
+ return { model, thinkingLevel };
156
+ }
72
157
  export function resolveHarnessActiveToolNames(recorded, tools, sessionId) {
73
158
  const anyDeferred = tools.some(isDeferredTool);
74
159
  const initial = tools.filter((t) => !isDeferredTool(t)).map((t) => t.name);
@@ -99,17 +184,20 @@ export function piHarnessFactory(options) {
99
184
  const { systemPrompt } = options;
100
185
  const prompt = fresh ? fresh.systemPrompt : typeof systemPrompt === "function" ? systemPrompt() : systemPrompt;
101
186
  const skills = fresh ? fresh.skills : options.skills;
187
+ // Session overrides (set_model / set_thinking) win over the assembly defaults — same entry walk.
188
+ const overrides = resolveHarnessOverrides(entries, options.models, { model: options.model, thinkingLevel: options.thinkingLevel ?? DEFAULT_THINKING_LEVEL }, sessionId);
102
189
  const harness = new AgentHarness({
103
190
  env: options.env,
104
191
  session,
105
192
  models: options.models,
106
- model: options.model,
107
- thinkingLevel: options.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
193
+ model: overrides.model,
194
+ thinkingLevel: overrides.thinkingLevel,
108
195
  tools: options.tools,
109
196
  activeToolNames: resolveHarnessActiveToolNames(activated.length > 0 ? activated : null, options.tools ?? [], sessionId),
110
197
  systemPrompt: prompt,
111
198
  resources: skills ? { skills } : undefined,
112
199
  streamOptions: { maxRetries: PROVIDER_MAX_RETRIES },
200
+ retry: SUMMARIZATION_RETRY_POLICY,
113
201
  });
114
202
  harnessSessions.set(harness, session);
115
203
  return harness;
@@ -1,5 +1,20 @@
1
+ /**
2
+ * The turn mechanism (request-time): fan pi AgentHarness's two ports (subscribe event side-channel
3
+ * + prompt final value) into SPEC's single event stream, under a single-writer-per-session lease.
4
+ *
5
+ * §1 Lease — single-writer concurrency floor (injectable port + in-process default)
6
+ * §2 translate — the single pi↔SPEC translation point (both directions)
7
+ * §3 EventQueue — push→pull plumbing for pi's two-port shape
8
+ * §4 createPiAgentFromHarness — composes §1–§3 into Agent.invoke
9
+ *
10
+ * Concurrency: at most one in-flight turn per session; a second invoke fails fast with
11
+ * `failed{retryable}` ("session busy"), leaving dedupe/queueing/steering to the channel. Each
12
+ * invoke builds a fresh harness bound to the session and discards it (stateless multi-session).
13
+ */
14
+ import type { AgentHarnessEvent } from "@earendil-works/pi-agent-core";
1
15
  import type { AssistantMessage } from "@earendil-works/pi-ai";
2
- import { type Agent, type AgentEvent } from "../../agent.ts";
16
+ import { type Agent, type AgentEvent, type Prompt } from "../../agent.ts";
17
+ import type { SessionEvent } from "../../session.ts";
3
18
  import { type PiHarnessFactory } from "./harness.ts";
4
19
  export type Release = () => void;
5
20
  export interface Lease {
@@ -12,17 +27,55 @@ export declare function classifyRetryable(details: string, signal: {
12
27
  status?: number;
13
28
  code?: unknown;
14
29
  }): boolean;
30
+ /**
31
+ * In-stream event mapping — pi events are translated ONCE into the rich `SessionEvent` vocabulary;
32
+ * the SPEC `AgentEvent` stream is a narrow {@link projectAgentEvent} of it (design §6: one
33
+ * translation plus one projection, never two parallel translations). pi events with no session
34
+ * vocabulary yet (turn_start, agent_start, …) are dropped.
35
+ */
36
+ export declare function toSessionEvent(pe: AgentHarnessEvent, runId: string): SessionEvent | null;
37
+ /** The SPEC projection of the rich stream. Events with no `AgentEvent` counterpart (progress,
38
+ * message boundaries, run boundaries) project to null — the invoke terminal is produced from the
39
+ * resolved message ({@link toTerminal}), not from `run_settled`. */
40
+ export declare function projectAgentEvent(se: SessionEvent): AgentEvent | null;
41
+ /** Live modulation handles for one active run — what the control plane's `dispatch` routes to.
42
+ * Built inside the invoke closure (it owns the harness); registered with the observer at
43
+ * run_started, gone after run_settled. RACE WINDOW (all three commands, symmetric): the run may
44
+ * resolve between the settled-check and the engine call landing — an accepted `abort` can still
45
+ * settle `completed`, and an accepted `steer`/`followUp` can settle without the prompt ever being
46
+ * consumed. Acceptance is not outcome; the settlement is the truth. */
47
+ export interface RunControls {
48
+ steer(prompt: Prompt): Promise<void>;
49
+ followUp(prompt: Prompt): Promise<void>;
50
+ abort(): Promise<void>;
51
+ }
52
+ /** The DATA-plane observation seam: every rich event of every run, pushed as it happens. `run`
53
+ * carries the live {@link RunControls}, attached to the `run_started` event only. A hub
54
+ * (session-control.ts) implements this to serve `events()`/`state()`/`dispatch`; absent = zero
55
+ * overhead. Scope: RUN events only — the hub's own boundary-mutation events (`state_changed`,
56
+ * `compaction_*`) originate in the hub and reach full-vocabulary taps via the hub's `tap` option,
57
+ * not this seam. TRUST BOUNDARY: since Phase 2a this seam hands every wired observer the run's
58
+ * modulation handles — it is the trusted hub seam, not a public fan-out point. Do not wire
59
+ * untrusted taps here; give third parties the read-only `events()` stream instead. */
60
+ export type SessionObserver = (session: string, event: SessionEvent, run?: RunControls) => void;
15
61
  /**
16
62
  * Terminal mapping, decided by the resolved message's stopReason: pi's prompt() resolves a message
17
63
  * with stopReason "error"/"aborted" rather than throwing, so relying on catch alone would miss this
18
64
  * entire failure class (violating SPEC MUST 1).
19
65
  */
20
66
  export declare function toTerminal(message: AssistantMessage): AgentEvent;
21
- export declare function errorToTerminal(error: unknown): AgentEvent;
67
+ export declare function errorToTerminal(error: unknown): Extract<AgentEvent, {
68
+ type: "failed";
69
+ }>;
22
70
  export interface CreatePiAgentFromHarnessOptions {
23
71
  harnessFactory: PiHarnessFactory;
24
72
  /** Single-writer lease. Defaults to the in-process per-session fail-fast lease. */
25
73
  lease?: Lease;
74
+ /** Observation-plane tap (see {@link SessionObserver}). Optional; invoke behavior is identical
75
+ * with or without it — the SPEC stream is a projection of what the observer sees. */
76
+ observer?: SessionObserver;
77
+ /** Working directory exposed to FastAgent-defined tools. Defaults to process.cwd(). */
78
+ cwd?: string;
26
79
  }
27
80
  /** "From a harness factory": engine wired by the caller; adds only the concurrency/stream shell. */
28
81
  export declare function createPiAgentFromHarness(options: CreatePiAgentFromHarnessOptions): Agent;