@fastagent-sh/fastagent 0.22.1 → 0.24.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 (128) hide show
  1. package/dist/agent.d.ts +10 -0
  2. package/dist/channels/agentcore-protocol.d.ts +2 -1
  3. package/dist/channels/agentcore-service.js +10 -18
  4. package/dist/channels/agentcore.js +6 -3
  5. package/dist/channels/control.js +23 -0
  6. package/dist/channels/kit/preview-kit.d.ts +6 -1
  7. package/dist/channels/kit/preview-kit.js +21 -4
  8. package/dist/channels/slack/config-api.d.ts +2 -2
  9. package/dist/channels/slack/config-api.js +2 -2
  10. package/dist/channels/slack/onboard.js +2 -2
  11. package/dist/channels/slack/preview.js +2 -1
  12. package/dist/channels/slack/register-webhook.js +2 -2
  13. package/dist/channels/slack/slack-api.js +4 -4
  14. package/dist/cli/add-feishu.js +4 -2
  15. package/dist/cli/auth-view.d.ts +15 -6
  16. package/dist/cli/auth-view.js +24 -8
  17. package/dist/cli/box-login.d.ts +33 -0
  18. package/dist/cli/box-login.js +162 -0
  19. package/dist/cli/commands/deploy/agentcore.js +48 -11
  20. package/dist/cli/commands/deploy/docker.js +24 -13
  21. package/dist/cli/commands/deploy/fly.js +30 -7
  22. package/dist/cli/commands/deploy/railway.js +21 -7
  23. package/dist/cli/commands/deploy/shared.d.ts +18 -17
  24. package/dist/cli/commands/deploy/shared.js +20 -21
  25. package/dist/cli/commands/destroy.js +2 -2
  26. package/dist/cli/commands/info.js +15 -12
  27. package/dist/cli/commands/invoke.js +2 -2
  28. package/dist/cli/commands/login.d.ts +7 -1
  29. package/dist/cli/commands/login.js +123 -10
  30. package/dist/cli/commands/logs.js +2 -2
  31. package/dist/cli/commands/models.d.ts +4 -1
  32. package/dist/cli/commands/models.js +37 -4
  33. package/dist/cli/commands/routine-run.js +2 -2
  34. package/dist/cli/commands/start.d.ts +0 -5
  35. package/dist/cli/commands/start.js +3 -13
  36. package/dist/cli/invoke-stream.js +14 -0
  37. package/dist/cli/kernel.d.ts +2 -0
  38. package/dist/cli/kernel.js +1 -1
  39. package/dist/cli/login-relay.d.ts +38 -0
  40. package/dist/cli/login-relay.js +121 -0
  41. package/dist/cli/program.js +46 -8
  42. package/dist/cli/shared.d.ts +6 -5
  43. package/dist/cli/shared.js +29 -36
  44. package/dist/deploy/agentcore/plan.d.ts +10 -11
  45. package/dist/deploy/agentcore/plan.js +8 -14
  46. package/dist/deploy/agentcore/run.d.ts +8 -1
  47. package/dist/deploy/agentcore/run.js +20 -14
  48. package/dist/deploy/agentcore/shell.d.ts +25 -0
  49. package/dist/deploy/agentcore/shell.js +202 -0
  50. package/dist/deploy/box-shell.d.ts +43 -0
  51. package/dist/deploy/box-shell.js +28 -0
  52. package/dist/deploy/build-context.js +16 -7
  53. package/dist/deploy/channel-ingress.d.ts +20 -1
  54. package/dist/deploy/channel-ingress.js +34 -7
  55. package/dist/deploy/container.d.ts +14 -0
  56. package/dist/deploy/container.js +45 -1
  57. package/dist/deploy/docker/plan.d.ts +7 -3
  58. package/dist/deploy/docker/plan.js +9 -14
  59. package/dist/deploy/docker/run.d.ts +14 -7
  60. package/dist/deploy/docker/run.js +58 -35
  61. package/dist/deploy/fly/plan.d.ts +6 -3
  62. package/dist/deploy/fly/plan.js +6 -8
  63. package/dist/deploy/fly/run.d.ts +8 -2
  64. package/dist/deploy/fly/run.js +15 -25
  65. package/dist/deploy/preflight.d.ts +9 -7
  66. package/dist/deploy/preflight.js +92 -20
  67. package/dist/deploy/railway/plan.d.ts +6 -3
  68. package/dist/deploy/railway/plan.js +7 -9
  69. package/dist/deploy/railway/run.d.ts +10 -6
  70. package/dist/deploy/railway/run.js +29 -13
  71. package/dist/deploy/runner.d.ts +16 -0
  72. package/dist/deploy/runner.js +31 -0
  73. package/dist/deploy/secrets.d.ts +4 -13
  74. package/dist/deploy/secrets.js +8 -27
  75. package/dist/dev-supervisor.js +7 -4
  76. package/dist/engines/pi/agent-models.d.ts +83 -0
  77. package/dist/engines/pi/agent-models.js +120 -0
  78. package/dist/engines/pi/agent-session-factory.d.ts +13 -13
  79. package/dist/engines/pi/agent-session-factory.js +132 -159
  80. package/dist/engines/pi/auth.d.ts +56 -3
  81. package/dist/engines/pi/auth.js +68 -109
  82. package/dist/engines/pi/config.d.ts +0 -25
  83. package/dist/engines/pi/config.js +3 -37
  84. package/dist/engines/pi/create.d.ts +34 -21
  85. package/dist/engines/pi/create.js +90 -57
  86. package/dist/engines/pi/entry-images.d.ts +14 -0
  87. package/dist/engines/pi/entry-images.js +23 -0
  88. package/dist/engines/pi/invoke-session.js +88 -20
  89. package/dist/engines/pi/locked-file.d.ts +9 -0
  90. package/dist/engines/pi/locked-file.js +102 -0
  91. package/dist/engines/pi/login.d.ts +27 -14
  92. package/dist/engines/pi/login.js +41 -18
  93. package/dist/engines/pi/machine.d.ts +23 -0
  94. package/dist/engines/pi/machine.js +24 -2
  95. package/dist/engines/pi/models.d.ts +120 -36
  96. package/dist/engines/pi/models.js +316 -66
  97. package/dist/engines/pi/open.d.ts +65 -20
  98. package/dist/engines/pi/open.js +88 -34
  99. package/dist/engines/pi/report.d.ts +3 -0
  100. package/dist/engines/pi/report.js +11 -0
  101. package/dist/engines/pi/service.d.ts +2 -2
  102. package/dist/engines/pi/service.js +1 -0
  103. package/dist/engines/pi/session-builder.d.ts +3 -2
  104. package/dist/engines/pi/session-builder.js +30 -76
  105. package/dist/engines/pi/session-control.d.ts +4 -4
  106. package/dist/engines/pi/session-control.js +86 -43
  107. package/dist/engines/pi/session-settings.d.ts +9 -0
  108. package/dist/engines/pi/session-settings.js +17 -0
  109. package/dist/engines/pi/session-store.d.ts +2 -2
  110. package/dist/engines/pi/tool-context.d.ts +3 -5
  111. package/dist/engines/pi/tool-context.js +4 -16
  112. package/dist/engines/pi/tool.d.ts +32 -21
  113. package/dist/engines/pi/tool.js +21 -25
  114. package/dist/engines/pi/turn-kit.d.ts +18 -9
  115. package/dist/engines/pi/turn-kit.js +115 -32
  116. package/dist/open-url.d.ts +15 -1
  117. package/dist/open-url.js +35 -3
  118. package/dist/paths.d.ts +4 -2
  119. package/dist/paths.js +6 -2
  120. package/dist/pi.d.ts +5 -3
  121. package/dist/pi.js +3 -2
  122. package/dist/service.d.ts +5 -8
  123. package/dist/session-remote.js +21 -0
  124. package/dist/session.d.ts +88 -15
  125. package/dist/session.js +5 -1
  126. package/package.json +6 -5
  127. package/dist/engines/pi/search-tools.d.ts +0 -4
  128. package/dist/engines/pi/search-tools.js +0 -109
