@fastagent-sh/fastagent 0.20.0 → 0.21.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 (155) hide show
  1. package/README.md +5 -1
  2. package/dist/channels/agentcore-protocol.d.ts +112 -0
  3. package/dist/channels/agentcore-protocol.js +22 -0
  4. package/dist/channels/agentcore-service.d.ts +0 -4
  5. package/dist/channels/agentcore-service.js +1 -6
  6. package/dist/channels/agentcore-state.d.ts +5 -11
  7. package/dist/channels/agentcore-state.js +4 -1
  8. package/dist/channels/agentcore.d.ts +1 -67
  9. package/dist/channels/agentcore.js +90 -89
  10. package/dist/channels/control.d.ts +1 -1
  11. package/dist/channels/control.js +13 -62
  12. package/dist/channels/discover.d.ts +0 -1
  13. package/dist/channels/discover.js +1 -3
  14. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  15. package/dist/channels/feishu/context-buffer.js +0 -38
  16. package/dist/channels/feishu/crypto.d.ts +0 -2
  17. package/dist/channels/feishu/crypto.js +3 -8
  18. package/dist/channels/feishu/feishu-api.js +2 -4
  19. package/dist/channels/feishu/feishu.js +56 -115
  20. package/dist/channels/feishu/parse.d.ts +4 -3
  21. package/dist/channels/feishu/parse.js +5 -4
  22. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  23. package/dist/channels/feishu/register-webhook.js +43 -57
  24. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  25. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  26. package/dist/channels/feishu/setup-mode.js +26 -0
  27. package/dist/channels/feishu/shared-api.d.ts +10 -0
  28. package/dist/channels/feishu/shared-api.js +38 -0
  29. package/dist/channels/http.d.ts +0 -8
  30. package/dist/channels/http.js +4 -56
  31. package/dist/channels/kit/turn-runner.d.ts +59 -0
  32. package/dist/channels/kit/turn-runner.js +84 -0
  33. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  34. package/dist/channels/registration.d.ts +36 -1
  35. package/dist/channels/registration.js +57 -1
  36. package/dist/channels/secret.d.ts +1 -0
  37. package/dist/channels/secret.js +16 -0
  38. package/dist/channels/slack/config-api.d.ts +35 -3
  39. package/dist/channels/slack/config-api.js +51 -9
  40. package/dist/channels/slack/manifest.js +5 -1
  41. package/dist/channels/slack/onboard.d.ts +5 -5
  42. package/dist/channels/slack/onboard.js +41 -20
  43. package/dist/channels/slack/register-webhook.d.ts +3 -2
  44. package/dist/channels/slack/register-webhook.js +34 -14
  45. package/dist/channels/slack/scaffold/channel.ts +3 -10
  46. package/dist/channels/slack/scaffold/slack-send.ts +18 -126
  47. package/dist/channels/slack/shared-api.d.ts +10 -0
  48. package/dist/channels/slack/shared-api.js +34 -0
  49. package/dist/channels/slack/slack-api.d.ts +20 -2
  50. package/dist/channels/slack/slack-api.js +81 -22
  51. package/dist/channels/slack/slack.d.ts +0 -10
  52. package/dist/channels/slack/slack.js +60 -99
  53. package/dist/channels/sse.d.ts +4 -0
  54. package/dist/channels/sse.js +66 -0
  55. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  56. package/dist/channels/telegram/register-webhook.js +44 -42
  57. package/dist/channels/telegram/telegram.js +47 -142
  58. package/dist/channels/wait-health.js +7 -4
  59. package/dist/cli/add-feishu.js +3 -10
  60. package/dist/cli/add-slack.js +7 -20
  61. package/dist/cli/commands/add.d.ts +0 -1
  62. package/dist/cli/commands/add.js +22 -31
  63. package/dist/cli/commands/chat.js +6 -12
  64. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  65. package/dist/cli/commands/deploy/agentcore.js +178 -0
  66. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  67. package/dist/cli/commands/deploy/docker.js +119 -0
  68. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  69. package/dist/cli/commands/deploy/fly.js +131 -0
  70. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  71. package/dist/cli/commands/deploy/railway.js +71 -0
  72. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  73. package/dist/cli/commands/deploy/shared.js +124 -0
  74. package/dist/cli/commands/deploy.d.ts +17 -49
  75. package/dist/cli/commands/deploy.js +39 -618
  76. package/dist/cli/commands/dev.js +17 -46
  77. package/dist/cli/commands/fire.js +6 -12
  78. package/dist/cli/commands/info.js +2 -1
  79. package/dist/cli/commands/invoke.js +4 -11
  80. package/dist/cli/commands/schedule.js +1 -1
  81. package/dist/cli/commands/start.js +17 -46
  82. package/dist/cli/commands/tool.js +3 -8
  83. package/dist/cli/kernel.d.ts +0 -2
  84. package/dist/cli/kernel.js +0 -2
  85. package/dist/cli/program.js +7 -12
  86. package/dist/cli/serve.d.ts +36 -19
  87. package/dist/cli/serve.js +91 -7
  88. package/dist/cli/shared.d.ts +13 -18
  89. package/dist/cli/shared.js +20 -4
  90. package/dist/deploy/agentcore/forwarder.js +250 -0
  91. package/dist/deploy/agentcore/plan.d.ts +23 -2
  92. package/dist/deploy/agentcore/plan.js +29 -244
  93. package/dist/deploy/agentcore/run.d.ts +4 -20
  94. package/dist/deploy/agentcore/run.js +22 -7
  95. package/dist/deploy/channel-ingress.js +1 -1
  96. package/dist/deploy/docker/run.d.ts +17 -3
  97. package/dist/deploy/docker/run.js +30 -8
  98. package/dist/deploy/fly/plan.js +7 -0
  99. package/dist/deploy/fly/run.d.ts +27 -0
  100. package/dist/deploy/fly/run.js +102 -20
  101. package/dist/deploy/hosts.d.ts +5 -0
  102. package/dist/deploy/hosts.js +4 -0
  103. package/dist/deploy/preflight.js +3 -3
  104. package/dist/deploy/railway/plan.d.ts +5 -0
  105. package/dist/deploy/railway/plan.js +7 -0
  106. package/dist/deploy/railway/run.d.ts +2 -1
  107. package/dist/deploy/railway/run.js +5 -4
  108. package/dist/deploy/secrets.js +0 -17
  109. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  110. package/dist/engines/pi/agent-session-factory.js +109 -85
  111. package/dist/engines/pi/config.d.ts +1 -1
  112. package/dist/engines/pi/create.d.ts +22 -39
  113. package/dist/engines/pi/create.js +58 -73
  114. package/dist/engines/pi/definition.js +8 -8
  115. package/dist/engines/pi/invoke-session.js +13 -30
  116. package/dist/engines/pi/open.d.ts +2 -2
  117. package/dist/engines/pi/open.js +31 -41
  118. package/dist/engines/pi/retry-event.d.ts +6 -0
  119. package/dist/engines/pi/retry-event.js +15 -0
  120. package/dist/engines/pi/session-builder.js +26 -113
  121. package/dist/engines/pi/session-control.d.ts +10 -18
  122. package/dist/engines/pi/session-control.js +47 -81
  123. package/dist/engines/pi/session-settings.d.ts +1 -1
  124. package/dist/engines/pi/session-settings.js +1 -1
  125. package/dist/engines/pi/session-store.d.ts +12 -17
  126. package/dist/engines/pi/session-store.js +18 -40
  127. package/dist/engines/pi/tool-context.d.ts +3 -3
  128. package/dist/engines/pi/tool.d.ts +6 -9
  129. package/dist/engines/pi/tool.js +1 -0
  130. package/dist/feishu.d.ts +1 -0
  131. package/dist/feishu.js +1 -0
  132. package/dist/lark.d.ts +1 -0
  133. package/dist/lark.js +1 -0
  134. package/dist/loader.d.ts +2 -0
  135. package/dist/loader.js +5 -0
  136. package/dist/log.d.ts +9 -17
  137. package/dist/log.js +25 -30
  138. package/dist/paths.d.ts +7 -3
  139. package/dist/paths.js +17 -4
  140. package/dist/scaffold/add-channel.d.ts +6 -1
  141. package/dist/scaffold/add-channel.js +48 -65
  142. package/dist/schedule/wake-alarm.d.ts +1 -12
  143. package/dist/schedule/wake-alarm.js +2 -3
  144. package/dist/service.d.ts +23 -22
  145. package/dist/service.js +7 -73
  146. package/dist/session-remote.d.ts +5 -5
  147. package/dist/session-remote.js +25 -28
  148. package/dist/session.d.ts +1 -1
  149. package/dist/slack.d.ts +2 -0
  150. package/dist/slack.js +1 -0
  151. package/dist/tunnel.d.ts +16 -6
  152. package/dist/tunnel.js +53 -12
  153. package/package.json +5 -4
  154. package/dist/channels/slack/bot-auth.d.ts +0 -15
  155. package/dist/channels/slack/bot-auth.js +0 -135
