@fastagent-sh/fastagent 0.18.0 → 0.20.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 (222) hide show
  1. package/README.md +25 -7
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/atomic-write.d.ts +23 -0
  5. package/dist/atomic-write.js +53 -0
  6. package/dist/{host/node.d.ts → channel.d.ts} +24 -28
  7. package/dist/channel.js +1 -0
  8. package/dist/channels/agentcore-limits.d.ts +9 -0
  9. package/dist/channels/agentcore-limits.js +9 -0
  10. package/dist/channels/agentcore-service.d.ts +55 -0
  11. package/dist/channels/agentcore-service.js +107 -0
  12. package/dist/channels/agentcore.d.ts +22 -20
  13. package/dist/channels/agentcore.js +20 -11
  14. package/dist/channels/busy.d.ts +1 -1
  15. package/dist/channels/busy.js +1 -1
  16. package/dist/channels/control.d.ts +55 -7
  17. package/dist/channels/control.js +312 -67
  18. package/dist/channels/discover.d.ts +59 -0
  19. package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
  20. package/dist/channels/feishu/card.js +1 -1
  21. package/dist/channels/feishu/context-buffer.d.ts +1 -1
  22. package/dist/channels/feishu/context-buffer.js +4 -4
  23. package/dist/channels/feishu/feishu-api.js +6 -9
  24. package/dist/channels/feishu/feishu.d.ts +1 -1
  25. package/dist/channels/feishu/feishu.js +41 -24
  26. package/dist/channels/feishu/invoke-turn.d.ts +1 -1
  27. package/dist/channels/feishu/invoke-turn.js +3 -4
  28. package/dist/channels/feishu/preview.d.ts +1 -1
  29. package/dist/channels/feishu/preview.js +7 -3
  30. package/dist/channels/feishu/ws-ingress.d.ts +1 -1
  31. package/dist/channels/github/github.d.ts +1 -1
  32. package/dist/channels/http.d.ts +11 -7
  33. package/dist/channels/http.js +1 -109
  34. package/dist/channels/kit/attachment-path.d.ts +12 -0
  35. package/dist/channels/kit/attachment-path.js +43 -0
  36. package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
  37. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
  38. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
  39. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
  40. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  41. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  42. package/dist/channels/kit/signature.d.ts +12 -0
  43. package/dist/channels/kit/signature.js +17 -0
  44. package/dist/channels/{state.js → kit/state.js} +4 -7
  45. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  46. package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
  47. package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
  48. package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
  49. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
  50. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  51. package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
  52. package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
  53. package/dist/channels/lark/lark.d.ts +1 -1
  54. package/dist/channels/serve.d.ts +73 -0
  55. package/dist/channels/serve.js +243 -0
  56. package/dist/channels/slack/bot-auth.js +3 -14
  57. package/dist/channels/slack/context-buffer.d.ts +2 -2
  58. package/dist/channels/slack/context-buffer.js +2 -2
  59. package/dist/channels/slack/invoke-turn.d.ts +2 -2
  60. package/dist/channels/slack/invoke-turn.js +13 -6
  61. package/dist/channels/slack/onboard.js +5 -5
  62. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  63. package/dist/channels/slack/onboarding-state.js +13 -20
  64. package/dist/channels/slack/parse.js +2 -2
  65. package/dist/channels/slack/preview.d.ts +1 -1
  66. package/dist/channels/slack/preview.js +11 -3
  67. package/dist/channels/slack/register-webhook.js +1 -1
  68. package/dist/channels/slack/scaffold/slack-send.ts +2 -17
  69. package/dist/channels/slack/slack-api.js +23 -55
  70. package/dist/channels/slack/slack.d.ts +1 -1
  71. package/dist/channels/slack/slack.js +20 -20
  72. package/dist/channels/slack/welcomed.js +1 -1
  73. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  74. package/dist/channels/telegram/context-buffer.js +2 -2
  75. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  76. package/dist/channels/telegram/invoke-turn.js +1 -1
  77. package/dist/channels/telegram/parse.d.ts +21 -1
  78. package/dist/channels/telegram/parse.js +67 -13
  79. package/dist/channels/telegram/preview.d.ts +1 -1
  80. package/dist/channels/telegram/preview.js +6 -2
  81. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  82. package/dist/channels/telegram/telegram-api.js +5 -6
  83. package/dist/channels/telegram/telegram.d.ts +3 -3
  84. package/dist/channels/telegram/telegram.js +60 -72
  85. package/dist/channels/telegram/turn-store.d.ts +2 -2
  86. package/dist/channels/telegram/turn-store.js +2 -2
  87. package/dist/cli/add-slack.js +6 -5
  88. package/dist/cli/commands/add.js +1 -5
  89. package/dist/cli/commands/attach.d.ts +2 -4
  90. package/dist/cli/commands/attach.js +17 -15
  91. package/dist/cli/commands/deploy.d.ts +50 -5
  92. package/dist/cli/commands/deploy.js +183 -134
  93. package/dist/cli/commands/dev.js +25 -40
  94. package/dist/cli/commands/fire.js +1 -1
  95. package/dist/cli/commands/info.js +7 -5
  96. package/dist/cli/commands/init.js +1 -2
  97. package/dist/cli/commands/schedule.js +1 -1
  98. package/dist/cli/commands/start.js +80 -121
  99. package/dist/cli/commands/tool.js +5 -7
  100. package/dist/cli/program.js +1 -1
  101. package/dist/cli/serve.d.ts +22 -70
  102. package/dist/cli/serve.js +66 -257
  103. package/dist/cli/shared.d.ts +40 -10
  104. package/dist/cli/shared.js +53 -6
  105. package/dist/core.d.ts +3 -5
  106. package/dist/core.js +7 -5
  107. package/dist/deploy/agentcore/logs.d.ts +2 -2
  108. package/dist/deploy/agentcore/logs.js +2 -2
  109. package/dist/deploy/agentcore/plan.d.ts +12 -10
  110. package/dist/deploy/agentcore/plan.js +39 -31
  111. package/dist/deploy/agentcore/run.d.ts +5 -4
  112. package/dist/deploy/agentcore/run.js +13 -36
  113. package/dist/deploy/channel-ingress.d.ts +73 -0
  114. package/dist/deploy/channel-ingress.js +101 -0
  115. package/dist/deploy/docker/plan.d.ts +3 -7
  116. package/dist/deploy/docker/plan.js +4 -15
  117. package/dist/deploy/docker/run.d.ts +18 -1
  118. package/dist/deploy/docker/run.js +10 -0
  119. package/dist/deploy/fly/plan.d.ts +4 -5
  120. package/dist/deploy/fly/plan.js +7 -22
  121. package/dist/deploy/fly/run.d.ts +9 -23
  122. package/dist/deploy/fly/run.js +32 -68
  123. package/dist/deploy/preflight.d.ts +6 -7
  124. package/dist/deploy/preflight.js +27 -23
  125. package/dist/deploy/railway/plan.d.ts +4 -5
  126. package/dist/deploy/railway/plan.js +11 -26
  127. package/dist/deploy/railway/run.d.ts +9 -9
  128. package/dist/deploy/railway/run.js +40 -40
  129. package/dist/deploy/runner.js +22 -2
  130. package/dist/deploy/secrets.d.ts +18 -9
  131. package/dist/deploy/secrets.js +64 -12
  132. package/dist/dev-supervisor.js +11 -6
  133. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  134. package/dist/engines/pi/agent-session-factory.js +295 -0
  135. package/dist/engines/pi/auth.js +43 -43
  136. package/dist/engines/pi/config.d.ts +5 -5
  137. package/dist/engines/pi/config.js +1 -1
  138. package/dist/engines/pi/create.d.ts +67 -42
  139. package/dist/engines/pi/create.js +142 -72
  140. package/dist/engines/pi/definition.d.ts +42 -8
  141. package/dist/engines/pi/definition.js +114 -11
  142. package/dist/engines/pi/invoke-session.d.ts +22 -19
  143. package/dist/engines/pi/invoke-session.js +286 -100
  144. package/dist/engines/pi/login.js +39 -20
  145. package/dist/engines/pi/models.d.ts +14 -0
  146. package/dist/engines/pi/models.js +9 -1
  147. package/dist/engines/pi/open.d.ts +10 -8
  148. package/dist/engines/pi/open.js +21 -11
  149. package/dist/engines/pi/report.d.ts +0 -4
  150. package/dist/engines/pi/report.js +0 -7
  151. package/dist/engines/pi/search-tools.js +1 -1
  152. package/dist/engines/pi/service.d.ts +23 -0
  153. package/dist/engines/pi/service.js +27 -0
  154. package/dist/engines/pi/session-builder.js +87 -118
  155. package/dist/engines/pi/session-control.d.ts +28 -14
  156. package/dist/engines/pi/session-control.js +570 -438
  157. package/dist/engines/pi/session-inheritance.d.ts +30 -0
  158. package/dist/engines/pi/session-inheritance.js +261 -0
  159. package/dist/engines/pi/session-markers.d.ts +48 -0
  160. package/dist/engines/pi/session-markers.js +59 -0
  161. package/dist/engines/pi/session-settings.d.ts +15 -3
  162. package/dist/engines/pi/session-settings.js +22 -1
  163. package/dist/engines/pi/session-store.d.ts +140 -0
  164. package/dist/engines/pi/session-store.js +666 -0
  165. package/dist/engines/pi/tool-context.d.ts +36 -13
  166. package/dist/engines/pi/tool-context.js +51 -4
  167. package/dist/engines/pi/tool.d.ts +5 -5
  168. package/dist/engines/pi/tool.js +3 -3
  169. package/dist/engines/pi/turn-kit.d.ts +32 -6
  170. package/dist/engines/pi/turn-kit.js +28 -1
  171. package/dist/engines/pi/wake-tool.js +1 -1
  172. package/dist/env.js +1 -2
  173. package/dist/index.d.ts +1 -0
  174. package/dist/index.js +1 -0
  175. package/dist/loader.d.ts +49 -7
  176. package/dist/loader.js +79 -18
  177. package/dist/log.d.ts +3 -0
  178. package/dist/log.js +5 -1
  179. package/dist/node.d.ts +16 -0
  180. package/dist/node.js +20 -0
  181. package/dist/paths.d.ts +26 -0
  182. package/dist/paths.js +37 -2
  183. package/dist/pi.d.ts +14 -8
  184. package/dist/pi.js +6 -5
  185. package/dist/scaffold/add-channel.js +2 -2
  186. package/dist/scaffold/init.js +7 -2
  187. package/dist/scaffold/templates/persona.md +2 -2
  188. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  189. package/dist/schedule/discover.js +3 -15
  190. package/dist/schedule/state.js +4 -9
  191. package/dist/schedule/wake-alarm.d.ts +13 -7
  192. package/dist/schedule/wake-alarm.js +88 -46
  193. package/dist/schedule/wakeups.d.ts +2 -2
  194. package/dist/schedule/wakeups.js +11 -8
  195. package/dist/service.d.ts +157 -0
  196. package/dist/service.js +376 -0
  197. package/dist/session-remote.d.ts +8 -3
  198. package/dist/session-remote.js +162 -92
  199. package/dist/session.d.ts +229 -93
  200. package/dist/session.js +61 -23
  201. package/dist/telegram.d.ts +1 -1
  202. package/dist/telegram.js +1 -1
  203. package/dist/tunnel.d.ts +18 -3
  204. package/dist/tunnel.js +32 -40
  205. package/package.json +14 -5
  206. package/dist/engines/pi/channel.d.ts +0 -37
  207. package/dist/engines/pi/harness.d.ts +0 -115
  208. package/dist/engines/pi/harness.js +0 -154
  209. package/dist/engines/pi/invoke.d.ts +0 -60
  210. package/dist/engines/pi/invoke.js +0 -459
  211. package/dist/engines/pi/read-image.d.ts +0 -4
  212. package/dist/engines/pi/read-image.js +0 -62
  213. package/dist/engines/pi/sessions.d.ts +0 -60
  214. package/dist/engines/pi/sessions.js +0 -464
  215. package/dist/host/node.js +0 -52
  216. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  217. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  218. /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
  219. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  220. /package/dist/channels/{text.js → kit/text.js} +0 -0
  221. /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
  222. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
