@fastagent-sh/fastagent 0.18.0 → 0.19.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 (159) 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 +19 -0
  5. package/dist/atomic-write.js +48 -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-service.d.ts +54 -0
  9. package/dist/channels/agentcore-service.js +111 -0
  10. package/dist/channels/agentcore.d.ts +12 -2
  11. package/dist/channels/agentcore.js +6 -4
  12. package/dist/channels/busy.d.ts +1 -1
  13. package/dist/channels/busy.js +1 -1
  14. package/dist/channels/control.d.ts +16 -6
  15. package/dist/channels/control.js +106 -4
  16. package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
  17. package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
  18. package/dist/channels/feishu/card.js +1 -1
  19. package/dist/channels/feishu/context-buffer.d.ts +1 -1
  20. package/dist/channels/feishu/context-buffer.js +4 -4
  21. package/dist/channels/feishu/feishu-api.js +1 -1
  22. package/dist/channels/feishu/feishu.d.ts +1 -1
  23. package/dist/channels/feishu/feishu.js +7 -7
  24. package/dist/channels/feishu/invoke-turn.d.ts +1 -1
  25. package/dist/channels/feishu/invoke-turn.js +3 -4
  26. package/dist/channels/feishu/preview.d.ts +1 -1
  27. package/dist/channels/feishu/preview.js +7 -3
  28. package/dist/channels/feishu/ws-ingress.d.ts +1 -1
  29. package/dist/channels/github/github.d.ts +1 -1
  30. package/dist/channels/http.d.ts +11 -7
  31. package/dist/channels/http.js +1 -109
  32. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
  33. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
  34. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
  35. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  36. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  37. package/dist/channels/{state.js → kit/state.js} +4 -7
  38. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  39. package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
  40. package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
  41. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
  42. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  43. package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
  44. package/dist/channels/lark/lark.d.ts +1 -1
  45. package/dist/channels/serve.d.ts +73 -0
  46. package/dist/channels/serve.js +243 -0
  47. package/dist/channels/slack/bot-auth.js +3 -14
  48. package/dist/channels/slack/context-buffer.d.ts +2 -2
  49. package/dist/channels/slack/context-buffer.js +2 -2
  50. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  51. package/dist/channels/slack/invoke-turn.js +8 -5
  52. package/dist/channels/slack/parse.js +2 -2
  53. package/dist/channels/slack/preview.d.ts +1 -1
  54. package/dist/channels/slack/preview.js +11 -3
  55. package/dist/channels/slack/slack-api.js +1 -1
  56. package/dist/channels/slack/slack.d.ts +1 -1
  57. package/dist/channels/slack/slack.js +8 -8
  58. package/dist/channels/slack/welcomed.js +1 -1
  59. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  60. package/dist/channels/telegram/context-buffer.js +2 -2
  61. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  62. package/dist/channels/telegram/invoke-turn.js +1 -1
  63. package/dist/channels/telegram/parse.js +2 -2
  64. package/dist/channels/telegram/preview.d.ts +1 -1
  65. package/dist/channels/telegram/preview.js +6 -2
  66. package/dist/channels/telegram/telegram.d.ts +1 -1
  67. package/dist/channels/telegram/telegram.js +3 -3
  68. package/dist/channels/telegram/turn-store.d.ts +2 -2
  69. package/dist/channels/telegram/turn-store.js +2 -2
  70. package/dist/cli/commands/add.js +1 -1
  71. package/dist/cli/commands/dev.js +25 -13
  72. package/dist/cli/commands/fire.js +1 -1
  73. package/dist/cli/commands/info.js +7 -4
  74. package/dist/cli/commands/schedule.js +1 -1
  75. package/dist/cli/commands/start.js +63 -94
  76. package/dist/cli/commands/tool.js +5 -7
  77. package/dist/cli/program.js +1 -1
  78. package/dist/cli/serve.d.ts +18 -57
  79. package/dist/cli/serve.js +57 -243
  80. package/dist/cli/shared.d.ts +1 -1
  81. package/dist/cli/shared.js +2 -2
  82. package/dist/core.d.ts +3 -5
  83. package/dist/core.js +7 -5
  84. package/dist/deploy/preflight.js +1 -1
  85. package/dist/dev-supervisor.js +9 -3
  86. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  87. package/dist/engines/pi/agent-session-factory.js +314 -0
  88. package/dist/engines/pi/config.d.ts +4 -4
  89. package/dist/engines/pi/config.js +1 -1
  90. package/dist/engines/pi/create.d.ts +56 -43
  91. package/dist/engines/pi/create.js +141 -71
  92. package/dist/engines/pi/definition.d.ts +41 -7
  93. package/dist/engines/pi/definition.js +115 -11
  94. package/dist/engines/pi/invoke-session.d.ts +22 -19
  95. package/dist/engines/pi/invoke-session.js +286 -100
  96. package/dist/engines/pi/login.js +7 -4
  97. package/dist/engines/pi/models.d.ts +14 -0
  98. package/dist/engines/pi/models.js +9 -1
  99. package/dist/engines/pi/open.d.ts +9 -6
  100. package/dist/engines/pi/open.js +20 -10
  101. package/dist/engines/pi/report.d.ts +0 -4
  102. package/dist/engines/pi/report.js +0 -7
  103. package/dist/engines/pi/service.d.ts +23 -0
  104. package/dist/engines/pi/service.js +19 -0
  105. package/dist/engines/pi/session-builder.js +84 -88
  106. package/dist/engines/pi/session-control.d.ts +10 -9
  107. package/dist/engines/pi/session-control.js +114 -77
  108. package/dist/engines/pi/session-inheritance.d.ts +44 -0
  109. package/dist/engines/pi/session-inheritance.js +242 -0
  110. package/dist/engines/pi/session-settings.d.ts +14 -2
  111. package/dist/engines/pi/session-settings.js +18 -0
  112. package/dist/engines/pi/session-store.d.ts +70 -0
  113. package/dist/engines/pi/session-store.js +313 -0
  114. package/dist/engines/pi/tool-context.d.ts +18 -8
  115. package/dist/engines/pi/tool-context.js +23 -1
  116. package/dist/engines/pi/tool.d.ts +5 -5
  117. package/dist/engines/pi/tool.js +1 -1
  118. package/dist/engines/pi/turn-kit.d.ts +32 -6
  119. package/dist/engines/pi/turn-kit.js +28 -1
  120. package/dist/engines/pi/wake-tool.js +1 -1
  121. package/dist/index.d.ts +1 -0
  122. package/dist/index.js +1 -0
  123. package/dist/log.d.ts +3 -0
  124. package/dist/log.js +5 -1
  125. package/dist/node.d.ts +16 -0
  126. package/dist/node.js +20 -0
  127. package/dist/paths.d.ts +7 -0
  128. package/dist/paths.js +10 -0
  129. package/dist/pi.d.ts +14 -8
  130. package/dist/pi.js +6 -5
  131. package/dist/scaffold/templates/persona.md +2 -2
  132. package/dist/schedule/state.js +4 -9
  133. package/dist/schedule/wakeups.d.ts +1 -1
  134. package/dist/schedule/wakeups.js +1 -1
  135. package/dist/service.d.ts +158 -0
  136. package/dist/service.js +354 -0
  137. package/dist/session-remote.d.ts +1 -1
  138. package/dist/session.d.ts +4 -2
  139. package/dist/session.js +1 -1
  140. package/package.json +14 -5
  141. package/dist/engines/pi/harness.d.ts +0 -115
  142. package/dist/engines/pi/harness.js +0 -154
  143. package/dist/engines/pi/invoke.d.ts +0 -60
  144. package/dist/engines/pi/invoke.js +0 -459
  145. package/dist/engines/pi/read-image.d.ts +0 -4
  146. package/dist/engines/pi/read-image.js +0 -62
  147. package/dist/engines/pi/sessions.d.ts +0 -60
  148. package/dist/engines/pi/sessions.js +0 -464
  149. package/dist/host/node.js +0 -52
  150. /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
  151. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  152. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  153. /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
  154. /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
  155. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  156. /package/dist/channels/{text.js → kit/text.js} +0 -0
  157. /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
  158. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
  159. /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