@@ -29,7 +29,7 @@ export interface FastagentConfig {
29
29
  * per-boot bearer token and write `<stateRoot>/control.json` for local discovery. The serve
30
30
  * binds all interfaces by default, so the routes are LAN-reachable with the token as the only
31
31
  * protection — bind loopback (`--bind 127.0.0.1`; not `http.host`, which travels into a deployed
32
- * image), firewall the port, or wrap it for real exposure (design §14).
32
+ * image), firewall the port, or wrap it for real exposure (docs/design/session-control.md §14).
33
33
  */
34
34
  sessionControl?: boolean;
35
35
  /** Deploy-time declarations for what the agent needs on the box, so real agents don't hand-write a
@@ -1,16 +1,4 @@
1
- /**
2
- * Agent assembly (configuration-time): the engine assets (tools, prompt) plus the reusable ladder
3
- * that puts a pi agent together.
4
- *
5
- * L2 createPiAgentFromDefinition(dir, options) — load a definition directory, assemble, then L1.
6
- * L1 createPiAgent(options) — assemble from typed parts (the canonical ctor).
7
- * L0 createPiAgentFromSession({ sessionFactory }) — in invoke-session.ts (the turn mechanism).
8
- *
9
- * Above L2 sits the agent opener createPiAgentFromDir (open.ts), which both `dev` and
10
- * `start` drive. Each rung calls the one below; options narrow as you go up (L2 owns systemPrompt/skills —
11
- * they come from the definition; the openers own model/tools — from config resolution).
12
- */
13
- import { type ExecutionEnv, type Skill, type ThinkingLevel } from "@earendil-works/pi-agent-core";
1
+ import type { ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
14
2
  import { type ModelRuntime } from "@earendil-works/pi-coding-agent";
15
3
  import type { Provider } from "@earendil-works/pi-ai";
16
4
  import type { Agent } from "../../agent.ts";
@@ -66,19 +54,14 @@ export interface AssembleSystemPromptOptions {
66
54
  path: string;
67
55
  content: string;
68
56
  }>;
69
- /** ③ Skills for the <available_skills> listing. */
70
- skills?: Skill[];
71
- /** ④ Env context, caller-provided (keeps this function pure). Omitted = segment omitted. */
72
- cwd?: string;
73
57
  }