@@ -16,7 +16,7 @@ import { createPiSessionControl } from "./session-control.js";
16
16
  import { withWakeTool } from "./wake-tool.js";
17
17
  import { loadAgentSkills } from "./definition.js";
18
18
  import { reportFindingsIfChanged } from "./report.js";
19
- import { jsonlSessionStore } from "./sessions.js";
19
+ import { piSessionRecordStore } from "./session-store.js";
20
20
  export async function resolveAgentAssembly(dir, options = {}) {
21
21
  // Placement is structural (resolvePlacement): the AGENT DIR carries definition + config + machinery;
22
22
  // its parent — the WORKSPACE — is what the agent works on: its cwd and the start of the ②-context
@@ -27,7 +27,7 @@ export async function resolveAgentAssembly(dir, options = {}) {
27
27
  if (!modelSpec) {
28
28
  throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
29
29
  }
30
- const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir);
30
+ const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir, workspace);
31
31
  // The state root: sessions/channel state/schedule state derive from it (FASTAGENT_STATE_DIR moves it
32
32
  // in one knob — a container points it at its volume); the finer overrides below still win.
33
33
  const stateRoot = resolveStateRoot(agentDir);
@@ -62,13 +62,14 @@ export async function createPiAgentFromDir(dir, options = {}) {
62
62
  const mountedTools = withWakeTool(tools, stateRoot, !!options.serving && !!config.selfSchedule);
63
63
  const sessionsDir = options.sessionsDir ?? defaultSessionsDir(stateRoot);
64
64
  await mkdir(sessionsDir, { recursive: true });
65
- const sessions = jsonlSessionStore({ dir: sessionsDir, cwd: workspace });
65
+ const sessions = piSessionRecordStore({ dir: sessionsDir, cwd: workspace });
66
66
  // The hub is wired HERE because the store is created here: chicken-and-egg otherwise (the hub
67
- // needs the store; the agent needs the hub's observer). Boundary parts (models/factory/lease)
67
+ // needs the store; the agent needs the hub's observer). Boundary parts (factory/lease/registry)
68
68
  // only exist after the assembly below — the hub takes them as a lazy thunk, filled by the
69
69
  // assembly's onAssembly callback (assembly completes before this function returns, so every
70
70
  // dispatch sees them). An extra caller observer composes after the hub's (TRUSTED seam).
71
71
  let boundaryParts;
72
+ let assembled;
72
73
  const caller = options.observer;
73
74
  const wantControl = options.sessionControl ?? (config.sessionControl === true && options.serving === true);
74
75
  const hub = wantControl
@@ -94,7 +95,7 @@ export async function createPiAgentFromDir(dir, options = {}) {
94
95
  },
95
96
  // The caller tap's boundary-event half: state_changed/compaction_* originate in the hub
96
97
  // and never cross the data plane's observer seam — without this, an audit tap wired here
97
- // would miss exactly the mutations it most needs to see (set_model).
98
+ // would miss exactly the mutations it most needs to see (`update({ model })`).
98
99
  tap: caller ? (session, event) => caller(session, event) : undefined,
99
100
  })