@@ -3,9 +3,11 @@
3
3
  * drive.
4
4
  */
5
5
  import { mkdir } from "node:fs/promises";
6
- import { loadConfig, resolveAuthLayers, resolveModelSpec, } from "./config.js";
7
- import { resolveSessionsDir, resolveStateRoot, resolvePlacement } from "../../paths.js";
8
- import { agentOf, assemblePiFromDefinition, definitionModelRuntime, resolveAgentTools } from "./create.js";
6
+ import { join } from "node:path";
7
+ import { loadConfig, resolveModelSpec } from "./config.js";
8
+ import { AGENT_MODEL_CATALOG_FILE, resolveSessionsDir, resolveStateRoot, resolvePlacement } from "../../paths.js";
9
+ import { describeModels } from "./session-settings.js";
10
+ import { agentOf, assemblePiFromDefinition, resolveAgentTools } from "./create.js";
9
11
  import { createPiSessionControl } from "./session-control.js";
10
12
  import { withWakeTool } from "./wake-tool.js";
11
13
  import { refuseBrokenDeclarations } from "../../loader.js";
@@ -13,6 +15,8 @@ import { loadAgentSkills } from "./definition.js";
13
15
  import { servedExtensionCommands } from "./agent-session-factory.js";
14
16
  import { reportFindingsIfChanged } from "./report.js";