@@ -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
- }
@@ -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, 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,19 @@
1
+ import { mountAgentService } from "../../service.js";
2
+ import { createPiAgentFromDir } from "./open.js";
3
+ /**
4
+ * Open an agent directory as a live service: one handler, mounted wherever you serve.
5
+ *
6
+ * ```ts
7
+ * const service = await createAgentService("./my-agent");
8
+ * app.use("/agent", nodeListener(service.handler));
9
+ * ```
10
+ */
11
+ export async function createAgentService(dir, options = {}) {
12
+ const opened = await createPiAgentFromDir(dir, {
13
+ ...(options.model !== undefined ? { model: options.model } : {}),
14
+ ...(options.authPath !== undefined ? { authPath: options.authPath } : {}),
15
+ ...(options.sessionsDir !== undefined ? { sessionsDir: options.sessionsDir } : {}),
16
+ serving: true, // a mounted service is long-running: the scheduler poller runs
17
+ });
18
+ return mountAgentService(opened, options);
19
+ }
@@ -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,19 @@
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
39
  import { log } from "../../log.js";
39
- import { additiveActivation, turnContext } from "./tool-context.js";
40
- import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
40
+ import { additiveActivation, agentSessionManager, turnContext, } from "./tool-context.js";
41
+ import { reportFindingsIfChanged, reportToolCollisions } from "./report.js";
42
+ import { reportModuleLoadFailures } from "../../log.js";
41
43
  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
