@fastagent-sh/fastagent 0.17.1 → 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 (170) hide show
  1. package/README.md +25 -7
  2. package/dist/agent.d.ts +12 -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.d.ts +4 -2
  22. package/dist/channels/feishu/feishu-api.js +1 -1
  23. package/dist/channels/feishu/feishu.d.ts +1 -1
  24. package/dist/channels/feishu/feishu.js +45 -15
  25. package/dist/channels/feishu/invoke-turn.d.ts +9 -3
  26. package/dist/channels/feishu/invoke-turn.js +152 -34
  27. package/dist/channels/feishu/parse.js +6 -0
  28. package/dist/channels/feishu/preview.d.ts +1 -1
  29. package/dist/channels/feishu/preview.js +7 -3
  30. package/dist/channels/feishu/ws-ingress.d.ts +1 -1
  31. package/dist/channels/github/github.d.ts +1 -1
  32. package/dist/channels/http.d.ts +11 -7
  33. package/dist/channels/http.js +16 -111
  34. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
  35. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +18 -4
  36. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +23 -8
  37. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  38. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  39. package/dist/channels/kit/state.d.ts +6 -0
  40. package/dist/channels/{state.js → kit/state.js} +4 -24
  41. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  42. package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
  43. package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
  44. package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +7 -0
  45. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +4 -1
  46. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  47. package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
  48. package/dist/channels/lark/lark.d.ts +1 -1
  49. package/dist/channels/serve.d.ts +73 -0
  50. package/dist/channels/serve.js +243 -0
  51. package/dist/channels/slack/bot-auth.js +3 -14
  52. package/dist/channels/slack/context-buffer.d.ts +2 -2
  53. package/dist/channels/slack/context-buffer.js +2 -2
  54. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  55. package/dist/channels/slack/invoke-turn.js +9 -6
  56. package/dist/channels/slack/parse.js +2 -2
  57. package/dist/channels/slack/preview.d.ts +1 -1
  58. package/dist/channels/slack/preview.js +11 -3
  59. package/dist/channels/slack/slack-api.js +1 -1
  60. package/dist/channels/slack/slack.d.ts +1 -1
  61. package/dist/channels/slack/slack.js +8 -12
  62. package/dist/channels/slack/welcomed.js +1 -1
  63. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  64. package/dist/channels/telegram/context-buffer.js +2 -2
  65. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  66. package/dist/channels/telegram/invoke-turn.js +2 -2
  67. package/dist/channels/telegram/parse.js +2 -2
  68. package/dist/channels/telegram/preview.d.ts +1 -1
  69. package/dist/channels/telegram/preview.js +6 -2
  70. package/dist/channels/telegram/telegram.d.ts +1 -1
  71. package/dist/channels/telegram/telegram.js +3 -3
  72. package/dist/channels/telegram/turn-store.d.ts +2 -2
  73. package/dist/channels/telegram/turn-store.js +2 -2
  74. package/dist/cli/commands/add.js +1 -1
  75. package/dist/cli/commands/deploy.js +13 -5
  76. package/dist/cli/commands/dev.js +26 -14
  77. package/dist/cli/commands/fire.js +2 -2
  78. package/dist/cli/commands/info.js +28 -5
  79. package/dist/cli/commands/invoke.js +1 -1
  80. package/dist/cli/commands/schedule.js +1 -1
  81. package/dist/cli/commands/start.js +64 -95
  82. package/dist/cli/commands/tool.js +5 -7
  83. package/dist/cli/program.js +1 -1
  84. package/dist/cli/serve.d.ts +18 -57
  85. package/dist/cli/serve.js +57 -243
  86. package/dist/cli/shared.d.ts +5 -3
  87. package/dist/cli/shared.js +14 -7
  88. package/dist/collect.d.ts +30 -4
  89. package/dist/collect.js +39 -6
  90. package/dist/core.d.ts +3 -5
  91. package/dist/core.js +7 -5
  92. package/dist/deploy/preflight.d.ts +8 -2
  93. package/dist/deploy/preflight.js +22 -4
  94. package/dist/deploy/secrets.d.ts +3 -0
  95. package/dist/deploy/secrets.js +6 -0
  96. package/dist/dev-supervisor.js +16 -4
  97. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  98. package/dist/engines/pi/agent-session-factory.js +314 -0
  99. package/dist/engines/pi/config.d.ts +4 -4
  100. package/dist/engines/pi/config.js +1 -1
  101. package/dist/engines/pi/create.d.ts +56 -42
  102. package/dist/engines/pi/create.js +150 -75
  103. package/dist/engines/pi/definition.d.ts +41 -7
  104. package/dist/engines/pi/definition.js +115 -11
  105. package/dist/engines/pi/invoke-session.d.ts +35 -0
  106. package/dist/engines/pi/invoke-session.js +357 -0
  107. package/dist/engines/pi/login.js +7 -4
  108. package/dist/engines/pi/models.d.ts +59 -11
  109. package/dist/engines/pi/models.js +64 -9
  110. package/dist/engines/pi/open.d.ts +9 -6
  111. package/dist/engines/pi/open.js +20 -10
  112. package/dist/engines/pi/report.d.ts +0 -4
  113. package/dist/engines/pi/report.js +0 -7
  114. package/dist/engines/pi/service.d.ts +23 -0
  115. package/dist/engines/pi/service.js +19 -0
  116. package/dist/engines/pi/session-builder.js +88 -90
  117. package/dist/engines/pi/session-control.d.ts +11 -9
  118. package/dist/engines/pi/session-control.js +114 -77
  119. package/dist/engines/pi/session-inheritance.d.ts +44 -0
  120. package/dist/engines/pi/session-inheritance.js +242 -0
  121. package/dist/engines/pi/session-settings.d.ts +14 -2
  122. package/dist/engines/pi/session-settings.js +18 -0
  123. package/dist/engines/pi/session-store.d.ts +70 -0
  124. package/dist/engines/pi/session-store.js +313 -0
  125. package/dist/engines/pi/tool-context.d.ts +18 -8
  126. package/dist/engines/pi/tool-context.js +23 -1
  127. package/dist/engines/pi/tool.d.ts +5 -5
  128. package/dist/engines/pi/tool.js +1 -1
  129. package/dist/engines/pi/{invoke.d.ts → turn-kit.d.ts} +46 -45
  130. package/dist/engines/pi/turn-kit.js +188 -0
  131. package/dist/engines/pi/wake-tool.js +1 -1
  132. package/dist/index.d.ts +1 -0
  133. package/dist/index.js +1 -0
  134. package/dist/log.d.ts +3 -0
  135. package/dist/log.js +5 -1
  136. package/dist/node.d.ts +16 -0
  137. package/dist/node.js +20 -0
  138. package/dist/paths.d.ts +13 -0
  139. package/dist/paths.js +16 -0
  140. package/dist/pi.d.ts +15 -8
  141. package/dist/pi.js +7 -6
  142. package/dist/scaffold/templates/fastagent.config.mjs +2 -0
  143. package/dist/scaffold/templates/persona.md +2 -2
  144. package/dist/schedule/state.js +4 -9
  145. package/dist/schedule/wakeups.d.ts +1 -1
  146. package/dist/schedule/wakeups.js +1 -1
  147. package/dist/service.d.ts +158 -0
  148. package/dist/service.js +354 -0
  149. package/dist/session-remote.d.ts +1 -1
  150. package/dist/session-remote.js +10 -2
  151. package/dist/session.d.ts +4 -2
  152. package/dist/session.js +1 -1
  153. package/package.json +14 -5
  154. package/dist/channels/state.d.ts +0 -16
  155. package/dist/engines/pi/harness.d.ts +0 -112
  156. package/dist/engines/pi/harness.js +0 -153
  157. package/dist/engines/pi/invoke.js +0 -618
  158. package/dist/engines/pi/read-image.d.ts +0 -4
  159. package/dist/engines/pi/read-image.js +0 -62
  160. package/dist/engines/pi/sessions.d.ts +0 -44
  161. package/dist/engines/pi/sessions.js +0 -182
  162. package/dist/host/node.js +0 -52
  163. /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
  164. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  165. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  166. /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
  167. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  168. /package/dist/channels/{text.js → kit/text.js} +0 -0
  169. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
  170. /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
