@fastagent-sh/fastagent 0.15.0 → 0.16.1

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 (181) hide show
  1. package/README.md +14 -4
  2. package/dist/bind.d.ts +34 -0
  3. package/dist/bind.js +74 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +266 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +5 -0
  13. package/dist/channels/context-buffer.js +5 -0
  14. package/dist/channels/feishu/context-buffer.d.ts +13 -8
  15. package/dist/channels/feishu/context-buffer.js +60 -6
  16. package/dist/channels/feishu/feishu-api.js +4 -1
  17. package/dist/channels/feishu/feishu.d.ts +0 -12
  18. package/dist/channels/feishu/feishu.js +143 -54
  19. package/dist/channels/feishu/invoke-turn.js +35 -18
  20. package/dist/channels/feishu/model.d.ts +0 -1
  21. package/dist/channels/feishu/normalize.js +0 -1
  22. package/dist/channels/feishu/parse.d.ts +21 -7
  23. package/dist/channels/feishu/parse.js +24 -7
  24. package/dist/channels/feishu/preview.js +3 -2
  25. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  26. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  27. package/dist/channels/feishu/setup-mode.d.ts +30 -1
  28. package/dist/channels/feishu/setup-mode.js +27 -1
  29. package/dist/channels/github/github.js +8 -1
  30. package/dist/channels/http.js +1 -1
  31. package/dist/channels/lark/scaffold/channel.ts +9 -8
  32. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  33. package/dist/channels/preview-kit.d.ts +7 -1
  34. package/dist/channels/preview-kit.js +3 -2
  35. package/dist/channels/slack/parse.d.ts +16 -1
  36. package/dist/channels/slack/parse.js +46 -3
  37. package/dist/channels/slack/preview.d.ts +1 -2
  38. package/dist/channels/slack/preview.js +68 -24
  39. package/dist/channels/slack/scaffold/channel.ts +5 -5
  40. package/dist/channels/slack/slack-api.d.ts +3 -23
  41. package/dist/channels/slack/slack-api.js +6 -22
  42. package/dist/channels/slack/slack.d.ts +13 -20
  43. package/dist/channels/slack/slack.js +95 -50
  44. package/dist/channels/state.d.ts +11 -4
  45. package/dist/channels/state.js +19 -12
  46. package/dist/channels/tasks.d.ts +0 -6
  47. package/dist/channels/tasks.js +16 -1
  48. package/dist/channels/telegram/parse.d.ts +0 -7
  49. package/dist/channels/telegram/parse.js +4 -2
  50. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  51. package/dist/channels/telegram/telegram.js +1 -1
  52. package/dist/channels/text.d.ts +14 -0
  53. package/dist/channels/text.js +14 -0
  54. package/dist/channels/thread-participants.d.ts +21 -0
  55. package/dist/channels/thread-participants.js +132 -0
  56. package/dist/channels/turn-queue.js +7 -0
  57. package/dist/cli/add-feishu.d.ts +7 -4
  58. package/dist/cli/add-feishu.js +57 -37
  59. package/dist/cli/add-slack.d.ts +2 -1
  60. package/dist/cli/add-slack.js +6 -11
  61. package/dist/cli/commands/add.js +50 -51
  62. package/dist/cli/commands/attach.js +8 -4
  63. package/dist/cli/commands/chat.js +8 -8
  64. package/dist/cli/commands/deploy.d.ts +14 -1
  65. package/dist/cli/commands/deploy.js +330 -78
  66. package/dist/cli/commands/dev.d.ts +1 -0
  67. package/dist/cli/commands/dev.js +38 -25
  68. package/dist/cli/commands/fire.js +15 -16
  69. package/dist/cli/commands/info.js +36 -29
  70. package/dist/cli/commands/init.d.ts +1 -1
  71. package/dist/cli/commands/init.js +65 -53
  72. package/dist/cli/commands/invoke.js +9 -6
  73. package/dist/cli/commands/login.js +35 -21
  74. package/dist/cli/commands/schedule.js +6 -8
  75. package/dist/cli/commands/start.d.ts +1 -0
  76. package/dist/cli/commands/start.js +101 -37
  77. package/dist/cli/commands/tool.js +30 -18
  78. package/dist/cli/fail.d.ts +17 -0
  79. package/dist/cli/fail.js +24 -0
  80. package/dist/cli/program.js +66 -36
  81. package/dist/cli/serve.d.ts +52 -8
  82. package/dist/cli/serve.js +133 -32
  83. package/dist/cli/shared.d.ts +21 -2
  84. package/dist/cli/shared.js +44 -18
  85. package/dist/deploy/agentcore/plan.d.ts +117 -0
  86. package/dist/deploy/agentcore/plan.js +721 -0
  87. package/dist/deploy/agentcore/run.d.ts +73 -0
  88. package/dist/deploy/agentcore/run.js +412 -0
  89. package/dist/deploy/agentcore/zip.d.ts +17 -0
  90. package/dist/deploy/agentcore/zip.js +68 -0
  91. package/dist/deploy/container.d.ts +26 -25
  92. package/dist/deploy/container.js +93 -89
  93. package/dist/deploy/docker/plan.d.ts +1 -1
  94. package/dist/deploy/docker/plan.js +12 -17
  95. package/dist/deploy/fly/plan.d.ts +2 -0
  96. package/dist/deploy/fly/plan.js +27 -19
  97. package/dist/deploy/fly/run.d.ts +12 -1
  98. package/dist/deploy/fly/run.js +36 -2
  99. package/dist/deploy/preflight.d.ts +11 -5
  100. package/dist/deploy/preflight.js +235 -65
  101. package/dist/deploy/railway/plan.d.ts +7 -0
  102. package/dist/deploy/railway/plan.js +41 -16
  103. package/dist/deploy/railway/run.d.ts +8 -1
  104. package/dist/deploy/railway/run.js +7 -2
  105. package/dist/deploy/runner.d.ts +5 -2
  106. package/dist/deploy/runner.js +9 -3
  107. package/dist/dev-supervisor.d.ts +11 -8
  108. package/dist/dev-supervisor.js +53 -51
  109. package/dist/engines/pi/auth.d.ts +8 -7
  110. package/dist/engines/pi/auth.js +12 -10
  111. package/dist/engines/pi/channel.d.ts +1 -1
  112. package/dist/engines/pi/channel.js +5 -5
  113. package/dist/engines/pi/chat.js +2 -2
  114. package/dist/engines/pi/config.d.ts +12 -48
  115. package/dist/engines/pi/config.js +29 -110
  116. package/dist/engines/pi/create.d.ts +32 -24
  117. package/dist/engines/pi/create.js +47 -23
  118. package/dist/engines/pi/definition.d.ts +7 -26
  119. package/dist/engines/pi/definition.js +8 -54
  120. package/dist/engines/pi/harness.d.ts +19 -5
  121. package/dist/engines/pi/harness.js +3 -5
  122. package/dist/engines/pi/login.d.ts +1 -1
  123. package/dist/engines/pi/models.d.ts +3 -3
  124. package/dist/engines/pi/models.js +1 -1
  125. package/dist/engines/pi/{workspace.d.ts → open.d.ts} +31 -25
  126. package/dist/engines/pi/{workspace.js → open.js} +27 -29
  127. package/dist/engines/pi/read-image.d.ts +4 -0
  128. package/dist/engines/pi/read-image.js +62 -0
  129. package/dist/engines/pi/search-tools.d.ts +6 -4
  130. package/dist/engines/pi/search-tools.js +3 -1
  131. package/dist/engines/pi/session-builder.d.ts +2 -2
  132. package/dist/engines/pi/session-builder.js +18 -13
  133. package/dist/engines/pi/tool.d.ts +13 -5
  134. package/dist/engines/pi/tool.js +4 -0
  135. package/dist/engines/pi/wake-tool.d.ts +3 -3
  136. package/dist/env.d.ts +16 -4
  137. package/dist/env.js +43 -5
  138. package/dist/host/node.d.ts +4 -2
  139. package/dist/host/node.js +2 -1
  140. package/dist/loader.d.ts +2 -2
  141. package/dist/loader.js +3 -3
  142. package/dist/log.d.ts +1 -1
  143. package/dist/log.js +1 -1
  144. package/dist/paths.d.ts +138 -0
  145. package/dist/paths.js +326 -0
  146. package/dist/pi.d.ts +2 -2
  147. package/dist/pi.js +2 -2
  148. package/dist/runtime.d.ts +7 -5
  149. package/dist/runtime.js +2 -2
  150. package/dist/scaffold/add-channel.d.ts +7 -3
  151. package/dist/scaffold/add-channel.js +55 -29
  152. package/dist/scaffold/init.d.ts +32 -41
  153. package/dist/scaffold/init.js +161 -185
  154. package/dist/scaffold/templates/env.example +15 -6
  155. package/dist/scaffold/templates/fastagent.config.mjs +1 -1
  156. package/dist/scaffold/templates/gitignore +14 -6
  157. package/dist/scaffold/templates/persona.md +4 -2
  158. package/dist/scaffold/templates/secrets.gitignore +5 -0
  159. package/dist/scaffold/templates.d.ts +1 -7
  160. package/dist/scaffold/templates.js +3 -25
  161. package/dist/scaffold/vendor-skill.d.ts +2 -2
  162. package/dist/scaffold/vendor-skill.js +13 -13
  163. package/dist/schedule/discover.js +4 -4
  164. package/dist/schedule/scheduler.d.ts +40 -1
  165. package/dist/schedule/scheduler.js +89 -56
  166. package/dist/schedule/state.js +1 -1
  167. package/dist/schedule/wake-alarm.d.ts +47 -0
  168. package/dist/schedule/wake-alarm.js +136 -0
  169. package/dist/schedule/wakeups.d.ts +1 -0
  170. package/dist/schedule/wakeups.js +18 -0
  171. package/dist/tunnel.d.ts +3 -3
  172. package/dist/tunnel.js +7 -7
  173. package/package.json +7 -4
  174. package/dist/channels/feishu/owned-threads.d.ts +0 -7
  175. package/dist/channels/feishu/owned-threads.js +0 -47
  176. package/dist/channels/slack/owned-threads.d.ts +0 -6
  177. package/dist/channels/slack/owned-threads.js +0 -43
  178. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  179. package/dist/scaffold/templates/gitignore.kit +0 -2
  180. package/dist/workspace.d.ts +0 -9
  181. package/dist/workspace.js +0 -45