44
  /**
58
45
  * Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
59
46
  * skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
@@ -63,7 +50,7 @@ export async function buildAgentSessionRuntime(dir, options = {},
63
50
  /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
64
51
  sessionManager) {
65
52
  /** 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
53
+ * serving bridge, so the SAME builtin search_tools serves both paths. Additive; unknown names
67
54
  * filtered (`setActiveToolsByName` is authoritative on the session and rebuilds its prompt — our
68
55
  * static override keeps the prompt identical to serving). */
69
56
  function sessionToolActivation(session) {
@@ -99,36 +86,27 @@ sessionManager) {
99
86
  // (what you iterate is what you serve): the initial active set excludes deferred tools (applied
100
87
  // on the session in createRuntime — pi's session starts all-active), and the activation bridge
101
88
  // above rides the same turn context, so the SAME search_tools works against pi's AgentSession
102
- // instead of fastagent's harness.
89
+ // instead of the served one.
103
90
  const { config, modelSpec, agentDir, authPath, stateRoot, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
104
91
  reportToolCollisions(toolCollisions);
105
92
  reportModuleLoadFailures(toolFailures);
106
- // ONE hub owns model resolution AND per-request auth the ModelRuntime-shaped sibling of
107
- // serving's createPiModels; see models.ts.
93
+ // ONE hub owns model resolution AND per-request auth; see models.ts.
108
94
  // agentDir carries the agent's own models.json (custom endpoints); stateRoot keeps pi's generated
109
95
  // catalog cache out of the definition dir. See createPiModelRuntime.
110
96
  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
97
  const env = new NodeExecutionEnv({ cwd });
123
98
  const definition = await loadAgentDefinition(agentDir, { cwd, env });
124
99
  reportFindingsIfChanged(definition.dir, definition);
125
- const defaultNames = piDefaultTools().map((t) => t.name);
126
- const customTools = tools.filter((t) => !defaultNames.includes(t.name));
100
+ // Assembly-time, like serving's: this whole function is memoized, so the scan and its warnings
101
+ // happen once per runtime rather than per session rebuild (/new, /resume, fork).
102
+ const extensionPaths = await loadExtensionPaths(agentDir, { cwd, env });
103
+ // fastagent mounts pi's complete coding set itself; `noTools: "builtin"` below keeps the runtime
104
+ // from adding duplicate copies.
127
105
  // Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
128
106
  // it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
129
107
  // 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) => ({
108
+ // resolves through sessionRef at call time, exactly like the serving path resolves its session.
109
+ const customToolDefs = tools.map((t) => ({
132
110
  name: t.name,
133
111
  label: t.name,
134
112
  description: t.description ?? "",
@@ -144,10 +122,8 @@ sessionManager) {
144
122
  if (!bound)
145
123
  throw new Error("tool executed before its session was built (lifecycle invariant broken)");
146
124
  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.
125
+ // Lower-level MountedTools may consume the fifth-argument env. Directory coding tools are
126
+ // cwd-bound and ignore it; authored tools read FastAgent's turnContext instead.
151
127
  () => t.execute(id, params, signal, undefined, { env }));
152
128
  },
153
129
  }));
