pi-subagents 0.64.0 → 0.65.1
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 +65 -0
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +19 -17
- package/docs/configuration.md +23 -15
- package/docs/extension-api.md +18 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +15 -8
- package/docs/watchdog.md +1 -1
- package/docs/workflows.md +13 -3
- package/package.json +5 -4
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +7 -3
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- 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 +7 -4
- package/src/extension/config.ts +4 -2
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +58 -7
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +3 -2
- package/src/extension/tool-description.ts +13 -8
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +159 -95
- package/src/intercom/supervisor-ui.ts +244 -0
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/async-execution.ts +51 -25
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +14 -3
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +2 -0
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +614 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +152 -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 -901
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +204 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +161 -91
- 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 +14 -1
- 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 +337 -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/llm-intent-arbiter.ts +20 -11
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +84 -15
- package/src/runs/shared/model-fallback.ts +78 -6
- package/src/runs/shared/nested-events.ts +32 -48
- 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 +1 -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 +2 -0
- 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 +31 -9
- package/src/runs/shared/worktree.ts +192 -40
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +63 -28
- package/src/shared/utils.ts +6 -4
- package/src/shared/watch-strategy.ts +2 -0
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +254 -41
- package/src/watchdog/child-status.ts +0 -1
- package/src/watchdog/register-child.ts +12 -12
- package/src/workflows/scripted-workflow.ts +80 -7
- package/src/workflows/workflow-checklist.ts +4 -3
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1059
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
4
|
import type { Details, ForegroundRunControl, SubagentState } from "../../shared/types.ts";
|
|
6
5
|
import { readStatus } from "../../shared/utils.ts";
|
|
7
6
|
import { steeringReceipt } from "../background/steering.ts";
|
|
8
|
-
import {
|
|
9
|
-
consumeSteerAckFromDir,
|
|
10
|
-
readSteerCapability,
|
|
11
|
-
steerAcksDir,
|
|
12
|
-
steerCapabilityPath,
|
|
13
|
-
stepSteerInboxDir,
|
|
14
|
-
writeSteerRequestToExistingDir,
|
|
15
|
-
type SteerDeliveryMode,
|
|
16
|
-
type SteerRequest,
|
|
17
|
-
} from "../background/control-channel.ts";
|
|
7
|
+
import type { SteerDeliveryMode } from "../background/control-channel.ts";
|
|
18
8
|
|
|
19
9
|
export interface WorkflowForegroundSteeringTarget {
|
|
20
10
|
control: ForegroundRunControl;
|
|
@@ -40,7 +30,6 @@ function activeWorkflowError(state: SubagentState, workflowRunId: string, asyncD
|
|
|
40
30
|
function controlIsLiveInWorkflow(control: ForegroundRunControl, workflowRunId: string, sessionId: string): boolean {
|
|
41
31
|
return control.parentWorkflowRunId === workflowRunId
|
|
42
32
|
&& control.sessionId === sessionId
|
|
43
|
-
&& Boolean(control.workflowSteeringDir)
|
|
44
33
|
&& (control.activeChildren?.size ?? 0) > 0;
|
|
45
34
|
}
|
|
46
35
|
|
|
@@ -77,17 +66,18 @@ function managementError(message: string): AgentToolResult<Details> {
|
|
|
77
66
|
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
78
67
|
}
|
|
79
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Steer a live workflow-owned foreground child through its in-process session.
|
|
71
|
+
* `steer` (and `auto`) interrupt the child at its next safe point; `follow_up`
|
|
72
|
+
* queues the message until the current run settles.
|
|
73
|
+
*/
|
|
80
74
|
export async function steerWorkflowForegroundTarget(input: {
|
|
81
75
|
target: WorkflowForegroundSteeringTarget;
|
|
82
76
|
message: string;
|
|
83
77
|
mode?: SteerDeliveryMode;
|
|
84
78
|
index?: number;
|
|
85
|
-
signal?: AbortSignal;
|
|
86
|
-
ackTimeoutMs?: number;
|
|
87
79
|
}): Promise<AgentToolResult<Details>> {
|
|
88
80
|
const { control, sourceRunId } = input.target;
|
|
89
|
-
const routeDir = control.workflowSteeringDir;
|
|
90
|
-
if (!routeDir || !fs.existsSync(routeDir)) return managementError(`Foreground run '${control.runId}' has no live workflow steering route.`);
|
|
91
81
|
const activeIndexes = [...(control.activeChildren?.keys() ?? [])].sort((left, right) => left - right);
|
|
92
82
|
const index = input.index ?? (activeIndexes.length === 1 ? activeIndexes[0] : undefined);
|
|
93
83
|
if (index === undefined) {
|
|
@@ -95,94 +85,30 @@ export async function steerWorkflowForegroundTarget(input: {
|
|
|
95
85
|
? `Foreground run '${control.runId}' has no live child session.`
|
|
96
86
|
: `Foreground run '${control.runId}' has ${activeIndexes.length} live child sessions; provide index.`);
|
|
97
87
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
88
|
+
const child = control.activeChildren?.get(index);
|
|
89
|
+
if (!child) return managementError(`Foreground run '${control.runId}' child ${index} is not live.`);
|
|
90
|
+
if (!child.steer) return managementError(`Foreground run '${control.runId}' child ${index} does not support steering.`);
|
|
101
91
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
targetIndex: index,
|
|
109
|
-
source: "steer-action",
|
|
110
|
-
};
|
|
111
|
-
try {
|
|
112
|
-
writeSteerRequestToExistingDir(stepSteerInboxDir(routeDir, index), request);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
115
|
-
return managementError(`Foreground run '${control.runId}' has no live workflow steering route.`);
|
|
116
|
-
}
|
|
117
|
-
return managementError(`Failed to queue steering for foreground run ${control.runId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const deadline = Date.now() + (input.ackTimeoutMs ?? 3_000);
|
|
121
|
-
let ack;
|
|
122
|
-
let routeRemoved = false;
|
|
123
|
-
while (Date.now() <= deadline) {
|
|
124
|
-
ack = consumeSteerAckFromDir(steerAcksDir(routeDir, index), request.id);
|
|
125
|
-
if (ack || input.signal?.aborted) break;
|
|
126
|
-
if (!fs.existsSync(routeDir)) {
|
|
127
|
-
routeRemoved = true;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
await new Promise<void>((resolve) => setTimeout(resolve, Math.min(50, Math.max(1, deadline - Date.now()))));
|
|
131
|
-
}
|
|
132
|
-
if (routeRemoved || (!ack && !input.signal?.aborted && !fs.existsSync(routeDir))) {
|
|
133
|
-
return managementError(`Foreground run '${control.runId}' has no live child session.`);
|
|
134
|
-
}
|
|
135
|
-
const target = ack?.state === "delivered"
|
|
136
|
-
? { index, state: "delivered" as const, deliveredAt: ack.ts }
|
|
137
|
-
: ack?.state === "queued"
|
|
92
|
+
const message = input.message.trim();
|
|
93
|
+
const requestId = randomUUID();
|
|
94
|
+
const outcome = await child.steer({ message, ...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}) });
|
|
95
|
+
const target = outcome.state === "delivered"
|
|
96
|
+
? { index, state: "delivered" as const, deliveredAt: Date.now() }
|
|
97
|
+
: outcome.state === "queued"
|
|
138
98
|
? { index, state: "queued" as const }
|
|
139
|
-
:
|
|
140
|
-
? { index, state: "failed" as const, reason: ack.message }
|
|
141
|
-
: { index, state: "pending" as const };
|
|
99
|
+
: { index, state: "failed" as const, reason: outcome.reason };
|
|
142
100
|
const steering = {
|
|
143
|
-
requestId
|
|
144
|
-
state:
|
|
145
|
-
deliveryStatus:
|
|
101
|
+
requestId,
|
|
102
|
+
state: outcome.state === "delivered" ? "delivered" as const : outcome.state === "failed" ? "failed" as const : "pending" as const,
|
|
103
|
+
deliveryStatus: outcome.state === "delivered" ? "delivered" as const : "queued" as const,
|
|
146
104
|
sourceRunId,
|
|
147
105
|
targets: [target],
|
|
148
106
|
};
|
|
149
|
-
if (
|
|
150
|
-
return { content: [{ type: "text", text: steeringReceipt(
|
|
151
|
-
}
|
|
152
|
-
if (ack?.state === "delivered") {
|
|
153
|
-
return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering delivered for foreground run ${control.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering } };
|
|
154
|
-
}
|
|
155
|
-
if (ack?.state === "queued") {
|
|
156
|
-
return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering queued for foreground run ${control.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering } };
|
|
107
|
+
if (outcome.state === "delivered") {
|
|
108
|
+
return { content: [{ type: "text", text: steeringReceipt(message, `Steering delivered for foreground run ${control.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering } };
|
|
157
109
|
}
|
|
158
|
-
if (
|
|
159
|
-
return { content: [{ type: "text", text: steeringReceipt(
|
|
110
|
+
if (outcome.state === "queued") {
|
|
111
|
+
return { content: [{ type: "text", text: steeringReceipt(message, `Steering queued for foreground run ${control.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering } };
|
|
160
112
|
}
|
|
161
|
-
return { content: [{ type: "text", text: steeringReceipt(
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function workflowForegroundSteeringDir(asyncDirRoot: string, workflowRunId: string, childRunId: string): string {
|
|
165
|
-
return path.join(asyncDirRoot, workflowRunId, "control", "workflow-foreground", childRunId);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function removeWorkflowForegroundSteeringRoute(control: ForegroundRunControl): void {
|
|
169
|
-
if (!control.workflowSteeringDir) return;
|
|
170
|
-
try {
|
|
171
|
-
fs.rmSync(control.workflowSteeringDir, { recursive: true, force: true });
|
|
172
|
-
} catch (error) {
|
|
173
|
-
console.warn(`[pi-subagents] Failed to remove workflow foreground steering route '${control.workflowSteeringDir}': ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function workflowForegroundSteeringLaunchOptions(control: ForegroundRunControl | undefined, index: number): Pick<import("../../shared/types.ts").RunSyncOptions, "steerInboxDir" | "steerCapabilityPath" | "steerAckDir"> {
|
|
178
|
-
if (!control?.workflowSteeringDir) return {};
|
|
179
|
-
const steerInboxDir = stepSteerInboxDir(control.workflowSteeringDir, index);
|
|
180
|
-
const steerAckDir = steerAcksDir(control.workflowSteeringDir, index);
|
|
181
|
-
fs.mkdirSync(steerInboxDir, { recursive: true });
|
|
182
|
-
fs.mkdirSync(steerAckDir, { recursive: true });
|
|
183
|
-
return {
|
|
184
|
-
steerInboxDir,
|
|
185
|
-
steerCapabilityPath: steerCapabilityPath(control.workflowSteeringDir, index),
|
|
186
|
-
steerAckDir,
|
|
187
|
-
};
|
|
113
|
+
return { content: [{ type: "text", text: steeringReceipt(message, `Steering failed for foreground run ${control.runId} (request ${requestId}): ${outcome.reason ?? "unknown error"}`) }], isError: true, details: { mode: "management", results: [], steering } };
|
|
188
114
|
}
|
|
@@ -101,9 +101,9 @@ export function planAbortRecovery(input: {
|
|
|
101
101
|
if (input.alreadyResumed) return settle("resume already attempted");
|
|
102
102
|
if (!input.sessionAvailable) return settle("retained session unavailable");
|
|
103
103
|
if (input.stopped || input.interrupted) return settle("explicit stop or interrupt");
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// that cleanup signal non-authoritative; explicit stop/interrupt still wins.
|
|
104
|
+
// An external-job runner reports a process signal when its terminal drain
|
|
105
|
+
// ends a child left open by a compaction abort. The compaction + settlement
|
|
106
|
+
// markers make that cleanup signal non-authoritative; explicit stop/interrupt still wins.
|
|
107
107
|
if (input.processSignal && !compactionAbortCandidate) return settle("process terminated by signal");
|
|
108
108
|
if (input.timedOut) return settle("elapsed timeout");
|
|
109
109
|
if (input.toolBudgetExhausted || input.usageBudgetExhausted) return settle("budget exhausted");
|
|
@@ -202,10 +202,23 @@ export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInp
|
|
|
202
202
|
return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
|
|
203
203
|
}
|
|
204
204
|
if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
|
|
205
|
-
if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
|
|
205
|
+
if (acceptance !== undefined && acceptance !== false) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(gate, acceptance) };
|
|
206
206
|
return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export function describeGateAcceptanceConflict(gate: unknown, acceptance: unknown): string {
|
|
210
|
+
const render = (value: unknown): string => {
|
|
211
|
+
let encoded: string;
|
|
212
|
+
try {
|
|
213
|
+
encoded = JSON.stringify(value) ?? String(value);
|
|
214
|
+
} catch {
|
|
215
|
+
encoded = String(value);
|
|
216
|
+
}
|
|
217
|
+
return encoded.length > 120 ? `${encoded.slice(0, 120)}...` : encoded;
|
|
218
|
+
};
|
|
219
|
+
return ` Both fields were present: gate=${render(gate)} acceptance=${render(acceptance)}.`;
|
|
220
|
+
}
|
|
221
|
+
|
|
209
222
|
function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
|
|
210
223
|
return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
|
|
211
224
|
}
|
|
@@ -2,7 +2,6 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
|
|
3
3
|
export const SUBAGENT_CAPABILITY_CEILING_VERSION = 1 as const;
|
|
4
4
|
export const SUBAGENT_CAPABILITY_CEILING_REGISTRY_KEY = "pi-subagents.capability-ceiling.v1";
|
|
5
|
-
export const SUBAGENT_CAPABILITY_CEILING_ENV = "PI_SUBAGENT_CAPABILITY_CEILING_V1";
|
|
6
5
|
|
|
7
6
|
export type SubagentCapabilityCeiling =
|
|
8
7
|
| { allowedTools: readonly string[]; allowedAgents?: readonly string[]; denyExtensions?: boolean }
|
|
@@ -167,7 +166,7 @@ export function resolveSubagentCapabilityCeiling(sessionId: string | undefined,
|
|
|
167
166
|
}
|
|
168
167
|
|
|
169
168
|
export function resolveCurrentSubagentCapabilityCeiling(sessionId: string | undefined): ResolvedSubagentCapabilityCeiling | undefined {
|
|
170
|
-
return resolveSubagentCapabilityCeiling(sessionId,
|
|
169
|
+
return resolveSubagentCapabilityCeiling(sessionId, undefined);
|
|
171
170
|
}
|
|
172
171
|
|
|
173
172
|
export function isAgentAllowedByCapabilityCeiling(agentName: string, ceiling: ResolvedSubagentCapabilityCeiling | undefined): boolean {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import registerFanoutChildSubagentExtension from "../../extension/fanout-child.ts";
|
|
3
|
+
import registerSubagentFastModeExtension from "./fast-mode-extension.ts";
|
|
4
|
+
import registerSubagentPromptRuntime from "./subagent-prompt-runtime.ts";
|
|
5
|
+
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
6
|
+
|
|
7
|
+
/** Inline extension shape accepted by pi's resource loader (`extensionFactories`). */
|
|
8
|
+
export interface ChildHookExtension {
|
|
9
|
+
name: string;
|
|
10
|
+
factory: (pi: ExtensionAPI) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The child-side hooks pi-subagents installs in every child, keyed off the
|
|
15
|
+
* launch config. The registrations live in `subagent-prompt-runtime.ts`,
|
|
16
|
+
* `fast-mode-extension.ts`, and `fanout-child.ts`.
|
|
17
|
+
*/
|
|
18
|
+
export function createChildHooks(config: ChildRuntimeConfig): ChildHookExtension[] {
|
|
19
|
+
const hooks: ChildHookExtension[] = [
|
|
20
|
+
{ name: "pi-subagents:prompt-runtime", factory: (pi) => registerSubagentPromptRuntime(pi, config) },
|
|
21
|
+
];
|
|
22
|
+
if (config.fast) hooks.push({ name: "pi-subagents:fast-mode", factory: (pi) => registerSubagentFastModeExtension(pi) });
|
|
23
|
+
if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, config) });
|
|
24
|
+
return hooks;
|
|
25
|
+
}
|
|
@@ -14,11 +14,11 @@ export type AsyncStatusChildResolution =
|
|
|
14
14
|
| { ok: false; code: "not_found" | "ambiguous"; message: string };
|
|
15
15
|
|
|
16
16
|
export function asyncStatusChildIdentity(step: AsyncStatusStep, index: number): string {
|
|
17
|
-
return step
|
|
17
|
+
return asyncStatusChildIdentityCandidates(step, index)[0]!;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function asyncStatusChildIdentityCandidates(step: AsyncStatusStep, index: number): string[] {
|
|
21
|
-
return [...new Set([step.workflowKey, step.runId, `step:${index}`].filter((value): value is string => typeof value === "string" && value.length > 0))];
|
|
21
|
+
return [...new Set([step.childId, step.workflowKey, step.runId, `step:${index}`].filter((value): value is string => typeof value === "string" && value.length > 0))];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function resolveAsyncStatusChild(
|
|
@@ -49,3 +49,14 @@ export function resolveAsyncStatusChild(
|
|
|
49
49
|
export function isStoppableAsyncStatusStep(step: AsyncStatusStep): boolean {
|
|
50
50
|
return step.status === "pending" || step.status === "running";
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export function stopStoppableAsyncStatusChildren(
|
|
54
|
+
status: Pick<AsyncStatus, "steps">,
|
|
55
|
+
stopChild: ((childId: string, message?: string) => boolean) | undefined,
|
|
56
|
+
message: string,
|
|
57
|
+
): void {
|
|
58
|
+
if (!stopChild) return;
|
|
59
|
+
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
60
|
+
if (isStoppableAsyncStatusStep(step)) stopChild(asyncStatusChildIdentity(step, index), message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the in-process launch for one child: the tool plan, the typed child
|
|
3
|
+
* runtime config the hooks read, and the session launch input. Foreground
|
|
4
|
+
* children are built in the parent process; background children are built in
|
|
5
|
+
* the detached runner from the step it received in its config.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { ChildWatchdogConfig, ChildWatchdogStatusEvent } from "../../watchdog/child-status.ts";
|
|
10
|
+
import type { ThinkingLevel } from "../../shared/model-info.ts";
|
|
11
|
+
import { intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
|
|
12
|
+
import {
|
|
13
|
+
resolveChildDepth,
|
|
14
|
+
type LaunchResolvedChildExtensionsV1,
|
|
15
|
+
type ResolvedToolBudget,
|
|
16
|
+
type RunFanoutBudgetDescriptor,
|
|
17
|
+
} from "../../shared/types.ts";
|
|
18
|
+
import type { NestedPathEntry } from "./nested-path.ts";
|
|
19
|
+
import type { McpRuntimeSnapshotHost } from "./mcp-direct-tool-allowlist.ts";
|
|
20
|
+
import type { PermissionRules } from "./permissions.ts";
|
|
21
|
+
import type { StructuredOutputRuntime } from "./structured-output.ts";
|
|
22
|
+
import type { ChildToolDiagnostic } from "./tool-availability.ts";
|
|
23
|
+
import type { RuntimeAcknowledgedChildExtensionsV1 } from "../../shared/types.ts";
|
|
24
|
+
import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
25
|
+
import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type ExtensionBindings } from "./extension-bindings.ts";
|
|
26
|
+
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "./capability-ceiling.ts";
|
|
27
|
+
import {
|
|
28
|
+
isSubagentRuntimeExtensionPath,
|
|
29
|
+
projectLaunchResolvedChildExtensions,
|
|
30
|
+
resolvePiLaunchToolPlan,
|
|
31
|
+
supervisorChannelDir,
|
|
32
|
+
type PiLaunchToolPlan,
|
|
33
|
+
} from "./child-tool-plan.ts";
|
|
34
|
+
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
35
|
+
import { createChildHooks } from "./child-hooks.ts";
|
|
36
|
+
import type { ChildSessionLaunch, ChildSessionStorage } from "./child-session.ts";
|
|
37
|
+
|
|
38
|
+
/** Environment variable pi-mcp-adapter reads for the tools a child may expose. */
|
|
39
|
+
export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The parts of the launching executor's own child runtime that a child it
|
|
43
|
+
* launches inherits. Serialized into the background runner config; the
|
|
44
|
+
* foreground path passes the executor's full `ChildRuntimeConfig`.
|
|
45
|
+
*/
|
|
46
|
+
export type InheritedChildRuntime = Pick<ChildRuntimeConfig, "depth" | "maxDepth" | "nestedRoute" | "nestedParent" | "capabilityCeiling" | "thinkingCeiling" | "runFanoutBudget">;
|
|
47
|
+
|
|
48
|
+
export function inheritedChildRuntime(config: ChildRuntimeConfig | undefined): InheritedChildRuntime | undefined {
|
|
49
|
+
if (!config) return undefined;
|
|
50
|
+
return {
|
|
51
|
+
depth: config.depth,
|
|
52
|
+
...(config.maxDepth !== undefined ? { maxDepth: config.maxDepth } : {}),
|
|
53
|
+
...(config.nestedRoute ? { nestedRoute: config.nestedRoute } : {}),
|
|
54
|
+
...(config.nestedParent ? { nestedParent: config.nestedParent } : {}),
|
|
55
|
+
...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
|
|
56
|
+
...(config.thinkingCeiling ? { thinkingCeiling: config.thinkingCeiling } : {}),
|
|
57
|
+
...(config.runFanoutBudget ? { runFanoutBudget: config.runFanoutBudget } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BuildInProcessChildLaunchInput {
|
|
62
|
+
parentSessionId?: string;
|
|
63
|
+
forkCacheKey?: string;
|
|
64
|
+
sessionEnabled: boolean;
|
|
65
|
+
sessionDir?: string;
|
|
66
|
+
sessionFile?: string;
|
|
67
|
+
/** Model reference with the thinking suffix already applied. */
|
|
68
|
+
model?: string;
|
|
69
|
+
systemPromptMode?: "append" | "replace";
|
|
70
|
+
inheritProjectContext: boolean;
|
|
71
|
+
inheritGlobalContext: boolean;
|
|
72
|
+
inheritSkills: boolean;
|
|
73
|
+
requireReadTool?: boolean;
|
|
74
|
+
tools?: string[];
|
|
75
|
+
excludeTools?: string[];
|
|
76
|
+
extensions?: string[];
|
|
77
|
+
subagentOnlyExtensions?: string[];
|
|
78
|
+
systemPrompt?: string | null;
|
|
79
|
+
mcpDirectTools?: string[];
|
|
80
|
+
extensionBindings?: ExtensionBindings;
|
|
81
|
+
cwd: string;
|
|
82
|
+
intercomSessionName?: string;
|
|
83
|
+
sessionName?: string;
|
|
84
|
+
orchestratorIntercomTarget?: string;
|
|
85
|
+
runId?: string;
|
|
86
|
+
childAgentName: string;
|
|
87
|
+
childIndex: number;
|
|
88
|
+
nestedRoute?: { rootRunId: string; eventSink: string; controlInbox: string; capabilityToken: string };
|
|
89
|
+
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
90
|
+
structuredOutput?: StructuredOutputRuntime;
|
|
91
|
+
fast?: boolean;
|
|
92
|
+
modelCandidates?: readonly string[];
|
|
93
|
+
toolBudget?: ResolvedToolBudget;
|
|
94
|
+
permissionRules?: PermissionRules;
|
|
95
|
+
permissionAuditPath?: string;
|
|
96
|
+
childWatchdog?: ChildWatchdogConfig;
|
|
97
|
+
watchdogStatus?: (event: ChildWatchdogStatusEvent) => void;
|
|
98
|
+
waitToolEnabled?: boolean;
|
|
99
|
+
waitToolDefaultTimeoutMs?: number;
|
|
100
|
+
allowNestedSubagents?: boolean;
|
|
101
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
102
|
+
thinkingCeiling?: ThinkingLevel;
|
|
103
|
+
maxSubagentDepth?: number;
|
|
104
|
+
runtimeSnapshotHost?: McpRuntimeSnapshotHost;
|
|
105
|
+
/** The launching executor's own child runtime when it is itself an in-process child. */
|
|
106
|
+
inherited?: InheritedChildRuntime;
|
|
107
|
+
/**
|
|
108
|
+
* Which process hosts the session. The parent never loads ambient extensions
|
|
109
|
+
* or writes child environment values (it shares its process with the parent
|
|
110
|
+
* session); the runner loads ambient extensions when the tool plan allows
|
|
111
|
+
* them and exposes the child environment external extensions read.
|
|
112
|
+
*/
|
|
113
|
+
host: "parent" | "runner";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface InProcessChildCapture {
|
|
117
|
+
structuredOutput(): { called: boolean; value?: unknown; acceptanceReport?: unknown; acceptanceReportProvided: boolean };
|
|
118
|
+
toolDiagnostic(): ChildToolDiagnostic | undefined;
|
|
119
|
+
runtimeAcknowledgedExtensions(): RuntimeAcknowledgedChildExtensionsV1 | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface InProcessChildLaunch {
|
|
123
|
+
toolPlan: PiLaunchToolPlan;
|
|
124
|
+
config: ChildRuntimeConfig;
|
|
125
|
+
session: Omit<ChildSessionLaunch, "onExtensionError">;
|
|
126
|
+
capture: InProcessChildCapture;
|
|
127
|
+
launchResolvedExtensions: LaunchResolvedChildExtensionsV1;
|
|
128
|
+
warnings: string[];
|
|
129
|
+
capabilityAudit?: SubagentCapabilityAudit;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Escape XML-significant characters in a string for safe attribute interpolation. */
|
|
133
|
+
function escapeXmlAttr(value: string): string {
|
|
134
|
+
return value
|
|
135
|
+
.replace(/&/g, "&")
|
|
136
|
+
.replace(/"/g, """)
|
|
137
|
+
.replace(/</g, "<")
|
|
138
|
+
.replace(/>/g, ">");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function inheritedCapabilityCeiling(inherited: InheritedChildRuntime | undefined): ResolvedSubagentCapabilityCeiling | undefined {
|
|
142
|
+
return inherited?.capabilityCeiling;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Environment values external child extensions read; only the runner applies them. */
|
|
146
|
+
function childProcessEnv(input: BuildInProcessChildLaunchInput, toolPlan: PiLaunchToolPlan): Record<string, string | undefined> {
|
|
147
|
+
const env: Record<string, string | undefined> = {};
|
|
148
|
+
env[PI_SUBAGENT_EXTENSION_BINDINGS_ENV] = encodeExtensionBindings(input.extensionBindings);
|
|
149
|
+
if (!toolPlan.capabilityCeiling && input.mcpDirectTools?.length) env[MCP_DIRECT_TOOLS_ENV] = input.mcpDirectTools.join(",");
|
|
150
|
+
else if (toolPlan.capabilityCeiling && toolPlan.effectiveMcpSelections.length && !toolPlan.capabilityCeiling.denyExtensions) {
|
|
151
|
+
env[MCP_DIRECT_TOOLS_ENV] = toolPlan.effectiveMcpSelections.map((selection) => selection.selector).join(",");
|
|
152
|
+
} else env[MCP_DIRECT_TOOLS_ENV] = "__none__";
|
|
153
|
+
return env;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function childStorage(input: BuildInProcessChildLaunchInput): ChildSessionStorage {
|
|
157
|
+
if (input.sessionFile) {
|
|
158
|
+
fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
|
|
159
|
+
return { kind: "file", sessionFile: input.sessionFile };
|
|
160
|
+
}
|
|
161
|
+
if (!input.sessionEnabled) return { kind: "memory" };
|
|
162
|
+
if (input.sessionDir) {
|
|
163
|
+
fs.mkdirSync(input.sessionDir, { recursive: true });
|
|
164
|
+
return { kind: "dir", sessionDir: input.sessionDir };
|
|
165
|
+
}
|
|
166
|
+
return { kind: "default" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput): InProcessChildLaunch {
|
|
170
|
+
const toolPlan = resolvePiLaunchToolPlan({
|
|
171
|
+
tools: input.tools,
|
|
172
|
+
excludeTools: input.excludeTools,
|
|
173
|
+
allowNestedSubagents: input.allowNestedSubagents,
|
|
174
|
+
extensions: input.extensions,
|
|
175
|
+
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
176
|
+
mcpDirectTools: input.mcpDirectTools,
|
|
177
|
+
cwd: input.cwd,
|
|
178
|
+
requireReadTool: input.requireReadTool,
|
|
179
|
+
structuredOutput: Boolean(input.structuredOutput),
|
|
180
|
+
fast: input.fast,
|
|
181
|
+
model: input.model,
|
|
182
|
+
modelCandidates: input.modelCandidates,
|
|
183
|
+
capabilityCeiling: input.capabilityCeiling,
|
|
184
|
+
inheritedCapabilityCeiling: inheritedCapabilityCeiling(input.inherited),
|
|
185
|
+
agentName: input.childAgentName,
|
|
186
|
+
permissionRules: input.permissionRules,
|
|
187
|
+
runtimeSnapshotHost: input.runtimeSnapshotHost,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const inherited = input.inherited;
|
|
191
|
+
const fanout = toolPlan.fanoutAuthorized;
|
|
192
|
+
const inheritedRoute = inherited?.nestedRoute;
|
|
193
|
+
const parentRunId = input.runId ?? inherited?.nestedParent?.parentRunId ?? "";
|
|
194
|
+
const parentChildIndex = input.childIndex;
|
|
195
|
+
const parentDepth = inheritedRoute && inherited?.nestedParent ? inherited.nestedParent.depth + 1 : 1;
|
|
196
|
+
const parentPath: NestedPathEntry[] = [
|
|
197
|
+
...(inherited?.nestedParent?.path ?? []),
|
|
198
|
+
...(parentRunId ? [{ runId: parentRunId, stepIndex: parentChildIndex, agent: input.childAgentName }] : []),
|
|
199
|
+
];
|
|
200
|
+
const nestedRoute = fanout ? (input.nestedRoute ?? inheritedRoute) : undefined;
|
|
201
|
+
const childDepth = resolveChildDepth(input.maxSubagentDepth, inherited);
|
|
202
|
+
const permissions = input.permissionRules && Object.keys(input.permissionRules).length > 0
|
|
203
|
+
? { rules: input.permissionRules, ...(input.permissionAuditPath ? { auditPath: input.permissionAuditPath } : {}) }
|
|
204
|
+
: undefined;
|
|
205
|
+
let supervisorDir: string | undefined;
|
|
206
|
+
if (input.orchestratorIntercomTarget && input.parentSessionId && input.runId) {
|
|
207
|
+
supervisorDir = supervisorChannelDir(input.runId, input.childAgentName, input.childIndex);
|
|
208
|
+
fs.mkdirSync(path.join(supervisorDir, "requests"), { recursive: true });
|
|
209
|
+
fs.mkdirSync(path.join(supervisorDir, "replies"), { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
const thinkingCeiling = intersectThinkingCeilings(input.thinkingCeiling, inherited?.thinkingCeiling);
|
|
212
|
+
|
|
213
|
+
let structuredValue: unknown;
|
|
214
|
+
let structuredAcceptanceReport: unknown;
|
|
215
|
+
let structuredCalled = false;
|
|
216
|
+
let structuredAcceptanceProvided = false;
|
|
217
|
+
let toolDiagnostic: ChildToolDiagnostic | undefined;
|
|
218
|
+
let acknowledgedIds: string[] | undefined;
|
|
219
|
+
|
|
220
|
+
const config: ChildRuntimeConfig = {
|
|
221
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
222
|
+
agent: input.childAgentName,
|
|
223
|
+
childIndex: input.childIndex,
|
|
224
|
+
fanoutChild: fanout,
|
|
225
|
+
...(input.sessionName?.trim() ? { sessionName: input.sessionName.trim() } : {}),
|
|
226
|
+
...(input.intercomSessionName ? { intercomSessionName: input.intercomSessionName } : {}),
|
|
227
|
+
...(input.orchestratorIntercomTarget ? { orchestratorTarget: input.orchestratorIntercomTarget } : {}),
|
|
228
|
+
...(input.parentSessionId ? { orchestratorSessionId: input.parentSessionId, parentSessionId: input.parentSessionId } : {}),
|
|
229
|
+
...(supervisorDir ? { supervisorChannelDir: supervisorDir } : {}),
|
|
230
|
+
...(nestedRoute ? { nestedRoute } : {}),
|
|
231
|
+
...(fanout && parentRunId ? { nestedParent: { parentRunId, parentChildIndex, depth: parentDepth, path: parentPath } } : {}),
|
|
232
|
+
...(fanout && (input.runFanoutBudget ?? inherited?.runFanoutBudget) ? { runFanoutBudget: input.runFanoutBudget ?? inherited?.runFanoutBudget } : {}),
|
|
233
|
+
depth: childDepth.depth,
|
|
234
|
+
maxDepth: childDepth.maxDepth,
|
|
235
|
+
...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}),
|
|
236
|
+
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
237
|
+
inheritProjectContext: input.inheritProjectContext,
|
|
238
|
+
inheritGlobalContext: input.inheritGlobalContext,
|
|
239
|
+
inheritSkills: input.inheritSkills,
|
|
240
|
+
...(input.forkCacheKey?.trim() ? { forkCacheKey: input.forkCacheKey.trim() } : {}),
|
|
241
|
+
...(permissions ? { permissions } : {}),
|
|
242
|
+
...(input.toolBudget ? { toolBudget: input.toolBudget } : {}),
|
|
243
|
+
...(input.childWatchdog ? { childWatchdog: input.childWatchdog } : {}),
|
|
244
|
+
...(input.watchdogStatus ? { watchdogStatus: input.watchdogStatus } : {}),
|
|
245
|
+
waitTool: {
|
|
246
|
+
enabled: input.waitToolEnabled ?? true,
|
|
247
|
+
...(input.waitToolDefaultTimeoutMs !== undefined ? { defaultTimeoutMs: input.waitToolDefaultTimeoutMs } : {}),
|
|
248
|
+
},
|
|
249
|
+
...(input.structuredOutput
|
|
250
|
+
? {
|
|
251
|
+
structuredOutput: {
|
|
252
|
+
schema: input.structuredOutput.schema,
|
|
253
|
+
...(input.structuredOutput.acceptanceReportPath
|
|
254
|
+
? { acceptanceReport: input.structuredOutput.acceptanceReportRequired ? "required" as const : "optional" as const }
|
|
255
|
+
: {}),
|
|
256
|
+
capture: (value: unknown, acceptanceReport: unknown) => {
|
|
257
|
+
structuredCalled = true;
|
|
258
|
+
structuredValue = value;
|
|
259
|
+
structuredAcceptanceProvided = acceptanceReport !== undefined;
|
|
260
|
+
structuredAcceptanceReport = acceptanceReport;
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
: {}),
|
|
265
|
+
...(toolPlan.requiredChildTools.length > 0 ? { requiredTools: toolPlan.requiredChildTools } : {}),
|
|
266
|
+
...(toolPlan.effectiveMcpTools.length > 0 ? { mcpDirectTools: toolPlan.effectiveMcpTools } : {}),
|
|
267
|
+
toolDiagnostic: (diagnostic) => { toolDiagnostic = diagnostic; },
|
|
268
|
+
runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
|
|
269
|
+
fast: input.fast === true,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const extensionPaths = toolPlan.extensionArgs.filter((extensionPath) => !isSubagentRuntimeExtensionPath(extensionPath));
|
|
273
|
+
const ambientExtensions = input.host === "runner" && !toolPlan.disableAmbientExtensions;
|
|
274
|
+
const launchResolvedExtensions = projectLaunchResolvedChildExtensions({
|
|
275
|
+
runtimeExtensions: toolPlan.runtimeExtensions,
|
|
276
|
+
configuredExtensions: toolPlan.configuredExtensions,
|
|
277
|
+
extensionArgs: toolPlan.extensionArgs,
|
|
278
|
+
disableAmbientExtensions: !ambientExtensions,
|
|
279
|
+
});
|
|
280
|
+
const taggedPrompt = input.systemPrompt !== undefined && input.systemPrompt !== null
|
|
281
|
+
? `<active_agent name="${escapeXmlAttr(input.childAgentName)}"/>\n\n${input.systemPrompt}`
|
|
282
|
+
: undefined;
|
|
283
|
+
const session: Omit<ChildSessionLaunch, "onExtensionError"> = {
|
|
284
|
+
cwd: input.cwd,
|
|
285
|
+
storage: childStorage(input),
|
|
286
|
+
...(input.model ? { model: input.model } : {}),
|
|
287
|
+
...(toolPlan.explicitToolAllowlist ? { tools: toolPlan.effectiveToolAllowlist } : {}),
|
|
288
|
+
...(!toolPlan.explicitToolAllowlist && toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
289
|
+
extensionPaths,
|
|
290
|
+
ambientExtensions,
|
|
291
|
+
hooks: createChildHooks(config),
|
|
292
|
+
...(input.host === "runner" ? { processEnv: childProcessEnv(input, toolPlan) } : {}),
|
|
293
|
+
runtime: config,
|
|
294
|
+
noSkills: !input.inheritSkills,
|
|
295
|
+
noContextFiles: !input.inheritProjectContext,
|
|
296
|
+
...(taggedPrompt !== undefined
|
|
297
|
+
? input.systemPromptMode === "replace" ? { systemPrompt: taggedPrompt } : { appendSystemPrompt: taggedPrompt }
|
|
298
|
+
: {}),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
toolPlan,
|
|
303
|
+
config,
|
|
304
|
+
session,
|
|
305
|
+
capture: {
|
|
306
|
+
structuredOutput: () => ({ called: structuredCalled, value: structuredValue, acceptanceReport: structuredAcceptanceReport, acceptanceReportProvided: structuredAcceptanceProvided }),
|
|
307
|
+
toolDiagnostic: () => toolDiagnostic,
|
|
308
|
+
runtimeAcknowledgedExtensions: () => (acknowledgedIds ? projectRuntimeAcknowledgedExtensions(acknowledgedIds) : undefined),
|
|
309
|
+
},
|
|
310
|
+
launchResolvedExtensions,
|
|
311
|
+
warnings: toolPlan.warnings,
|
|
312
|
+
...(toolPlan.capabilityAudit ? { capabilityAudit: toolPlan.capabilityAudit } : {}),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child lifecycle projection: when the observed child session events mean
|
|
3
|
+
* the run is settling and the observer may start (or must cancel) its final
|
|
4
|
+
* drain window.
|
|
5
|
+
*/
|
|
6
|
+
export type ChildLifecycleAction = "start-drain" | "cancel-drain" | "none";
|
|
7
|
+
|
|
8
|
+
export interface ChildLifecycleState {
|
|
9
|
+
compactionRetryActive: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function projectChildLifecycle(event: { type?: string; willRetry?: unknown }, terminalAssistantStop = false, state?: ChildLifecycleState): ChildLifecycleAction {
|
|
13
|
+
if (event.type === "compaction_end") {
|
|
14
|
+
if (state) state.compactionRetryActive = event.willRetry === true;
|
|
15
|
+
return event.willRetry === true ? "cancel-drain" : "none";
|
|
16
|
+
}
|
|
17
|
+
if (event.type === "agent_start" || event.type === "auto_retry_start") {
|
|
18
|
+
if (state) state.compactionRetryActive = false;
|
|
19
|
+
}
|
|
20
|
+
if (event.type === "agent_end" && event.willRetry === true) return "cancel-drain";
|
|
21
|
+
if (event.type === "agent_end" && state) state.compactionRetryActive = false;
|
|
22
|
+
if (event.type === "agent_settled") return state?.compactionRetryActive ? "none" : "start-drain";
|
|
23
|
+
if (terminalAssistantStop) return "start-drain";
|
|
24
|
+
return "none";
|
|
25
|
+
}
|