15
17
  import { readMachine, withMachine } from "./machine.js";
18
+ import { createPiModelRuntime, globalCatalogPath, machineModelRuntime, refreshCatalog } from "./models.js";
19
+ import { agentModels } from "./agent-models.js";
16
20
  import { piSessionRecordStore } from "./session-store.js";
17
21
  import { gateSecrets } from "../../secrets-gate.js";
18
22
  /**
@@ -33,9 +37,11 @@ export async function agentCommands(agentDir, workspace, served) {
33
37
  reportFindingsIfChanged(own.dir, own);
34
38
  const machine = await readMachine(workspace);
35
39
  const { extensionPaths } = served;
36
- const extensionCommands = extensionPaths.length === 0
37
- ? []
38
- : await servedExtensionCommands({ cwd: workspace, modelRuntime: await served.modelRuntime(), extensionPaths });
40
+ const extensionCommands = await servedExtensionCommands({
41
+ cwd: workspace,
42
+ modelRuntime: await served.modelRuntime(),
43
+ extensionPaths,
44
+ });
39
45
  // pi dispatches an extension command before it expands a template, so a template it shadows never runs.
40
46
  const shadowed = new Set(extensionCommands.map((command) => command.invocationName));
41
47
  return [
@@ -66,7 +72,7 @@ export async function resolveAgentAssembly(dir, options = {}) {
66
72
  if (!modelSpec) {
67
73
  throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
68
74
  }
69
- const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures, toolSecrets } = await resolveAgentTools(config, agentDir, workspace);
75
+ const { tools, toolNames, indirectTools, toolCollisions, toolFailures, toolSecrets } = await resolveAgentTools(config, agentDir, workspace);
70
76
  // THE serving-path gate for tool declarations, in the order that costs the fewest round trips. A file that could
71
77
  // not be imported declares nothing, so its `secrets:` are missing from `toolSecrets` — gating secrets first would
72
78
  // report an incomplete set, and fixing the file could then reveal more missing values. Refuse the broken file
@@ -76,15 +82,13 @@ export async function resolveAgentAssembly(dir, options = {}) {
76
82
  // definition and must survive both faults; every path through this function is about to run ALL of the tools, so
77
83
  // no `owner`.
78
84
  //
79
- // DEFERRED tools gate too, deliberately: `search_tools` can activate one mid-turn, so "registered"
85
+ // DEFERRED tools gate too, deliberately: native discovery can activate one mid-turn, so "registered"
80
86
  // means "may run in this process" — letting it start would put the empty-credential failure back
81
87
  // inside a turn. An author who does not want that opts out per tool by not declaring.
82
88
  gateSecrets({ declared: toolSecrets, failures: [] });
83
89
  // The state root: sessions/channel state/schedule state derive from it (FASTAGENT_STATE_DIR moves it in one knob —
84
90
  // a container points it at its volume).
85
91
  const stateRoot = resolveStateRoot(agentDir);
86
- // The credentials file: project-level by default (under `<agentDir>/.secrets`).
87
- const auth = resolveAuthLayers(agentDir, options.authPath);
88
92
  return {
89
93
  config,
90
94
  configPath,
@@ -92,37 +96,94 @@ export async function resolveAgentAssembly(dir, options = {}) {
92
96
  agentDir,
93
97
  workspace,
94
98
  stateRoot,
95
- auth,
99
+ // Project-level by default (under `<agentDir>/.secrets`), with the global file behind it per provider.
100
+ models: agentModels(agentDir, options, { cwd: workspace }),
96
101
  tools,
97
102
  toolNames,
98
- deferredToolNames,
103
+ indirectTools,
99
104
  toolCollisions,
100
105
  toolSecrets,
101
106
  };
102
107
  }
103
108
  /**
104
- * The model specs `createPiAgentFromDir(dir, { authPath })` could run now: the agent's registry (pi's built-ins plus
105
- * its `models.json`) filtered to the providers whose credentials are configured, sorted. The directory needs no model
106
- * set, and the same placement and credential layers as the opener apply, so a listed spec authenticates there.
107
- * Configuration is checked, not validity: no OAuth token is refreshed and no provider is called.
109
+ * L2 over a resolved front half: the ONE mapping from what the directory resolved to what the assembly runs on, for
110
+ * both session shapes (the served agent and `chat`'s resident one). Two copies of it drifted once (#566, chat ran at
111
+ * the wrong reasoning effort). `models` is the front's own, so the assembly runs on what the report describes.
112
+ */
113
+ export function assembleFront(front, extra = {}) {
114
+ return assemblePiFromDefinition(front.agentDir, {
115
+ model: front.modelSpec,
116
+ thinkingLevel: front.config.thinkingLevel,
117
+ cwd: front.workspace,
118
+ tools: extra.tools ?? front.tools,
119
+ models: front.models,
120
+ ...(extra.sessions ? { sessions: extra.sessions } : {}),
121
+ });
122
+ }
123
+ /**
124
+ * The models `createPiAgentFromDir(dir, { authPath })` could run now, as a picker shows them (spec, name, the thinking
125
+ * levels a session on it accepts, context window), sorted by spec: the agent's registry (pi's built-ins, its
126
+ * `models.json`, and what its `extensions/` declare) filtered to the providers whose credentials are configured. The
127
+ * directory needs no model set, and the same placement and credential layers as the opener apply, so a listed spec
128
+ * authenticates there. Configuration is checked, not validity: no OAuth token is refreshed and no provider is called.
108
129
  *
109
130
  * `warn` reaches the credential store. An unreadable or corrupt credentials file otherwise reads as "nothing
110
131
  * configured", so a client that must not show that as an empty list passes a sink that throws.
111
132
  */
