@fastagent-sh/fastagent 0.19.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 (212) hide show
  1. package/README.md +5 -1
  2. package/dist/atomic-write.d.ts +11 -7
  3. package/dist/atomic-write.js +16 -11
  4. package/dist/channels/agentcore-limits.d.ts +9 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-protocol.d.ts +112 -0
  7. package/dist/channels/agentcore-protocol.js +22 -0
  8. package/dist/channels/agentcore-service.d.ts +12 -15
  9. package/dist/channels/agentcore-service.js +15 -24
  10. package/dist/channels/agentcore-state.d.ts +5 -11
  11. package/dist/channels/agentcore-state.js +4 -1
  12. package/dist/channels/agentcore.d.ts +9 -83
  13. package/dist/channels/agentcore.js +101 -93
  14. package/dist/channels/control.d.ts +50 -12
  15. package/dist/channels/control.js +251 -157
  16. package/dist/channels/discover.d.ts +26 -5
  17. package/dist/channels/discover.js +13 -26
  18. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  19. package/dist/channels/feishu/context-buffer.js +0 -38
  20. package/dist/channels/feishu/crypto.d.ts +0 -2
  21. package/dist/channels/feishu/crypto.js +3 -8
  22. package/dist/channels/feishu/feishu-api.js +7 -12
  23. package/dist/channels/feishu/feishu.js +86 -128
  24. package/dist/channels/feishu/parse.d.ts +4 -3
  25. package/dist/channels/feishu/parse.js +5 -4
  26. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  27. package/dist/channels/feishu/register-webhook.js +43 -57
  28. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  29. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  30. package/dist/channels/feishu/setup-mode.js +26 -0
  31. package/dist/channels/feishu/shared-api.d.ts +10 -0
  32. package/dist/channels/feishu/shared-api.js +38 -0
  33. package/dist/channels/http.d.ts +0 -8
  34. package/dist/channels/http.js +4 -56
  35. package/dist/channels/kit/attachment-path.d.ts +12 -0
  36. package/dist/channels/kit/attachment-path.js +43 -0
  37. package/dist/channels/kit/context-buffer.d.ts +9 -0
  38. package/dist/channels/kit/context-buffer.js +11 -0
  39. package/dist/channels/kit/signature.d.ts +12 -0
  40. package/dist/channels/kit/signature.js +17 -0
  41. package/dist/channels/kit/stop-command.js +3 -3
  42. package/dist/channels/kit/tasks.d.ts +1 -1
  43. package/dist/channels/kit/tasks.js +10 -4
  44. package/dist/channels/kit/turn-runner.d.ts +59 -0
  45. package/dist/channels/kit/turn-runner.js +84 -0
  46. package/dist/channels/kit/turn-store.d.ts +19 -3
  47. package/dist/channels/kit/turn-store.js +29 -2
  48. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  49. package/dist/channels/registration.d.ts +36 -1
  50. package/dist/channels/registration.js +57 -1
  51. package/dist/channels/secret.d.ts +1 -0
  52. package/dist/channels/secret.js +16 -0
  53. package/dist/channels/slack/config-api.d.ts +35 -3
  54. package/dist/channels/slack/config-api.js +51 -9
  55. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  56. package/dist/channels/slack/invoke-turn.js +5 -1
  57. package/dist/channels/slack/manifest.js +5 -1
  58. package/dist/channels/slack/onboard.d.ts +5 -5
  59. package/dist/channels/slack/onboard.js +44 -23
  60. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  61. package/dist/channels/slack/onboarding-state.js +13 -20
  62. package/dist/channels/slack/register-webhook.d.ts +3 -2
  63. package/dist/channels/slack/register-webhook.js +35 -15
  64. package/dist/channels/slack/scaffold/channel.ts +3 -10
  65. package/dist/channels/slack/scaffold/slack-send.ts +18 -141
  66. package/dist/channels/slack/shared-api.d.ts +10 -0
  67. package/dist/channels/slack/shared-api.js +34 -0
  68. package/dist/channels/slack/slack-api.d.ts +20 -2
  69. package/dist/channels/slack/slack-api.js +100 -73
  70. package/dist/channels/slack/slack.d.ts +0 -10
  71. package/dist/channels/slack/slack.js +68 -107
  72. package/dist/channels/sse.d.ts +4 -0
  73. package/dist/channels/sse.js +66 -0
  74. package/dist/channels/telegram/parse.d.ts +21 -1
  75. package/dist/channels/telegram/parse.js +65 -11
  76. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  77. package/dist/channels/telegram/register-webhook.js +44 -42
  78. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  79. package/dist/channels/telegram/telegram-api.js +5 -6
  80. package/dist/channels/telegram/telegram.d.ts +2 -2
  81. package/dist/channels/telegram/telegram.js +97 -204
  82. package/dist/channels/wait-health.js +7 -4
  83. package/dist/cli/add-feishu.js +3 -10
  84. package/dist/cli/add-slack.js +13 -25
  85. package/dist/cli/commands/add.d.ts +0 -1
  86. package/dist/cli/commands/add.js +22 -35
  87. package/dist/cli/commands/attach.d.ts +2 -4
  88. package/dist/cli/commands/attach.js +17 -15
  89. package/dist/cli/commands/chat.js +6 -12
  90. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  91. package/dist/cli/commands/deploy/agentcore.js +178 -0
  92. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  93. package/dist/cli/commands/deploy/docker.js +119 -0
  94. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  95. package/dist/cli/commands/deploy/fly.js +131 -0
  96. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  97. package/dist/cli/commands/deploy/railway.js +71 -0
  98. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  99. package/dist/cli/commands/deploy/shared.js +124 -0
  100. package/dist/cli/commands/deploy.d.ts +38 -25
  101. package/dist/cli/commands/deploy.js +101 -631
  102. package/dist/cli/commands/dev.js +20 -76
  103. package/dist/cli/commands/fire.js +6 -12
  104. package/dist/cli/commands/info.js +1 -1
  105. package/dist/cli/commands/init.js +1 -2
  106. package/dist/cli/commands/invoke.js +4 -11
  107. package/dist/cli/commands/schedule.js +1 -1
  108. package/dist/cli/commands/start.js +41 -80
  109. package/dist/cli/commands/tool.js +3 -8
  110. package/dist/cli/kernel.d.ts +0 -2
  111. package/dist/cli/kernel.js +0 -2
  112. package/dist/cli/program.js +7 -12
  113. package/dist/cli/serve.d.ts +42 -34
  114. package/dist/cli/serve.js +107 -28
  115. package/dist/cli/shared.d.ts +52 -27
  116. package/dist/cli/shared.js +69 -6
  117. package/dist/deploy/agentcore/forwarder.js +250 -0
  118. package/dist/deploy/agentcore/logs.d.ts +2 -2
  119. package/dist/deploy/agentcore/logs.js +2 -2
  120. package/dist/deploy/agentcore/plan.d.ts +34 -11
  121. package/dist/deploy/agentcore/plan.js +52 -259
  122. package/dist/deploy/agentcore/run.d.ts +9 -24
  123. package/dist/deploy/agentcore/run.js +35 -43
  124. package/dist/deploy/channel-ingress.d.ts +73 -0
  125. package/dist/deploy/channel-ingress.js +101 -0
  126. package/dist/deploy/docker/plan.d.ts +3 -7
  127. package/dist/deploy/docker/plan.js +4 -15
  128. package/dist/deploy/docker/run.d.ts +35 -4
  129. package/dist/deploy/docker/run.js +40 -8
  130. package/dist/deploy/fly/plan.d.ts +4 -5
  131. package/dist/deploy/fly/plan.js +14 -22
  132. package/dist/deploy/fly/run.d.ts +36 -23
  133. package/dist/deploy/fly/run.js +129 -83
  134. package/dist/deploy/hosts.d.ts +5 -0
  135. package/dist/deploy/hosts.js +4 -0
  136. package/dist/deploy/preflight.d.ts +6 -7
  137. package/dist/deploy/preflight.js +27 -23
  138. package/dist/deploy/railway/plan.d.ts +9 -5
  139. package/dist/deploy/railway/plan.js +18 -26
  140. package/dist/deploy/railway/run.d.ts +11 -10
  141. package/dist/deploy/railway/run.js +44 -43
  142. package/dist/deploy/runner.js +22 -2
  143. package/dist/deploy/secrets.d.ts +18 -9
  144. package/dist/deploy/secrets.js +63 -28
  145. package/dist/dev-supervisor.js +3 -4
  146. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  147. package/dist/engines/pi/agent-session-factory.js +111 -106
  148. package/dist/engines/pi/auth.js +43 -43
  149. package/dist/engines/pi/config.d.ts +2 -2
  150. package/dist/engines/pi/create.d.ts +22 -27
  151. package/dist/engines/pi/create.js +59 -74
  152. package/dist/engines/pi/definition.d.ts +1 -1
  153. package/dist/engines/pi/definition.js +11 -12
  154. package/dist/engines/pi/invoke-session.js +13 -30
  155. package/dist/engines/pi/login.js +32 -16
  156. package/dist/engines/pi/open.d.ts +3 -4
  157. package/dist/engines/pi/open.js +32 -42
  158. package/dist/engines/pi/retry-event.d.ts +6 -0
  159. package/dist/engines/pi/retry-event.js +15 -0
  160. package/dist/engines/pi/search-tools.js +1 -1
  161. package/dist/engines/pi/service.d.ts +1 -1
  162. package/dist/engines/pi/service.js +8 -0
  163. package/dist/engines/pi/session-builder.js +26 -140
  164. package/dist/engines/pi/session-control.d.ts +27 -22
  165. package/dist/engines/pi/session-control.js +551 -490
  166. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  167. package/dist/engines/pi/session-inheritance.js +95 -76
  168. package/dist/engines/pi/session-markers.d.ts +48 -0
  169. package/dist/engines/pi/session-markers.js +59 -0
  170. package/dist/engines/pi/session-settings.d.ts +5 -5
  171. package/dist/engines/pi/session-settings.js +8 -5
  172. package/dist/engines/pi/session-store.d.ts +91 -26
  173. package/dist/engines/pi/session-store.js +413 -82
  174. package/dist/engines/pi/tool-context.d.ts +24 -11
  175. package/dist/engines/pi/tool-context.js +29 -4
  176. package/dist/engines/pi/tool.d.ts +6 -9
  177. package/dist/engines/pi/tool.js +3 -2
  178. package/dist/env.js +1 -2
  179. package/dist/feishu.d.ts +1 -0
  180. package/dist/feishu.js +1 -0
  181. package/dist/lark.d.ts +1 -0
  182. package/dist/lark.js +1 -0
  183. package/dist/loader.d.ts +51 -7
  184. package/dist/loader.js +84 -18
  185. package/dist/log.d.ts +9 -17
  186. package/dist/log.js +25 -30
  187. package/dist/paths.d.ts +26 -3
  188. package/dist/paths.js +43 -5
  189. package/dist/scaffold/add-channel.d.ts +6 -1
  190. package/dist/scaffold/add-channel.js +50 -67
  191. package/dist/scaffold/init.js +7 -2
  192. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  193. package/dist/schedule/discover.js +3 -15
  194. package/dist/schedule/wake-alarm.d.ts +14 -19
  195. package/dist/schedule/wake-alarm.js +89 -48
  196. package/dist/schedule/wakeups.d.ts +1 -1
  197. package/dist/schedule/wakeups.js +10 -7
  198. package/dist/service.d.ts +24 -24
  199. package/dist/service.js +33 -77
  200. package/dist/session-remote.d.ts +12 -7
  201. package/dist/session-remote.js +185 -118
  202. package/dist/session.d.ts +227 -93
  203. package/dist/session.js +61 -23
  204. package/dist/slack.d.ts +2 -0
  205. package/dist/slack.js +1 -0
  206. package/dist/telegram.d.ts +1 -1
  207. package/dist/telegram.js +1 -1
  208. package/dist/tunnel.d.ts +34 -9
  209. package/dist/tunnel.js +83 -50
  210. package/package.json +5 -4
  211. package/dist/channels/slack/bot-auth.d.ts +0 -15
  212. package/dist/channels/slack/bot-auth.js +0 -135
