pi-subagents 0.63.0 → 0.65.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 (107) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +92 -114
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +6 -5
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +46 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/notify.ts +41 -2
  34. package/src/runs/background/process-terminal.ts +1 -1
  35. package/src/runs/background/run-child-session.ts +613 -0
  36. package/src/runs/background/run-status.ts +0 -1
  37. package/src/runs/background/runner-aliases.ts +125 -0
  38. package/src/runs/background/runner-child-sessions.ts +31 -0
  39. package/src/runs/background/scheduled-runs.ts +18 -4
  40. package/src/runs/background/subagent-runner.ts +203 -898
  41. package/src/runs/foreground/async-steering-action.ts +1 -17
  42. package/src/runs/foreground/execution.ts +196 -378
  43. package/src/runs/foreground/foreground-control.ts +4 -0
  44. package/src/runs/foreground/subagent-executor.ts +121 -58
  45. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  46. package/src/runs/shared/abort-recovery.ts +3 -3
  47. package/src/runs/shared/acceptance.ts +10 -0
  48. package/src/runs/shared/async-status-projection.ts +11 -43
  49. package/src/runs/shared/capability-ceiling.ts +1 -2
  50. package/src/runs/shared/child-hooks.ts +25 -0
  51. package/src/runs/shared/child-identity.ts +13 -2
  52. package/src/runs/shared/child-launch.ts +314 -0
  53. package/src/runs/shared/child-lifecycle.ts +25 -0
  54. package/src/runs/shared/child-runtime-config.ts +126 -0
  55. package/src/runs/shared/child-session.ts +342 -0
  56. package/src/runs/shared/child-tool-plan.ts +530 -0
  57. package/src/runs/shared/claude-code-adapter.ts +5 -1
  58. package/src/runs/shared/completion-guard.ts +1 -1
  59. package/src/runs/shared/external-cli-preflight.ts +16 -0
  60. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  61. package/src/runs/shared/model-exclusions.ts +82 -14
  62. package/src/runs/shared/model-fallback.ts +47 -4
  63. package/src/runs/shared/nested-events.ts +29 -45
  64. package/src/runs/shared/nested-path.ts +0 -14
  65. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  66. package/src/runs/shared/parallel-utils.ts +0 -2
  67. package/src/runs/shared/permissions.ts +0 -13
  68. package/src/runs/shared/process-signal.ts +4 -1
  69. package/src/runs/shared/run-fanout-budget.ts +0 -13
  70. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  71. package/src/runs/shared/structured-output.ts +17 -4
  72. package/src/runs/shared/subagent-control.ts +6 -2
  73. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  74. package/src/runs/shared/tool-availability.ts +18 -62
  75. package/src/runs/shared/tool-budget.ts +0 -14
  76. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  77. package/src/runs/shared/worktree.ts +117 -30
  78. package/src/shared/child-session-name.ts +1 -1
  79. package/src/shared/jsonl-writer.ts +11 -0
  80. package/src/shared/thinking-ceiling.ts +0 -6
  81. package/src/shared/types.ts +64 -30
  82. package/src/shared/utils.ts +3 -4
  83. package/src/slash/slash-commands.ts +0 -6
  84. package/src/tui/fleet.ts +0 -1
  85. package/src/tui/render.ts +238 -31
  86. package/src/watchdog/child-status.ts +54 -34
  87. package/src/watchdog/diff-tool.ts +77 -0
  88. package/src/watchdog/emission-guard.ts +5 -3
  89. package/src/watchdog/guidance.ts +20 -0
  90. package/src/watchdog/register-child.ts +28 -25
  91. package/src/watchdog/register-main.ts +10 -9
  92. package/src/watchdog/render.ts +4 -5
  93. package/src/watchdog/review.ts +15 -4
  94. package/src/watchdog/rules.ts +70 -0
  95. package/src/watchdog/runtime.ts +75 -92
  96. package/src/watchdog/scope.ts +0 -11
  97. package/src/watchdog/settings.ts +48 -104
  98. package/src/watchdog/types.ts +18 -32
  99. package/src/watchdog/warning-format.ts +0 -1
  100. package/src/workflows/chat-progress.ts +3 -2
  101. package/src/workflows/scripted-workflow.ts +48 -1
  102. package/src/workflows/workflow-checklist.ts +10 -12
  103. package/src/workflows/workflow-preflight.ts +28 -1
  104. package/src/runs/shared/child-protocol.ts +0 -415
  105. package/src/runs/shared/pi-args.ts +0 -1062
  106. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  107. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -0,0 +1,342 @@