74
58
  export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
75
59
  /**
76
- * INTERNAL seam (workspace assembly): hands the hub-wiring consumer the assembly's live parts
77
- * the SAME session factory and lease the agent runs with, plus the model registry behind a thunk —
78
- * so boundary mutations (session-control.ts) contend on the real lease and validate against the real
79
- * registry. Called synchronously, exactly once, before the agent is returned. Not public surface.
60
+ * The assembly, as a value: what every rung builds and what the agent runs on. The opener hands it to
61
+ * the control plane too boundary mutations contend on the SAME lease and validate against the SAME
62
+ * registry the runs use, which is only true if there is one of each to hand over.
80
63
  */
81
- export type PiAssemblyParts = {
64
+ export interface PiAssembly {
82
65
  lease: Lease;
83
66
  sessionFactory: PiAgentSessionFactory;
84
67
  /** The registry and configured model, resolved on first use (a credential read is async). */
@@ -88,8 +71,9 @@ export type PiAssemblyParts = {
88
71
  }>;
89
72
  /** The configured reasoning effort — the other half of the pair a session without overrides runs on. */
90
73
  thinkingLevel: ThinkingLevel;
91
- };
92
- type OnAssembly = (parts: PiAssemblyParts) => void;
74
+ }
75
+ /** The agent an assembly runs as: the L0 over its lease and session factory. */
76
+ export declare function agentOf(assembly: PiAssembly, observer?: SessionObserver): Agent;
93
77
  /** L1 options. Tier 1: model (spec) + instructions + tools. Tier 2: the injectable ports. */