@@ -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
- import { createCodingTools, createPowerShellTool, createReadOnlyTools } from "@earendil-works/pi-coding-agent";
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
  }
@@ -1,4 +1,4 @@
1
- import type { ExecutionEnv, Skill, SkillDiagnostic } from "@earendil-works/pi-agent-core";
1
+ import { type ExecutionEnv, type Skill, type SkillDiagnostic } from "@earendil-works/pi-agent-core";
2
2
  /** A same-name skill collision (the discarded side). Surfaced, never swallowed. */
3
3
  export interface SkillCollision {
4
4
  name: string;
@@ -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 [];
@@ -131,15 +131,14 @@ export async function loadExtensionPaths(agentDir, options = {}) {
131
131
  return paths.sort();
132
132
  }
133
133
  /**
134
- * The first candidate that is a REAL file. `exists` would follow a symlink, which is how a
135
- * subdirectory's `index.ts` could otherwise point outside the definition and slip past the rule the
136
- * top-level entries already follow.
134
+ * The first candidate that is a REAL file, and whether one was found but REFUSED as a symlink.
135
+ * `exists` would follow the link, which is how a subdirectory's `index.ts` could otherwise point
136
+ * outside the definition and slip past the rule the top-level entries already follow.
137
137
  */
138
- /** The first real file among the candidates, and whether one was found but REFUSED as a symlink. */
139
138
  async function firstRealFile(e, candidates) {
140
139
  let refused = false;
141
140
  for (const candidate of candidates) {
142
- const info = await e.fileInfo(candidate);
141
+ const info = await e.fileInfo(candidate, BACKGROUND_CONTEXT);
143
142
  if (!info.ok) {
144
143
  if (info.error.code === "not_found")
145
144
  continue;
@@ -165,7 +164,7 @@ async function readSkills(e, root) {
165
164
  // definition loads the same skills on every machine — and, like tools/channels/schedules, a symlink
166
165
  // that escapes the agent dir is refused rather than followed (the fourth of four surfaces).
167
166
  await assertInsideAgentDir(root, "skills");
168
- const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")]);
167
+ const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")], BACKGROUND_CONTEXT);
169
168
  const byName = new Map();
170
169
  const collisions = [];
171
170
  for (const skill of raw) {
@@ -188,7 +187,7 @@ async function readSkills(e, root) {
188
187
  export async function loadAgentSkills(agentDir, options = {}) {
189
188
  const cwd = options.cwd ?? agentDir;
190
189
  const e = options.env ?? new NodeExecutionEnv({ cwd });
191
- const rootResult = await e.absolutePath(agentDir);
190
+ const rootResult = await e.absolutePath(agentDir, BACKGROUND_CONTEXT);
192
191
  if (!rootResult.ok)
193
192
  throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
194
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
@@ -30,6 +30,18 @@ function authCallbacks(io, userSignal, doneSignal) {
30
30
  signal: userSignal ?? new AbortController().signal,
31
31
  prompt: async (p) => {
32
32
  if (p.type === "select") {
33
+ // This branch is UNCANCELLABLE: `LoginIO.select` takes no signal, so all three the line
34
+ // below composes — the provider's own, the caller's `loginFlow({ signal })`, and the
35
+ // pending-prompt backstop — are dropped here.
36
+ //
37
+ // It costs nothing against pi 0.84's providers, where every `select` is the FIRST call of
38
+ // `login()` (bedrock, vertex, openai-codex, radius — all of them asking which login method
39
+ // to use). Nothing is racing it: no callback server is up yet, and `doneSignal` fires when
40
+ // `auth.login()` RETURNS, which it cannot do while blocked on this await.
41
+ //
42
+ // That is a property of the providers, not a guarantee, and nothing here holds it. A
43
+ // provider that ever issues a `select` after starting its callback server shows up as the
44
+ // CLI parked on stdin — widen `LoginIO.select` with a signal then.
33
45
  const v = await io.select(p.message, p.options.map((o) => ({ value: o.id, label: o.label, hint: o.description })));
34
46
  if (v === undefined)
35
47
  throw new LoginCancelled("cancelled");
@@ -94,9 +106,14 @@ async function selectProvider(io, providers, method, store) {
94
106
  return { value: p.id, label: auth?.name ?? p.name, hint: cred ? `configured (${cred.type})` : undefined };
95
107
  }));
96
108
  const id = await io.select("Select a provider", options);
97
- if (!id)
109
+ // Answers the PROVIDER, not its id: the caller needs the object, and resolving it here means the
110
+ // one place that can fail to is the one that just offered the list. Cancel and an id that was
111
+ // never offered are the same answer — nothing was chosen — and `find` gives both, since a
112
+ // provider id is always a string and `undefined` matches none of them.
113
+ const chosen = candidates.find((p) => p.id === id);
114
+ if (!chosen)
98
115
  throw new LoginCancelled("no provider selected");
99
- return id;
116
+ return chosen;
100
117
  }
101
118
  /**
102
119
  * Resolve method + provider (asking only what is not given), run the login flow, and persist. A no-op
@@ -106,26 +123,25 @@ export async function loginFlow(io, options = {}) {
106
123
  const store = options.store ?? fastagentCredentialStore(options.authPath ?? GLOBAL_AUTH_PATH);
107
124
  const providers = options.providers ?? builtinProviders();
108
125
  let method;
109
- let providerId;
126
+ let provider;
110
127
  if (options.provider) {
111
- providerId = options.provider;
112
- const p = providers.find((x) => x.id === providerId);
113
- if (!p)
114
- throw new Error(`unknown provider "${providerId}"`);
115
- method = options.method ?? (await methodForProvider(io, p));
128
+ const named = providers.find((x) => x.id === options.provider);
129
+ if (!named)
130
+ throw new Error(`unknown provider "${options.provider}"`);
131
+ provider = named;
132
+ method = options.method ?? (await methodForProvider(io, provider));
116
133
  }
117
134
  else {
118
135
  method = options.method ?? (await selectMethod(io));
119
- providerId = await selectProvider(io, providers, method, store);
136
+ provider = await selectProvider(io, providers, method, store);
120
137
  }
121
138
  // Preflight: a no-op modify runs the refuse-corrupt / writability check BEFORE the flow.
122
- await store.modify(providerId, async () => undefined);
123
- const provider = providers.find((p) => p.id === providerId);
124
- if (!provider)
125
- throw new Error(`unknown provider "${providerId}"`);
139
+ await store.modify(provider.id, async () => undefined);
140
+ // Reachable even though both branches above resolved a provider: an explicit `method` bypasses
141
+ // methodForProvider, so `{ provider: "openai-codex", method: "api_key" }` arrives here.
126
142
  const auth = method === "oauth" ? provider.auth.oauth : provider.auth.apiKey;
127
143
  if (!auth?.login)
128
- throw new Error(`provider "${providerId}" has no ${method} login`);
144
+ throw new Error(`provider "${provider.id}" has no ${method} login`);
129
145
  // `done` cancels any prompt left pending when login resolves (manual-code race backstop).
130
146
  const done = new AbortController();
131
147
  let credential;
@@ -135,6 +151,6 @@ export async function loginFlow(io, options = {}) {
135
151
  finally {
136
152
  done.abort();
137
153
  }
138
- await store.modify(providerId, async () => credential);
139
- return { provider: providerId, method };
154
+ await store.modify(provider.id, async () => credential);
155
+ return { provider: provider.id, method };
140
156
  }
@@ -5,8 +5,7 @@ import type { SessionObserver } from "./turn-kit.ts";
5
5
  import type { ModuleLoadFailure } from "../../loader.ts";
6
6
  import { type LoadedDefinition } from "./definition.ts";
7
7
  import { type PiSessionRecordStore } from "./session-store.ts";
8
- import type { ToolCollision } from "./tool.ts";
9
- import type { MountedTool } from "./tool.ts";
8
+ import type { ToolCollision, MountedTool } from "./tool.ts";
10
9
  export interface CreatePiAgentFromDirOptions {
11
10
  /** Model spec override (e.g. the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
12
11
  model?: string;
@@ -37,8 +36,8 @@ export interface CreatePiAgentFromDirOptions {
37
36
  sessionControl?: boolean;
38
37
  /** Additional raw tap with the FULL vocabulary: run events composed after the
39
38
  * {@link sessionControl} hub's observer, plus the hub's own boundary-mutation events
40
- * (`state_changed`/`compaction_*`) via the hub's tap. TRUSTED seam: since Phase 2a an observer
41
- * 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
42
41
  * use the hub's `events()` stream instead. */
43
42
  observer?: SessionObserver;
44
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
@@ -95,10 +112,10 @@ export async function createPiAgentFromDir(dir, options = {}) {
95
112
  },
96
113
  // The caller tap's boundary-event half: state_changed/compaction_* originate in the hub
97
114
  // and never cross the data plane's observer seam — without this, an audit tap wired here
98
- // would miss exactly the mutations it most needs to see (set_model).
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;
@@ -0,0 +1,15 @@
1
+ /** Manual compaction retries are session-scoped; retries inside an invoke carry its runId. */
2
+ export function toRetryScheduledEvent(event, runId) {
3
+ return {
4
+ type: "retry_scheduled",
5
+ timestamp: Date.now(),
6
+ ...(runId === undefined ? {} : { runId }),
7
+ data: {
8
+ operation: event.type === "auto_retry_start" ? "assistant" : "compaction",
9
+ attempt: event.attempt,
10
+ maxAttempts: event.maxAttempts,
11
+ delayMs: event.delayMs,
12
+ error: event.errorMessage,
13
+ },
14
+ };
15
+ }