100
101
  : undefined;
@@ -117,20 +118,29 @@ export async function createPiAgentFromDir(dir, options = {}) {
117
118
  observer,
118
119
  onAssembly: hub
119
120
  ? (parts) => {
120
- boundaryParts = {
121
- lease: parts.lease,
122
- models: parts.models,
123
- harnessFactory: parts.harnessFactory,
124
- defaults: parts.defaults,
125
- };
121
+ assembled = parts;
126
122
  }
127
123
  : undefined,
128
124
  });
125
+ if (hub && assembled) {
126
+ // The hub's surface is synchronous (`capabilities()` lists the allowed models), while building
127
+ // the registry reads credentials and is not. Resolve it ONCE here — the opener is async anyway,
128
+ // and the first turn would have paid the same read — so every boundary mutation validates
129
+ // against the same registry the runs use.
130
+ const { modelRuntime, model } = await assembled.engine();
131
+ boundaryParts = {
132
+ lease: assembled.lease,
133
+ models: modelRuntime,
134
+ sessionFactory: assembled.sessionFactory,
135
+ defaults: { model, thinkingLevel: assembled.thinkingLevel },
136
+ };
137
+ }
129
138
  return {
130
139
  agent,
131
140
  definition,
132
141
  sessions,
133
142
  sessionControl: hub?.control,
143
+ selfSchedule: config.selfSchedule ?? false,
134
144
  agentDir,
135
145
  workspace,
136
146
  config,
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import type { SkillDiagnostic } from "@earendil-works/pi-agent-core";
6
6
  import type { SkillCollision } from "./definition.ts";
7
- import type { ModuleLoadFailure } from "../../loader.ts";
8
7
  import type { ToolCollision } from "./tool.ts";
9
8
  type Findings = {
10
9
  collisions: SkillCollision[];
@@ -23,7 +22,4 @@ type Findings = {
23
22
  export declare function reportFindingsIfChanged(dir: string, def: Findings): void;
24
23
  export declare function reportDefinitionWarnings(collisions: SkillCollision[], diagnostics: SkillDiagnostic[]): void;
25
24
  export declare function reportToolCollisions(collisions: ToolCollision[]): void;
26
- /** Report per-file module failures. The caller decides whether they are degradations (tools/schedules)
27
- * or fatal (declared channels on the serving path). */
28
- export declare function reportModuleLoadFailures(failures: ModuleLoadFailure[]): void;
29
25
  export {};
@@ -42,10 +42,3 @@ export function reportToolCollisions(collisions) {
42
42
  log.warn(`[fastagent] tool "${c.name}" (${c.source}) dropped — a default/config tool already uses that name`);
43
43
  }
44
44
  }
45
- /** Report per-file module failures. The caller decides whether they are degradations (tools/schedules)
46
- * or fatal (declared channels on the serving path). */
47
- export function reportModuleLoadFailures(failures) {
48
- for (const f of failures) {
49
- log.warn(`[fastagent] ${f.label} failed to load, skipping it — ${f.message}`);
50
- }
51
- }
@@ -124,7 +124,7 @@ export function makeSearchToolsTool() {
124
124
  .map((t) => t.name)
125
125
  .join(", ")}${more > 0 ? ` … and ${more} more` : ""}.${activeNote ? ` ${activeNote}` : ""}`;
126
126
  }
127
- const activated = await ctx.tools.activate(inactiveMatches.map((t) => t.name));
127
+ const activated = ctx.tools.activate(inactiveMatches.map((t) => t.name));
128
128
  // Report what actually happened, not what was attempted: a parallel sibling call may have
129
129
  // activated the same matches first, leaving nothing new here — an empty "Activated:" would lie.
130
130
  if (activated.length === 0) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `createAgentService` — the product as one call, with pi supplying the engine.
3
+ *
4
+ * The ASSEMBLY is engine-neutral and lives in `src/service.ts`: it takes a {@link MountableAgent}
5
+ * (the SPEC contract plus three paths) and knows nothing about how that agent was built. What is
6
+ * pi-specific is opening a DIRECTORY into one, which is why this shortcut lives here and the
7
+ * assembly does not. A second engine ships its own opener and reuses `mountAgentService` unchanged.
8
+ */
9
+ import { type AgentService, type MountAgentServiceOptions } from "../../service.ts";
10
+ export interface CreateAgentServiceOptions extends MountAgentServiceOptions {
11
+ model?: string;
12
+ authPath?: string;
13
+ sessionsDir?: string;
14
+ }
15
+ /**
16
+ * Open an agent directory as a live service: one handler, mounted wherever you serve.
17
+ *
18
+ * ```ts
19
+ * const service = await createAgentService("./my-agent");
20
+ * app.use("/agent", nodeListener(service.handler));
21
+ * ```
22
+ */
23
+ export declare function createAgentService(dir: string, options?: CreateAgentServiceOptions): Promise<AgentService>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `createAgentService` — the product as one call, with pi supplying the engine.
3
+ *
4
+ * The ASSEMBLY is engine-neutral and lives in `src/service.ts`: it takes a {@link MountableAgent}
5
+ * (the SPEC contract plus three paths) and knows nothing about how that agent was built. What is
6
+ * pi-specific is opening a DIRECTORY into one, which is why this shortcut lives here and the
7
+ * assembly does not. A second engine ships its own opener and reuses `mountAgentService` unchanged.
8
+ */
9
+ import { mountAgentService } from "../../service.js";
10
+ import { createPiAgentFromDir } from "./open.js";
11
+ /**
12
+ * Open an agent directory as a live service: one handler, mounted wherever you serve.
13
+ *
14
+ * ```ts
15
+ * const service = await createAgentService("./my-agent");
16
+ * app.use("/agent", nodeListener(service.handler));
17
+ * ```
18
+ */
19
+ export async function createAgentService(dir, options = {}) {
20
+ const opened = await createPiAgentFromDir(dir, {
21
+ ...(options.model !== undefined ? { model: options.model } : {}),
22
+ ...(options.authPath !== undefined ? { authPath: options.authPath } : {}),
23
+ ...(options.sessionsDir !== undefined ? { sessionsDir: options.sessionsDir } : {}),
24
+ serving: true, // a mounted service is long-running: the scheduler poller runs
25
+ });
26
+ return mountAgentService(opened, options);
27
+ }
@@ -11,8 +11,8 @@
11
11
  * - prompt → systemPromptOverride = base + instructions ONLY; pi appends the skill section and env
12
12
  * (cwd) itself (including it here would duplicate it).
13
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).
14
+ * - tools → all seven coding tools plus authored tools through pi's customTools path; pi's builtin
15
+ * copies are suppressed, and the injected set survives /new, /resume, and fork.
16
16
  * - models → a ModelRuntime with builtins only (`modelsPath: null`, no availability network), so
17
17
  * the model surface equals serving's `createPiModels()` — pi's machine-global
18
18
  * models.json does not leak in.
@@ -28,32 +28,18 @@
28
28
  * workspace.
29
29
  */
30
30
  import { existsSync, readFileSync } from "node:fs";
31
- import { dirname, join, resolve } from "node:path";
31
+ import { join, resolve } from "node:path";
32
32
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
33
33
  import { SessionManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
34
+ import { definitionResourceLoaderOptions, reportExtensionErrors } from "./agent-session-factory.js";
34
35
  import { resolveModel } from "./config.js";
35
- import { assembleSystemPrompt, piBasePrompt, piDefaultTools } from "./create.js";
36
- import { canonicalPath, loadAgentDefinition } from "./definition.js";
36
+ import { assembleSystemPrompt, piBasePrompt } from "./create.js";
37
+ import { canonicalPath, loadAgentDefinition, loadExtensionPaths } from "./definition.js";
37
38
  import { createPiModelRuntime, probeAuthSource } from "./models.js";
38
- import { log } from "../../log.js";
39
- import { additiveActivation, turnContext } from "./tool-context.js";
40
- import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
39
+ import { log, reportModuleLoadFailures } from "../../log.js";
40
+ import { agentSessionManager, sessionToolActivation, turnContext, } from "./tool-context.js";
41
+ import { reportFindingsIfChanged, reportToolCollisions } from "./report.js";
41
42
  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
43
  /**
58
44
  * Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
59
45
  * skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
@@ -62,34 +48,6 @@ function toolChatSessionManager(session) {
62
48
  export async function buildAgentSessionRuntime(dir, options = {},
63
49
  /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
64
50
  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
51
  async function resolveAssembly(cwd) {
94
52
  // The shared front half — the SAME placement/config/model-spec/tool/auth resolution the serving
95
53
  // opener uses (open.ts); those inputs cannot drift between the two consumption shapes.
@@ -99,36 +57,27 @@ sessionManager) {
99
57
  // (what you iterate is what you serve): the initial active set excludes deferred tools (applied
100
58
  // on the session in createRuntime — pi's session starts all-active), and the activation bridge
101
59
  // above rides the same turn context, so the SAME search_tools works against pi's AgentSession
102
- // instead of fastagent's harness.
60
+ // instead of the served one.
103
61
  const { config, modelSpec, agentDir, authPath, stateRoot, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
104
62
  reportToolCollisions(toolCollisions);
105
63
  reportModuleLoadFailures(toolFailures);
106
- // ONE hub owns model resolution AND per-request auth the ModelRuntime-shaped sibling of
107
- // serving's createPiModels; see models.ts.
64
+ // ONE hub owns model resolution AND per-request auth; see models.ts.
108
65
  // agentDir carries the agent's own models.json (custom endpoints); stateRoot keeps pi's generated
109
66
  // catalog cache out of the definition dir. See createPiModelRuntime.
110
67
  const modelRuntime = await createPiModelRuntime({ authPath, agentDir, stateRoot });
111
- // MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth;
112
- // it now reads the agent's credential file like every other command. Probe the RESOLVED
113
- // model's provider through the normal resolution path (stored credential OR env var — an
114
- // env-authed user is fine and must not be warned): only when that provider has no usable auth
115
- // AND pi's old file exists does the bare provider error get its cause named.
116
- if ((await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
117
- existsSync(join(getAgentDir(), "auth.json"))) {
118
- log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
119
- `pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
120
- }
121
- const model = resolveModel(modelRuntime, modelSpec);
122
68
  const env = new NodeExecutionEnv({ cwd });
123
69
  const definition = await loadAgentDefinition(agentDir, { cwd, env });
124
70
  reportFindingsIfChanged(definition.dir, definition);
125
- const defaultNames = piDefaultTools().map((t) => t.name);
126
- const customTools = tools.filter((t) => !defaultNames.includes(t.name));
71
+ // Assembly-time, like serving's: this whole function is memoized, so the scan and its warnings
72
+ // happen once per runtime rather than per session rebuild (/new, /resume, fork).
73
+ const extensionPaths = await loadExtensionPaths(agentDir, { cwd, env });
74
+ // fastagent mounts pi's complete coding set itself; `noTools: "builtin"` below keeps the runtime
75
+ // from adding duplicate copies.
127
76
  // Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
128
77
  // it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
129
78
  // assembly is memoized across /new//resume/fork rebuilds while the session changes, so the bridge
130
- // resolves through sessionRef at call time, exactly like the serving path resolves its harness.
131
- const customToolDefs = customTools.map((t) => ({
79
+ // resolves through sessionRef at call time, exactly like the serving path resolves its session.
80
+ const customToolDefs = tools.map((t) => ({
132
81
  name: t.name,
133
82
  label: t.name,
134
83
  description: t.description ?? "",
@@ -144,10 +93,8 @@ sessionManager) {
144
93
  if (!bound)
145
94
  throw new Error("tool executed before its session was built (lifecycle invariant broken)");
146
95
  return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation },
147
- // pi's per-turn TOOL context (5th parameter) is read only by its default coding tools, and
148
- // those are filtered out of `customTools` above; fastagent's own take theirs from
149
- // `turnContext` (AsyncLocalStorage). So this env exists to satisfy the shape, and it is the
150
- // chat cwd's — the same root a default tool would have got, had one reached here.
96
+ // Lower-level MountedTools may consume the fifth-argument env. Directory coding tools are
97
+ // cwd-bound and ignore it; authored tools read FastAgent's turnContext instead.
151
98
  () => t.execute(id, params, signal, undefined, { env }));
152
99
  },
153
100
  }));
@@ -158,13 +105,14 @@ sessionManager) {
158
105
  contextFiles: definition.contextFiles,
159
106
  });