@@ -7,9 +7,10 @@
7
7
  * historical entries back into context via buildContext().
8
8
  */
9
9
  import { AgentHarness } from "@earendil-works/pi-agent-core";
10
- import type { AgentTool, ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
10
+ import type { ExecutionEnv, ExecutionToolContext, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
11
11
  import type { Model, Models } from "@earendil-works/pi-ai";
12
12
  import type { PiSessionStore } from "./sessions.ts";
13
+ import { type MountedTool } from "./tool.ts";
13
14
  /**
14
15
  * The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
15
16
  * tools a loader activated in that call. The DEDICATED record the resolve below reads: pi's own
@@ -19,18 +20,30 @@ import type { PiSessionStore } from "./sessions.ts";
19
20
  * carry only what was actually discovered.
20
21
  */
21
22
  export declare const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
23
+ /** The session a factory-built harness is bound to — the seam the activation bridge (invoke.ts) uses
24
+ * to write {@link TOOL_ACTIVATION_ENTRY} deltas (pi's harness keeps its session private). Absent for
25
+ * a harness built outside {@link piHarnessFactory}: activation still works in-turn there, but is not
26
+ * recorded — the factory owns persistence. */
27
+ type AnyHarness = AgentHarness<any>;
22
28
  export type PiSession = Awaited<ReturnType<PiSessionStore["openOrCreate"]>>;
23
- export declare function harnessSession(harness: AgentHarness): PiSession | undefined;
29
+ export declare function harnessSession(harness: AnyHarness): PiSession | undefined;
24
30
  /**
25
31
  * pi's Model with the API-shape generic erased — fastagent only passes models through to the
26
32
  * harness, so the generic carries no information. One alias keeps the `any` auditable.
27
33
  */
28
34
  export type AnyModel = Model<any>;
29
35
  /** Builds a pi harness bound to the given session — called once per invoke. */
30
- export type PiHarnessFactory = (session: string) => AgentHarness | Promise<AgentHarness>;
36
+ /** The harness fastagent builds: context-typed on {@link ExecutionToolContext}, because that is what
37
+ * pi's env-backed default tools read (pi 0.83). Custom tools are context-FREE and stay assignable — a
38
+ * four-parameter `execute` satisfies the five-parameter one, so `defineTool` is untouched by this. */
39
+ type PiHarness = AgentHarness<ExecutionToolContext>;
40
+ export type PiHarnessFactory = (session: string) => PiHarness | Promise<PiHarness>;
31
41
  export interface PiHarnessFactoryOptions {
32
42
  /** Session persistence. Continuity = same backing store + same session id. */
33
43
  sessions: PiSessionStore;
44
+ /** Filesystem/process environment for the default coding tools. Handed to the harness as the TURN's
45
+ * tool context (pi 0.83), which is how read/bash/edit/write reach the machine at all — so this is the
46
+ * ONE seam a sandbox adapter implements, not a knob beside the tools that ignore it. */
34
47
  env: ExecutionEnv;
35
48
  /** Provider collection for all model requests; {@link model} must belong to it (same provider id). */
36
49
  models: Models;
@@ -38,7 +51,7 @@ export interface PiHarnessFactoryOptions {
38
51
  /** Reasoning effort for the model (pi's scale). Unset = fastagent's pinned default ("medium", pi
39
52
  * TUI parity — see {@link DEFAULT_THINKING_LEVEL}); unsupported levels are clamped by pi per model. */
40
53
  thinkingLevel?: ThinkingLevel;
41
- tools?: AgentTool[];
54
+ tools?: MountedTool[];
42
55
  /**
43
56
  * Final assembled prompt, or a SYNC factory re-evaluated per invoke (how L1 serves dynamic
44
57
  * `instructions` + the skills listing). Distinct from {@link live}, which is the directory rung's
@@ -109,6 +122,7 @@ export declare function resolveHarnessOverrides(entries: OverrideEntryLike[], mo
109
122
  model: AnyModel;
110
123
  thinkingLevel: ThinkingLevel;
111
124
  };
112
- export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: AgentTool[], sessionId: string): string[] | undefined;
125
+ export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: MountedTool[], sessionId: string): string[] | undefined;
113
126
  /** Open-or-create the session per invoke: existing → open (history via buildContext); missing → create. */
114
127
  export declare function piHarnessFactory(options: PiHarnessFactoryOptions): PiHarnessFactory;
128
+ export {};
@@ -18,10 +18,6 @@ import { isDeferredTool } from "./tool.js";
18
18
  * carry only what was actually discovered.
19
19
  */
20
20
  export const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
21
- /** The session a factory-built harness is bound to — the seam the activation bridge (invoke.ts) uses
22
- * to write {@link TOOL_ACTIVATION_ENTRY} deltas (pi's harness keeps its session private). Absent for
23
- * a harness built outside {@link piHarnessFactory}: activation still works in-turn there, but is not
24
- * recorded — the factory owns persistence. */
25
21
  const harnessSessions = new WeakMap();
26
22
  export function harnessSession(harness) {
27
23
  return harnessSessions.get(harness);
@@ -187,7 +183,9 @@ export function piHarnessFactory(options) {
187
183
  // Session overrides (set_model / set_thinking) win over the assembly defaults — same entry walk.
188
184
  const overrides = resolveHarnessOverrides(entries, options.models, { model: options.model, thinkingLevel: options.thinkingLevel ?? DEFAULT_THINKING_LEVEL }, sessionId);
189
185
  const harness = new AgentHarness({
190
- env: options.env,
186
+ // Static, not a per-turn provider: the env is fixed for the agent's lifetime, and resolving a
187
+ // constant per turn would only add a promise to the turn's critical path.
188
+ toolContext: { env: options.env },
191
189
  session,
192
190
  models: options.models,
193
191
  model: overrides.model,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `fastagent login`: authenticate a MODEL PROVIDER into the resolved auth file (project-level
3
- * `<dir>/.fastagent/auth.json` by default, or `--auth-path`/`FASTAGENT_AUTH_PATH`) via the same
3
+ * `<root>/.secrets/auth.json` by default, or `--auth-path`/`FASTAGENT_AUTH_PATH`) via the same
4
4
  * {@link fastagentCredentialStore} the runtime uses (one writer, one lock/corruption semantics).
5
5
  *
6
6
  * Flow (pi-ai's unified `ProviderAuth` API): pick a method (OAuth or API key), then a provider that
@@ -9,8 +9,8 @@ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
9
9
  import { type FastagentAuthOptions } from "./auth.ts";
10
10
  import { type InteractiveLoginKind } from "./login.ts";
11
11
  export interface CreatePiModelsOptions extends FastagentAuthOptions {
12
- /** Credentials file path. Defaults to the global `~/.fastagent/auth.json`; the directory opener passes
13
- * the project-level `<dir>/.fastagent/auth.json`. */
12
+ /** Credentials file path. Defaults to the global `~/.fastagent/.secrets/auth.json`; the directory opener passes
13
+ * the project-level `<root>/.secrets/auth.json`. */
14
14
  authPath?: string;
15
15
  /** Extra providers registered on top of the built-ins (same id overrides a built-in). */
16
16
  providers?: Provider[];
@@ -18,7 +18,7 @@ export interface CreatePiModelsOptions extends FastagentAuthOptions {
18
18
  /**
19
19
  * A `Models` with every built-in pi provider, wired to fastagent's auth: stored credentials from the
20
20
  * {@link CreatePiModelsOptions.authPath} file (via {@link fastagentCredentialStore}; the global
21
- * `~/.fastagent/auth.json` unless the opener passes a project-level path), then ambient env vars. A
21
+ * `~/.fastagent/.secrets/auth.json` unless the opener passes a project-level path), then ambient env vars. A
22
22
  * stored credential owns the provider; env is consulted only when nothing is stored (resolution order
23
23
  * is upstream-owned).
24
24
  */
@@ -12,7 +12,7 @@ import { interactiveLoginKind } from "./login.js";
12
12
  /**
13
13
  * A `Models` with every built-in pi provider, wired to fastagent's auth: stored credentials from the
14
14
  * {@link CreatePiModelsOptions.authPath} file (via {@link fastagentCredentialStore}; the global
15
- * `~/.fastagent/auth.json` unless the opener passes a project-level path), then ambient env vars. A
15
+ * `~/.fastagent/.secrets/auth.json` unless the opener passes a project-level path), then ambient env vars. A
16
16
  * stored credential owns the provider; env is consulted only when nothing is stored (resolution order
17
17
  * is upstream-owned).
18
18
  */
@@ -1,25 +1,25 @@
1
- import type { AgentTool } from "@earendil-works/pi-agent-core";
2
1
  import type { Agent } from "../../agent.ts";
3
2
  import { type FastagentConfig } from "./config.ts";
4
3
  import type { SessionControl } from "../../session.ts";
5
4
  import type { SessionObserver } from "./invoke.ts";
6
5
  import type { PiSessionReader, PiSessionStore } from "./sessions.ts";
7
6
  import type { ModuleLoadFailure } from "../../loader.ts";
8
- import { type LoadedDefinition } from "./definition.ts";
7
+ import type { LoadedDefinition } from "./definition.ts";
9
8
  import type { ToolCollision } from "./tool.ts";
10
- export interface CreatePiAgentFromWorkspaceOptions {
9
+ import type { MountedTool } from "./tool.ts";
10
+ export interface CreatePiAgentFromDirOptions {
11
11
  /** Model spec override (e.g. the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
12
12
  model?: string;
13
13
  /**
14
- * Session store directory. Default `<dir>/.fastagent/sessions` (gitignored machine state). `start`
14
+ * Session store directory. Default `<agentDir>/.state/sessions` (machine state). `start`
15
15
  * overrides it (--sessions-dir / FASTAGENT_SESSIONS_DIR / a mounted volume) so production continuity
16
16
  * survives redeploys.
17
17
  */
18
18
  sessionsDir?: string;
19
19
  /**
20
- * Credentials file override. Default `<dir>/.fastagent/auth.json` (project-level, gitignored under
21
- * the same `*`-ignored `.fastagent`). Override via --auth-path / FASTAGENT_AUTH_PATH; point it at
22
- * `~/.fastagent/auth.json` to share one credential across projects.
20
+ * Credentials file override. Default `<agentDir>/.secrets/auth.json` (project-level, under the
21
+ * `.secrets/`). Override via --auth-path / FASTAGENT_AUTH_PATH; point it at
22
+ * the global `~/.fastagent/.secrets/auth.json` to share one credential across projects.
23
23
  */
24
24
  authPath?: string;
25
25
  /**
@@ -28,7 +28,7 @@ export interface CreatePiAgentFromWorkspaceOptions {
28
28
  * and never poll). The built-in `wake` tool mounts only when this is set AND `config.selfSchedule` is on.
29
29
  */
30
30
  serving?: boolean;
31
- /** Assemble the session control plane over this workspace's session store and return it as
31
+ /** Assemble the session control plane over this agent's session store and return it as
32
32
  * {@link sessionControl} — the store is created inside this opener, so the hub must be wired
33
33
  * here too (an external `createPiSessionControl` cannot exist before the store does).
34
34
  * Default: `config.sessionControl` AND {@link serving} — the config key means "serve the control
@@ -43,51 +43,57 @@ export interface CreatePiAgentFromWorkspaceOptions {
43
43
  observer?: SessionObserver;
44
44
  }
45
45
  /**
46
- * The workspace assembly FRONT HALF — everything that is independent of how pi consumes the
46
+ * The agent assembly FRONT HALF — everything that is independent of how pi consumes the
47
47
  * definition (transient harness for serving vs resident AgentSession for chat / session control):
48
- * config → model spec → agentDir → the full tool surface ({@link resolveWorkspaceTools} — the ONE
49
- * place it is computed) → state root → auth path. Both {@link createPiAgentFromWorkspace} and the
48
+ * placement resolution → config → model spec → the full tool surface ({@link resolveAgentTools} — the
49
+ * ONE place it is computed) → state root → auth path. Both {@link createPiAgentFromDir} and the
50
50
  * session builder (session-builder.ts) consume this, so THESE inputs cannot drift between the two
51
51
  * consumption shapes. (Definition loading and prompt assembly stay per-consumer: serving re-reads
52
52
  * them live per invoke, the session builder snapshots at startup and lets pi append skills/env.)
53
53
  */
54
- export interface WorkspaceAssembly {
54
+ export interface AgentAssembly {
55
55
  config: FastagentConfig;
56
56
  configPath?: string;
57
57
  /** The resolved "provider/modelId" spec in use. */
58
58
  modelSpec: string;
59
- /** Absolute agent-definition dir (config.agentDir resolved against dir; = dir when unset). */
59
+ /** Absolute agent dir definition + config + machinery live here (resolvePlacement().agentDir). */
60
60
  agentDir: string;
61
- /** Absolute state root (FASTAGENT_STATE_DIR > <dir>/.fastagent). */
61
+ /** Absolute workspace the agent's cwd and the start of the ②-context walk: the agent dir's parent
62
+ * when the agent sits inside it, the agent dir ITSELF when you point at the agent. */
63
+ workspace: string;
64
+ /** Absolute state root (FASTAGENT_STATE_DIR > <agentDir>/.state). */
62
65
  stateRoot: string;
63
- /** Absolute credentials file (--auth-path/authPath option > FASTAGENT_AUTH_PATH > <stateRoot>/auth.json). */
66
+ /** Absolute credentials file (--auth-path/authPath option > FASTAGENT_AUTH_PATH > <agentDir>/.secrets/auth.json). */
64
67
  authPath: string;
65
68
  /** The full mounted tool surface (config.tools + discovered tools/, search_tools applied). */
66
- tools: AgentTool[];
69
+ tools: MountedTool[];
67
70
  toolNames: string[];
68
71
  deferredToolNames: string[];
69
72
  toolCollisions: ToolCollision[];
70
73
  toolFailures: ModuleLoadFailure[];
71
74
  }
72
- export declare function resolveWorkspaceAssembly(dir: string, options?: {
75
+ export declare function resolveAgentAssembly(dir: string, options?: {
73
76
  model?: string;
74
77
  authPath?: string;
75
- }): Promise<WorkspaceAssembly>;
78
+ }): Promise<AgentAssembly>;
76
79
  /**
77
- * "Point at a workspace → agent": load the config, resolve model and tools, then L2. Throws a clear
78
- * error when no model source is set (fail visibly at startup). Returns everything an entry point needs
79
- * to report what it assembled.
80
+ * "Point at a directory → agent": resolve the placement (`dir` may be either end the workspace or
81
+ * the agent dir itself), load the config, resolve model and tools, then L2. Throws a clear error when
82
+ * no model source is set (fail visibly at startup). Returns everything an entry point needs to report
83
+ * what it assembled.
80
84
  */
81
- export declare function createPiAgentFromWorkspace(dir: string, options?: CreatePiAgentFromWorkspaceOptions): Promise<{
85
+ export declare function createPiAgentFromDir(dir: string, options?: CreatePiAgentFromDirOptions): Promise<{
82
86
  agent: Agent;
83
87
  definition: LoadedDefinition;
84
88
  config: FastagentConfig;
85
89
  configPath?: string;
86
90
  /** The resolved "provider/modelId" spec actually in use. */
87
91
  modelSpec: string;
88
- /** Absolute agent-definition dir in use (config.agentDir resolved against dir; = dir when unset). Channels/tools/persona come from here. */
92
+ /** Absolute agent dir in use channels/tools/persona come from here. */
89
93
  agentDir: string;
90
- /** Absolute state root in use (FASTAGENT_STATE_DIR > <dir>/.fastagent) — the ChannelContext's stateRoot. */
94
+ /** Absolute workspace in use — the agent's cwd: ALWAYS the directory that was pointed at. */
95
+ workspace: string;
96
+ /** Absolute state root in use (FASTAGENT_STATE_DIR > <agentDir>/.state) — the ChannelContext's stateRoot. */
91
97
  stateRoot: string;
92
98
  /** Absolute session store directory in use (for the startup report). */
93
99
  sessionsDir: string;
@@ -95,7 +101,7 @@ export declare function createPiAgentFromWorkspace(dir: string, options?: Create
95
101
  authPath: string;
96
102
  /** The session store in use — also a {@link PiSessionReader}. */
97
103
  sessions: PiSessionStore & PiSessionReader;
98
- /** The observation plane over this workspace's sessions; present iff `options.sessionControl`. */
104
+ /** The observation plane over this agent's sessions; present iff `options.sessionControl`. */
99
105
  sessionControl?: SessionControl;
100
106
  /** Non-default, active-by-default tool names in effect: config.tools + discovered tools/. Each name
101
107
  * lives in exactly one report slot — deferred names are in {@link deferredToolNames} instead. */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Open a definition directory into an agent — the single workspace opener BOTH `fastagent dev` and
2
+ * Open a definition directory into an agent — the single agent opener BOTH `fastagent dev` and
3
3
  * `fastagent start` drive.
4
4
  *
5
5
  * A thin command-posture composition over L2 `createPiAgentFromDefinition`: open the directory →
@@ -9,39 +9,35 @@
9
9
  * watch and can point sessions at a mounted volume.
10
10
  */
11
11
  import { mkdir } from "node:fs/promises";
12
- import { defaultSessionsDir, loadConfig, resolveAgentDir, resolveAuthPath, resolveModelSpec, resolveStateRoot, } from "./config.js";
13
- import { createPiAgentFromDefinition, resolveWorkspaceTools } from "./create.js";
12
+ import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModelSpec, } from "./config.js";
13
+ import { resolveStateRoot, resolvePlacement } from "../../paths.js";
14
+ import { createPiAgentFromDefinition, resolveAgentTools } from "./create.js";
14
15
  import { createPiSessionControl } from "./session-control.js";
15
16
  import { withWakeTool } from "./wake-tool.js";
16
- import { ensureStateRootSelfIgnored } from "./definition.js";
17
17
  import { jsonlSessionStore } from "./sessions.js";
18
- export async function resolveWorkspaceAssembly(dir, options = {}) {
19
- const { config, path: configPath } = await loadConfig(dir);
18
+ export async function resolveAgentAssembly(dir, options = {}) {
19
+ // Placement is structural (resolvePlacement): the AGENT DIR carries definition + config + machinery;
20
+ // its parent — the WORKSPACE — is what the agent works on: its cwd and the start of the ②-context
21
+ // walk (that is where it reads the project's AGENTS.md from).
22
+ const { agentDir, workspace } = resolvePlacement(dir);
23
+ const { config, path: configPath } = await loadConfig(agentDir);
20
24
  const modelSpec = resolveModelSpec(options.model, config);
21
25
  if (!modelSpec) {
22
26
  throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
23
27
  }
24
- // The run root is `dir` (cwd where config lives, whose AGENTS.md is ② context); the agent's own
25
- // surface (persona/skills/tools/channels) lives in `agentDir` (config.agentDir, or `dir` when flat).
26
- const agentDir = resolveAgentDir(dir, config);
27
- const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveWorkspaceTools(config, agentDir, dir);
28
- // The state root: auth/sessions/channel state all derive from it, so FASTAGENT_STATE_DIR moves the
29
- // whole machine-state home in one knob (a container mounts one volume); the finer overrides below
30
- // still win for their specific path.
31
- const stateRoot = resolveStateRoot(dir);
32
- // The credentials file: project-level by default (under the state root); only resolved here, never
28
+ const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir);
29
+ // The state root: sessions/channel state/schedule state derive from it (FASTAGENT_STATE_DIR moves it
30
+ // in one knob — a container points it at its volume); the finer overrides below still win.
31
+ const stateRoot = resolveStateRoot(agentDir);
32
+ // The credentials file: project-level by default (under `<agentDir>/.secrets`); only resolved here, never
33
33
  // created (a missing file reads as not-configured — `fastagent login` creates it).
34
- const authPath = resolveAuthPath(dir, options.authPath);
35
- // Self-ignore the state root iff it lands in-tree — which covers everything under it (sessions, auth,
36
- // every channel's `channels/<kind>` home). HERE, not in the serving opener: every consumer of this
37
- // assembly can WRITE under the state root (serving: sessions/channels; the session builder: pi's
38
- // `/login` writing auth.json), so resolving a workspace's state root must make it leak-safe.
39
- await ensureStateRootSelfIgnored(dir, stateRoot);
34
+ const authPath = resolveAuthPath(agentDir, options.authPath);
40
35
  return {
41
36
  config,
42
37
  configPath,
43
38
  modelSpec,
44
39
  agentDir,
40
+ workspace,
45
41
  stateRoot,
46
42
  authPath,
47
43
  tools,
@@ -52,18 +48,19 @@ export async function resolveWorkspaceAssembly(dir, options = {}) {
52
48
  };
53
49
  }
54
50
  /**
55
- * "Point at a workspace → agent": load the config, resolve model and tools, then L2. Throws a clear
56
- * error when no model source is set (fail visibly at startup). Returns everything an entry point needs
57
- * to report what it assembled.
51
+ * "Point at a directory → agent": resolve the placement (`dir` may be either end the workspace or
52
+ * the agent dir itself), load the config, resolve model and tools, then L2. Throws a clear error when
53
+ * no model source is set (fail visibly at startup). Returns everything an entry point needs to report
54
+ * what it assembled.
58
55
  */
59
- export async function createPiAgentFromWorkspace(dir, options = {}) {
60
- const { config, configPath, modelSpec, agentDir, stateRoot, authPath, tools, toolNames, deferredToolNames, toolCollisions, toolFailures, } = await resolveWorkspaceAssembly(dir, options);
56
+ export async function createPiAgentFromDir(dir, options = {}) {
57
+ const { config, configPath, modelSpec, agentDir, workspace, stateRoot, authPath, tools, toolNames, deferredToolNames, toolCollisions, toolFailures, } = await resolveAgentAssembly(dir, options);
61
58
  // Mount the built-in `wake` tool only when BOTH: this is a long-running serve (the poller honors it) AND
62
- // the author opted into self-scheduling (config.selfSchedule). The workspace's own `wake` wins if defined.
59
+ // the author opted into self-scheduling (config.selfSchedule). The agent's own `wake` wins if defined.
63
60
  const mountedTools = withWakeTool(tools, stateRoot, !!options.serving && !!config.selfSchedule);
64
61
  const sessionsDir = options.sessionsDir ?? defaultSessionsDir(stateRoot);
65
62
  await mkdir(sessionsDir, { recursive: true });
66
- const sessions = jsonlSessionStore({ dir: sessionsDir, cwd: dir });
63
+ const sessions = jsonlSessionStore({ dir: sessionsDir, cwd: workspace });
67
64
  // The hub is wired HERE because the store is created here: chicken-and-egg otherwise (the hub
68
65
  // needs the store; the agent needs the hub's observer). Boundary parts (models/factory/lease)
69
66
  // only exist after the assembly below — the hub takes them as a lazy thunk, filled by the
@@ -93,7 +90,7 @@ export async function createPiAgentFromWorkspace(dir, options = {}) {
93
90
  const { agent, definition } = await createPiAgentFromDefinition(agentDir, {
94
91
  model: modelSpec,
95
92
  thinkingLevel: config.thinkingLevel,
96
- cwd: dir,
93
+ cwd: workspace,
97
94
  tools: mountedTools,
98
95
  authPath,
99
96
  // Skills are definition-only (the agent is its directory), so dev mirrors deployment exactly.
@@ -115,6 +112,7 @@ export async function createPiAgentFromWorkspace(dir, options = {}) {
115
112
  sessions,
116
113
  sessionControl: hub?.control,
117
114
  agentDir,
115
+ workspace,
118
116
  config,
119
117
  configPath,
120
118
  modelSpec,
@@ -0,0 +1,4 @@
1
+ import type { ReadImageProcessor } from "@earendil-works/pi-agent-core";
2
+ /** The `read` tool's image processor. Matches pi-coding-agent's messages verbatim: they reach the model
3
+ * as tool output, so a reworded one is a different prompt, not a different implementation detail. */
4
+ export declare const readImageProcessor: ReadImageProcessor;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The image pipeline pi's `read` tool needs: normalize an unsupported format to PNG, resize below the
3
+ * inline limit, and hand back the hints that tell the model what it is looking at.
4
+ *
5
+ * pi-agent-core's `createReadTool` takes this as an INJECTED processor and does nothing without one —
6
+ * unlike pi-coding-agent's, which wires its private `processImage` internally. That function is not
7
+ * exported (nor reachable: the package's `exports` map has no deep paths), so this rebuilds it from the
8
+ * two halves that ARE public, `convertToPng` and `resizeImage`/`formatDimensionNote`.
9
+ *
10
+ * It is upstream logic restated, which is a real cost — without it `read` on a screenshot sends the raw
11
+ * bytes (measured: 7.48 MB of base64 where pi-coding-agent sends 3.48 MB, and no dimension note for the
12
+ * model's coordinate math), and a bmp is dropped entirely while the tool's own description still
13
+ * advertises it. test/tools-parity.test.ts compares this against pi-coding-agent's real `read` on both
14
+ * paths, so upstream changing the pipeline surfaces as a failing test rather than as drift.
15
+ */
16
+ import { convertToPng, formatDimensionNote, resizeImage } from "@earendil-works/pi-coding-agent";
17
+ /** Formats a provider takes inline as-is; everything else has to become a PNG first. */
18
+ const INLINE_MIME = {
19
+ "image/png": "image/png",
20
+ "image/jpeg": "image/jpeg",
21
+ "image/jpg": "image/jpeg",
22
+ "image/gif": "image/gif",
23
+ "image/webp": "image/webp",
24
+ };
25
+ /** The `read` tool's image processor. Matches pi-coding-agent's messages verbatim: they reach the model
26
+ * as tool output, so a reworded one is a different prompt, not a different implementation detail. */
27
+ export const readImageProcessor = async (bytes, mimeType, options) => {
28
+ const base = mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
29
+ const inline = INLINE_MIME[base];
30
+ let normalized;
31
+ if (inline) {
32
+ normalized = { bytes, mimeType: inline };
33
+ }
34
+ else {
35
+ const png = await convertToPng(Buffer.from(bytes).toString("base64"), base);
36
+ if (!png)
37
+ return { ok: false, message: "[Image omitted: could not be converted to a supported inline image format.]" };
38
+ normalized = { bytes: Buffer.from(png.data, "base64"), mimeType: png.mimeType, convertedFrom: base };
39
+ }
40
+ const hints = [];
41
+ const converted = (to) => normalized.convertedFrom && normalized.convertedFrom !== to
42
+ ? `[Image converted from ${normalized.convertedFrom} to ${to}.]`
43
+ : undefined;
44
+ if (!options.autoResizeImages) {
45
+ const hint = converted(normalized.mimeType);
46
+ if (hint)
47
+ hints.push(hint);
48
+ return { ok: true, data: Buffer.from(normalized.bytes).toString("base64"), mimeType: normalized.mimeType, hints };
49
+ }
50
+ const resized = await resizeImage(normalized.bytes, normalized.mimeType);
51
+ if (!resized)
52
+ return { ok: false, message: "[Image omitted: could not be resized below the inline image size limit.]" };
53
+ const hint = converted(resized.mimeType);
54
+ if (hint)
55
+ hints.push(hint);
56
+ // The scale factor the model needs to map coordinates back onto the original — dropping it is what
57
+ // makes a resized screenshot unusable for anything positional.
58
+ const note = formatDimensionNote(resized);
59
+ if (note)
60
+ hints.push(note);
61
+ return { ok: true, data: resized.data, mimeType: resized.mimeType, hints };
62
+ };
@@ -1,10 +1,12 @@
1
- import type { AgentTool } from "@earendil-works/pi-agent-core";
2
- /** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own. */
3
- export declare function withSearchTool(tools: AgentTool[]): AgentTool[];
1
+ import { type MountedTool } from "./tool.ts";
2
+ /** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own.
3
+ * Typed on the MOUNTED tool, not the authored one: it inspects names and deferral and never executes,
4
+ * so narrowing here would reject the very set it is handed (pi's defaults take the turn's context). */
5
+ export declare function withSearchTool(tools: MountedTool[]): MountedTool[];
4
6
  /** Build the `search_tools` loader. Keyword search over the inactive tools' name+description.
5
7
  *
6
8
  * `executionMode: "sequential"` — pi turns any batch containing a sequential tool serial. Required for
7
9
  * correct load-point attribution everywhere an OUTER active-set diff exists: pi wraps SDK customTools
8
10
  * (the chat path) in a before/after diff, and two parallel loader calls would both snapshot the
9
11
  * pre-activation set and get stamped with the same activation. Custom loader authors must set it too. */
10
- export declare function makeSearchToolsTool(): AgentTool;
12
+ export declare function makeSearchToolsTool(): MountedTool;
@@ -8,7 +8,9 @@
8
8
  import { z } from "zod";
9
9
  import { log } from "../../log.js";
10
10
  import { defineTool, isDeferredTool, stripDeferredMarker } from "./tool.js";
11
- /** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own. */
11
+ /** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own.
12
+ * Typed on the MOUNTED tool, not the authored one: it inspects names and deferral and never executes,
13
+ * so narrowing here would reject the very set it is handed (pi's defaults take the turn's context). */
12
14
  export function withSearchTool(tools) {
13
15
  if (!tools.some(isDeferredTool))
14
16
  return tools;
@@ -3,7 +3,7 @@ export interface BuildSessionRuntimeOptions {
3
3
  /** Model spec override (the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
4
4
  model?: string;
5
5
  /** Credentials file override (the CLI --auth-path flag). Precedence: this > FASTAGENT_AUTH_PATH >
6
- * the workspace default `<stateRoot>/auth.json`. */
6
+ * the agent's default `<agentDir>/.secrets/auth.json`. */
7
7
  authPath?: string;
8
8
  }
9
9
  /**
@@ -11,6 +11,6 @@ export interface BuildSessionRuntimeOptions {
11
11
  * skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
12
12
  * assembly — the fidelity-critical part — is inspectable and reusable without launching a TUI.
13
13
  */
14
- export declare function buildWorkspaceSessionRuntime(dir: string, options?: BuildSessionRuntimeOptions,
14
+ export declare function buildAgentSessionRuntime(dir: string, options?: BuildSessionRuntimeOptions,
15
15
  /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
16
16
  sessionManager?: SessionManager): Promise<AgentSessionRuntime>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The shared definition-aware session builder: open a workspace's assembled agent as a resident pi
2
+ * The shared definition-aware session builder: open a directory's assembled agent as a resident pi
3
3
  * `AgentSessionRuntime`. Extracted from chat.ts (session-control Phase 0) as the proof of the
4
4
  * assembly seam — independently instantiable, running the SAME agent that `dev`/`start` serve. The
5
5
  * TUI (chat.ts) is its one consumer: the session control plane (Phases 1–3) was built on the invoke
@@ -16,8 +16,8 @@
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.
19
- * - auth → fastagent's credential store at the workspace auth path (same resolution as the
20
- * serving opener: `--auth-path`/`FASTAGENT_AUTH_PATH`, else `<stateRoot>/auth.json`).
19
+ * - auth → fastagent's credential store at the AGENT's auth path (same resolution as the
20
+ * serving opener: `--auth-path`/`FASTAGENT_AUTH_PATH`, else `<root>/.secrets/auth.json`).
21
21
  * pi's TUI `/login` writes through the injected store into the SAME file, so `fastagent
22
22
  * login` and chat share one credential lifecycle. pi's `~/.pi` auth is not consulted.
23
23
  *
@@ -38,7 +38,7 @@ import { createPiModelRuntime, probeAuthSource } from "./models.js";
38
38
  import { log } from "../../log.js";
39
39
  import { additiveActivation, turnContext } from "./tool-context.js";
40
40
  import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
41
- import { resolveWorkspaceAssembly } from "./workspace.js";
41
+ import { resolveAgentAssembly } from "./open.js";
42
42
  /** Adapt coding-agent's resident SessionManager to FastAgent's shared tool-runtime manager port. */
43
43
  function toolChatSessionManager(session) {
44
44
  return {
@@ -59,7 +59,7 @@ function toolChatSessionManager(session) {
59
59
  * skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
60
60
  * assembly — the fidelity-critical part — is inspectable and reusable without launching a TUI.
61
61
  */
62
- export async function buildWorkspaceSessionRuntime(dir, options = {},
62
+ export async function buildAgentSessionRuntime(dir, options = {},
63
63
  /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
64
64
  sessionManager) {
65
65
  /** The turn's {@link ToolActivation} over pi's AgentSession — the counterpart of invoke.ts's
@@ -91,8 +91,8 @@ sessionManager) {
91
91
  };
92
92
  }
93
93
  async function resolveAssembly(cwd) {
94
- // The shared front half — the SAME config/model-spec/agentDir/tool/auth resolution the serving
95
- // opener uses (workspace.ts); those inputs cannot drift between the two consumption shapes.
94
+ // The shared front half — the SAME placement/config/model-spec/tool/auth resolution the serving
95
+ // opener uses (open.ts); those inputs cannot drift between the two consumption shapes.
96
96
  // (Definition→prompt assembly is NOT shared: serving re-reads live per invoke, this runtime is a
97
97
  // startup snapshot and pi appends skills/env itself — see the header.) `tools` arrives with
98
98
  // search_tools applied; deferral is EMULATED below like serving
@@ -100,27 +100,27 @@ sessionManager) {
100
100
  // on the session in createRuntime — pi's session starts all-active), and the activation bridge
101
101
  // above rides the same turn context, so the SAME search_tools works against pi's AgentSession
102
102
  // instead of fastagent's harness.
103
- const { config, modelSpec, agentDir, authPath, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveWorkspaceAssembly(cwd, options);
103
+ const { config, modelSpec, agentDir, authPath, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
104
104
  reportToolCollisions(toolCollisions);
105
105
  reportModuleLoadFailures(toolFailures);
106
106
  // ONE hub owns model resolution AND per-request auth — the ModelRuntime-shaped sibling of
107
107
  // serving's createPiModels; see models.ts.
108
108
  const modelRuntime = await createPiModelRuntime({ authPath });
109
109
  // MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth;
110
- // it now reads the workspace credential file like every other command. Probe the RESOLVED
110
+ // it now reads the agent's credential file like every other command. Probe the RESOLVED
111
111
  // model's provider through the normal resolution path (stored credential OR env var — an
112
112
  // env-authed user is fine and must not be warned): only when that provider has no usable auth
113
113
  // AND pi's old file exists does the bare provider error get its cause named.
114
114
  if ((await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
115
115
  existsSync(join(getAgentDir(), "auth.json"))) {
116
116
  log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
117
- `pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this workspace`);
117
+ `pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
118
118
  }
119
119
  const model = resolveModel(modelRuntime, modelSpec);
120
120
  const env = new NodeExecutionEnv({ cwd });
121
121
  const definition = await loadAgentDefinition(agentDir, { cwd, env });
122
122
  reportDefinitionWarnings(definition.collisions, definition.diagnostics);
123
- const defaultNames = piDefaultTools(cwd).map((t) => t.name);
123
+ const defaultNames = piDefaultTools().map((t) => t.name);
124
124
  const customTools = tools.filter((t) => !defaultNames.includes(t.name));
125
125
  // Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
126
126
  // it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
@@ -141,7 +141,12 @@ sessionManager) {
141
141
  // session-lifecycle invariant as a normal out-of-turn call (fail visibly).
142
142
  if (!bound)
143
143
  throw new Error("tool executed before its session was built (lifecycle invariant broken)");
144
- return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation }, () => t.execute(id, params, signal));
144
+ return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation },
145
+ // pi's per-turn TOOL context (5th parameter) is read only by its default coding tools, and
146
+ // those are filtered out of `customTools` above; fastagent's own take theirs from
147
+ // `turnContext` (AsyncLocalStorage). So this env exists to satisfy the shape, and it is the
148
+ // chat cwd's — the same root a default tool would have got, had one reached here.
149
+ () => t.execute(id, params, signal, undefined, { env }));
145
150
  },
146
151
  }));
147
152
  // base + instructions ONLY — pi appends the skill section and env (cwd) itself (including
@@ -153,7 +158,7 @@ sessionManager) {
153
158
  return {
154
159
  model,
155
160
  modelRuntime,
156
- // Serving honors config.thinkingLevel (workspace → L2); the resident session must too (fidelity).
161
+ // Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
157
162
  thinkingLevel: config.thinkingLevel,
158
163
  definition,
159
164
  defaultNames,