@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
@@ -0,0 +1,308 @@
1
+ /**
2
+ * The shared definition-aware session builder: open a directory's assembled agent as a resident pi
3
+ * `AgentSessionRuntime`. Extracted from chat.ts (session-control Phase 0) as the proof of the
4
+ * assembly seam — independently instantiable, running the SAME agent that `dev`/`start` serve. The
5
+ * TUI (chat.ts) is its one consumer: the session control plane (Phases 1–3) was built on the invoke
6
+ * pipeline instead of this resident runtime, so control-plane observation covers invoke-driven runs
7
+ * and deliberately not chat sessions (design §10/§15).
8
+ *
9
+ * FIDELITY: pi's vanilla discovery (AGENTS.md walk to repo root, machine-global skills/extensions)
10
+ * is suppressed; fastagent's assembly is INJECTED into pi's session:
11
+ * - prompt → systemPromptOverride = base + instructions ONLY; pi appends the skill section and env
12
+ * (cwd) itself (including it here would duplicate it).
13
+ * - skills → skillsOverride (fastagent's skills, for the section + invocation).
14
+ * - tools → default coding tools by NAME (pi rebuilds them cwd-bound for rich rendering) +
15
+ * fastagent's custom tools via pi's customTools path (so they survive /new, /resume, fork).
16
+ * - models → a ModelRuntime with builtins only (`modelsPath: null`, no availability network), so
17
+ * the model surface equals serving's `createPiModels()` — pi's machine-global
18
+ * models.json does not leak in.
19
+ * - auth → fastagent's credential store at the AGENT's auth path (same resolution as the
20
+ * serving opener: `--auth-path`/`FASTAGENT_AUTH_PATH`, else `<root>/.secrets/auth.json`).
21
+ * pi's TUI `/login` writes through the injected store into the SAME file, so `fastagent
22
+ * login` and chat share one credential lifecycle. pi's `~/.pi` auth is not consulted.
23
+ *
24
+ * `getAgentDir()` (pi's `~/.pi`) remains ONLY for presentation-level TUI settings (theme,
25
+ * keybindings) — user preference, not agent definition; no auth or discovery flows from it.
26
+ *
27
+ * Cross-workspace session switches are rejected: `.env` is process-global, so one runtime is one
28
+ * workspace.
29
+ */
30
+ import { existsSync, readFileSync } from "node:fs";
31
+ import { dirname, join, resolve } from "node:path";
32
+ import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
33
+ import { SessionManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
34
+ import { resolveModel } from "./config.js";
35
+ import { assembleSystemPrompt, piBasePrompt, piDefaultTools } from "./create.js";
36
+ import { canonicalPath, loadAgentDefinition } from "./definition.js";
37
+ import { createPiModelRuntime, probeAuthSource } from "./models.js";
38
+ import { log } from "../../log.js";
39
+ import { additiveActivation, turnContext } from "./tool-context.js";
40
+ import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
41
+ import { resolveAgentAssembly } from "./open.js";
42
+ /** Adapt coding-agent's resident SessionManager to FastAgent's shared tool-runtime manager port. */
43
+ function toolChatSessionManager(session) {
44
+ return {
45
+ getSessionId: () => session.sessionManager.getSessionId(),
46
+ async getHeader() {
47
+ const header = session.sessionManager.getHeader();
48
+ if (!header)
49
+ throw new Error("chat session has no metadata header");
50
+ return { id: header.id, timestamp: header.timestamp };
51
+ },
52
+ async getBranch() {
53
+ return session.sessionManager.getBranch();
54
+ },
55
+ };
56
+ }
57
+ /**
58
+ * Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
59
+ * skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
60
+ * assembly — the fidelity-critical part — is inspectable and reusable without launching a TUI.
61
+ */
62
+ export async function buildAgentSessionRuntime(dir, options = {},
63
+ /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
64
+ sessionManager) {
65
+ /** The turn's {@link ToolActivation} over pi's AgentSession — the counterpart of invoke.ts's
66
+ * harness bridge, so the SAME builtin search_tools serves both paths. Additive; unknown names
67
+ * filtered (`setActiveToolsByName` is authoritative on the session and rebuilds its prompt — our
68
+ * static override keeps the prompt identical to serving). */
69
+ function sessionToolActivation(session) {
70
+ // Same serialization as invoke.ts's bridge (there per turn; here per session — interactive turns
71
+ // make per-session equivalent): the read-modify-write below is only race-free while nothing awaits
72
+ // between read and write, and pi's session setters happening to be synchronous today is not a
73
+ // contract worth betting parallel tool batches on. Built ONCE per session (createRuntime), so
74
+ // parallel calls actually share the chain.
75
+ let chain = Promise.resolve([]);
76
+ return {
77
+ active: () => session.getActiveToolNames(),
78
+ registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
79
+ activate(names) {
80
+ const run = async () => {
81
+ const current = session.getActiveToolNames();
82
+ const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
83
+ if (added.length > 0)
84
+ session.setActiveToolsByName([...current, ...added]);
85
+ return added;
86
+ };
87
+ const result = chain.then(run, run); // run after the predecessor settles, success or failure
88
+ chain = result.catch(() => []); // the caller sees a rejection on `result`; the chain stays usable
89
+ return result;
90
+ },
91
+ };
92
+ }
93
+ async function resolveAssembly(cwd) {
94
+ // The shared front half — the SAME placement/config/model-spec/tool/auth resolution the serving
95
+ // opener uses (open.ts); those inputs cannot drift between the two consumption shapes.
96
+ // (Definition→prompt assembly is NOT shared: serving re-reads live per invoke, this runtime is a
97
+ // startup snapshot and pi appends skills/env itself — see the header.) `tools` arrives with
98
+ // search_tools applied; deferral is EMULATED below like serving
99
+ // (what you iterate is what you serve): the initial active set excludes deferred tools (applied
100
+ // on the session in createRuntime — pi's session starts all-active), and the activation bridge
101
+ // above rides the same turn context, so the SAME search_tools works against pi's AgentSession
102
+ // instead of fastagent's harness.
103
+ const { config, modelSpec, agentDir, authPath, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
104
+ reportToolCollisions(toolCollisions);
105
+ reportModuleLoadFailures(toolFailures);
106
+ // ONE hub owns model resolution AND per-request auth — the ModelRuntime-shaped sibling of
107
+ // serving's createPiModels; see models.ts.
108
+ const modelRuntime = await createPiModelRuntime({ authPath });
109
+ // MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth;
110
+ // it now reads the agent's credential file like every other command. Probe the RESOLVED
111
+ // model's provider through the normal resolution path (stored credential OR env var — an
112
+ // env-authed user is fine and must not be warned): only when that provider has no usable auth
113
+ // AND pi's old file exists does the bare provider error get its cause named.
114
+ if ((await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
115
+ existsSync(join(getAgentDir(), "auth.json"))) {
116
+ log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
117
+ `pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
118
+ }
119
+ const model = resolveModel(modelRuntime, modelSpec);
120
+ const env = new NodeExecutionEnv({ cwd });
121
+ const definition = await loadAgentDefinition(agentDir, { cwd, env });
122
+ reportDefinitionWarnings(definition.collisions, definition.diagnostics);
123
+ const defaultNames = piDefaultTools(cwd).map((t) => t.name);
124
+ const customTools = tools.filter((t) => !defaultNames.includes(t.name));
125
+ // Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
126
+ // it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
127
+ // assembly is memoized across /new//resume/fork rebuilds while the session changes, so the bridge
128
+ // resolves through sessionRef at call time, exactly like the serving path resolves its harness.
129
+ const customToolDefs = customTools.map((t) => ({
130
+ name: t.name,
131
+ label: t.name,
132
+ description: t.description ?? "",
133
+ parameters: t.parameters,
134
+ // Propagate the execution mode — an activating tool (the builtin loader) declares "sequential"
135
+ // so pi serializes its batch; without this, pi's outer active-set diff double-stamps parallels.
136
+ executionMode: t.executionMode,
137
+ execute: (id, params, signal) => {
138
+ const bound = sessionRef.current;
139
+ // Unreachable by construction (createRuntime sets sessionRef before any turn can run a tool).
140
+ // Throw rather than silently run outside the turn context — that would disguise a broken
141
+ // session-lifecycle invariant as a normal out-of-turn call (fail visibly).
142
+ if (!bound)
143
+ throw new Error("tool executed before its session was built (lifecycle invariant broken)");
144
+ return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation }, () => t.execute(id, params, signal));
145
+ },
146
+ }));
147
+ // base + instructions ONLY — pi appends the skill section and env (cwd) itself (including
148
+ // them here would duplicate them).
149
+ const systemPrompt = assembleSystemPrompt({
150
+ base: piBasePrompt({ tools, persona: definition.persona }),
151
+ contextFiles: definition.contextFiles,
152
+ });
153
+ return {
154
+ model,
155
+ modelRuntime,
156
+ // Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
157
+ thinkingLevel: config.thinkingLevel,
158
+ definition,
159
+ defaultNames,
160
+ customTools,
161
+ customToolDefs,
162
+ deferredToolNames,
163
+ systemPrompt,
164
+ };
165
+ }
166
+ // pi calls the factory again on /new, /resume, switch, and fork. Config/tools dynamic imports are
167
+ // ESM-cached, so treating same-cwd rebuilds as hot reload would yield a half-fresh agent (fresh
168
+ // AGENTS.md/skills, stale config/tools). Keep the runtime a coherent startup snapshot: restart to
169
+ // load edits. And keep it workspace-scoped — `.env` is process-global, so a switch to another cwd
170
+ // would leak env or require mutating global env at runtime.
171
+ const rootCwd = canonicalPath(dir);
172
+ // The CURRENT pi session + its activation bridge, BOUND TOGETHER — rebuilt on /new//resume/fork
173
+ // while the memoized assembly (and its tool execute closures) stays. The bridge must share the
174
+ // session's lifetime, NOT be rebuilt per tool call (a per-call chain serializes nothing). Note on
175
+ // parallel batches: pi wraps SDK customTools in its own before/after active-set diff, so an
176
+ // activating tool must carry `executionMode: "sequential"` (the builtin loader does) — pi then runs
177
+ // the whole batch serially and the outer diff sees correct snapshots.
178
+ const sessionRef = {};
179
+ let assembly;
180
+ const assemblyFor = (cwd) => {
181
+ // Canonical paths: pi's process.cwd() fallback is a realpath, so a symlinked workspace would
182
+ // otherwise mismatch a non-realpath rootCwd.
183
+ const activeCwd = canonicalPath(cwd);
184
+ if (activeCwd !== rootCwd) {
185
+ throw workspaceScopeError(activeCwd);
186
+ }
187
+ assembly ??= resolveAssembly(rootCwd);
188
+ return assembly;
189
+ };
190
+ const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
191
+ const { model, modelRuntime, thinkingLevel, definition, defaultNames, customTools, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
192
+ const services = await createAgentSessionServices({
193
+ cwd,
194
+ // fastagent's models + auth hub replaces pi's default (~/.pi-backed) one — the auth
195
+ // unification point; see the header.
196
+ modelRuntime,
197
+ resourceLoaderOptions: {
198
+ // Definition-only, like dev/start: suppress pi's machine-global discovery (the developer's own
199
+ // ~/.pi extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) so this runtime runs
200
+ // the same agent that gets served, not the authoring machine's pi setup on top.
201
+ noExtensions: true,
202
+ noPromptTemplates: true,
203
+ noContextFiles: true,
204
+ systemPromptOverride: () => systemPrompt,
205
+ appendSystemPromptOverride: () => [],
206
+ // Replace pi's discovered skills with fastagent's. fastagent's Skill (content inline) is
207
+ // reshaped to pi-coding-agent's (read from filePath/baseDir at invocation time).
208
+ skillsOverride: (base) => ({
209
+ skills: definition.skills.map((s) => ({
210
+ name: s.name,
211
+ description: s.description,
212
+ filePath: s.filePath,
213
+ baseDir: dirname(s.filePath),
214
+ sourceInfo: {
215
+ path: s.filePath,
216
+ source: "fastagent",
217
+ scope: "project",
218
+ origin: "top-level",
219
+ baseDir: dirname(s.filePath),
220
+ },
221
+ disableModelInvocation: s.disableModelInvocation ?? false,
222
+ })),
223
+ diagnostics: base.diagnostics,
224
+ }),
225
+ },
226
+ });
227
+ const result = await createAgentSessionFromServices({
228
+ services,
229
+ sessionManager,
230
+ sessionStartEvent,
231
+ model,
232
+ thinkingLevel,
233
+ tools: [...defaultNames, ...customTools.map((t) => t.name)],
234
+ customTools: customToolDefs,
235
+ });
236
+ sessionRef.current = {
237
+ session: result.session,
238
+ sessionManager: toolChatSessionManager(result.session),
239
+ activation: sessionToolActivation(result.session),
240
+ };
241
+ // Deferral emulation: pi's session starts with everything active — narrow it by SUBTRACTING
242
+ // the deferred names from whatever is active (robust to pi mounting tools of its own; an
243
+ // exact-set-equality gate would silently stop narrowing the day pi adds one). Applied on EVERY
244
+ // build including /resume: pi's chat session does not record activations (its SessionContext has
245
+ // no activeToolNames), so "restore prior activations" is not implementable here — deferral stays
246
+ // consistently ON and a resumed conversation re-discovers via search_tools (documented divergence
247
+ // from serving, where activations persist in the session). The deferred SET comes from the shared
248
+ // assembly (one definition of "deferred"), never recomputed here.
249
+ if (deferredToolNames.length > 0) {
250
+ const active = result.session.getActiveToolNames();
251
+ if (deferredToolNames.some((n) => active.includes(n))) {
252
+ result.session.setActiveToolsByName(active.filter((n) => !deferredToolNames.includes(n)));
253
+ }
254
+ }
255
+ return { ...result, services, diagnostics: services.diagnostics };
256
+ };
257
+ const runtime = await createAgentSessionRuntime(createRuntime, {
258
+ cwd: rootCwd,
259
+ agentDir: getAgentDir(),
260
+ sessionManager: sessionManager ?? SessionManager.create(rootCwd),
261
+ });
262
+ enforceWorkspaceScopedSessionSwitches(runtime, rootCwd);
263
+ return runtime;
264
+ }
265
+ function workspaceScopeError(targetCwd) {
266
+ return new Error(`fastagent sessions are workspace-scoped: cannot switch to ${targetCwd}; open that workspace instead`);
267
+ }
268
+ function readSessionHeaderCwd(sessionPath) {
269
+ const resolvedPath = resolve(sessionPath);
270
+ if (!existsSync(resolvedPath))
271
+ return undefined;
272
+ for (const line of readFileSync(resolvedPath, "utf8").split(/\r?\n/)) {
273
+ if (!line.trim())
274
+ continue;
275
+ try {
276
+ const entry = JSON.parse(line);
277
+ if (entry.type === "session")
278
+ return typeof entry.cwd === "string" ? canonicalPath(entry.cwd) : undefined;
279
+ }
280
+ catch {
281
+ // Ignore malformed lines the same way pi's session loader does; no header cwd → caller pins root.
282
+ }
283
+ }
284
+ return undefined;
285
+ }
286
+ /**
287
+ * Keep resume/import inside the runtime's single workspace, deciding BEFORE delegating to pi. The
288
+ * process is chdir'd into rootCwd, so a session with no cwd header already lands on rootCwd. The gap
289
+ * is a session that EXPLICITLY records a different cwd: pi would bind it and the factory would reject
290
+ * it — but only AFTER tearing the live session down. Reject such a switch up front.
291
+ */
292
+ function enforceWorkspaceScopedSessionSwitches(runtime, rootCwd) {
293
+ const rejectForeignTarget = (sessionPath, cwdOverride) => {
294
+ const target = cwdOverride !== undefined ? canonicalPath(cwdOverride) : readSessionHeaderCwd(sessionPath);
295
+ if (target !== undefined && target !== rootCwd)
296
+ throw workspaceScopeError(target);
297
+ };
298
+ const switchSession = runtime.switchSession.bind(runtime);
299
+ runtime.switchSession = async (...args) => {
300
+ rejectForeignTarget(args[0], args[1]?.cwdOverride);
301
+ return switchSession(...args);
302
+ };
303
+ const importFromJsonl = runtime.importFromJsonl.bind(runtime);
304
+ runtime.importFromJsonl = async (...args) => {
305
+ rejectForeignTarget(args[0], args[1]);
306
+ return importFromJsonl(...args);
307
+ };
308
+ }
@@ -0,0 +1,50 @@
1
+ import type { Models } from "@earendil-works/pi-ai";
2
+ import { type SessionControl, type SessionEvent } from "../../session.ts";
3
+ import type { Lease, SessionObserver } from "./invoke.ts";
4
+ import { type PiHarnessFactory } from "./harness.ts";
5
+ import type { PiSessionReader } from "./sessions.ts";
6
+ /** Ceiling for one subscriber's unconsumed backlog. A consumer this far behind (a stalled remote
7
+ * connection — the wire's ReadableStream backpressure stops pulling while invokes keep pushing)
8
+ * has its buffer FROZEN at the cap (memory bounded — the actual goal: ≈10k small events ≈ a few
9
+ * MB worst case per stuck connection) and its subscription marked closed. The close is observed
10
+ * via pulls — which a stalled connection by definition does not make — so a consumer that RESUMES
11
+ * pulling first drains the frozen backlog, then gets done (no buffered event dropped), while a
12
+ * permanently stalled one holds the frozen buffer until its TCP connection dies. Recovery either
13
+ * way is the standard reconnect+backfill, semantically lossless. */
14
+ export declare const SUBSCRIBER_BUFFER_CAP = 10000;
15
+ /** What boundary mutations (compact / set_model / set_thinking) need — the SAME instances the
16
+ * agent assembly uses: the lease (mutations must not race a run), the model registry (validation +
17
+ * allowedModels), and the harness factory (compaction is a model call). Writes go through the
18
+ * session the hub's reader opened — after an existence check, so the control plane never creates
19
+ * a session (that is the data plane's monopoly). */
20
+ export interface PiBoundaryWiring {
21
+ lease: Lease;
22
+ models: Models;
23
+ harnessFactory: PiHarnessFactory;
24
+ }
25
+ export interface CreatePiSessionControlOptions {
26
+ /** Read-only access to the durable session repository (the same root the agent writes). */
27
+ sessions: PiSessionReader;
28
+ /** Boundary-mutation wiring, as a LAZY thunk: the hub's observer must exist before the agent
29
+ * assembly that produces these parts, so the hub asks for them at dispatch time instead
30
+ * (assembly completes before any dispatch can arrive). Absent / undefined → boundary commands
31
+ * are gated off in `capabilities()` and rejected `unsupported_capability`. */
32
+ boundary?: () => PiBoundaryWiring | undefined;
33
+ /** Tap for the events the HUB ITSELF generates (boundary mutations: `state_changed`,
34
+ * `compaction_*`) — those never pass through the data plane's observer seam, so a consumer
35
+ * composing a full-vocabulary tap wires the run events via the observer AND this. Called after
36
+ * the hub's own subscribers. */
37
+ tap?: (session: string, event: SessionEvent) => void;
38
+ }
39
+ /**
40
+ * Build the observation hub. Wire `observer` into the SAME agent assembly that serves the sessions:
41
+ *
42
+ * ```ts
43
+ * const { control, observer } = createPiSessionControl({ sessions });
44
+ * const agent = createPiAgent({ ..., sessions, observer });
45
+ * ```
46
+ */
47
+ export declare function createPiSessionControl(options: CreatePiSessionControlOptions): {
48
+ control: SessionControl;
49
+ observer: SessionObserver;
50
+ };