@@ -1,24 +1,30 @@
1
- import { type ExecutionEnv, type Skill, type ThinkingLevel } from "@earendil-works/pi-agent-core";
2
- import type { Models, Provider } from "@earendil-works/pi-ai";
1
+ import type { ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { Provider } from "@earendil-works/pi-ai";
3
3
  import type { Agent } from "../../agent.ts";
4
4
  import { type FastagentConfig } from "./config.ts";
5
5
  import { type LoadedDefinition } from "./definition.ts";
6
- import { type AnyModel, piHarnessFactory } from "./harness.ts";
7
- import { type PiSessionStore } from "./sessions.ts";
8
6
  import type { ModuleLoadFailure } from "../../loader.ts";
9
7
  import { type ToolCollision, type MountedTool } from "./tool.ts";
10
- import { type Lease, type SessionObserver } from "./invoke.ts";
11
- /** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
12
- * turn as the harness tool context — hence no argument here. */
13
- export declare function piDefaultTools(): MountedTool[];
14
- /** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
15
- export declare function resolveTools(config: FastagentConfig): MountedTool[];
8
+ import { type PiAgentSessionFactory } from "./invoke-session.ts";
9
+ import { type AnyModel } from "./models.ts";
10
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
11
+ import { type PiSessionRecordStore } from "./session-store.ts";
12
+ import { type Lease, type SessionObserver } from "./turn-kit.ts";
16
13
  /**
17
- * The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
18
- * existing win), plus the non-default tool names and collisions to report. One source for the
14
+ * Every pi coding tool, in canonical order, rooted at the workspace it operates in.
15
+ *
16
+ * pi ships two overlapping groupings and neither is the whole set: `createCodingTools` is the four it
17
+ * ACTIVATES for a terminal (read/bash/edit/write, no searching), `createReadOnlyTools` is
18
+ * read/grep/find/ls. `read` is in both; a directory agent mounts their union.
19
+ */
20
+ export declare const CODING_TOOL_NAMES: readonly ["read", "grep", "find", "ls", "bash", "edit", "write"];
21
+ export declare function piAllCodingTools(cwd: string): MountedTool[];
22
+ /**
23
+ * The full directory-agent tool set: all pi coding tools + `config.tools` + discovered `tools/`
24
+ * (deduped, existing win), plus the authored names and collisions to report. One source for the
19
25
  * dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
20
26
  */
21
- export declare function resolveAgentTools(config: FastagentConfig, agentDir: string): Promise<{
27
+ export declare function resolveAgentTools(config: FastagentConfig, agentDir: string, cwd: string): Promise<{
22
28
  tools: MountedTool[];
23
29
  toolNames: string[];
24
30
  /** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
@@ -56,20 +62,22 @@ export interface AssembleSystemPromptOptions {
56
62
  export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
57
63
  /**
58
64
  * INTERNAL seam (workspace ↔ assembly): hands the hub-wiring consumer the assembly's live parts —
59
- * the SAME models collection, harness factory, and lease the agent runs with so boundary
60
- * mutations (session-control.ts) contend on the real lease and validate against the real registry.
61
- * Called synchronously, exactly once, before the agent is returned. Not part of the public surface.
65
+ * the SAME session factory and lease the agent runs with, plus the model registry behind a thunk —
66
+ * so boundary mutations (session-control.ts) contend on the real lease and validate against the real
67
+ * registry. Called synchronously, exactly once, before the agent is returned. Not public surface.
62
68
  */
63
- type OnAssembly = (parts: {
64
- models: Models;
65
- harnessFactory: ReturnType<typeof piHarnessFactory>;
69
+ export type PiAssemblyParts = {
66
70
  lease: Lease;
67
- /** The resolved configured pair — what a session without overrides runs on. */
68
- defaults: {
71
+ sessionFactory: PiAgentSessionFactory;
72
+ /** The registry and configured model, resolved on first use (a credential read is async). */
73
+ engine: () => Promise<{
74
+ modelRuntime: ModelRuntime;
69
75
  model: AnyModel;
70
- thinkingLevel: ThinkingLevel;
71
- };
72
- }) => void;
76
+ }>;
77
+ /** The configured reasoning effort — the other half of the pair a session without overrides runs on. */
78
+ thinkingLevel: ThinkingLevel;
79
+ };
80
+ type OnAssembly = (parts: PiAssemblyParts) => void;
73
81
  /** L1 options. Tier 1: model (spec) + instructions + tools. Tier 2: the injectable ports. */
74
82
  export interface CreatePiAgentOptions {
75
83
  /** Model spec "provider/modelId" (e.g. "openai-codex/gpt-5.5"), resolved against {@link models}. */
@@ -77,14 +85,18 @@ export interface CreatePiAgentOptions {
77
85
  /** Reasoning effort (pi's scale). Unset = pi's default; unsupported levels are clamped per model. */
78
86
  thinkingLevel?: ThinkingLevel;
79
87
  /**
80
- * The system prompt itself — verbatim, no engine base and no wrapping (unlike the directory path,
81
- * which assembles the engine base + AGENTS.md as segment ② + persona.md as segment ①). A plain string
82
- * or a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
88
+ * The system prompt itself — no engine base and no wrapping (unlike the directory path, which
89
+ * assembles the engine base + AGENTS.md as segment ② + persona.md as segment ①). A plain string or
90
+ * a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
91
+ *
92
+ * Not byte-for-byte verbatim: pi appends its own `Current working directory:` line to whatever
93
+ * prompt it is given. What this rung guarantees is that no engine IDENTITY is imposed — a
94
+ * hand-built agent is not told it is a coding assistant.
83
95
  */
84
96
  instructions?: string | (() => string);
85
97
  /** The tool set to mount. `FastagentTool` (AgentTool plus the optional `deferred` marker, see
86
98
  * {@link DefineToolOptions}) widens into {@link MountedTool}, which additionally admits pi's default
87
- * coding tools — they read the turn's ExecutionEnv as a fifth `execute` parameter. */
99
+ * coding tools — those bind their workspace at construction rather than reading a turn context. */
88
100
  tools?: MountedTool[];
89
101
  skills?: Skill[];
90
102
  /**
@@ -99,12 +111,15 @@ export interface CreatePiAgentOptions {
99
111
  * consulted when a provider is absent from the file (resolution order is upstream-owned).
100
112
  */
101
113
  authPath?: string;
102
- /** Session persistence. Defaults to in-memory; inject jsonlSessionStore for restart-surviving continuity. */
103
- sessions?: PiSessionStore;
104
- /** Filesystem/process environment. Defaults to a local NodeExecutionEnv at `process.cwd()`, and its
105
- * cwd is the agent's. The default coding tools (read/bash/edit/write) take it as the turn's tool
106
- * context, so injecting a constrained one narrows where the agent reads, writes and shells. It does
107
- * NOT constrain author-written `tools/`, which are code and can import anything. */
114
+ /** Session persistence. Defaults to in-memory; inject piSessionRecordStore for restart-surviving
115
+ * continuity. */
116
+ sessions?: PiSessionRecordStore;
117
+ /** Filesystem/process environment, handed to tools that read one as the turn's context. Defaults to
118
+ * a local NodeExecutionEnv at `process.cwd()`. At THIS rung nothing else consumes it: L1 loads no
119
+ * definition. It does NOT constrain the coding tools (pi's own, rooted at the workspace they were
120
+ * built for) or author-written `tools/`, which are code and can import anything. Not a
121
+ * sandbox — see {@link createPiAgentFromDefinition} for the rung where it also reads the
122
+ * definition. */
108
123
  env?: ExecutionEnv;
109
124
  /** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
110
125
  lease?: Lease;
@@ -126,11 +141,11 @@ export interface CreatePiAgentFromDefinitionOptions {
126
141
  /** Override the engine base prompt (segment ①). Defaults to piBasePrompt({ tools, persona }) using the
127
142
  * live-read persona.md; pass base to fully opt out of persona.md. */
128
143
  base?: string;
129
- /** Override tools. Defaults to {@link piDefaultTools} (lock down with a custom list). An authored
144
+ /** Override tools. Defaults to {@link piAllCodingTools} (lock down with a custom list). An authored
130
145
  * `FastagentTool[]` (AgentTool plus the optional `deferred` marker) widens into {@link MountedTool}. */
131
146
  tools?: MountedTool[];
132
147
  /**
133
- * The agent's working directory: where the default tools operate AND whose ancestors are walked for
148
+ * The agent's working directory: where the coding tools operate AND whose ancestors are walked for
134
149
  * ② project context (AGENTS.md). Defaults to `dir`. Set it to the enclosing repo so a coding agent
135
150
  * whose definition lives in `dir` operates on — and reads the AGENTS.md of — that repo (core.md
136
151
  * scenario grid); that is what the CLI's opener does with the workspace.
@@ -144,12 +159,11 @@ export interface CreatePiAgentFromDefinitionOptions {
144
159
  * dir) — unlike the dir-less {@link createPiAgent}/{@link createPiModels}, which default global.
145
160
  */
146
161
  authPath?: string;
147
- sessions?: PiSessionStore;
148
- /** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. At THIS rung it does more
149
- * than root the default tools: persona.md and skills/ are read through it too. Two surfaces stay
150
- * OUTSIDE it project context (pi's loadProjectContextFiles uses node fs directly; see
151
- * definition.ts) and author-written `tools/`, which are code and can import anything. Injecting an
152
- * env narrows the blast radius rather than closing it. */
162
+ sessions?: PiSessionRecordStore;
163
+ /** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. At THIS rung it reads
164
+ * persona.md and skills/. The seven coding tools, project context (pi's
165
+ * loadProjectContextFiles uses node fs directly), and author-written `tools/` stay outside it.
166
+ * Injecting an env narrows the blast radius rather than closing it. */
153
167
  env?: ExecutionEnv;
154
168
  lease?: Lease;
155
169
  /** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
@@ -4,72 +4,97 @@
4
4
  *
5
5
  * L2 createPiAgentFromDefinition(dir, options) — load a definition directory, assemble, then L1.
6
6
  * L1 createPiAgent(options) — assemble from typed parts (the canonical ctor).
7
- * L0 createPiAgentFromHarness({ harnessFactory }) — in invoke.ts (its body is the turn mechanism).
7
+ * L0 createPiAgentFromSession({ sessionFactory }) — in invoke-session.ts (the turn mechanism).
8
8
  *
9
9
  * Above L2 sits the agent opener createPiAgentFromDir (open.ts), which both `dev` and
10
10
  * `start` drive. Each rung calls the one below; options narrow as you go up (L2 owns systemPrompt/skills —
11
11
  * they come from the definition; the openers own model/tools — from config resolution).
12
12
  */
13
13
  import { formatSkillsForSystemPrompt } from "@earendil-works/pi-agent-core";
14
- import { createBashTool, createEditTool, createReadTool, createWriteTool, } from "@earendil-works/pi-agent-core";
15
14
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
16
- import { readImageProcessor } from "./read-image.js";
15
+ import { createCodingTools, createPowerShellTool, createReadOnlyTools } from "@earendil-works/pi-coding-agent";
17
16
  import { defaultAuthPath, resolveModel } from "./config.js";
18
17
  import { resolveSecretsDir } from "../../paths.js";
19
- import { loadAgentDefinition } from "./definition.js";
20
- import { DEFAULT_THINKING_LEVEL, piHarnessFactory } from "./harness.js";
21
- import { createPiModels } from "./models.js";
18
+ import { loadAgentDefinition, loadExtensionPaths } from "./definition.js";
22
19
  import { reportFindingsIfChanged } from "./report.js";
23
- import { inMemorySessionStore } from "./sessions.js";
24
20
  import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
25
21
  import { withSearchTool } from "./search-tools.js";
26
- import { createPiAgentFromHarness, inProcessLease } from "./invoke.js";
22
+ import { createPiAgentFromSession } from "./invoke-session.js";
23
+ import { piAgentSessionFactory } from "./agent-session-factory.js";
24
+ import { DEFAULT_THINKING_LEVEL, createPiModelRuntime } from "./models.js";
25
+ import { piInMemorySessionRecordStore } from "./session-store.js";
26
+ import { inProcessLease } from "./turn-kit.js";
27
27
  // ── §1 tools ─────────────────────────────────────────────────────────────────
28
28
  //
29
- // The full pi toolset is the default for fidelity: authors vibe in local pi with it, so serving with
30
- // fewer tools is behavior drift. Locking down for public exposure = passing a restricted `tools` list
31
- // (a deployment posture).
29
+ // A directory agent gets every pi coding tool. Restricting what those tools can reach belongs at the
30
+ // sandbox boundary, not in an allowlist that leaves authored tools unrestricted. L1/L2 library callers
31
+ // can still replace the coding defaults through `tools`; deferred tools may add `search_tools`.
32
32
  //
33
- // These are pi-agent-core's tools, which reach the filesystem and the shell through the
34
- // {@link ExecutionEnv} the harness hands them per turn — NOT pi-coding-agent's, which are the same four
35
- // tools wired to `node:fs` directly. Going through the env is the point, and the whole of it: it makes
36
- // {@link CreatePiAgentOptions.env} the one seam a sandbox adapter has to implement, instead of a knob
37
- // that governed everything except the tools that actually touch the machine. (It buys no decoupling
38
- // from pi-coding-agent — definition.ts, models.ts and read-image.ts all import it regardless.)
33
+ // This is not a security boundary and no default here could be one: the built-in POST /invoke has no
34
+ // authentication, author-written `tools/` import whatever they like, and a WebSocket or Socket-Mode
35
+ // channel dials OUT, so no bind address constrains who can message the agent. Whoever can reach an
36
+ // agent can use everything it mounts put it behind something that decides who may.
39
37
  //
40
- // The swap holds only while the two behave alike, which they do NOT for free: core's `read` does
41
- // nothing with images unless a processor is injected (read-image.ts), and both families are compared
42
- // on every path in test/tools-parity.test.ts.
38
+ // All seven come from pi-coding-agent and reach the machine through `node:fs`, bypassing the per-turn
39
+ // {@link ExecutionEnv}. pi-agent-core ships env-routed look-alikes, but that seam narrows a blast
40
+ // radius rather than closing one (authored `tools/` ignore it), so a real sandbox must wrap the
41
+ // process instead.
43
42
  //
44
- // `chat` is unaffected: it takes these NAMES only and lets pi's own runtime rebuild the tools it
45
- // renders (see session-builder.ts).
46
- /** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
47
- * turn as the harness tool context — hence no argument here. */
48
- export function piDefaultTools() {
49
- // `read` needs its image pipeline INJECTED (core ships none); see read-image.ts for what is at stake.
43
+ // Chat receives the same tool objects through pi's `customTools` path; pi's builtin copies stay off.
44
+ /**
45
+ * Every pi coding tool, in canonical order, rooted at the workspace it operates in.
46
+ *
47
+ * pi ships two overlapping groupings and neither is the whole set: `createCodingTools` is the four it
48
+ * ACTIVATES for a terminal (read/bash/edit/write, no searching), `createReadOnlyTools` is
49
+ * read/grep/find/ls. `read` is in both; a directory agent mounts their union.
50
+ */
51
+ export const CODING_TOOL_NAMES = ["read", "grep", "find", "ls", "bash", "edit", "write"];
52
+ export function piAllCodingTools(cwd) {
53
+ const mutating = createCodingTools(cwd).filter((tool) => tool.name !== "read");
54
+ return [...createReadOnlyTools(cwd), ...mutating];
55
+ }
56
+ /**
57
+ * Every tool an `AgentSession` registers on its own, whether or not a directory agent mounts it.
58
+ *
59
+ * Wider than {@link CODING_TOOL_NAMES} on purpose: pi 0.84.3 added `powershell`, which the session
60
+ * registers regardless of the tools we pass. A name absent from THIS list is a name never excluded,
61
+ * and an excluded name is the only thing that cannot be activated — so leaving it out would let
62
+ * `tools: [read]` keep a reachable shell.
63
+ */
64
+ function piRegisteredToolNames(cwd) {
50
65
  return [
51
- createReadTool({ imageProcessor: readImageProcessor }),
52
- createBashTool(),
53
- createEditTool(),
54
- createWriteTool(),
66
+ ...createReadOnlyTools(cwd).map((tool) => tool.name),
67
+ ...createCodingTools(cwd).map((tool) => tool.name),
68
+ createPowerShellTool(cwd).name,
55
69
  ];
56
70
  }
57
- /** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
58
- export function resolveTools(config) {
59
- const defaults = piDefaultTools();
60
- return config.tools ? [...defaults, ...config.tools] : defaults;
71
+ /** Built-ins omitted by an explicit lower-level tool list. A reused name stays mounted. */
72
+ function omittedBuiltinNames(mounted, cwd) {
73
+ const mountedNames = new Set(mounted.map((tool) => tool.name));
74
+ return [...new Set(piRegisteredToolNames(cwd))].filter((name) => !mountedNames.has(name));
61
75
  }
62
76
  /**
63
- * The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
64
- * existing win), plus the non-default tool names and collisions to report. One source for the
77
+ * The full directory-agent tool set: all pi coding tools + `config.tools` + discovered `tools/`
78
+ * (deduped, existing win), plus the authored names and collisions to report. One source for the
65
79
  * dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
66
80
  */
67
- export async function resolveAgentTools(config, agentDir) {
68
- // Discovered `tools/` come from `agentDir` (the agent's own surface); the default coding tools carry
69
- // no root of their ownthey operate through the ExecutionEnv handed to them per turn, whose cwd is
70
- // the workspace.
81
+ export async function resolveAgentTools(config, agentDir, cwd) {
82
+ // Discovered `tools/` come from `agentDir` (the agent's own surface); the coding tools are
83
+ // rooted at `cwd`, the WORKSPACEthe project the agent works on, which is where an author expects
84
+ // `read`/`bash` to land, and which is not always the definition directory.
71
85
  const discovered = await loadTools(agentDir);
72
- const merged = mergeDiscoveredTools(resolveTools(config), discovered.tools);
86
+ const configured = piAllCodingTools(cwd);
87
+ const configuredNames = new Set(configured.map((tool) => tool.name));
88
+ const configuredCollisions = [];
89
+ for (const tool of config.tools ?? []) {
90
+ if (configuredNames.has(tool.name)) {
91
+ configuredCollisions.push({ name: tool.name, source: "config.tools" });
92
+ continue;
93
+ }
94
+ configuredNames.add(tool.name);
95
+ configured.push(tool);
96
+ }
97
+ const merged = mergeDiscoveredTools(configured, discovered.tools);
73
98
  // The built-in `search_tools` loader mounts here — the one place the agent's full tool set is
74
99
  // computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; an
75
100
  // agent-defined search_tools wins).
@@ -77,12 +102,12 @@ export async function resolveAgentTools(config, agentDir) {
77
102
  // Builtin = a search_tools that was ABSENT before withSearchTool (a reference compare would misfire
78
103
  // on the deferred-authored-loader case, where withSearchTool returns a new array without adding one).
79
104
  const builtinLoaderMounted = !merged.tools.some((t) => t.name === "search_tools") && tools.some((t) => t.name === "search_tools");
80
- const toolCollisions = [...discovered.collisions, ...merged.collisions];
81
- // `toolNames` is the AUTHOR's active-by-default surface (config.tools + tools/): exclude pi
82
- // defaults, the builtin loader (like wake, a builtin gets its own report line, not an anonymous
83
- // slot in the author's list — an author-DEFINED search_tools still shows), and deferred tools
84
- // each name lives in exactly ONE report slot, and deferred names live in `deferredToolNames`.
85
- const defaultNames = new Set(piDefaultTools().map((t) => t.name));
105
+ const toolCollisions = [...discovered.collisions, ...configuredCollisions, ...merged.collisions];
106
+ // `toolNames` is the AUTHOR's active-by-default surface (config.tools + tools/): exclude pi coding
107
+ // tools, the builtin loader (like wake, a builtin gets its own report line, not an anonymous slot in
108
+ // the author's list — an author-DEFINED search_tools still shows), and deferred tools. Each name
109
+ // lives in exactly ONE report slot, and deferred names live in `deferredToolNames`.
110
+ const defaultNames = new Set(CODING_TOOL_NAMES);
86
111
  const toolNames = tools
87
112
  .filter((t) => !defaultNames.has(t.name) && !isDeferredTool(t) && !(builtinLoaderMounted && t.name === "search_tools"))
88
113
  .map((t) => t.name);
@@ -122,9 +147,16 @@ export function piBasePrompt(options = {}) {
122
147
  const deferredCount = mounted.length - tools.length;
123
148
  const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
124
149
  // Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
125
- // (core.md §11), keeping the tools list + guidelines below.
150
+ // (core.md §11), keeping the tools list + guidelines below. Preserve pi's coding identity only for
151
+ // the full coding surface; a partial/empty surface must not claim machine capabilities it lacks.
152
+ // The four this sentence NAMES — reading, executing, editing, writing. Searching is not part of the
153
+ // claim, so requiring it would demote an agent that can do everything the identity says it can.
154
+ const mountedNames = new Set(mounted.map((tool) => tool.name));
155
+ const fullCodingSurface = ["read", "bash", "edit", "write"].every((name) => mountedNames.has(name));
126
156
  const identity = options.persona?.trim() ||
127
- "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
157
+ (fullCodingSurface
158
+ ? "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files."
159
+ : "You are an AI assistant operating inside pi, an agent harness. Help users using only the tools and context available to you.");
128
160
  const deferredNote = deferredCount > 0
129
161
  ? `\n\n${deferredCount} additional tool(s) are registered but inactive — use search_tools to discover and activate them before concluding a capability is missing.`
130
162
  : "";
@@ -164,30 +196,56 @@ export function assembleSystemPrompt(options) {
164
196
  * directory) and route through here.
165
197
  */
166
198
  function buildPiAgent(opts) {
167
- const models = createPiModels({ providers: opts.providers, authPath: opts.authPath });
168
199
  const env = opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() });
169
- // Materialized here (not defaulted inside createPiAgentFromHarness) so the exposed parts carry
170
- // the SAME lease instance the agent runs under boundary mutations must contend on it.
200
+ const cwd = opts.cwd ?? env.cwd;
201
+ // Materialized here (not defaulted inside the L0) so the exposed parts carry the SAME lease
202
+ // instance the agent runs under — boundary mutations must contend on it.
171
203
  const lease = opts.lease ?? inProcessLease();
172
- // The assembly's configured PAIR handed to the factory and to the control plane as ONE value, so
173
- // there is no wiring in which they could disagree (which levels exist depends on the model).
174
- const defaults = {
175
- model: resolveModel(models, opts.model),
176
- thinkingLevel: opts.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
204
+ const sessions = opts.sessions ?? piInMemorySessionRecordStore({ cwd });
205
+ // The model and its runtime resolve on FIRST USE: building a ModelRuntime is async while
206
+ // assembling an agent is not, so the credential read belongs on the first turn rather than in the
207
+ // caller's constructor. Memoized by the factory, and shared with the control plane below so a
208
+ // boundary mutation validates against the registry the runs actually use.
209
+ let engine;
210
+ const resolveEngine = () => {
211
+ engine ??= (async () => {
212
+ // The caller's registry when there is one — the directory rung builds it from the agent's own
213
+ // models.json, so a custom endpoint declared there is the one a turn resolves against.
214
+ const modelRuntime = opts.models ?? (await createPiModelRuntime({ authPath: opts.authPath }));
215
+ // ModelRuntime registers providers by config record, so an injected Provider INSTANCE (a
216
+ // gateway, a self-hosted endpoint, a test fake) goes in through its native seam.
217
+ for (const provider of opts.providers ?? [])
218
+ modelRuntime.registerNativeProvider(provider);
219
+ return { modelRuntime, model: resolveModel(modelRuntime, opts.model) };
220
+ })();
221
+ return engine;
177
222
  };
178
- const harnessFactory = piHarnessFactory({
179
- sessions: opts.sessions ?? inMemorySessionStore(),
180
- env,
181
- models,
182
- model: defaults.model,
223
+ const sessionFactory = piAgentSessionFactory({
224
+ sessions,
225
+ engine: resolveEngine,
183
226
  thinkingLevel: opts.thinkingLevel,
184
- systemPrompt: opts.systemPrompt,
185
227
  tools: opts.tools,
228
+ systemPrompt: opts.systemPrompt,
186
229
  skills: opts.skills,
187
230
  live: opts.live,
231
+ cwd,
232
+ ...(opts.agentDir ? { agentDir: opts.agentDir } : {}),
233
+ ...(opts.extensionPaths ? { extensionPaths: opts.extensionPaths } : {}),
234
+ // `noTools: "builtin"` leaves pi's built-ins in the registry; a lower-level replacement must also
235
+ // deny every omitted coding name so a loader cannot reactivate one later.
236
+ excludedToolNames: omittedBuiltinNames(opts.tools ?? [], cwd),
237
+ env,
238
+ });
239
+ opts.onAssembly?.({
240
+ lease,
241
+ sessionFactory,
242
+ // The control plane needs the registry and the configured pair as VALUES, and both only exist
243
+ // after the first credential read. Asking for them lazily keeps assembly synchronous without
244
+ // making the hub wait on a runtime it may never need (a control-less deployment never calls it).
245
+ engine: resolveEngine,
246
+ thinkingLevel: opts.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
188
247
  });
189
- opts.onAssembly?.({ models, harnessFactory, lease, defaults });
190
- return createPiAgentFromHarness({ lease, observer: opts.observer, cwd: env.cwd, harnessFactory });
248
+ return createPiAgentFromSession({ lease, observer: opts.observer, sessionFactory });
191
249
  }
192
250
  /**
193
251
  * L1 system prompt: `instructions` ARE the prompt (no engine base, no wrapping); the skills listing
@@ -232,22 +290,32 @@ export async function createPiAgentFromDefinition(dir, options) {
232
290
  const env = options.env ?? new NodeExecutionEnv({ cwd });
233
291
  // Boot-time load: fail-visibly at startup on a broken directory, and give callers the snapshot to
234
292
  // report (skills/diagnostics/collisions). Serving does NOT close over it — see `live` below.
235
- const definition = await loadAgentDefinition(dir, { cwd: env.cwd, env });
293
+ // `cwd`, not `env.cwd`, for the same reason as the tools below: `cwd` is the run root whose
294
+ // ancestors carry ② project context. Reading it off the env pointed the AGENTS.md walk at the
295
+ // loader's directory whenever a caller supplied both.
296
+ const definition = await loadAgentDefinition(dir, { cwd, env });
297
+ // Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
298
+ // it; a caller's own search_tools wins).
299
+ // `cwd`, not `env.cwd`: the workspace is what this option MEANS, and a caller may hand a custom env
300
+ // for definition loading whose root is a different directory. Taking it off the env would silently
301
+ // point the coding tools at the loader's directory.
302
+ const tools = withSearchTool(options.tools ?? piAllCodingTools(cwd));
236
303
  // Boot findings go through the SAME memoized reporter every later reader uses (report.ts, keyed by
237
304
  // the resolved dir): announced once here, and re-announced by a turn or by the control plane's
238
305
  // command list only when the set CHANGES — a runtime-written bad skill surfaces the moment it
239
306
  // appears, a static one does not spam. Log dedup, not session state (stateless invoke holds).
240
307
  reportFindingsIfChanged(definition.dir, definition);
241
- // Deferred tools need their loader on every rung (idempotent the workspace opener already applied
242
- // it; a caller's own search_tools wins).
243
- const tools = withSearchTool(options.tools ?? piDefaultTools());
308
+ // Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
309
+ // passes an explicit authPath, so this only affects direct L2 callers).
310
+ const authPath = options.authPath ?? defaultAuthPath(resolveSecretsDir(dir));
244
311
  const agent = buildPiAgent({
245
312
  model: options.model,
246
313
  thinkingLevel: options.thinkingLevel,
247
- providers: options.providers,
248
- // Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
249
- // passes an explicit authPath, so this only affects direct L2 callers).
250
- authPath: options.authPath ?? defaultAuthPath(resolveSecretsDir(dir)),
314
+ // THE directory rung's model surface: built-ins + the agent's own models.json (custom endpoints,
315
+ // which are definition data and travel with the artifact) + any injected Provider instance. This
316
+ // is what makes `dev`/`start`/`invoke` and an embedded L2 caller resolve the same specs.
317
+ models: await createPiModelRuntime({ agentDir: dir, authPath, providers: options.providers }),
318
+ authPath,
251
319
  // The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills
252
320
  // edits (the author's, or the agent's own self-modification) take effect on the next turn with
253
321
  // no process restart — restarts are reserved for code (tools/channels/config, module cache).
@@ -259,7 +327,7 @@ export async function createPiAgentFromDefinition(dir, options) {
259
327
  // not silently vanish from the agent, and a static one must not spam every turn's log. The
260
328
  // next good edit heals both.
261
329
  live: async () => {
262
- const def = await loadAgentDefinition(dir, { cwd: env.cwd, env });
330
+ const def = await loadAgentDefinition(dir, { cwd, env });
263
331
  reportFindingsIfChanged(def.dir, def);
264
332
  return {
265
333
  systemPrompt: assembleSystemPrompt({
@@ -269,13 +337,20 @@ export async function createPiAgentFromDefinition(dir, options) {
269
337
  // ② project context: AGENTS.md files (agentDir + cwd-ancestor walk) via loadProjectContextFiles.
270
338
  contextFiles: def.contextFiles,
271
339
  skills: def.skills,
272
- cwd: env.cwd,
340
+ // The workspace, matching where the tools operate. Telling the model one directory while
341
+ // `read`/`bash` resolve relative paths against another is a lie it cannot detect.
342
+ cwd,
273
343
  }),
274
344
  skills: def.skills,
275
345
  };
276
346
  },
277
347
  tools,
278
348
  sessions: options.sessions,
349
+ // Discovered so the serving assembly can WARN that it does not run them (and so the refusals
350
+ // apply to the artifact either way) — `chat` is where they load. Boot-resolved: the set cannot
351
+ // change without a restart, which is why this sits outside `live` above.
352
+ extensionPaths: await loadExtensionPaths(dir, { cwd, env }),
353
+ cwd,
279
354
  env,
280
355
  lease: options.lease,
281
356
  observer: options.observer,
@@ -41,6 +41,47 @@ export interface LoadAgentDefinitionOptions {
41
41
  }
42
42
  /** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
43
43
  export declare function loadAgentDefinition(agentDir: string, options?: LoadAgentDefinitionOptions): Promise<LoadedDefinition>;
44
+ /**
45
+ * Extension entry-point FILES under `<agentDir>/extensions/`, empty when there are none.
46
+ *
47
+ * Paths, not loaded objects — unlike skills (data: content inline, serializable), an extension is
48
+ * CODE that pi loads with jiti and binds to its own eventBus/runtime, so loading it here would
49
+ * reimplement pi's loader. The engine binding hands these to pi as `additionalExtensionPaths`, which
50
+ * survives `noExtensions: true` — that flag suppresses MACHINE-GLOBAL discovery (`~/.pi`), and the
51
+ * definition's own extensions were only ever collateral to it. FILES, not the directory: pi's paths
52
+ * are module specifiers, and a directory fails as `Cannot find module` into a
53
+ * `LoadExtensionsResult.errors` entry nothing reads.
54
+ *
55
+ * SEPARATE from {@link loadAgentDefinition} on purpose. Prompt and skills are re-read every invoke
56
+ * ("the directory is the agent, LIVE"); scanning for extension entry points is not, since the set
57
+ * cannot change without a restart. Called once per assembly, like `tools/`.
58
+ *
59
+ * Discovery runs on BOTH paths; loading does not. `chat` hands these to pi and runs them fully.
60
+ * Serving only announces them — pi's extension runtime is shared across sessions, and serving has
61
+ * concurrent turns for unrelated conversations (see `PiAgentSessionFactoryOptions.extensionPaths`).
62
+ * The refusals below are therefore about what the ARTIFACT may contain, and hold for both.
63
+ *
64
+ * Discovery follows pi's own rules, so an extension that works in pi works here:
65
+ *
66
+ * 1. a direct `*.ts` / `*.js` file;
67
+ * 2. a subdirectory with `index.ts` / `index.js`.
68
+ *
69
+ * pi has a third rule — a subdirectory whose `package.json` declares a `pi` field — which is NOT
70
+ * implemented here. That shape is reported rather than skipped: a definition whose extension silently
71
+ * fails to load is the failure mode this whole path exists to remove.
72
+ *
73
+ * Containment matches skills/tools/channels/schedules (the fifth of five surfaces): a symlinked
74
+ * `extensions/` escaping the agent dir is refused, and — like `loadModuleDir`, whose `entry.isFile()`
75
+ * excludes them — a symlinked ENTRY is not loaded either. pi's own discovery does follow those, but
76
+ * an extension reached through a link out of the definition is code the artifact does not carry: it
77
+ * resolves on the authoring machine and is missing in the container. Refused loudly, never silently.
78
+ *
79
+ * Absent is normal and silent; a FILE at that path is not — that is an author who meant something.
80
+ */
81
+ export declare function loadExtensionPaths(agentDir: string, options?: {
82
+ cwd?: string;
83
+ env?: ExecutionEnv;
84
+ }): Promise<string[]>;
44
85
  /**
45
86
  * The definition's skills ALONE, resolved the same way `loadAgentDefinition` resolves them (same
46
87
  * loader, same containment guard, same first-wins collision rule) — for readers that need only the
@@ -56,13 +97,6 @@ export declare function loadAgentSkills(agentDir: string, options?: {
56
97
  collisions: SkillCollision[];
57
98
  dir: string;
58
99
  }>;
59
- /**
60
- * Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
61
- * this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
62
- * dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
63
- * does not act on where a user's paths point.
64
- */
65
- export declare function isUnderDir(targetPath: string, baseDir: string): boolean;
66
100
  /** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
67
101
  * A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
68
102
  export declare function canonicalPath(p: string): string;