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.
- package/CHANGELOG.md +57 -1
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +18 -16
- package/docs/configuration.md +7 -15
- package/docs/extension-api.md +15 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +7 -6
- package/docs/watchdog.md +92 -114
- package/docs/workflows.md +5 -3
- package/package.json +3 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -5
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +2 -3
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +27 -5
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -8
- package/src/intercom/native-supervisor-channel.ts +138 -60
- package/src/intercom/supervisor-ui.ts +243 -0
- package/src/runs/background/async-execution.ts +46 -24
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +11 -2
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +41 -2
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/run-child-session.ts +613 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +125 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +203 -898
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +196 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +121 -58
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/acceptance.ts +10 -0
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +342 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +82 -14
- package/src/runs/shared/model-fallback.ts +47 -4
- package/src/runs/shared/nested-events.ts +29 -45
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +0 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +6 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
- package/src/runs/shared/worktree.ts +117 -30
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +64 -30
- package/src/shared/utils.ts +3 -4
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +238 -31
- package/src/watchdog/child-status.ts +54 -34
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +28 -25
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/workflows/workflow-checklist.ts +10 -12
- package/src/workflows/workflow-preflight.ts +28 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1062
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- 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
|
+
}
|