160
107
  return {
161
- model,
162
108
  modelRuntime,
109
+ modelSpec,
110
+ authPath,
163
111
  // Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
164
112
  thinkingLevel: config.thinkingLevel,
165
113
  definition,
166
- defaultNames,
167
- customTools,
114
+ extensionPaths,
115
+ customTools: tools,
168
116
  customToolDefs,
169
117
  deferredToolNames,
170
118
  systemPrompt,
@@ -177,11 +125,13 @@ sessionManager) {
177
125
  // would leak env or require mutating global env at runtime.
178
126
  const rootCwd = canonicalPath(dir);
179
127
  // The CURRENT pi session + its activation bridge, BOUND TOGETHER — rebuilt on /new//resume/fork
180
- // while the memoized assembly (and its tool execute closures) stays. The bridge must share the
181
- // session's lifetime, NOT be rebuilt per tool call (a per-call chain serializes nothing). Note on
128
+ // while the memoized assembly (and its tool execute closures) stays. The bridge shares the
129
+ // session's lifetime because a tool call has to see what the previous one activated. Note on
182
130
  // parallel batches: pi wraps SDK customTools in its own before/after active-set diff, so an
183
131
  // activating tool must carry `executionMode: "sequential"` (the builtin loader does) — pi then runs
184
132
  // the whole batch serially and the outer diff sees correct snapshots.
133
+ // NO activation record here: pi's chat session has nowhere to put one (see sessionToolActivation's
134
+ // `onActivated`), which is the documented divergence from serving — a resumed chat re-discovers.
185
135
  const sessionRef = {};
186
136
  let assembly;
187
137
  const assemblyFor = (cwd) => {
@@ -194,65 +144,84 @@ sessionManager) {
194
144
  assembly ??= resolveAssembly(rootCwd);
195
145
  return assembly;
196
146
  };
147
+ // The credential hint belongs to the RUNTIME, not to each session it builds: model resolution
148
+ // moved into createRuntime (extensions must load first), and repeating this on every /new,
149
+ // /resume and fork would nag about a setting that did not change.
150
+ let credentialHintShown = false;
197
151
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
198
- const { model, modelRuntime, thinkingLevel, definition, defaultNames, customTools, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
152
+ const { modelRuntime, modelSpec, authPath, thinkingLevel, definition, extensionPaths, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
153
+ // Per session, NOT memoized with the assembly: pi replaces the session on /new, /resume and
154
+ // fork, and its extension contract is that the replacement gets freshly loaded extensions
155
+ // rather than the previous session's objects. The expensive halves (model hub, auth) are shared
156
+ // through the assembly; only the resource loader is rebuilt.
199
157
  const services = await createAgentSessionServices({
200
158
  cwd,
201
159
  // fastagent's models + auth hub replaces pi's default (~/.pi-backed) one — the auth
202
160
  // unification point; see the header.
203
161
  modelRuntime,
204
- resourceLoaderOptions: {
205
- // Definition-only, like dev/start: suppress pi's machine-global discovery (the developer's own
206
- // ~/.pi extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) so this runtime runs
207
- // the same agent that gets served, not the authoring machine's pi setup on top.
208
- noExtensions: true,
209
- noPromptTemplates: true,
210
- noContextFiles: true,
211
- systemPromptOverride: () => systemPrompt,
212
- appendSystemPromptOverride: () => [],
213
- // Replace pi's discovered skills with fastagent's. fastagent's Skill (content inline) is
214
- // reshaped to pi-coding-agent's (read from filePath/baseDir at invocation time).
215
- skillsOverride: (base) => ({
216
- skills: definition.skills.map((s) => ({
217
- name: s.name,
218
- description: s.description,
219
- filePath: s.filePath,
220
- baseDir: dirname(s.filePath),
221
- sourceInfo: {
222
- path: s.filePath,
223
- source: "fastagent",
224
- scope: "project",
225
- origin: "top-level",
226
- baseDir: dirname(s.filePath),
227
- },
228
- disableModelInvocation: s.disableModelInvocation ?? false,
229
- })),
230
- diagnostics: base.diagnostics,
231
- }),
232
- },
162
+ // Chat's assembly is fixed for the life of the runtime (a rebuild makes a new one), so these
163
+ // read constants where serving passes accessors that change per turn — and it DOES pass its
164
+ // extension paths: chat runs them.
165
+ resourceLoaderOptions: definitionResourceLoaderOptions({
166
+ systemPrompt: () => systemPrompt,
167
+ skills: () => definition.skills,
168
+ extensionPaths,
169
+ }),
233
170
  });
171
+ reportExtensionErrors(services);
172
+ // AFTER the services, because an extension may be what defines the model. `registerProvider()`
173
+ // is pi's documented way for one to add providers, and extensions do not execute until the
174
+ // services are built — resolving first fails a definition whose configured model comes from its
175
+ // own extension with a bare "unknown model", after warning about credentials for a provider
176
+ // that does not exist yet.
177
+ //
178
+ // MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth; it
179
+ // now reads the agent's credential file like every other command. Probe the RESOLVED model's
180
+ // provider through the normal resolution path (stored credential OR env var — an env-authed
181
+ // user is fine and must not be warned): only when that provider has no usable auth AND pi's old
182
+ // file exists does the bare provider error get its cause named.
183
+ if (!credentialHintShown &&
184
+ (await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
185
+ existsSync(join(getAgentDir(), "auth.json"))) {
186
+ credentialHintShown = true;
187
+ log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
188
+ `pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
189
+ }
190
+ const model = resolveModel(modelRuntime, modelSpec);
234
191
  const result = await createAgentSessionFromServices({
235
192
  services,
236
193
  sessionManager,
237
194
  sessionStartEvent,
238
195
  model,
239
196
  thinkingLevel,
240
- tools: [...defaultNames, ...customTools.map((t) => t.name)],
197
+ // NO `tools` allowlist. It would freeze the tool set at build time, and pi lets an extension
198
+ // register from `session_start`, a command, or any handler — those names would not be in a
199
+ // startup snapshot, so `refreshTools()` would filter them straight back out and the extension
200
+ // would look like it did nothing. `noTools: "builtin"` gets the same guarantee the allowlist
201
+ // was really there for (the machine's `defaultTools` setting cannot add pi's own copies on top
202
+ // of ours) without freezing anything.
203
+ noTools: "builtin",
241
204
  customTools: customToolDefs,
242
205
  });
243
206
  sessionRef.current = {
244
207
  session: result.session,
245
- sessionManager: toolChatSessionManager(result.session),
208
+ sessionManager: agentSessionManager(result.session, result.session.sessionManager.getSessionId()),
246
209
  activation: sessionToolActivation(result.session),
247
210
  };
248
- // Deferral emulation: pi's session starts with everything active — narrow it by SUBTRACTING
249
- // the deferred names from whatever is active (robust to pi mounting tools of its own; an
250
- // exact-set-equality gate would silently stop narrowing the day pi adds one). Applied on EVERY
251
- // build including /resume: pi's chat session does not record activations (its SessionContext has
252
- // no activeToolNames), so "restore prior activations" is not implementable here deferral stays
253
- // consistently ON and a resumed conversation re-discovers via search_tools (documented divergence
254
- // from serving, where activations persist in the session). The deferred SET comes from the shared
255
- // assembly (one definition of "deferred"), never recomputed here.
211
+ // NOT bound here: the HOST does it. InteractiveMode.bindCurrentSessionExtensions() calls
212
+ // session.bindExtensions() with the TUI's uiContext, abort handler and command actions binding
213
+ // here too would emit session_start twice per chat, so an extension opening a resource on start
214
+ // would open two.
215
+ // Deferral emulation: pi starts THIS agent's tools active every coding tool it mounted, plus
216
+ // every custom and extension tool. So narrow by SUBTRACTING the deferred names from whatever is
217
+ // active, rather than stating a set: an exact-set-equality gate would silently stop narrowing the
218
+ // day pi activates one more.
219
+ //
220
+ // Applied on EVERY build including /resume: pi's chat session does not record activations (its
221
+ // SessionContext has no activeToolNames), so "restore prior activations" is not implementable
222
+ // here — deferral stays consistently ON and a resumed conversation re-discovers via search_tools
223
+ // (a documented divergence from serving, where activations persist in the session). The deferred
224
+ // SET comes from the shared assembly (one definition of "deferred"), never recomputed here.
256
225
  if (deferredToolNames.length > 0) {
257
226
  const active = result.session.getActiveToolNames();
258
227
  if (deferredToolNames.some((n) => active.includes(n))) {
@@ -1,10 +1,24 @@
1
- import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
- import type { Models } from "@earendil-works/pi-ai";
1
+ /**
2
+ * The pi implementation of the session control plane. `createPiSessionControl` returns the neutral
3
+ * `SessionControl` plus the {@link SessionObserver} to plug into the invoke pipeline
4
+ * (`createPiAgent({ observer })`).
5
+ *
6
+ * It holds no durable state of its own: live truth comes from the event stream (plus the
7
+ * {@link RunControls} a `run_started` carries), durable truth from {@link PiSessionRecordStore} —
8
+ * which is also what performs every write, so how a record takes a property is not knowledge this
9
+ * file has. What it owns is the vocabulary: capability gating, the lease, error codes, and the
10
+ * events its own writes emit.
11
+ *
12
+ * Writes take the same lease as runs. Without boundary wiring they reject before acceptance with
13
+ * `unsupported_capability` — a client gating on `capabilities()` never sends them.
14
+ */
15
+ import { type ThinkingLevel } from "@earendil-works/pi-agent-core";
16
+ import { type Models } from "@earendil-works/pi-ai";
3
17
  import { type AgentCommand, type SessionControl, type SessionEvent } from "../../session.ts";
4
- import type { SessionObserver } from "./invoke.ts";
5
- import type { Lease } from "./turn-kit.ts";
6
- import { type AnyModel, type PiHarnessFactory } from "./harness.ts";
7
- import { type PiSessionReader } from "./sessions.ts";
18
+ import type { SessionObserver, Lease } from "./turn-kit.ts";
19
+ import type { AnyModel } from "./models.ts";
20
+ import type { PiAgentSessionFactory } from "./invoke-session.ts";
21
+ import type { PiSessionRecordStore } from "./session-store.ts";
8
22
  /** Ceiling for one subscriber's unconsumed backlog. A consumer this far behind (a stalled remote
9
23
  * connection — the wire's ReadableStream backpressure stops pulling while invokes keep pushing)
10
24
  * has its buffer FROZEN at the cap (memory bounded — the actual goal: ≈10k small events ≈ a few
@@ -14,27 +28,27 @@ import { type PiSessionReader } from "./sessions.ts";
14
28
  * permanently stalled one holds the frozen buffer until its TCP connection dies. Recovery either
15
29
  * way is the standard reconnect+backfill, semantically lossless. */
16
30
  export declare const SUBSCRIBER_BUFFER_CAP = 10000;
17
- /** What boundary mutations (compact / set_model / set_thinking / navigate) need — the SAME instances the
18
- * agent assembly uses: the lease (mutations must not race a run), the model registry (validation +
19
- * allowedModels), and the harness factory (compaction is a model call). Writes go through the
20
- * session the hub's reader opened — after an existence check, so the control plane never creates
31
+ /** What the plane's writes (`update` / `compact` / `fork` / `delete`) need — the SAME instances the
32
+ * agent assembly uses: the lease (a write must not race a run), the model registry (validation +
33
+ * allowedModels), and the session factory (compaction is a model call). Writes go through the
34
+ * record the hub's reader opened — after an existence check, so the control plane never creates
21
35
  * a session (that is the data plane's monopoly). */
22
36
  export interface PiBoundaryWiring {
23
37
  lease: Lease;
24
38
  models: Models;
25
- harnessFactory: PiHarnessFactory;
39
+ sessionFactory: PiAgentSessionFactory;
26
40
  /** The assembly's configured PAIR — what a session with no overrides runs on. One field because
27
41
  * model and thinking level are one setting: which levels exist is a property of the model, so a
28
42
  * wiring that could carry them apart could carry a pair no run uses. Must be what
29
- * {@link harnessFactory} was built with. */
43
+ * {@link sessionFactory} was built with. */
30
44
  defaults: {
31
45
  model: AnyModel;
32
46
  thinkingLevel: ThinkingLevel;
33
47
  };
34
48
  }
35
49
  export interface CreatePiSessionControlOptions {
36
- /** Read-only access to the durable session repository (the same root the agent writes). */
37
- sessions: PiSessionReader;
50
+ /** Read-only access to the durable session records (the same root the agent writes). */
51
+ sessions: PiSessionRecordStore;
38
52
  /** Boundary-mutation wiring, as a LAZY thunk: the hub's observer must exist before the agent
39
53
  * assembly that produces these parts, so the hub asks for them at dispatch time instead
40
54
  * (assembly completes before any dispatch can arrive). Absent / undefined → boundary commands