112
133
  export async function availableModelsFromDir(dir, options = {}) {
134
+ const { agentDir, workspace } = resolvePlacement(dir);
135
+ const models = await agentModels(agentDir, options, { cwd: workspace }).runtime();
136
+ return describeModels(await models.getAvailable());
137
+ }
138
+ /**
139
+ * Refresh the agent's model catalog (`<agent dir>/models-store.json`) with the credentials
140
+ * `createPiAgentFromDir(dir, { authPath })` would use, so a model released after the installed pi appears in
141
+ * {@link availableModelsFromDir} and runs. Only providers those credentials authenticate are fetched (pi asks pi.dev
142
+ * for no other). Nothing refreshes it on its own. The file is part of the definition: commit it, and it ships with a
143
+ * deploy, so the deployed agent knows the same models without a network call.
144
+ *
145
+ * Rejects, naming each provider that failed, when the refresh fails, outlasts 15 seconds or `PI_OFFLINE` is set, and
146
+ * when the credentials authenticate no provider at all.
147
+ */
148
+ export function refreshModelCatalog(dir, options = {}) {
149
+ return refreshModelCatalogOver(dir, options);
150
+ }
151
+ /** {@link refreshModelCatalog} against another catalog server. Not public: a test seam. */
152
+ export async function refreshModelCatalogOver(dir, options, catalogBaseUrl) {
113
153
  const { agentDir } = resolvePlacement(dir);
114
- const models = await definitionModelRuntime(agentDir, {
115
- auth: resolveAuthLayers(agentDir, options.authPath),
116
- ...(options.warn ? { warn: options.warn } : {}),
117
- });
118
- return (await models.getAvailable()).map((model) => `${model.provider}/${model.id}`).sort();
154
+ const { credentials } = agentModels(agentDir, options);
155
+ await refreshCatalog(join(agentDir, AGENT_MODEL_CATALOG_FILE), (catalogFile) => createPiModelRuntime({
156
+ agentDir,
157
+ credentials,
158
+ catalogFile,
159
+ ...(catalogBaseUrl ? { catalogBaseUrl } : {}),
160
+ }), options.signal ? { signal: options.signal } : {});
161
+ }
162
+ /**
163
+ * Refresh the MACHINE's model catalog (`~/.fastagent/models-store.json`), which every agent here reads under its own,
164
+ * so one refresh serves them all and no file is written into any agent. Credentials resolve as for
165
+ * {@link refreshModelCatalog} without a directory: `credentialStore`, else `authPath`, else the global credentials file;
166
+ * the environment applies either way. Rejects for the same reasons. The file never ships with a deploy.
167
+ */
168
+ export function refreshMachineModelCatalog(options = {}) {
169
+ return refreshMachineModelCatalogOver(options);
170
+ }
171
+ /** {@link refreshMachineModelCatalog} against another catalog server. Not public: a test seam. */
172
+ export async function refreshMachineModelCatalogOver(options, catalogBaseUrl) {
173
+ const { credentials } = agentModels(undefined, options);
174
+ await refreshCatalog(globalCatalogPath(), (catalogFile) => machineModelRuntime({
175
+ credentials,
176
+ catalogFile,
177
+ ...(catalogBaseUrl ? { catalogBaseUrl } : {}),
178
+ }), options.signal ? { signal: options.signal } : {});
119
179
  }
