pi-subagents 0.67.0 → 0.69.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 +90 -0
- package/README.md +1 -1
- package/docs/agents.md +41 -12
- package/docs/configuration.md +61 -19
- package/docs/extension-api.md +5 -1
- package/docs/missions.md +2 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +18 -8
- package/docs/standalone-background.md +13 -3
- package/docs/tool-reference.md +38 -14
- package/docs/watchdog.md +10 -12
- package/docs/workflows.md +59 -1
- package/index.ts +5 -2
- package/package.json +4 -2
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +25 -11
- package/skills/pi-subagents/SKILL.md +18 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +6 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +185 -72
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/agents/skills.ts +1 -1
- package/src/api/preflight.ts +20 -16
- package/src/api/required-child-extensions.ts +6 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +3 -0
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +42 -31
- package/src/extension/public-execution.ts +3 -3
- package/src/extension/schemas.ts +23 -6
- package/src/extension/tool-description.ts +8 -7
- package/src/inspectors/ghostty/plugin.ts +13 -1
- package/src/intercom/native-supervisor-channel.ts +22 -18
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +309 -126
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +13 -4
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +20 -10
- package/src/runs/background/binary-bootstrap.ts +5 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/notify.ts +74 -6
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +20 -29
- package/src/runs/background/runner-aliases.ts +4 -33
- package/src/runs/background/runner-child-launch.ts +4 -1
- package/src/runs/background/runner-child-sessions.ts +2 -2
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +11 -5
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/subagent-runner.ts +413 -276
- package/src/runs/background/subagent-wait.ts +128 -23
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +4 -2
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +115 -219
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/subagent-executor.ts +281 -80
- package/src/runs/shared/acceptance.ts +194 -37
- package/src/runs/shared/async-status-projection.ts +123 -33
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +19 -6
- package/src/runs/shared/child-runtime-config.ts +5 -0
- package/src/runs/shared/child-session.ts +94 -50
- package/src/runs/shared/child-tool-plan.ts +28 -16
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +30 -8
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/launch-contract.ts +6 -9
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +81 -29
- package/src/slash/slash-commands.ts +0 -6
- package/src/slash/subagents-admin.ts +13 -9
- package/src/tui/render.ts +20 -10
- package/src/watchdog/child-status.ts +28 -36
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +1 -1
- package/src/watchdog/register-child.ts +10 -3
- package/src/watchdog/register-main.ts +20 -20
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +14 -30
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +23 -12
- package/src/watchdog/settings.ts +3 -6
- package/src/watchdog/types.ts +3 -5
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +68 -7
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
3
3
|
import { resolveChildCwd } from "../../shared/utils.ts";
|
|
4
|
-
import type { OutputMode } from "../../shared/types.ts";
|
|
4
|
+
import type { JsonSchemaObject, OutputMode } from "../../shared/types.ts";
|
|
5
5
|
import { resolveSingleOutputPath } from "./single-output.ts";
|
|
6
6
|
|
|
7
7
|
export interface ResolvedStepBehavior {
|
|
@@ -12,6 +12,7 @@ export interface ResolvedStepBehavior {
|
|
|
12
12
|
skills: string[] | false;
|
|
13
13
|
model?: string;
|
|
14
14
|
fast?: boolean;
|
|
15
|
+
outputSchema?: JsonSchemaObject;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export type OutputOverrideInput = string | boolean;
|
|
@@ -24,6 +25,7 @@ export interface StepOverrides {
|
|
|
24
25
|
skills?: string[] | false;
|
|
25
26
|
model?: string;
|
|
26
27
|
fast?: boolean;
|
|
28
|
+
outputSchema?: JsonSchemaObject | false;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface ChildLaunchPlanInput {
|
|
@@ -35,6 +37,7 @@ export interface ChildLaunchPlanInput {
|
|
|
35
37
|
runtimeCwd: string;
|
|
36
38
|
stepCwdInput?: string;
|
|
37
39
|
behaviorCwd?: string;
|
|
40
|
+
machineCwd?: string;
|
|
38
41
|
chainSkills?: string[];
|
|
39
42
|
outputBaseDir?: string;
|
|
40
43
|
parallelOutputNamespace?: { stepIndex: number; taskIndex?: number };
|
|
@@ -58,6 +61,14 @@ export function normalizeOutputOverride(output: unknown): string | false | undef
|
|
|
58
61
|
return typeof output === "string" && output.length > 0 ? output : undefined;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
export const resolveEffectiveOutputSchema = (agentConfig: AgentConfig, override?: JsonSchemaObject | false): JsonSchemaObject | undefined => override === false ? undefined : override !== undefined ? override : agentConfig.outputSchema;
|
|
65
|
+
|
|
66
|
+
type OutputSchemaStep = { agent: string; outputSchema?: JsonSchemaObject | false };
|
|
67
|
+
export function projectChainOutputSchemas<S extends OutputSchemaStep, R = S, G = { parallel: R[] | R }>(chain: readonly (S | { parallel: S[] | S })[], agents: AgentConfig[], projectStep?: (step: S, outputSchema: JsonSchemaObject | undefined) => R, projectGroup?: (step: { parallel: S[] | S }, parallel: R[] | R) => G): Array<R | G> {
|
|
68
|
+
const project = (step: S): R => { const agent = agents.find((candidate) => candidate.name === step.agent); if (!agent && !projectStep) return step as unknown as R; const outputSchema = agent && resolveEffectiveOutputSchema(agent, step.outputSchema); return projectStep ? projectStep(step, outputSchema) : { ...step, outputSchema } as unknown as R; };
|
|
69
|
+
return chain.map((step) => { if (!("parallel" in step)) return project(step as S); const parallel = Array.isArray(step.parallel) ? step.parallel.map(project) : project(step.parallel as S); return projectGroup ? projectGroup(step, parallel) : { ...step, parallel } as unknown as G; });
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
export function resolveStepBehavior(
|
|
62
73
|
agentConfig: AgentConfig,
|
|
63
74
|
stepOverrides: StepOverrides,
|
|
@@ -94,7 +105,8 @@ export function resolveStepBehavior(
|
|
|
94
105
|
const outputMode = stepOverrides.outputMode ?? agentConfig.outputMode ?? "inline";
|
|
95
106
|
const model = stepOverrides.model ?? agentConfig.model;
|
|
96
107
|
const fast = stepOverrides.fast ?? agentConfig.fast;
|
|
97
|
-
|
|
108
|
+
const outputSchema = resolveEffectiveOutputSchema(agentConfig, stepOverrides.outputSchema);
|
|
109
|
+
return { output, outputMode, reads, progress, skills, model, fast, ...(outputSchema !== undefined ? { outputSchema } : {}) };
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
export function resolveTaskTextForFileUpdatePolicy(task: string | undefined, originalTask?: string): string | undefined {
|
|
@@ -117,7 +129,7 @@ export function suppressProgressForReadOnlyTask(behavior: ResolvedStepBehavior,
|
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
export function planChildLaunch(input: ChildLaunchPlanInput): ChildLaunchPlan {
|
|
120
|
-
const stepCwd = resolveChildCwd(input.runnerCwd, input.stepCwdInput);
|
|
132
|
+
const stepCwd = input.machineCwd ?? resolveChildCwd(input.runnerCwd, input.stepCwdInput);
|
|
121
133
|
const instructionCwd = input.behaviorCwd ?? stepCwd;
|
|
122
134
|
const readExistenceCwd = input.behaviorCwd ? stepCwd : instructionCwd;
|
|
123
135
|
let behavior = suppressProgressForReadOnlyTask(
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type LaunchResolvedChildExtensions,
|
|
15
15
|
type ResolvedToolBudget,
|
|
16
16
|
type RunFanoutBudgetDescriptor,
|
|
17
|
+
type HerdrMachineReference,
|
|
17
18
|
} from "../../shared/types.ts";
|
|
18
19
|
import type { NestedPathEntry } from "./nested-path.ts";
|
|
19
20
|
import type { McpRuntimeSnapshotHost } from "./mcp-direct-tool-allowlist.ts";
|
|
@@ -35,6 +36,7 @@ import { createCapturedChildHooks, withChildSessionErrorReporting } from "./chil
|
|
|
35
36
|
import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
36
37
|
import type { ChildSessionLaunch, ChildSessionStorage } from "./child-session.ts";
|
|
37
38
|
import type { ArbiterModelContext } from "./llm-intent-arbiter.ts";
|
|
39
|
+
import { resolveRequiredChildExtensions, type RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
|
|
38
40
|
|
|
39
41
|
/** Environment variable pi-mcp-adapter reads for the tools a child may expose. */
|
|
40
42
|
export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
|
|
@@ -44,7 +46,7 @@ export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
|
|
|
44
46
|
* launches inherits. Serialized into the background runner config; the
|
|
45
47
|
* foreground path passes the executor's full `ChildRuntimeConfig`.
|
|
46
48
|
*/
|
|
47
|
-
export type InheritedChildRuntime = Pick<ChildRuntimeConfig, "depth" | "maxDepth" | "nestedRoute" | "nestedParent" | "capabilityCeiling" | "thinkingCeiling" | "runFanoutBudget">;
|
|
49
|
+
export type InheritedChildRuntime = Pick<ChildRuntimeConfig, "depth" | "maxDepth" | "nestedRoute" | "nestedParent" | "capabilityCeiling" | "thinkingCeiling" | "runFanoutBudget" | "requiredExtensions">;
|
|
48
50
|
|
|
49
51
|
export function inheritedChildRuntime(config: ChildRuntimeConfig | undefined): InheritedChildRuntime | undefined {
|
|
50
52
|
if (!config) return undefined;
|
|
@@ -56,10 +58,14 @@ export function inheritedChildRuntime(config: ChildRuntimeConfig | undefined): I
|
|
|
56
58
|
...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
|
|
57
59
|
...(config.thinkingCeiling ? { thinkingCeiling: config.thinkingCeiling } : {}),
|
|
58
60
|
...(config.runFanoutBudget ? { runFanoutBudget: config.runFanoutBudget } : {}),
|
|
61
|
+
...(config.requiredExtensions ? { requiredExtensions: config.requiredExtensions } : {}),
|
|
59
62
|
};
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
export interface BuildInProcessChildLaunchInput {
|
|
66
|
+
machine?: HerdrMachineReference;
|
|
67
|
+
remoteSkillNames?: string[];
|
|
68
|
+
remoteReads?: string[] | false;
|
|
63
69
|
parentSessionId?: string;
|
|
64
70
|
forkCacheKey?: string;
|
|
65
71
|
sessionEnabled: boolean;
|
|
@@ -76,6 +82,8 @@ export interface BuildInProcessChildLaunchInput {
|
|
|
76
82
|
excludeTools?: string[];
|
|
77
83
|
extensions?: string[];
|
|
78
84
|
subagentOnlyExtensions?: string[];
|
|
85
|
+
/** Serialized launch snapshot; omitted only for a top-level parent-process lookup. */
|
|
86
|
+
requiredExtensions?: RequiredChildExtensionSnapshot;
|
|
79
87
|
systemPrompt?: string | null;
|
|
80
88
|
mcpDirectTools?: string[];
|
|
81
89
|
extensionBindings?: ExtensionBindings;
|
|
@@ -90,7 +98,6 @@ export interface BuildInProcessChildLaunchInput {
|
|
|
90
98
|
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
91
99
|
structuredOutput?: StructuredOutputRuntime;
|
|
92
100
|
fast?: boolean;
|
|
93
|
-
modelCandidates?: readonly string[];
|
|
94
101
|
toolBudget?: ResolvedToolBudget;
|
|
95
102
|
permissionRules?: PermissionRules;
|
|
96
103
|
permissionAuditPath?: string;
|
|
@@ -113,9 +120,9 @@ export interface BuildInProcessChildLaunchInput {
|
|
|
113
120
|
*/
|
|
114
121
|
host: "parent" | "runner";
|
|
115
122
|
/**
|
|
116
|
-
*
|
|
117
|
-
* intersect
|
|
118
|
-
*
|
|
123
|
+
* Pi core tool names the host runtime provides. When set, child tool plans
|
|
124
|
+
* intersect known core slots with this set; declared non-core names remain
|
|
125
|
+
* for child startup validation. Review/scout lanes fail closed when a requested,
|
|
119
126
|
* still-permitted repository inspection tool is missing from that set.
|
|
120
127
|
*/
|
|
121
128
|
hostAvailableBuiltins?: readonly string[];
|
|
@@ -182,19 +189,20 @@ function childStorage(input: BuildInProcessChildLaunchInput): ChildSessionStorag
|
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput): InProcessChildLaunch {
|
|
192
|
+
const requiredExtensions = input.requiredExtensions ?? input.inherited?.requiredExtensions ?? resolveRequiredChildExtensions(input.parentSessionId);
|
|
185
193
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
186
194
|
tools: input.tools,
|
|
187
195
|
excludeTools: input.excludeTools,
|
|
188
196
|
allowNestedSubagents: input.allowNestedSubagents,
|
|
189
197
|
extensions: input.extensions,
|
|
190
198
|
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
199
|
+
requiredExtensions,
|
|
191
200
|
mcpDirectTools: input.mcpDirectTools,
|
|
192
201
|
cwd: input.cwd,
|
|
193
202
|
requireReadTool: input.requireReadTool,
|
|
194
203
|
structuredOutput: Boolean(input.structuredOutput),
|
|
195
204
|
fast: input.fast,
|
|
196
205
|
model: input.model,
|
|
197
|
-
modelCandidates: input.modelCandidates,
|
|
198
206
|
capabilityCeiling: input.capabilityCeiling,
|
|
199
207
|
inheritedCapabilityCeiling: inheritedCapabilityCeiling(input.inherited),
|
|
200
208
|
agentName: input.childAgentName,
|
|
@@ -248,6 +256,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
248
256
|
maxDepth: childDepth.maxDepth,
|
|
249
257
|
...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}),
|
|
250
258
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
259
|
+
...(toolPlan.requiredExtensions.length > 0 ? { requiredExtensions: toolPlan.requiredExtensions } : {}),
|
|
251
260
|
inheritProjectContext: input.inheritProjectContext,
|
|
252
261
|
inheritGlobalContext: input.inheritGlobalContext,
|
|
253
262
|
inheritSkills: input.inheritSkills,
|
|
@@ -289,17 +298,21 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
289
298
|
configuredExtensions: toolPlan.configuredExtensions,
|
|
290
299
|
extensionArgs: toolPlan.extensionArgs,
|
|
291
300
|
disableAmbientExtensions: !ambientExtensions,
|
|
301
|
+
requiredExtensions: toolPlan.requiredExtensions,
|
|
292
302
|
});
|
|
293
303
|
const taggedPrompt = input.systemPrompt !== undefined && input.systemPrompt !== null
|
|
294
304
|
? `<active_agent name="${escapeXmlAttr(input.childAgentName)}"/>\n\n${input.systemPrompt}`
|
|
295
305
|
: undefined;
|
|
296
306
|
const session: Omit<ChildSessionLaunch, "onExtensionError"> = {
|
|
297
307
|
cwd: input.cwd,
|
|
308
|
+
...(input.machine ? { machine: input.machine } : {}),
|
|
309
|
+
...(input.machine ? { remoteResources: { agent: input.childAgentName, ...(input.remoteSkillNames ? { skills: input.remoteSkillNames } : {}), ...(input.remoteReads !== undefined ? { reads: input.remoteReads } : {}), ...(toolPlan.explicitToolAllowlist ? { toolCeiling: [...toolPlan.effectiveToolAllowlist] } : toolPlan.capabilityCeiling?.allowedTools ? { toolCeiling: [...toolPlan.capabilityCeiling.allowedTools] } : {}) } } : {}),
|
|
298
310
|
storage: childStorage(input),
|
|
299
311
|
...(input.model ? { model: input.model } : {}),
|
|
300
312
|
...(toolPlan.explicitToolAllowlist ? { tools: toolPlan.effectiveToolAllowlist } : {}),
|
|
301
313
|
...(!toolPlan.explicitToolAllowlist && toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
302
314
|
extensionPaths,
|
|
315
|
+
requiredExtensions: toolPlan.requiredExtensions,
|
|
303
316
|
ambientExtensions,
|
|
304
317
|
hooks: capturedHooks.hooks,
|
|
305
318
|
...(input.host === "runner" ? { processEnv: childProcessEnv(input, toolPlan) } : {}),
|
|
@@ -6,6 +6,7 @@ import type { ChildWatchdogConfig, ChildWatchdogStatusEvent } from "../../watchd
|
|
|
6
6
|
import type { ResolvedWaitToolConfig } from "../background/wait-config.ts";
|
|
7
7
|
import type { ChildToolDiagnostic } from "./tool-availability.ts";
|
|
8
8
|
import type { ResolvedSubagentCapabilityCeiling } from "./capability-ceiling.ts";
|
|
9
|
+
import type { RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Set in processes that host child sessions (the async runner). The extension
|
|
@@ -75,6 +76,8 @@ export interface ChildRuntimeConfig {
|
|
|
75
76
|
depth: number;
|
|
76
77
|
maxDepth?: number;
|
|
77
78
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
79
|
+
/** Immutable root-parent host policy propagated to nested native launches. */
|
|
80
|
+
requiredExtensions?: RequiredChildExtensionSnapshot;
|
|
78
81
|
thinkingCeiling?: ThinkingLevel;
|
|
79
82
|
inheritProjectContext?: boolean;
|
|
80
83
|
inheritGlobalContext?: boolean;
|
|
@@ -88,6 +91,8 @@ export interface ChildRuntimeConfig {
|
|
|
88
91
|
waitTool: ResolvedWaitToolConfig;
|
|
89
92
|
runtimeState?: SubagentState;
|
|
90
93
|
holdFinalDrain?: (held: boolean) => void;
|
|
94
|
+
/** Installation-local downward owner-channel barrier; never inherited or serialized into descendants. */
|
|
95
|
+
hasPendingSupervisorRequest?: () => boolean;
|
|
91
96
|
structuredOutput?: ChildStructuredOutput;
|
|
92
97
|
requiredTools?: string[];
|
|
93
98
|
mcpDirectTools?: string[];
|
|
@@ -5,21 +5,15 @@
|
|
|
5
5
|
* parent pi process for foreground children, the detached runner process for
|
|
6
6
|
* background children. The factory is injectable so tests can script a child
|
|
7
7
|
* without the real runtime; the default implementation wraps
|
|
8
|
-
* `createAgentSession` from a pi package module
|
|
9
|
-
* across every child it creates.
|
|
8
|
+
* `createAgentSession` from a pi package module.
|
|
10
9
|
*/
|
|
11
10
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
12
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { pinChildCacheRetention } from "../../shared/child-cache-retention.ts";
|
|
13
13
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
14
14
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
// Private runtime authority for host continuation planning; injected factories have none.
|
|
19
|
-
const readonlyModels = new WeakMap<ChildSession, { current: ModelInfo; resolve(reference: string): ModelInfo | undefined; requestBytes: number }>();
|
|
20
|
-
export function getReadonlyChildModels(child: ChildSession) {
|
|
21
|
-
return readonlyModels.get(child);
|
|
22
|
-
}
|
|
15
|
+
import type { RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
|
|
16
|
+
import type { HerdrMachineReference, HerdrRemoteGitStatus } from "../../shared/types.ts";
|
|
23
17
|
|
|
24
18
|
export interface ChildSessionEvent {
|
|
25
19
|
type: string;
|
|
@@ -54,6 +48,12 @@ export type ChildSessionStorage =
|
|
|
54
48
|
|
|
55
49
|
export interface ChildSessionLaunch {
|
|
56
50
|
cwd: string;
|
|
51
|
+
/** Resolved pane-native placement. Local launches omit this field. */
|
|
52
|
+
machine?: HerdrMachineReference;
|
|
53
|
+
/** Process-local provider source owned by the invoking foreground parent. */
|
|
54
|
+
parentProviderRegistry?: ParentProviderRegistry;
|
|
55
|
+
/** Logical names resolved only by the remote ambient package. */
|
|
56
|
+
remoteResources?: { agent: string; skills?: string[]; toolCeiling?: string[]; reads?: string[] | false };
|
|
57
57
|
storage: ChildSessionStorage;
|
|
58
58
|
/** Model reference as the agent config names it (`provider/id`, optionally `:thinking`). */
|
|
59
59
|
model?: string;
|
|
@@ -62,6 +62,8 @@ export interface ChildSessionLaunch {
|
|
|
62
62
|
excludeTools?: string[];
|
|
63
63
|
/** Extension files loaded for this child in addition to the inline hooks. */
|
|
64
64
|
extensionPaths: string[];
|
|
65
|
+
/** Canonical required paths and safe evidence identities for fail-closed loading. */
|
|
66
|
+
requiredExtensions?: RequiredChildExtensionSnapshot;
|
|
65
67
|
/**
|
|
66
68
|
* Discover the ambient extensions (agent dir, project, settings) the way a
|
|
67
69
|
* `pi` process would. False loads only `extensionPaths` and `hooks`.
|
|
@@ -99,6 +101,9 @@ export interface ChildSession {
|
|
|
99
101
|
readonly sessionFile: string | undefined;
|
|
100
102
|
readonly sessionId: string;
|
|
101
103
|
readonly modelId: string | undefined;
|
|
104
|
+
readonly machineEvidence?: { machineId: string; initial?: HerdrRemoteGitStatus; final?: HerdrRemoteGitStatus };
|
|
105
|
+
/** Event-updated pane-native status; reading it performs no network work. */
|
|
106
|
+
readonly placementSnapshot?: unknown;
|
|
102
107
|
/** Set by the foreground host once the run detached; `factory.dispose()` leaves such children running. */
|
|
103
108
|
detached?: boolean;
|
|
104
109
|
/** Set by `factory.dispose()` before it aborts the child, so the host can report the stop truthfully. */
|
|
@@ -134,6 +139,34 @@ export interface DefaultChildSessionFactoryOptions {
|
|
|
134
139
|
|
|
135
140
|
type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
|
|
136
141
|
|
|
142
|
+
export type ParentProviderRegistry = Pick<ModelRuntimeInstance, "getRegisteredProviderIds" | "getRegisteredProviderConfig" | "getRegisteredNativeProvider">;
|
|
143
|
+
|
|
144
|
+
function inheritParentProviders(modelRuntime: ModelRuntimeInstance, parentProviders: ParentProviderRegistry, claimedProviderIds: ReadonlySet<string>, onError: ((error: ChildSessionExtensionError) => void) | undefined): boolean {
|
|
145
|
+
let providerIds: readonly string[];
|
|
146
|
+
try {
|
|
147
|
+
providerIds = parentProviders.getRegisteredProviderIds();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
onError?.({ extensionPath: "<parent-providers>", event: "inherit_provider", error });
|
|
150
|
+
throw new Error(`Failed to enumerate parent providers: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
151
|
+
}
|
|
152
|
+
let registered = false;
|
|
153
|
+
for (const providerId of new Set(providerIds)) {
|
|
154
|
+
if (claimedProviderIds.has(providerId)) continue;
|
|
155
|
+
try {
|
|
156
|
+
const native = parentProviders.getRegisteredNativeProvider(providerId);
|
|
157
|
+
const config = native ? undefined : parentProviders.getRegisteredProviderConfig(providerId);
|
|
158
|
+
if (native) modelRuntime.registerNativeProvider(native);
|
|
159
|
+
else if (config) modelRuntime.registerProvider(providerId, config);
|
|
160
|
+
else throw new Error(`Parent provider '${providerId}' has no registered native provider or config.`);
|
|
161
|
+
registered = true;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
onError?.({ extensionPath: `<parent-provider:${providerId}>`, event: "inherit_provider", error });
|
|
164
|
+
throw new Error(`Failed to inherit parent provider '${providerId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return registered;
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
const CHILD_PROMPT_RUNTIME_EXTENSION_PATH = "<inline:pi-subagents:prompt-runtime>";
|
|
138
171
|
|
|
139
172
|
/** The prompt runtime filters parent-only context before ambient extensions inspect
|
|
@@ -173,34 +206,39 @@ function applyProcessEnv(values: Record<string, string | undefined> | undefined)
|
|
|
173
206
|
}
|
|
174
207
|
}
|
|
175
208
|
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
function flushQueuedProviderRegistrations(loader: InstanceType<PiCodingAgentModule["DefaultResourceLoader"]>, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined, requiredPaths: ReadonlySet<string>): { claimedProviderIds: Set<string>; registered: boolean } {
|
|
210
|
+
const claimedProviderIds = new Set<string>();
|
|
211
|
+
if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function") return { claimedProviderIds, registered: false };
|
|
178
212
|
const { runtime } = loader.getExtensions();
|
|
179
213
|
let registered = false;
|
|
180
214
|
for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations ?? []) {
|
|
215
|
+
claimedProviderIds.add(name);
|
|
181
216
|
try {
|
|
182
217
|
modelRuntime.registerProvider(name, config);
|
|
183
218
|
registered = true;
|
|
184
219
|
} catch (error) {
|
|
185
220
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
221
|
+
if (requiredPaths.has(extensionPath)) throw new Error(`Required child extension provider registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
186
222
|
}
|
|
187
223
|
}
|
|
188
224
|
if (Array.isArray(runtime.pendingProviderRegistrations)) runtime.pendingProviderRegistrations = [];
|
|
189
225
|
for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations ?? []) {
|
|
226
|
+
claimedProviderIds.add(provider.id);
|
|
190
227
|
try {
|
|
191
228
|
modelRuntime.registerNativeProvider(provider);
|
|
192
229
|
registered = true;
|
|
193
230
|
} catch (error) {
|
|
194
231
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
232
|
+
if (requiredPaths.has(extensionPath)) throw new Error(`Required child extension provider registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
195
233
|
}
|
|
196
234
|
}
|
|
197
235
|
if (Array.isArray(runtime.pendingNativeProviderRegistrations)) runtime.pendingNativeProviderRegistrations = [];
|
|
198
|
-
|
|
236
|
+
return { claimedProviderIds, registered };
|
|
199
237
|
}
|
|
200
238
|
|
|
201
239
|
/**
|
|
202
|
-
* Default factory:
|
|
203
|
-
*
|
|
240
|
+
* Default factory: detached/background sessions retain the existing shared
|
|
241
|
+
* runtime; each parent-bound foreground launch gets an isolated runtime.
|
|
204
242
|
*/
|
|
205
243
|
export function createDefaultChildSessionFactory(options: DefaultChildSessionFactoryOptions = {}): ChildSessionFactory {
|
|
206
244
|
const loadPiCodingAgent = options.loadPiCodingAgent ?? (() => import("@earendil-works/pi-coding-agent"));
|
|
@@ -218,9 +256,10 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
218
256
|
};
|
|
219
257
|
return {
|
|
220
258
|
async create(launch) {
|
|
221
|
-
const observeReadonly = prepareReadonlySessionEvidence(launch);
|
|
222
259
|
const pi = await loadPiCodingAgent();
|
|
223
|
-
const modelRuntime =
|
|
260
|
+
const modelRuntime = launch.parentProviderRegistry
|
|
261
|
+
? await pi.ModelRuntime.create()
|
|
262
|
+
: await sharedRuntime(pi);
|
|
224
263
|
const agentDir = getAgentDir();
|
|
225
264
|
const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
|
|
226
265
|
// Foreground children share Pi's global theme with the parent, so reinitializing it
|
|
@@ -245,13 +284,25 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
245
284
|
...(launch.appendSystemPrompt !== undefined ? { appendSystemPrompt: [launch.appendSystemPrompt] } : {}),
|
|
246
285
|
});
|
|
247
286
|
const open = async () => {
|
|
287
|
+
const requiredPaths = new Set((launch.requiredExtensions ?? []).map(({ path }) => path));
|
|
248
288
|
applyProcessEnv(launch.processEnv);
|
|
249
289
|
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.") });
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
290
|
+
await loader.reload();
|
|
291
|
+
const loadErrors = requiredPaths.size > 0
|
|
292
|
+
? loader.getExtensions().errors.filter(({ path }) => requiredPaths.has(path)) : [];
|
|
293
|
+
if (loadErrors.length > 0) throw new Error(`Required child extension failed to load: ${loadErrors.map(({ path, error }) => `${path}: ${error}`).join("; ")}`);
|
|
294
|
+
const queued = flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError, requiredPaths);
|
|
295
|
+
const inherited = launch.parentProviderRegistry
|
|
296
|
+
? inheritParentProviders(modelRuntime, launch.parentProviderRegistry, queued.claimedProviderIds, launch.onExtensionError)
|
|
297
|
+
: false;
|
|
298
|
+
if (queued.registered || inherited) {
|
|
299
|
+
try {
|
|
300
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
301
|
+
} catch (error) {
|
|
302
|
+
launch.onExtensionError?.({ extensionPath: "<provider-refresh>", event: "refresh_providers", error });
|
|
303
|
+
throw new Error(`Failed to refresh child providers: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
255
306
|
const sessionManager = launch.storage.kind === "file"
|
|
256
307
|
? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
|
|
257
308
|
: launch.storage.kind === "dir"
|
|
@@ -259,7 +310,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
259
310
|
: launch.storage.kind === "memory"
|
|
260
311
|
? pi.SessionManager.inMemory(launch.cwd)
|
|
261
312
|
: pi.SessionManager.create(launch.cwd);
|
|
262
|
-
observeReadonly?.opened(sessionManager);
|
|
263
313
|
const resolvedModel = launch.model
|
|
264
314
|
? pi.resolveCliModel({ cliModel: launch.model, modelRuntime })
|
|
265
315
|
: undefined;
|
|
@@ -277,6 +327,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
277
327
|
settingsManager,
|
|
278
328
|
sessionStartEvent: { type: "session_start", reason: "startup" },
|
|
279
329
|
});
|
|
330
|
+
pinChildCacheRetention(session.agent);
|
|
280
331
|
try {
|
|
281
332
|
await session.bindExtensions({
|
|
282
333
|
mode: "print",
|
|
@@ -291,9 +342,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
291
342
|
const opened = loading.catch(() => {}).then(open);
|
|
292
343
|
loading = opened;
|
|
293
344
|
const session = await opened;
|
|
294
|
-
let evidence: ReturnType<NonNullable<typeof observeReadonly>["observe"]>;
|
|
295
|
-
try { evidence = observeReadonly?.observe(pi, modelRuntime, session); }
|
|
296
|
-
catch (error) { session.dispose(); throw error; }
|
|
297
345
|
let pending: Promise<void> | undefined;
|
|
298
346
|
// pi's own hosts emit `session_shutdown` before disposing a session so the
|
|
299
347
|
// extensions loaded into it (ambient extensions included) release their
|
|
@@ -302,28 +350,20 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
302
350
|
try {
|
|
303
351
|
const runner = session.extensionRunner;
|
|
304
352
|
if (runner.hasHandlers("session_shutdown")) {
|
|
305
|
-
|
|
306
|
-
const settled = await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }).then(() => true), new Promise<false>((resolve) => setTimeout(() => resolve(false), shutdownTimeoutMs).unref?.())]);
|
|
307
|
-
if (!settled) evidence?.invalidate();
|
|
353
|
+
await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }), new Promise<void>((resolve) => setTimeout(resolve, shutdownTimeoutMs).unref?.())]);
|
|
308
354
|
}
|
|
309
355
|
} catch (error) {
|
|
310
|
-
evidence?.invalidate();
|
|
311
356
|
launch.onExtensionError?.({ extensionPath: "<session>", event: "session_shutdown", error });
|
|
312
357
|
} finally {
|
|
313
358
|
session.dispose();
|
|
314
|
-
evidence?.finish(child);
|
|
315
359
|
}
|
|
316
360
|
};
|
|
317
361
|
const child: ChildSession = {
|
|
318
362
|
subscribe: (listener) => session.subscribe((event) => listener(event as unknown as ChildSessionEvent)),
|
|
319
|
-
prompt: (text) =>
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
},
|
|
324
|
-
steer: (text) => { evidence?.invalidate(); return session.steer(text); },
|
|
325
|
-
followUp: (text) => { evidence?.invalidate(); return session.followUp(text); },
|
|
326
|
-
abort: () => { evidence?.invalidate(); return session.abort(); },
|
|
363
|
+
prompt: (text) => session.prompt(text),
|
|
364
|
+
steer: (text) => session.steer(text),
|
|
365
|
+
followUp: (text) => session.followUp(text),
|
|
366
|
+
abort: () => session.abort(),
|
|
327
367
|
hasQueuedMessages: () => session.agent?.hasQueuedMessages?.() === true,
|
|
328
368
|
dispose: () => {
|
|
329
369
|
if (!pending) {
|
|
@@ -340,16 +380,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
340
380
|
get sessionId() { return session.sessionId; },
|
|
341
381
|
get modelId() { return session.model ? `${session.model.provider}/${session.model.id}` : undefined; },
|
|
342
382
|
};
|
|
343
|
-
if (evidence && session.model) readonlyModels.set(child, {
|
|
344
|
-
current: toModelInfo(session.model),
|
|
345
|
-
requestBytes: Buffer.byteLength(session.systemPrompt) + Buffer.byteLength(JSON.stringify(session.agent.state.tools)),
|
|
346
|
-
resolve(reference) {
|
|
347
|
-
try {
|
|
348
|
-
const resolved = pi.resolveCliModel({ cliModel: reference, modelRuntime });
|
|
349
|
-
return !resolved.error && resolved.model ? toModelInfo(resolved.model) : undefined;
|
|
350
|
-
} catch { return undefined; }
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
383
|
live.add(child);
|
|
354
384
|
return child;
|
|
355
385
|
},
|
|
@@ -371,10 +401,24 @@ let activeFactoryModule: string | undefined;
|
|
|
371
401
|
|
|
372
402
|
/** The process-wide factory foreground runs use unless a run passes its own. */
|
|
373
403
|
export function childSessionFactory(): ChildSessionFactory {
|
|
374
|
-
activeFactory ??= createDefaultChildSessionFactory();
|
|
404
|
+
activeFactory ??= createLazyPlacementFactory(createDefaultChildSessionFactory());
|
|
375
405
|
return activeFactory;
|
|
376
406
|
}
|
|
377
407
|
|
|
408
|
+
function createLazyPlacementFactory(local: ChildSessionFactory): ChildSessionFactory {
|
|
409
|
+
let placed: ChildSessionFactory | undefined;
|
|
410
|
+
const factory = async () => placed ??= (await import("./herdr-placed-run.ts")).createPlacementAwareChildSessionFactory(local);
|
|
411
|
+
return {
|
|
412
|
+
async create(launch) { return launch.machine ? (await factory()).create(launch) : local.create(launch); },
|
|
413
|
+
async dispose() { if (placed) await placed.dispose(); else await local.dispose(); },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Default factory including pane-native placement; detached runners use the same boundary. */
|
|
418
|
+
export function createPlacementChildSessionFactory(options: DefaultChildSessionFactoryOptions = {}): ChildSessionFactory {
|
|
419
|
+
return createLazyPlacementFactory(createDefaultChildSessionFactory(options));
|
|
420
|
+
}
|
|
421
|
+
|
|
378
422
|
/**
|
|
379
423
|
* Replace the process-wide factory. Tests install a scripted factory; passing
|
|
380
424
|
* undefined restores the default on next use.
|