@@ -158,13 +134,14 @@ sessionManager) {
158
134
  contextFiles: definition.contextFiles,
159
135
  });
160
136
  return {
161
- model,
162
137
  modelRuntime,
138
+ modelSpec,
139
+ authPath,
163
140
  // Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
164
141
  thinkingLevel: config.thinkingLevel,
165
142
  definition,
166
- defaultNames,
167
- customTools,
143
+ extensionPaths,
144
+ customTools: tools,
168
145
  customToolDefs,
169
146
  deferredToolNames,
170
147
  systemPrompt,
@@ -194,65 +171,84 @@ sessionManager) {
194
171
  assembly ??= resolveAssembly(rootCwd);
195
172
  return assembly;
196
173
  };
174
+ // The credential hint belongs to the RUNTIME, not to each session it builds: model resolution
175
+ // moved into createRuntime (extensions must load first), and repeating this on every /new,
176
+ // /resume and fork would nag about a setting that did not change.
177
+ let credentialHintShown = false;
197
178
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
198
- const { model, modelRuntime, thinkingLevel, definition, defaultNames, customTools, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
179
+ const { modelRuntime, modelSpec, authPath, thinkingLevel, definition, extensionPaths, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
180
+ // Per session, NOT memoized with the assembly: pi replaces the session on /new, /resume and
181
+ // fork, and its extension contract is that the replacement gets freshly loaded extensions
182
+ // rather than the previous session's objects. The expensive halves (model hub, auth) are shared
183
+ // through the assembly; only the resource loader is rebuilt.
199
184
  const services = await createAgentSessionServices({
200
185
  cwd,
201
186
  // fastagent's models + auth hub replaces pi's default (~/.pi-backed) one — the auth
202
187
  // unification point; see the header.
203
188
  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
- },
189
+ // Chat's assembly is fixed for the life of the runtime (a rebuild makes a new one), so these
190
+ // read constants where serving passes accessors that change per turn — and it DOES pass its
191
+ // extension paths: chat runs them.
192
+ resourceLoaderOptions: definitionResourceLoaderOptions({
193
+ systemPrompt: () => systemPrompt,
194
+ skills: () => definition.skills,
195
+ extensionPaths,
196
+ }),
233
197
  });
198
+ reportExtensionErrors(services);
199
+ // AFTER the services, because an extension may be what defines the model. `registerProvider()`
200
+ // is pi's documented way for one to add providers, and extensions do not execute until the
201
+ // services are built — resolving first fails a definition whose configured model comes from its
202
+ // own extension with a bare "unknown model", after warning about credentials for a provider
203
+ // that does not exist yet.
204
+ //
205
+ // MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth; it
206
+ // now reads the agent's credential file like every other command. Probe the RESOLVED model's
207
+ // provider through the normal resolution path (stored credential OR env var — an env-authed
208
+ // user is fine and must not be warned): only when that provider has no usable auth AND pi's old
209
+ // file exists does the bare provider error get its cause named.
210
+ if (!credentialHintShown &&
211
+ (await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
212
+ existsSync(join(getAgentDir(), "auth.json"))) {
213
+ credentialHintShown = true;
214
+ log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
215
+ `pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
216
+ }
217
+ const model = resolveModel(modelRuntime, modelSpec);
234
218
  const result = await createAgentSessionFromServices({
235
219
  services,
236
220
  sessionManager,
237
221
  sessionStartEvent,
238
222
  model,
239
223
  thinkingLevel,
240
- tools: [...defaultNames, ...customTools.map((t) => t.name)],
224
+ // NO `tools` allowlist. It would freeze the tool set at build time, and pi lets an extension
225
+ // register from `session_start`, a command, or any handler — those names would not be in a
226
+ // startup snapshot, so `refreshTools()` would filter them straight back out and the extension
227
+ // would look like it did nothing. `noTools: "builtin"` gets the same guarantee the allowlist
228
+ // was really there for (the machine's `defaultTools` setting cannot add pi's own copies on top
229
+ // of ours) without freezing anything.
230
+ noTools: "builtin",
241
231
  customTools: customToolDefs,
242
232
  });
243
233
  sessionRef.current = {
244
234
  session: result.session,
245
- sessionManager: toolChatSessionManager(result.session),
235
+ sessionManager: agentSessionManager(result.session, result.session.sessionManager.getSessionId()),
246
236
  activation: sessionToolActivation(result.session),
247
237
  };
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.
238
+ // NOT bound here: the HOST does it. InteractiveMode.bindCurrentSessionExtensions() calls
239
+ // session.bindExtensions() with the TUI's uiContext, abort handler and command actions binding
240
+ // here too would emit session_start twice per chat, so an extension opening a resource on start
241
+ // would open two.
242
+ // Deferral emulation: pi starts THIS agent's tools active every coding tool it mounted, plus
243
+ // every custom and extension tool. So narrow by SUBTRACTING the deferred names from whatever is
244
+ // active, rather than stating a set: an exact-set-equality gate would silently stop narrowing the
245
+ // day pi activates one more.
246
+ //
247
+ // Applied on EVERY build including /resume: pi's chat session does not record activations (its
248
+ // SessionContext has no activeToolNames), so "restore prior activations" is not implementable
249
+ // here — deferral stays consistently ON and a resumed conversation re-discovers via search_tools
250
+ // (a documented divergence from serving, where activations persist in the session). The deferred
251
+ // SET comes from the shared assembly (one definition of "deferred"), never recomputed here.
256
252
  if (deferredToolNames.length > 0) {
257
253
  const active = result.session.getActiveToolNames();
258
254
  if (deferredToolNames.some((n) => active.includes(n))) {
@@ -1,10 +1,11 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Models } from "@earendil-works/pi-ai";
3
3
  import { type AgentCommand, type SessionControl, type SessionEvent } from "../../session.ts";
4
- import type { SessionObserver } from "./invoke.ts";
4
+ import type { SessionObserver } from "./turn-kit.ts";
5
5
  import type { Lease } from "./turn-kit.ts";
6
- import { type AnyModel, type PiHarnessFactory } from "./harness.ts";
7
- import { type PiSessionReader } from "./sessions.ts";
6
+ import type { AnyModel } from "./models.ts";
7
+ import type { PiAgentSessionFactory } from "./invoke-session.ts";
8
+ import type { PiSessionRecordStore } from "./session-store.ts";
8
9
  /** Ceiling for one subscriber's unconsumed backlog. A consumer this far behind (a stalled remote
9
10
  * connection — the wire's ReadableStream backpressure stops pulling while invokes keep pushing)
10
11
  * has its buffer FROZEN at the cap (memory bounded — the actual goal: ≈10k small events ≈ a few
@@ -16,25 +17,25 @@ import { type PiSessionReader } from "./sessions.ts";
16
17
  export declare const SUBSCRIBER_BUFFER_CAP = 10000;
17
18
  /** What boundary mutations (compact / set_model / set_thinking / navigate) need — the SAME instances the
18
19
  * 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
20
+ * allowedModels), and the session factory (compaction is a model call). Writes go through the
21
+ * record the hub's reader opened — after an existence check, so the control plane never creates
21
22
  * a session (that is the data plane's monopoly). */
22
23
  export interface PiBoundaryWiring {
23
24
  lease: Lease;
24
25
  models: Models;
25
- harnessFactory: PiHarnessFactory;
26
+ sessionFactory: PiAgentSessionFactory;
26
27
  /** The assembly's configured PAIR — what a session with no overrides runs on. One field because
27
28
  * model and thinking level are one setting: which levels exist is a property of the model, so a
28
29
  * wiring that could carry them apart could carry a pair no run uses. Must be what
29
- * {@link harnessFactory} was built with. */
30
+ * {@link sessionFactory} was built with. */
30
31
  defaults: {
31
32
  model: AnyModel;
32
33
  thinkingLevel: ThinkingLevel;
33
34
  };
34
35
  }
35
36
  export interface CreatePiSessionControlOptions {
36
- /** Read-only access to the durable session repository (the same root the agent writes). */
37
- sessions: PiSessionReader;
37
+ /** Read-only access to the durable session records (the same root the agent writes). */
38
+ sessions: PiSessionRecordStore;
38
39
  /** Boundary-mutation wiring, as a LAZY thunk: the hub's observer must exist before the agent
39
40
  * assembly that produces these parts, so the hub asks for them at dispatch time instead
40
41
  * (assembly completes before any dispatch can arrive). Absent / undefined → boundary commands