1
+ /**
2
+ * In-process child sessions.
3
+ *
4
+ * A child is a pi `AgentSession` created inside the process that owns it: the
5
+ * parent pi process for foreground children, the detached runner process for
6
+ * background children. The factory is injectable so tests can script a child
7
+ * without the real runtime; the default implementation wraps
8
+ * `createAgentSession` from a pi package module and shares one `ModelRuntime`
9
+ * across every child it creates.
10
+ */
11
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { getAgentDir } from "../../shared/utils.ts";
14
+ import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
15
+
16
+ export interface ChildSessionEvent {
17
+ type: string;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ /** Mirror pi's JSON event projection: `message_update` drops the partial message. */
22
+ export function projectChildSessionEventForJson(event: ChildSessionEvent): unknown {
23
+ if (event.type !== "message_update") return event;
24
+ const assistantMessageEvent = event.assistantMessageEvent as Record<string, unknown> | undefined;
25
+ if (!assistantMessageEvent || typeof assistantMessageEvent !== "object") return event;
26
+ const { partial: _partial, ...delta } = assistantMessageEvent;
27
+ return { type: "message_update", usage: (event.message as { usage?: unknown } | undefined)?.usage, assistantMessageEvent: delta };
28
+ }
29
+
30
+ export interface ChildSessionExtensionError {
31
+ extensionPath: string;
32
+ event: string;
33
+ error: unknown;
34
+ }
35
+
36
+ export interface ChildHookExtension {
37
+ name: string;
38
+ factory: (pi: ExtensionAPI) => void;
39
+ }
40
+
41
+ export type ChildSessionStorage =
42
+ | { kind: "file"; sessionFile: string }
43
+ | { kind: "dir"; sessionDir: string }
44
+ | { kind: "default" }
45
+ | { kind: "memory" };
46
+
47
+ export interface ChildSessionLaunch {
48
+ cwd: string;
49
+ storage: ChildSessionStorage;
50
+ /** Model reference as the agent config names it (`provider/id`, optionally `:thinking`). */
51
+ model?: string;
52
+ /** Explicit tool allowlist; undefined keeps pi's defaults. */
53
+ tools?: string[];
54
+ excludeTools?: string[];
55
+ /** Extension files loaded for this child in addition to the inline hooks. */
56
+ extensionPaths: string[];
57
+ /**
58
+ * Discover the ambient extensions (agent dir, project, settings) the way a
59
+ * `pi` process would. False loads only `extensionPaths` and `hooks`.
60
+ */
61
+ ambientExtensions: boolean;
62
+ hooks: ChildHookExtension[];
63
+ noSkills: boolean;
64
+ noContextFiles: boolean;
65
+ systemPrompt?: string;
66
+ appendSystemPrompt?: string;
67
+ /**
68
+ * Environment values that extensions loaded into the child read from
69
+ * `process.env`. Applied to the hosting process while the session is created
70
+ * and its extensions load and start; launches in one process take that
71
+ * window one at a time. An undefined value removes the variable.
72
+ */
73
+ processEnv?: Record<string, string | undefined>;
74
+ /** The typed runtime config the hooks were built from; informational for factories. */
75
+ runtime: ChildRuntimeConfig;
76
+ onExtensionError?: (error: ChildSessionExtensionError) => void;
77
+ }
78
+
79
+ export interface ChildSession {
80
+ subscribe(listener: (event: ChildSessionEvent) => void): () => void;
81
+ /** Resolves when the run ends, including after abort. */
82
+ prompt(text: string): Promise<void>;
83
+ steer(text: string): Promise<void>;
84
+ followUp(text: string): Promise<void>;
85
+ abort(): Promise<void>;
86
+ /** Emits `session_shutdown` to the child's extensions and disposes the session; resolves once that shutdown work is done. */
87
+ dispose(): Promise<void>;
88
+ readonly messages: readonly AgentMessage[];
89
+ readonly sessionFile: string | undefined;
90
+ readonly sessionId: string;
91
+ readonly modelId: string | undefined;
92
+ /** Set by the foreground host once the run detached; `factory.dispose()` leaves such children running. */
93
+ detached?: boolean;
94
+ /** Set by `factory.dispose()` before it aborts the child, so the host can report the stop truthfully. */
95
+ shutDown?: boolean;
96
+ }
97
+
98
+ export interface ChildSessionFactory {
99
+ create(launch: ChildSessionLaunch): Promise<ChildSession>;
100
+ /** Abort and dispose every live attached child; detached children keep running and hold the shared runtime. */
101
+ dispose(): Promise<void>;
102
+ }
103
+
104
+ export type PiCodingAgentModule = typeof import("@earendil-works/pi-coding-agent");
105
+
106
+ export interface DefaultChildSessionFactoryOptions {
107
+ /**
108
+ * Loads the pi package the sessions are created from. The parent process
109
+ * uses the host's in-process module; the detached runner imports the
110
+ * installed package by absolute path.
111
+ */
112
+ loadPiCodingAgent?: () => Promise<PiCodingAgentModule>;
113
+ /** Upper bound on a disposed child's `session_shutdown` handlers before the session is dropped anyway. */
114
+ shutdownTimeoutMs?: number;
115
+ }
116
+
117
+ type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
118
+ type QueuedProviderRegistration = { name: string; config: Parameters<ModelRuntimeInstance["registerProvider"]>[1]; extensionPath: string };
119
+ type QueuedNativeProviderRegistration = { provider: Parameters<ModelRuntimeInstance["registerNativeProvider"]>[0]; extensionPath: string };
120
+
121
+ interface LoaderWithExtensions {
122
+ getExtensions(): {
123
+ runtime: {
124
+ pendingProviderRegistrations: QueuedProviderRegistration[];
125
+ pendingNativeProviderRegistrations: QueuedNativeProviderRegistration[];
126
+ };
127
+ };
128
+ }
129
+
130
+ /** One launch at a time from env application through `session_start`, so parallel launches never observe each other's `processEnv` while their extensions load and start. */
131
+ let loading: Promise<unknown> = Promise.resolve();
132
+
133
+ /**
134
+ * pi caches extension factories per process and clears that cache only when a
135
+ * loader reloads a second time, so every child in one process would share each
136
+ * extension's module state. Marking the child's loader as already loaded makes
137
+ * its first `reload()` clear the cache, so the child gets its own instances the
138
+ * way a separate process had them. The flag is a private field of pi's loader.
139
+ */
140
+ function resetExtensionCacheOnReload(loader: object): boolean {
141
+ if (!("loaded" in loader)) return false;
142
+ (loader as { loaded: boolean }).loaded = true;
143
+ return true;
144
+ }
145
+
146
+ function applyProcessEnv(values: Record<string, string | undefined> | undefined): void {
147
+ if (!values) return;
148
+ for (const [name, value] of Object.entries(values)) {
149
+ if (value === undefined) delete process.env[name];
150
+ else process.env[name] = value;
151
+ }
152
+ }
153
+
154
+ function flushQueuedProviderRegistrations(loader: object, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined): void {
155
+ if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function") return;
156
+ const { runtime } = (loader as LoaderWithExtensions).getExtensions();
157
+ for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations) {
158
+ try {
159
+ modelRuntime.registerProvider(name, config);
160
+ } catch (error) {
161
+ onError?.({ extensionPath, event: "register_provider", error });
162
+ }
163
+ }
164
+ runtime.pendingProviderRegistrations = [];
165
+ for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations) {
166
+ try {
167
+ modelRuntime.registerNativeProvider(provider);
168
+ } catch (error) {
169
+ onError?.({ extensionPath, event: "register_provider", error });
170
+ }
171
+ }
172
+ runtime.pendingNativeProviderRegistrations = [];
173
+ }
174
+
175
+ /**
176
+ * Default factory: real pi sessions sharing one `ModelRuntime`, created lazily
177
+ * on the first child launch and dropped on `dispose()`.
178
+ */
179
+ export function createDefaultChildSessionFactory(options: DefaultChildSessionFactoryOptions = {}): ChildSessionFactory {
180
+ const loadPiCodingAgent = options.loadPiCodingAgent ?? (() => import("@earendil-works/pi-coding-agent"));
181
+ const shutdownTimeoutMs = options.shutdownTimeoutMs ?? 5_000;
182
+ let runtime: ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]> | undefined;
183
+ const live = new Set<ChildSession>();
184
+ /** Extension shutdowns still running for disposed children; `dispose()` waits for them. */
185
+ const shutdowns = new Set<Promise<void>>();
186
+ const sharedRuntime = async (pi: PiCodingAgentModule) => {
187
+ runtime ??= pi.ModelRuntime.create().catch((error: unknown) => {
188
+ runtime = undefined;
189
+ throw error;
190
+ });
191
+ return runtime;
192
+ };
193
+ return {
194
+ async create(launch) {
195
+ const pi = await loadPiCodingAgent();
196
+ const modelRuntime = await sharedRuntime(pi);
197
+ const agentDir = getAgentDir();
198
+ const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
199
+ const loader = new pi.DefaultResourceLoader({
200
+ cwd: launch.cwd,
201
+ agentDir,
202
+ settingsManager,
203
+ noExtensions: !launch.ambientExtensions,
204
+ noSkills: launch.noSkills,
205
+ noPromptTemplates: true,
206
+ noThemes: true,
207
+ noContextFiles: launch.noContextFiles,
208
+ additionalExtensionPaths: launch.extensionPaths,
209
+ extensionFactories: launch.hooks,
210
+ ...(launch.systemPrompt !== undefined ? { systemPrompt: launch.systemPrompt } : {}),
211
+ ...(launch.appendSystemPrompt !== undefined ? { appendSystemPrompt: [launch.appendSystemPrompt] } : {}),
212
+ });
213
+ const open = async () => {
214
+ applyProcessEnv(launch.processEnv);
215
+ if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length)) launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
216
+ await loader.reload();
217
+ flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
218
+ const sessionManager = launch.storage.kind === "file"
219
+ ? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
220
+ : launch.storage.kind === "dir"
221
+ ? pi.SessionManager.create(launch.cwd, launch.storage.sessionDir)
222
+ : launch.storage.kind === "memory"
223
+ ? pi.SessionManager.inMemory(launch.cwd)
224
+ : pi.SessionManager.create(launch.cwd);
225
+ const resolvedModel = launch.model
226
+ ? pi.resolveCliModel({ cliModel: launch.model, modelRuntime })
227
+ : undefined;
228
+ if (resolvedModel?.error) throw new Error(resolvedModel.error);
229
+ const { session } = await pi.createAgentSession({
230
+ cwd: launch.cwd,
231
+ agentDir,
232
+ modelRuntime,
233
+ ...(resolvedModel?.model ? { model: resolvedModel.model } : {}),
234
+ ...(resolvedModel?.thinkingLevel ? { thinkingLevel: resolvedModel.thinkingLevel } : {}),
235
+ ...(launch.tools ? { tools: launch.tools } : {}),
236
+ ...(launch.excludeTools?.length ? { excludeTools: launch.excludeTools } : {}),
237
+ resourceLoader: loader,
238
+ sessionManager,
239
+ settingsManager,
240
+ sessionStartEvent: { type: "session_start", reason: "startup" },
241
+ });
242
+ try {
243
+ await session.bindExtensions({
244
+ mode: "print",
245
+ onError: (error) => launch.onExtensionError?.({ extensionPath: error.extensionPath, event: error.event, error: error.error }),
246
+ });
247
+ } catch (error) {
248
+ session.dispose();
249
+ throw error;
250
+ }
251
+ return session;
252
+ };
253
+ const opened = loading.catch(() => {}).then(open);
254
+ loading = opened;
255
+ const session = await opened;
256
+ let pending: Promise<void> | undefined;
257
+ // pi's own hosts emit `session_shutdown` before disposing a session so the
258
+ // extensions loaded into it (ambient extensions included) release their
259
+ // watchers, servers, and timers. Do the same, then dispose.
260
+ const shutdown = async (): Promise<void> => {
261
+ try {
262
+ const runner = session.extensionRunner;
263
+ if (runner.hasHandlers("session_shutdown")) await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }), new Promise((resolve) => setTimeout(resolve, shutdownTimeoutMs).unref?.())]);
264
+ } catch (error) {
265
+ launch.onExtensionError?.({ extensionPath: "<session>", event: "session_shutdown", error });
266
+ } finally {
267
+ session.dispose();
268
+ }
269
+ };
270
+ const child: ChildSession = {
271
+ subscribe: (listener) => session.subscribe((event) => listener(event as unknown as ChildSessionEvent)),
272
+ prompt: (text) => session.prompt(text),
273
+ steer: (text) => session.steer(text),
274
+ followUp: (text) => session.followUp(text),
275
+ abort: () => session.abort(),
276
+ dispose: () => {
277
+ if (!pending) {
278
+ live.delete(child);
279
+ const shutdownDone = shutdown();
280
+ pending = shutdownDone;
281
+ shutdowns.add(shutdownDone);
282
+ void shutdownDone.finally(() => shutdowns.delete(shutdownDone));
283
+ }
284
+ return pending;
285
+ },
286
+ get messages() { return session.messages; },
287
+ get sessionFile() { return session.sessionFile; },
288
+ get sessionId() { return session.sessionId; },
289
+ get modelId() { return session.model ? `${session.model.provider}/${session.model.id}` : undefined; },
290
+ };
291
+ live.add(child);
292
+ return child;
293
+ },
294
+ async dispose() {
295
+ const children = [...live].filter((child) => !child.detached);
296
+ for (const child of children) child.shutDown = true;
297
+ await Promise.allSettled(children.map((child) => child.abort()));
298
+ for (const child of children) {
299
+ try { void child.dispose(); } catch { /* best effort */ }
300
+ }
301
+ await Promise.allSettled([...shutdowns]);
302
+ if (live.size === 0) runtime = undefined;
303
+ },
304
+ };
305
+ }
306
+
307
+ let activeFactory: ChildSessionFactory | undefined;
308
+ let activeFactoryModule: string | undefined;
309
+
310
+ /** The process-wide factory foreground runs use unless a run passes its own. */
311
+ export function childSessionFactory(): ChildSessionFactory {
312
+ activeFactory ??= createDefaultChildSessionFactory();
313
+ return activeFactory;
314
+ }
315
+
316
+ /**
317
+ * Replace the process-wide factory. Tests install a scripted factory; passing
318
+ * undefined restores the default on next use.
319
+ */
320
+ export function setChildSessionFactory(factory: ChildSessionFactory | undefined): void {
321
+ activeFactory = factory;
322
+ }
323
+
324
+ /**
325
+ * Module path the detached background runner imports its child session factory
326
+ * from. Tests point it at a scripted factory; production launches leave it
327
+ * unset and the runner creates real sessions from the installed pi package.
328
+ */
329
+ export function childSessionFactoryModule(): string | undefined {
330
+ return activeFactoryModule;
331
+ }
332
+
333
+ export function setChildSessionFactoryModule(modulePath: string | undefined): void {
334
+ activeFactoryModule = modulePath;
335
+ }
336
+
337
+ /** Abort and dispose every live in-process child and release the shared runtime. */
338
+ export async function disposeChildSessions(): Promise<void> {
339
+ const factory = activeFactory;
340
+ if (!factory) return;
341
+ await factory.dispose();
342
+ }