120
180
  /**
121
181
  * "Point at a directory → agent": resolve the placement (`dir` may be either end — the workspace or the agent dir
122
182
  * itself), load the config, resolve model and tools, then L2.
123
183
  */
124
184
  export async function createPiAgentFromDir(dir, options = {}) {
125
- const { config, configPath, modelSpec, agentDir, workspace, stateRoot, auth, tools, toolNames, deferredToolNames, toolCollisions, } = await resolveAgentAssembly(dir, options);
185
+ const front = await resolveAgentAssembly(dir, options);
186
+ const { config, configPath, modelSpec, agentDir, workspace, stateRoot, models, tools, toolNames, indirectTools, toolCollisions, } = front;
126
187
  // Every serve mounts the built-in `wake` tool: the agent's own follow-up work is a default capability, and a serve
127
188
  // is where the poller that honors it runs. A one-shot `invoke` has no poller, so nothing there would fire it.
128
189
  const mountedTools = withWakeTool(tools, stateRoot, !!options.serving);
@@ -131,15 +192,8 @@ export async function createPiAgentFromDir(dir, options = {}) {
131
192
  const sessionsDir = options.sessionsDir ?? resolveSessionsDir(agentDir);
132
193
  await mkdir(sessionsDir, { recursive: true });
133
194
  const sessions = piSessionRecordStore({ dir: sessionsDir, cwd: workspace });
134
- const { assembly, definition } = await assemblePiFromDefinition(agentDir, {
135
- model: modelSpec,
136
- thinkingLevel: config.thinkingLevel,
137
- cwd: workspace,
138
- tools: mountedTools,
139
- auth,
140
- // Skills are definition-only (the agent is its directory), so dev mirrors deployment exactly.
141
- sessions,
142
- });
195
+ // Skills are definition-only (the agent is its directory), so dev mirrors deployment exactly.
196
+ const { assembly, definition } = await assembleFront(front, { tools: mountedTools, sessions });
143
197
  // The hub is wired HERE because the store is created here (an external `createPiSessionControl` cannot exist before
144
198
  // the store does).
145
199
  const caller = options.observer;
@@ -169,7 +223,7 @@ export async function createPiAgentFromDir(dir, options = {}) {
169
223
  // the prompt would not expand — a second reading of "which skills exist" is how those two come to disagree.
170
224
  commands: () => agentCommands(agentDir, workspace, {
171
225
  extensionPaths: assembly.extensionPaths,
172
- modelRuntime: async () => (await assembly.engine()).modelRuntime,
226
+ modelRuntime: assembly.createModelRuntime,
173
227
  }),
174
228
  // The caller tap's boundary-event half: state_changed/compaction_* originate in the hub and never cross the
175
229
  // data plane's observer seam.
@@ -199,9 +253,9 @@ export async function createPiAgentFromDir(dir, options = {}) {
199
253
  modelSpec,
200
254
  stateRoot,
201
255
  sessionsDir,
202
- auth,
256
+ models,
203
257
  toolNames,
204
- deferredToolNames,
258
+ indirectTools,
205
259
  toolCollisions,
206
260
  };
207
261
  }
@@ -5,6 +5,7 @@
5
5
  import type { SkillDiagnostic } from "@earendil-works/pi-agent-core";
6
6
  import type { SkillCollision } from "./definition.ts";
7
7
  import type { ToolCollision } from "./tool.ts";
8
+ import type { IndirectTool } from "./create.ts";
8
9
  type Findings = {
9
10
  collisions: SkillCollision[];
10
11
  diagnostics: SkillDiagnostic[];
@@ -12,5 +13,7 @@ type Findings = {
12
13
  /** THE door for definition findings: warns only when this dir's set CHANGED since the last report. */
13
14
  export declare function reportFindingsIfChanged(dir: string, def: Findings): void;
14
15
  export declare function reportDefinitionWarnings(collisions: SkillCollision[], diagnostics: SkillDiagnostic[]): void;
16
+ /** The report line for the mounted tools the model is not given up front: each name with its way in. */
17
+ export declare function describeIndirectTools(tools: readonly IndirectTool[]): string;
15
18
  export declare function reportToolCollisions(collisions: ToolCollision[]): void;
16
19
  export {};
@@ -23,6 +23,17 @@ export function reportDefinitionWarnings(collisions, diagnostics) {
23
23
  log.warn(`[fastagent] ${d.code}: ${d.message} (${d.path})`);
24
24
  }
25
25
  }
26
+ const REACH = {
27
+ tool_search: "loaded via tool_search",
28
+ codemode: "codemode scripts only",
29
+ hidden: "hidden",
30
+ inactive: "inactive until an authored loader activates it",
31
+ unreachable: "unreachable: pi's settings disable the built-in extension it needs",
32
+ };
33
+ /** The report line for the mounted tools the model is not given up front: each name with its way in. */
34
+ export function describeIndirectTools(tools) {
35
+ return tools.map((tool) => `${tool.name} (${REACH[tool.reach]})`).join(", ");
36
+ }
26
37
  export function reportToolCollisions(collisions) {
27
38
  for (const c of collisions) {
28
39
  log.warn(`[fastagent] tool "${c.name}" (${c.source}) dropped — a default/config tool already uses that name`);
@@ -1,8 +1,8 @@
1
1
  /** `createAgentService` — the product as one call, with pi supplying the engine. */
2
2
  import { type AgentService, type MountAgentServiceOptions } from "../../service.ts";
3
- export interface CreateAgentServiceOptions extends MountAgentServiceOptions {
3
+ import type { CredentialSourceOptions } from "./auth.ts";
4
+ export interface CreateAgentServiceOptions extends MountAgentServiceOptions, CredentialSourceOptions {
4
5
  model?: string;
5
- authPath?: string;
6
6
  sessionsDir?: string;
7
7
  }
8
8
  /** Open an agent directory as a live service: one handler, mounted wherever you serve. */
@@ -6,6 +6,7 @@ export async function createAgentService(dir, options = {}) {
6
6
  const opened = await createPiAgentFromDir(dir, {
7
7
  ...(options.model !== undefined ? { model: options.model } : {}),
8
8
  ...(options.authPath !== undefined ? { authPath: options.authPath } : {}),
9
+ ...(options.credentialStore !== undefined ? { credentialStore: options.credentialStore } : {}),
9
10
  ...(options.sessionsDir !== undefined ? { sessionsDir: options.sessionsDir } : {}),
10
11
  serving: true, // a mounted service is long-running: the scheduler poller runs
11
12
  });
@@ -6,7 +6,8 @@ export interface BuildSessionRuntimeOptions {
6
6
  authPath?: string;
7
7
  }
8
8
  /**
9
- * Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools, skills, and auth
10
- * resolved exactly as the serving opener does).
9
+ * Build pi's interactive runtime over the SAME assembly `dev`/`start` serve ({@link assembleFront}): model
10
+ * registry, prompt, tools, skills, reasoning effort and credentials all come from it, so what differs is only the
11
+ * session's shape (one resident runtime rather than one session per invoke).
11
12
  */
12
13
  export declare function buildAgentSessionRuntime(dir: string, options?: BuildSessionRuntimeOptions, sessionManager?: SessionManager): Promise<AgentSessionRuntime>;
@@ -4,109 +4,63 @@
4
4
  */
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
- import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
8
- import { SessionManager, SettingsManager, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
9
- import { bindPiSession, definitionResourceLoaderOptions, reportExtensionErrors } from "./agent-session-factory.js";
10
- import { readMachine } from "./machine.js";
7
+ import { SessionManager, SettingsManager, createAgentSessionRuntime, getAgentDir, } from "@earendil-works/pi-coding-agent";
8
+ import { bindPiSession, definitionServices } from "./agent-session-factory.js";
11
9
  import { resolveModel } from "./config.js";
12
- import { assembleSystemPrompt, piBasePrompt } from "./create.js";
13
- import { canonicalPath, loadAgentDefinition, loadExtensionPaths } from "./definition.js";
14
- import { DEFAULT_THINKING_LEVEL, createPiModelRuntime } from "./models.js";
15
- import { reportFindingsIfChanged, reportToolCollisions } from "./report.js";
16
- import { resolveAgentAssembly } from "./open.js";
10
+ import { canonicalPath } from "./definition.js";
11
+ import { reportToolCollisions } from "./report.js";
12
+ import { assembleFront, resolveAgentAssembly } from "./open.js";
13
+ import { resolvePlacement } from "../../paths.js";
17
14
  /**
18
- * Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools, skills, and auth
19
- * resolved exactly as the serving opener does).
15
+ * Build pi's interactive runtime over the SAME assembly `dev`/`start` serve ({@link assembleFront}): model
16
+ * registry, prompt, tools, skills, reasoning effort and credentials all come from it, so what differs is only the
17
+ * session's shape (one resident runtime rather than one session per invoke).
20
18
  */
21
19
  export async function buildAgentSessionRuntime(dir, options = {}, sessionManager) {
22
- async function resolveAssembly(cwd) {
23
- // The shared front half — the SAME placement/config/model-spec/tool/auth resolution the serving opener uses
24
- // (open.ts).
25
- const { config, modelSpec, agentDir, auth, stateRoot, tools, toolCollisions } = await resolveAgentAssembly(cwd, options);
26
- reportToolCollisions(toolCollisions);
27
- // ONE hub owns model resolution AND per-request auth.
28
- // The fallback layer travels too: `chat` resolving credentials differently from dev/start/invoke is exactly the
29
- // divergence the layer exists to remove.
30
- const modelRuntime = await createPiModelRuntime({ auth, agentDir, stateRoot });
31
- const env = new NodeExecutionEnv({ cwd });
32
- const definition = await loadAgentDefinition(agentDir, { cwd, env });
33
- reportFindingsIfChanged(definition.dir, definition);
34
- // Assembly-time, like serving's: this whole function is memoized, so the scan and its warnings happen once per
35
- // runtime rather than per session rebuild (/new, /resume, fork).
36
- const extensionPaths = await loadExtensionPaths(agentDir, { cwd, env });
37
- // base + instructions ONLY — pi appends the skill section and env (cwd) itself (including them here would
38
- // duplicate them).
39
- const systemPrompt = assembleSystemPrompt({
40
- base: piBasePrompt({ tools, persona: definition.persona }),
41
- contextFiles: definition.contextFiles,
42
- });
43
- return {
44
- modelRuntime,
45
- modelSpec,
46
- // Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
47
- thinkingLevel: config.thinkingLevel,
48
- definition,
49
- extensionPaths,
50
- tools,
51
- systemPrompt,
52
- };
20
+ async function resolveAssembly() {
21
+ const front = await resolveAgentAssembly(dir, options);
22
+ reportToolCollisions(front.toolCollisions);
23
+ const { assembly } = await assembleFront(front);
24
+ // Read ONCE per runtime: a rebuild (/new, fork) keeps the startup snapshot, because config and tools stay in the
25
+ // import cache and a half-refreshed agent is worse than a stale one. Restart chat to pick up edits.
26
+ return { modelSpec: front.modelSpec, assembly, definition: await assembly.readDefinition() };
53
27
  }
54
- // pi calls the factory again on /new, /resume, switch, and fork.
55
- const rootCwd = canonicalPath(dir);
28
+ // The workspace, like serving's: where tools run and what session records are keyed to. Canonical, because pi's
29
+ // process.cwd() fallback is a realpath and a symlinked workspace would otherwise mismatch it.
30
+ const rootCwd = canonicalPath(resolvePlacement(dir).workspace);
31
+ // pi calls the factory again on /new, /resume, switch, and fork; the assembly is built once.
56
32
  let assembly;
57
33
  const assemblyFor = (cwd) => {
58
- // Canonical paths: pi's process.cwd() fallback is a realpath, so a symlinked workspace would otherwise mismatch a
59
- // non-realpath rootCwd.
60
34
  const activeCwd = canonicalPath(cwd);
61
35
  if (activeCwd !== rootCwd) {
62
36
  throw workspaceScopeError(activeCwd);
63
37
  }
64
- assembly ??= resolveAssembly(rootCwd);
38
+ assembly ??= resolveAssembly();
65
39
  return assembly;
66
40
  };
67
41
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
68
- const { modelRuntime, modelSpec, thinkingLevel, definition, extensionPaths, tools, systemPrompt } = await assemblyFor(cwd);
42
+ const { modelSpec, assembly, definition } = await assemblyFor(cwd);
69
43
  // Per session, NOT memoized with the assembly.
70
- const machine = await readMachine(cwd);
71
- const loaded = await createAgentSessionServices({
72
- cwd,
73
- // fastagent's models + auth hub replaces pi's default (~/.pi-backed) one — the auth unification point; see the
74
- // header.
75
- modelRuntime,
76
- // PACKAGELESS, for the loader: fastagent never installs a pi package (machine.ts). Handed pi's own settings, the
77
- // loader resolves `packages` itself — installing a missing one, and failing chat's start when that fails.
78
- settingsManager: machine.settingsManager(),
79
- // Chat's assembly is fixed for the life of the runtime (a rebuild makes a new one), so these read constants.
80
- resourceLoaderOptions: definitionResourceLoaderOptions({
81
- systemPrompt: () => systemPrompt,
82
- skills: () => definition.skills,
83
- machine,
84
- extensionPaths,
85
- }),
86
- });
44
+ const modelRuntime = await assembly.createModelRuntime();
45
+ const loaded = await definitionServices({ cwd, modelRuntime, definition, extensionPaths: assembly.extensionPaths });
87
46
  // ...while the SESSION keeps pi's own file-backed settings, so `/settings` in the TUI still saves. pi persists by
88
47
  // re-reading the file under its lock and writing only the fields that changed, so `packages` there is untouched.
89
48
  const services = { ...loaded, settingsManager: SettingsManager.create(cwd, loaded.agentDir) };
90
- reportExtensionErrors(services);
91
49
  // AFTER the services, because an extension may be what defines the model.
92
50
  const model = resolveModel(modelRuntime, modelSpec);
93
- // The same bind serving performs, minus the activation record: pi's chat session has nowhere to put one, which is
94
- // the documented divergence.
51
+ // Serving and chat restore the same native transcript declarations.
95
52
  const result = await bindPiSession({
96
53
  services,
97
54
  sessionManager,
98
55
  sessionStartEvent,
99
56
  model,
100
- // The SPELLING serving uses (agent-session-factory), never `thinkingLevel` alone: pi resolves an ABSENT level
101
- // from its own settings, which every posture now reads from the machine (`~/.pi/agent/settings.json`). A
102
- // `/thinking` + Ctrl+S saved for coding would otherwise silently make this the one posture that answers at a
103
- // different reasoning effort than the deployment does — the effort is the DEFINITION's (`thinkingLevel` in
104
- // fastagent.config.ts), unlike the engine knobs around it.
105
- thinkingLevel: thinkingLevel ?? DEFAULT_THINKING_LEVEL,
106
- tools,
57
+ // Always set, never left for pi to fill: pi resolves an ABSENT level from the machine's settings, and the effort
58
+ // is the definition's (`thinkingLevel` in fastagent.config.ts), like serving's.
59
+ thinkingLevel: assembly.thinkingLevel,
60
+ tools: assembly.tools,
61
+ excludedToolNames: assembly.excludedToolNames,
107
62
  // A tool must see one spelling of the workspace, including when opened through a symlink.
108
63
  cwd: rootCwd,
109
- recordActivations: false,
110
64
  });
111
65
  return { ...result, services, diagnostics: services.diagnostics };
112
66
  };
@@ -15,7 +15,7 @@
15
15
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
16
16
  import { type Models } from "@earendil-works/pi-ai";
17
17
  import { type AgentCommand, type SessionControl, type SessionEvent } from "../../session.ts";
18
- import type { SessionObserver, Lease } from "./turn-kit.ts";
18
+ import { type SessionObserver, type Lease } from "./turn-kit.ts";
19
19
  import type { AnyModel } from "./models.ts";
20
20
  import type { PiAgentSessionFactory } from "./invoke-session.ts";
21
21
  import type { PiSessionRecordStore } from "./session-store.ts";
@@ -25,9 +25,9 @@ export declare const SUBSCRIBER_BUFFER_CAP = 10000;
25
25
  export declare const SUBSCRIBER_BUFFER_BYTES: number;
26
26
  /** What the plane's writes (`update` / `compact` / `fork` / `delete`) need — the SAME instances the
27
27
  * agent assembly uses: the lease (a write must not race a run), the model registry (validation +
28
- * allowedModels), and the session factory (compaction is a model call). Writes go through the
29
- * record the hub's reader opened — after an existence check, so the control plane never creates
30
- * a session (that is the data plane's monopoly). */
28
+ * allowedModels), and the session factory (compaction is a model call). Two writes create a record:
29
+ * fork's `into`, and `update()` on an id that has none (through the store's `openOrCreate`, like an
30
+ * invoke). Every other write needs a record that exists. */
31
31
  export interface PiBoundaryWiring {
32
32
  lease: Lease;
33
33
  models: Models;