94
78
  export interface CreatePiAgentOptions {
95
79
  /** Model spec "provider/modelId" (e.g. "openai-codex/gpt-5.5"), resolved against {@link models}. */
@@ -99,16 +83,15 @@ export interface CreatePiAgentOptions {
99
83
  /**
100
84
  * The system prompt itself — no engine base and no wrapping (unlike the directory path, which
101
85
  * assembles the engine base + AGENTS.md as segment ② + persona.md as segment ①). A plain string or
102
- * a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
86
+ * a factory evaluated once per invoke, never during construction. Pi appends the skills listing
87
+ * when read is active.
103
88
  *
104
89
  * Not byte-for-byte verbatim: pi appends its own `Current working directory:` line to whatever
105
90
  * prompt it is given. What this rung guarantees is that no engine IDENTITY is imposed — a
106
91
  * hand-built agent is not told it is a coding assistant.
107
92
  */
108
93
  instructions?: string | (() => string);
109
- /** The tool set to mount. `FastagentTool` (AgentTool plus the optional `deferred` marker, see
110
- * {@link DefineToolOptions}) widens into {@link MountedTool}, which additionally admits pi's default
111
- * coding tools — those bind their workspace at construction rather than reading a turn context. */
94
+ /** The tool set to mount: authored tools or pi's cwd-bound coding tools, both AgentTool. */
112
95
  tools?: MountedTool[];
113
96
  skills?: Skill[];
114
97
  /**
@@ -126,12 +109,9 @@ export interface CreatePiAgentOptions {
126
109
  /** Session persistence. Defaults to in-memory; inject piSessionRecordStore for restart-surviving
127
110
  * continuity. */
128
111
  sessions?: PiSessionRecordStore;
129
- /** Filesystem/process environment, handed to tools that read one as the turn's context. Defaults to
130
- * a local NodeExecutionEnv at `process.cwd()`. At THIS rung nothing else consumes it: L1 loads no
131
- * definition. It does NOT constrain the coding tools (pi's own, rooted at the workspace they were
132
- * built for) or author-written `tools/`, which are code and can import anything. Not a
133
- * sandbox — see {@link createPiAgentFromDefinition} for the rung where it also reads the
134
- * definition. */
112
+ /** Supplies the working directory at L1 (default: process.cwd()), which loads no definition.
113
+ * At L2 it also reads persona.md and skills/.
114
+ * Tools and project-context discovery use the local process directly; this is not a sandbox. */
135
115
  env?: ExecutionEnv;
136
116
  /** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
137
117
  lease?: Lease;
@@ -180,15 +160,18 @@ export interface CreatePiAgentFromDefinitionOptions {
180
160
  lease?: Lease;
181
161
  /** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
182
162
  observer?: SessionObserver;
183
- /** INTERNAL seam for hub wiring; see {@link OnAssembly}. */
184
- onAssembly?: OnAssembly;
185
163
  }
186
164
  /**
187
- * L2: "point at a directory → agent": load + assemble (base + AGENTS.md + skills + env) + L1 in one
188
- * call. Returns the definition so callers can surface diagnostics/collisions.
165
+ * L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. Returns the
166
+ * definition so callers can surface diagnostics/collisions. The opener consumes this form because
167
+ * the control plane needs the parts; {@link createPiAgentFromDefinition} is it plus the L0.
189
168
  */
169
+ export declare function assemblePiFromDefinition(dir: string, options: Omit<CreatePiAgentFromDefinitionOptions, "observer">): Promise<{
170
+ assembly: PiAssembly;
171
+ definition: LoadedDefinition;
172
+ }>;
173
+ /** L2: "point at a directory → agent": {@link assemblePiFromDefinition} under the L0. */
190
174
  export declare function createPiAgentFromDefinition(dir: string, options: CreatePiAgentFromDefinitionOptions): Promise<{
191
175
  agent: Agent;
192
176
  definition: LoadedDefinition;
193
177
  }>;
194
- export {};
@@ -9,15 +9,20 @@
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
+ *
13
+ * Every rung assembles the same VALUE first — a {@link PiAssembly}: the lease, the session factory
14
+ * and the engine thunk — and the agent is that value under the L0. The opener needs the value
15
+ * itself (the control plane contends on the same lease and validates against the same registry), so
16
+ * `assemblePiFromDefinition` hands it out and `createPiAgentFromDefinition` is it plus the L0.
12
17
  */
13
- import { formatSkillsForSystemPrompt, } from "@earendil-works/pi-agent-core";
18
+ import { toUSVString } from "node:util";
14
19
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
15
20
  import { createCodingTools, createPowerShellTool, createReadOnlyTools, } from "@earendil-works/pi-coding-agent";
16
21
  import { defaultAuthPath, resolveModel } from "./config.js";
17
22
  import { resolveSecretsDir } from "../../paths.js";
18
23
  import { loadAgentDefinition, loadExtensionPaths } from "./definition.js";
19
24
  import { reportFindingsIfChanged } from "./report.js";
20
- import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
25
+ import { isDeferredTool, loadTools, mergeDiscoveredTools } from "./tool.js";
21
26
  import { withSearchTool } from "./search-tools.js";
22
27
  import { createPiAgentFromSession } from "./invoke-session.js";
23
28
  import { piAgentSessionFactory } from "./agent-session-factory.js";
@@ -50,7 +55,21 @@ import { inProcessLease } from "./turn-kit.js";
50
55
  */
51
56
  export const CODING_TOOL_NAMES = ["read", "grep", "find", "ls", "bash", "edit", "write"];
52
57
  export function piAllCodingTools(cwd) {
53
- const mutating = createCodingTools(cwd).filter((tool) => tool.name !== "read");
58
+ const mutating = createCodingTools(cwd, {
59
+ bash: {
60
+ spawnHook(context) {
61
+ const { env } = context;
62
+ const id = env.PI_SESSION_ID;
63
+ delete env.PI_SESSION_ID_ENCODING;
64
+ // OS environment strings cannot preserve NUL or unpaired UTF-16 surrogates.
65
+ if (id !== undefined && (id.includes("\u0000") || toUSVString(id) !== id)) {
66
+ env.PI_SESSION_ID = JSON.stringify(id);
67
+ env.PI_SESSION_ID_ENCODING = "json";
68
+ }
69
+ return context;
70
+ },
71
+ },
72
+ }).filter((tool) => tool.name !== "read");
54
73
  return [...createReadOnlyTools(cwd), ...mutating];
55
74
  }
56
75
  /**
@@ -119,18 +138,7 @@ export async function resolveAgentTools(config, agentDir, cwd) {
119
138
  toolFailures: discovered.failures,
120
139
  };
121
140
  }
122
- // ── §2 prompt: four-segment systemPrompt assembly ───────────────────────────
123
- //
124
- // systemPrompt = ① base (engine asset; a persona.md persona overrides its identity line)
125
- // + ② project context (AGENTS.md files via pi's loadProjectContextFiles, <project_context>-wrapped)
126
- // + ③ skills listing + ④ env context (cwd)
127
- //
128
- // AGENTS.md ≠ system prompt. Pure functions: segment ④ input (cwd) is caller-provided, so the
129
- // same inputs always produce the same prompt (testable, reproducible). No date: a date line would
130
- // invalidate the provider prompt cache (a prefix cache) for every session at each day boundary —
131
- // channel sessions routinely live for weeks (pi ≥0.80.7 dropped it from its default prompt for the
132
- // same reason). The model gets the date when it needs it: `bash date`, and the wake tool takes
133
- // relative delays ("30m") / cron — never an absolute now-derived instant.
141
+ // Fastagent owns identity and project context; Pi appends skills and cwd for both serving and chat.
134
142
  /**
135
143
  * The pi engine's base prompt (segment ①), mirroring pi-coding-agent's default path with two
136
144
  * deviations: the pi-TUI docs section is dropped (those paths don't exist in deployments), and the
@@ -147,7 +155,7 @@ export function piBasePrompt(options = {}) {
147
155
  const deferredCount = mounted.length - tools.length;
148
156
  const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
149
157
  // Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
150
- // (core.md §11), keeping the tools list + guidelines below. Preserve pi's coding identity only for
158
+ // (core.md §2), keeping the tools list + guidelines below. Preserve pi's coding identity only for
151
159
  // the full coding surface; a partial/empty surface must not claim machine capabilities it lacks.
152
160
  // The four this sentence NAMES — reading, executing, editing, writing. Searching is not part of the
153
161
  // claim, so requiring it would demote an agent that can do everything the identity says it can.
@@ -182,30 +190,23 @@ export function assembleSystemPrompt(options) {
182
190
  }
183
191
  prompt += `</project_context>\n`;
184
192
  }
185
- if (options.skills && options.skills.length > 0) {
186
- prompt += `\n${formatSkillsForSystemPrompt(options.skills)}\n`;
187
- }
188
- if (options.cwd)
189
- prompt += `\nCurrent working directory: ${options.cwd}`;
190
193
  return prompt;
191
194
  }
192
- // ── §3 the reusable assembly ladder: L1 / L2 ────────────────────────────────
193
195
  /**
194
196
  * Shared low-level wiring: resolve the model spec against the collection, default the K ports, build
195
- * the agent. Internal — the public rungs decide the systemPrompt (L1 from instructions, L2 from the
197
+ * the parts. Internal — the public rungs decide the systemPrompt (L1 from instructions, L2 from the
196
198
  * directory) and route through here.
197
199
  */
198
- function buildPiAgent(opts) {
199
- const env = opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() });
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.
200
+ function assemblePi(opts) {
201
+ const cwd = opts.cwd ?? opts.env?.cwd ?? process.cwd();
202
+ // Materialized here (not defaulted inside the L0) so the value carries the SAME lease instance the
203
+ // agent runs under boundary mutations must contend on it.
203
204
  const lease = opts.lease ?? inProcessLease();
204
205
  const sessions = opts.sessions ?? piInMemorySessionRecordStore({ cwd });
205
206
  // The model and its runtime resolve on FIRST USE: building a ModelRuntime is async while
206
207
  // 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.
208
+ // caller's constructor. Memoized, and shared with the control plane so a boundary mutation
209
+ // validates against the registry the runs actually use.
209
210
  let engine;
210
211
  const resolveEngine = () => {
211
212
  engine ??= (async () => {
@@ -225,71 +226,56 @@ function buildPiAgent(opts) {
225
226
  engine: resolveEngine,
226
227
  thinkingLevel: opts.thinkingLevel,
227
228
  tools: opts.tools,
228
- systemPrompt: opts.systemPrompt,
229
- skills: opts.skills,
230
- live: opts.live,
229
+ readDefinition: opts.readDefinition,
231
230
  cwd,
232
231
  ...(opts.agentDir ? { agentDir: opts.agentDir } : {}),
233
232
  ...(opts.extensionPaths ? { extensionPaths: opts.extensionPaths } : {}),
234
233
  // `noTools: "builtin"` leaves pi's built-ins in the registry; a lower-level replacement must also
235
234
  // deny every omitted coding name so a loader cannot reactivate one later.
236
235
  excludedToolNames: omittedBuiltinNames(opts.tools ?? [], cwd),
237
- env,
238
236
  });
239
- opts.onAssembly?.({
237
+ return {
240
238
  lease,
241
239
  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
240
  engine: resolveEngine,
246
241
  thinkingLevel: opts.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
247
- });
248
- return createPiAgentFromSession({ lease, observer: opts.observer, sessionFactory });
249
- }
250
- /**
251
- * L1 system prompt: `instructions` ARE the prompt (no engine base, no wrapping); the skills listing
252
- * is appended only when skills are mounted (the model must know what it can invoke). A factory so a
253
- * dynamic `instructions` and per-invoke freshness both work; undefined when there is nothing to send.
254
- */
255
- function instructionsPrompt(instructions, skills) {
256
- const hasSkills = skills !== undefined && skills.length > 0;
257
- if (instructions === undefined && !hasSkills)
258
- return undefined;
259
- return () => {
260
- const prose = typeof instructions === "function" ? instructions() : (instructions ?? "");
261
- const listing = hasSkills ? formatSkillsForSystemPrompt(skills) : "";
262
- return [prose, listing].filter((s) => s !== "").join("\n");
263
242
  };
264
243
  }
244
+ /** The agent an assembly runs as: the L0 over its lease and session factory. */
245
+ export function agentOf(assembly, observer) {
246
+ return createPiAgentFromSession({ lease: assembly.lease, observer, sessionFactory: assembly.sessionFactory });
247
+ }
265
248
  /** L1: assemble from typed parts. */
266
249
  export function createPiAgent(options) {
267
- return buildPiAgent({
250
+ const { instructions, skills = [] } = options;
251
+ return agentOf(assemblePi({
268
252
  model: options.model,
269
253
  thinkingLevel: options.thinkingLevel,
270
254
  providers: options.providers,
271
255
  authPath: options.authPath,
272
- systemPrompt: instructionsPrompt(options.instructions, options.skills),
256
+ readDefinition: () => ({
257
+ systemPrompt: typeof instructions === "function" ? instructions() : instructions,
258
+ skills,
259
+ }),
273
260
  // Deferred tools need their loader on every rung (idempotent; the caller's own search_tools wins).
274
261
  tools: options.tools ? withSearchTool(options.tools) : options.tools,
275
- skills: options.skills,
276
262
  sessions: options.sessions,
277
263
  env: options.env,
278
264
  lease: options.lease,
279
- observer: options.observer,
280
- });
265
+ }), options.observer);
281
266
  }
282
267
  /**
283
- * L2: "point at a directory → agent": load + assemble (base + AGENTS.md + skills + env) + L1 in one
284
- * call. Returns the definition so callers can surface diagnostics/collisions.
268
+ * L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. Returns the
269
+ * definition so callers can surface diagnostics/collisions. The opener consumes this form because
270
+ * the control plane needs the parts; {@link createPiAgentFromDefinition} is it plus the L0.
285
271
  */
286
- export async function createPiAgentFromDefinition(dir, options) {
272
+ export async function assemblePiFromDefinition(dir, options) {
287
273
  // `dir` = the agent-definition dir (persona.md/skills/); `cwd` (default = dir) is the run root where
288
274
  // tools operate and whose ancestors are walked for ② context.
289
275
  const cwd = options.cwd ?? dir;
290
276
  const env = options.env ?? new NodeExecutionEnv({ cwd });
291
277
  // Boot-time load: fail-visibly at startup on a broken directory, and give callers the snapshot to
292
- // report (skills/diagnostics/collisions). Serving does NOT close over it see `live` below.
278
+ // report (skills/diagnostics/collisions). Serving re-reads it through readDefinition below.
293
279
  // `cwd`, not `env.cwd`, for the same reason as the tools below: `cwd` is the run root whose
294
280
  // ancestors carry ② project context. Reading it off the env pointed the AGENTS.md walk at the
295
281
  // loader's directory whenever a caller supplied both.
@@ -308,7 +294,7 @@ export async function createPiAgentFromDefinition(dir, options) {
308
294
  // Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
309
295
  // passes an explicit authPath, so this only affects direct L2 callers).
310
296
  const authPath = options.authPath ?? defaultAuthPath(resolveSecretsDir(dir));
311
- const agent = buildPiAgent({
297
+ const assembly = assemblePi({
312
298
  model: options.model,
313
299
  thinkingLevel: options.thinkingLevel,
314
300
  // THE directory rung's model surface: built-ins + the agent's own models.json (custom endpoints,
@@ -326,7 +312,7 @@ export async function createPiAgentFromDefinition(dir, options) {
326
312
  // the finding set changes (boot findings are the baseline) — a runtime-written bad skill must
327
313
  // not silently vanish from the agent, and a static one must not spam every turn's log. The
328
314
  // next good edit heals both.
329
- live: async () => {
315
+ readDefinition: async () => {
330
316
  const def = await loadAgentDefinition(dir, { cwd, env });
331
317
  reportFindingsIfChanged(def.dir, def);
332
318
  return {
@@ -336,10 +322,6 @@ export async function createPiAgentFromDefinition(dir, options) {
336
322
  base: options.base ?? piBasePrompt({ tools, persona: def.persona }),
337
323
  // ② project context: AGENTS.md files (agentDir + cwd-ancestor walk) via loadProjectContextFiles.
338
324
  contextFiles: def.contextFiles,
339
- skills: def.skills,
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,
343
325
  }),
344
326
  skills: def.skills,
345
327
  };
@@ -348,13 +330,16 @@ export async function createPiAgentFromDefinition(dir, options) {
348
330
  sessions: options.sessions,
349
331
  // Discovered so the serving assembly can WARN that it does not run them (and so the refusals
350
332
  // 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.
333
+ // change without a restart, which is why this sits outside readDefinition above.
352
334
  extensionPaths: await loadExtensionPaths(dir, { cwd, env }),
353
335
  cwd,
354
336
  env,
355
337
  lease: options.lease,
356
- observer: options.observer,
357
- onAssembly: options.onAssembly,
358
338
  });
359
- return { agent, definition };
339
+ return { assembly, definition };
340
+ }
341
+ /** L2: "point at a directory → agent": {@link assemblePiFromDefinition} under the L0. */
342
+ export async function createPiAgentFromDefinition(dir, options) {
343
+ const { assembly, definition } = await assemblePiFromDefinition(dir, options);
344
+ return { agent: agentOf(assembly, options.observer), definition };
360
345
  }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { realpathSync } from "node:fs";
17
17
  import { join, resolve } from "node:path";
18
- import { loadSkills } from "@earendil-works/pi-agent-core";
18
+ import { BACKGROUND_CONTEXT, loadSkills, } from "@earendil-works/pi-agent-core";
19
19
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
20
20
  import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
21
21
  import { log } from "../../log.js";
@@ -26,7 +26,7 @@ export async function loadAgentDefinition(agentDir, options = {}) {
26
26
  // never diverge if a caller passes a relative agentDir.
27
27
  const cwd = options.cwd ?? agentDir;
28
28
  const e = options.env ?? new NodeExecutionEnv({ cwd });
29
- const rootResult = await e.absolutePath(agentDir);
29
+ const rootResult = await e.absolutePath(agentDir, BACKGROUND_CONTEXT);
30
30
  if (!rootResult.ok) {
31
31
  throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
32
32
  }
@@ -38,7 +38,7 @@ export async function loadAgentDefinition(agentDir, options = {}) {
38
38
  // persona.md → segment ① persona (overrides the identity line). Same error policy as AGENTS.md:
39
39
  // only not_found means "absent"; any other read error surfaces rather than silently dropping the persona.
40
40
  const personaPath = join(root, "persona.md");
41
- const personaRead = await e.readTextFile(personaPath);
41
+ const personaRead = await e.readTextFile(personaPath, BACKGROUND_CONTEXT);
42
42
  if (!personaRead.ok && personaRead.error.code !== "not_found") {
43
43
  throw new Error(`cannot read ${personaPath}: ${personaRead.error.message}`);
44
44
  }
@@ -86,13 +86,13 @@ export async function loadAgentDefinition(agentDir, options = {}) {
86
86
  export async function loadExtensionPaths(agentDir, options = {}) {
87
87
  const cwd = options.cwd ?? agentDir;
88
88
  const e = options.env ?? new NodeExecutionEnv({ cwd });
89
- const rootResult = await e.absolutePath(agentDir);
89
+ const rootResult = await e.absolutePath(agentDir, BACKGROUND_CONTEXT);
90
90
  if (!rootResult.ok)
91
91
  throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
92
92
  const root = rootResult.value;
93
93
  await assertInsideAgentDir(root, "extensions");
94
94
  const dir = join(root, "extensions");
95
- const listed = await e.listDir(dir);
95
+ const listed = await e.listDir(dir, BACKGROUND_CONTEXT);
96
96
  if (!listed.ok) {
97
97
  if (listed.error.code === "not_found")
98
98
  return [];
@@ -138,7 +138,7 @@ export async function loadExtensionPaths(agentDir, options = {}) {
138
138
  async function firstRealFile(e, candidates) {
139
139
  let refused = false;
140
140
  for (const candidate of candidates) {
141
- const info = await e.fileInfo(candidate);
141
+ const info = await e.fileInfo(candidate, BACKGROUND_CONTEXT);
142
142
  if (!info.ok) {
143
143
  if (info.error.code === "not_found")
144
144
  continue;
@@ -164,7 +164,7 @@ async function readSkills(e, root) {
164
164
  // definition loads the same skills on every machine — and, like tools/channels/schedules, a symlink
165
165
  // that escapes the agent dir is refused rather than followed (the fourth of four surfaces).
166
166
  await assertInsideAgentDir(root, "skills");
167
- const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")]);
167
+ const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")], BACKGROUND_CONTEXT);
168
168
  const byName = new Map();
169
169
  const collisions = [];
170
170
  for (const skill of raw) {
@@ -187,7 +187,7 @@ async function readSkills(e, root) {
187
187
  export async function loadAgentSkills(agentDir, options = {}) {
188
188
  const cwd = options.cwd ?? agentDir;
189
189
  const e = options.env ?? new NodeExecutionEnv({ cwd });
190
- const rootResult = await e.absolutePath(agentDir);
190
+ const rootResult = await e.absolutePath(agentDir, BACKGROUND_CONTEXT);
191
191
  if (!rootResult.ok)
192
192
  throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
193
193
  // `dir` is the RESOLVED root, like {@link LoadedDefinition.dir}: readers key per-definition state
@@ -1,4 +1,5 @@
1
1
  import { ABORTED_CODE, SESSION_BUSY_CODE, } from "../../agent.js";
2
+ import { toRetryScheduledEvent } from "./retry-event.js";
2
3
  import { cancellableStream } from "../../collect.js";
3
4
  import { log } from "../../log.js";
4
5
  import { EventQueue, errorToTerminal, inProcessLease, projectAgentEvent, toPiPromptOptions, toTerminal, } from "./turn-kit.js";
@@ -65,36 +66,9 @@ function toSessionEvent(event, runId) {
65
66
  runId,
66
67
  data: { steering: event.steering.length, followUp: event.followUp.length },
67
68
  };
68
- case "auto_retry_start": {
69
- const retry = {
70
- type: "retry_scheduled",
71
- timestamp: at,
72
- runId,
73
- data: {
74
- operation: "assistant",
75
- attempt: event.attempt,
76
- maxAttempts: event.maxAttempts,
77
- delayMs: event.delayMs,
78
- error: event.errorMessage,
79
- },
80
- };
81
- return retry;
82
- }
83
- case "summarization_retry_scheduled": {
84
- const retry = {
85
- type: "retry_scheduled",
86
- timestamp: at,
87
- runId,
88
- data: {
89
- operation: "compaction",
90
- attempt: event.attempt,
91
- maxAttempts: event.maxAttempts,
92
- delayMs: event.delayMs,
93
- error: event.errorMessage,
94
- },
95
- };
96
- return retry;
97
- }
69
+ case "auto_retry_start":
70
+ case "summarization_retry_scheduled":
71
+ return toRetryScheduledEvent(event, runId);
98
72
  default:
99
73
  return null;
100
74
  }
@@ -240,6 +214,15 @@ export function createPiAgentFromSession(options) {
240
214
  return; // decided; the retry's output is not ours
241
215
  if (event.type === "message_end" && event.message.role === "assistant") {
242
216
  finalAssistant = event.message;
217
+ // Error messages and details can contain provider payloads; log only diagnostic metadata.
218
+ for (const diagnostic of finalAssistant.diagnostics ?? []) {
219
+ log.warn(`[fastagent] provider diagnostic ${diagnostic.type} (${finalAssistant.provider}/${finalAssistant.model}, session ${scope.session}, run ${runId})`);
220
+ }
221
+ }
222
+ if (event.type === "compaction_end" && event.reason !== "manual") {
223
+ const status = event.aborted ? "aborted" : event.errorMessage ? "failed" : "completed";
224
+ const emit = event.errorMessage && !event.aborted ? log.warn : log.debug;
225
+ emit(`[fastagent] automatic compaction ${event.reason} (session ${scope.session}, run ${runId}): ${status}`);
243
226
  }
244
227
  // pi retries a failed assistant request by DISCARDING that attempt's assistant message and
245
228
  // asking again. Everything the turn achieved before it survives — executed tools keep
@@ -36,8 +36,8 @@ export interface CreatePiAgentFromDirOptions {
36
36
  sessionControl?: boolean;
37
37
  /** Additional raw tap with the FULL vocabulary: run events composed after the
38
38
  * {@link sessionControl} hub's observer, plus the hub's own boundary-mutation events
39
- * (`state_changed`/`compaction_*`) via the hub's tap. TRUSTED seam: since Phase 2a an observer
40
- * receives each run's live modulation handles (see `SessionObserver`) — for read-only consumers
39
+ * (`state_changed`/`compaction_*`) via the hub's tap. TRUSTED seam: an observer receives each
40
+ * run's live modulation handles (see `SessionObserver`) — for read-only consumers
41
41
  * use the hub's `events()` stream instead. */
42
42
  observer?: SessionObserver;
43
43
  }
@@ -11,7 +11,7 @@
11
11
  import { mkdir } from "node:fs/promises";
12
12
  import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModelSpec, } from "./config.js";
13
13
  import { resolveStateRoot, resolvePlacement } from "../../paths.js";
14
- import { createPiAgentFromDefinition, resolveAgentTools } from "./create.js";
14
+ import { agentOf, assemblePiFromDefinition, resolveAgentTools } from "./create.js";
15
15
  import { createPiSessionControl } from "./session-control.js";
16
16
  import { withWakeTool } from "./wake-tool.js";
17
17
  import { loadAgentSkills } from "./definition.js";
@@ -63,19 +63,36 @@ export async function createPiAgentFromDir(dir, options = {}) {
63
63
  const sessionsDir = options.sessionsDir ?? defaultSessionsDir(stateRoot);
64
64
  await mkdir(sessionsDir, { recursive: true });
65
65
  const sessions = piSessionRecordStore({ dir: sessionsDir, cwd: workspace });
66
- // The hub is wired HERE because the store is created here: chicken-and-egg otherwise (the hub
67
- // needs the store; the agent needs the hub's observer). Boundary parts (factory/lease/registry)
68
- // only exist after the assembly below — the hub takes them as a lazy thunk, filled by the
69
- // assembly's onAssembly callback (assembly completes before this function returns, so every
70
- // dispatch sees them). An extra caller observer composes after the hub's (TRUSTED seam).
71
- let boundaryParts;
72
- let assembled;
66
+ const { assembly, definition } = await assemblePiFromDefinition(agentDir, {
67
+ model: modelSpec,
68
+ thinkingLevel: config.thinkingLevel,
69
+ cwd: workspace,
70
+ tools: mountedTools,
71
+ authPath,
72
+ // Skills are definition-only (the agent is its directory), so dev mirrors deployment exactly.
73
+ sessions,
74
+ });
75
+ // The hub is wired HERE because the store is created here (an external `createPiSessionControl`
76
+ // cannot exist before the store does). Its writes contend on the assembly's own lease and validate
77
+ // against its own registry — the value above is what makes "same" true. An extra caller observer
78
+ // composes after the hub's (TRUSTED seam).
73
79
  const caller = options.observer;
74
80
  const wantControl = options.sessionControl ?? (config.sessionControl === true && options.serving === true);
75
- const hub = wantControl
76
- ? createPiSessionControl({
81
+ let hub;
82
+ if (wantControl) {
83
+ // The hub's surface is synchronous (`capabilities()` lists the allowed models), while building
84
+ // the registry reads credentials and is not. Resolve it ONCE here — the opener is async anyway,
85
+ // and the first turn would have paid the same read — so every boundary mutation validates
86
+ // against the same registry the runs use.
87
+ const { modelRuntime, model } = await assembly.engine();
88
+ hub = createPiSessionControl({
77
89
  sessions,
78
- boundary: () => boundaryParts,
90
+ boundary: {
91
+ lease: assembly.lease,
92
+ models: modelRuntime,
93
+ sessionFactory: assembly.sessionFactory,
94
+ defaults: { model, thinkingLevel: assembly.thinkingLevel },
95
+ },
79
96
  // Skills ARE the names a client offers — the resolved set, after collisions were decided
80
97
  // first-wins, which a client cannot reconstruct from the directory. Read LIVE (the directory
81
98
  // is the agent: a skill added while serving is in play on the next turn, so it must be
@@ -97,8 +114,8 @@ export async function createPiAgentFromDir(dir, options = {}) {
97
114
  // and never cross the data plane's observer seam — without this, an audit tap wired here
98
115
  // would miss exactly the mutations it most needs to see (`update({ model })`).
99
116
  tap: caller ? (session, event) => caller(session, event) : undefined,
100
- })
101
- : undefined;
117
+ });
118
+ }
102
119
  const observer = hub
103
120
  ? caller
104
121
  ? (session, event, run) => {
@@ -107,34 +124,7 @@ export async function createPiAgentFromDir(dir, options = {}) {
107
124
  }
108
125
  : hub.observer
109
126
  : caller;
110
- const { agent, definition } = await createPiAgentFromDefinition(agentDir, {
111
- model: modelSpec,
112
- thinkingLevel: config.thinkingLevel,
113
- cwd: workspace,
114
- tools: mountedTools,
115
- authPath,
116
- // Skills are definition-only (the agent is its directory), so dev mirrors deployment exactly.
117
- sessions,
118
- observer,
119
- onAssembly: hub
120
- ? (parts) => {
121
- assembled = parts;
122
- }
123
- : undefined,
124
- });
125
- if (hub && assembled) {
126
- // The hub's surface is synchronous (`capabilities()` lists the allowed models), while building
127
- // the registry reads credentials and is not. Resolve it ONCE here — the opener is async anyway,
128
- // and the first turn would have paid the same read — so every boundary mutation validates
129
- // against the same registry the runs use.
130
- const { modelRuntime, model } = await assembled.engine();
131
- boundaryParts = {
132
- lease: assembled.lease,
133
- models: modelRuntime,
134
- sessionFactory: assembled.sessionFactory,
135
- defaults: { model, thinkingLevel: assembled.thinkingLevel },
136
- };
137
- }
127
+ const agent = agentOf(assembly, observer);
138
128
  return {
139
129
  agent,
140
130
  definition,
@@ -0,0 +1,6 @@
1
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
2
+ import type { RetryScheduledEvent } from "../../session.ts";
3
+ /** Manual compaction retries are session-scoped; retries inside an invoke carry its runId. */
4
+ export declare function toRetryScheduledEvent(event: Extract<AgentSessionEvent, {
5
+ type: "auto_retry_start" | "summarization_retry_scheduled";
6
+ }>, runId?: string): RetryScheduledEvent;