pi-subagents 0.58.0 → 0.60.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 +102 -0
- package/docs/agents.md +5 -3
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +1 -1
- package/docs/models.md +24 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +93 -5
- package/docs/workflows.md +94 -3
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +87 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/skills/pi-subagents/references/execution-controls.md +52 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +159 -42
- package/src/agents/agent-serializer.ts +4 -2
- package/src/agents/agents.ts +67 -26
- package/src/agents/runtime-agent-registry.ts +9 -16
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +6 -9
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +51 -23
- package/src/extension/public-execution.ts +16 -2
- package/src/extension/schemas.ts +36 -10
- package/src/extension/tool-description.ts +24 -5
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +52 -37
- package/src/runs/background/async-job-tracker.ts +17 -12
- package/src/runs/background/async-resume.ts +23 -25
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +110 -8
- package/src/runs/background/chain-append.ts +6 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +158 -6
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +2 -0
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +30 -9
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +351 -358
- package/src/runs/background/subagent-wait.ts +68 -12
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +25 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +125 -122
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +576 -240
- package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +597 -0
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/mcp-config-sources.ts +42 -6
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
- package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
- package/src/runs/shared/model-fallback.ts +9 -2
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +67 -3
- package/src/runs/shared/single-output.ts +72 -22
- package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
- package/src/runs/shared/workflow-graph.ts +15 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +5 -2
- package/src/shared/launch-contract.ts +1 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +232 -30
- package/src/shared/utils.ts +35 -55
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/tui/fleet-status.ts +134 -75
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +895 -121
- package/src/watchdog/change-signature.ts +40 -1
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +235 -0
- package/src/workflows/scripted-workflow.ts +503 -38
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +43 -4
- package/src/workflows/workflow-settlement.ts +246 -0
- package/src/runs/shared/turn-budget.ts +0 -98
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
encodeChildWatchdogConfig,
|
|
49
49
|
type ChildWatchdogConfig,
|
|
50
50
|
} from "../../watchdog/child-status.ts";
|
|
51
|
-
import { WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
|
|
51
|
+
import { WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV, WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
|
|
52
52
|
import {
|
|
53
53
|
PI_CODING_AGENT_PACKAGE_ROOT_ENV,
|
|
54
54
|
getAgentDir,
|
|
@@ -117,6 +117,7 @@ const FAST_MODE_ALLOWED_MODELS = new Set([
|
|
|
117
117
|
"openai-codex/gpt-5.6-luna",
|
|
118
118
|
"openai-codex/gpt-5.6-sol",
|
|
119
119
|
]);
|
|
120
|
+
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
120
121
|
export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
121
122
|
export const SUBAGENT_ORCHESTRATOR_TARGET_ENV =
|
|
122
123
|
"PI_SUBAGENT_ORCHESTRATOR_TARGET";
|
|
@@ -139,6 +140,7 @@ export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
|
|
|
139
140
|
export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV =
|
|
140
141
|
"PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
|
|
141
142
|
export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
|
|
143
|
+
export const SUBAGENT_FORK_CACHE_KEY_ENV = "PI_SUBAGENT_FORK_CACHE_KEY";
|
|
142
144
|
export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
|
|
143
145
|
export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
|
|
144
146
|
export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
|
|
@@ -146,8 +148,16 @@ export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
|
|
|
146
148
|
export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
|
|
147
149
|
export const SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_GLOBAL_CONTEXT";
|
|
148
150
|
|
|
151
|
+
export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
|
|
152
|
+
const parent = parentSessionId?.trim();
|
|
153
|
+
if (!parent) return undefined;
|
|
154
|
+
const digest = createHash("sha256").update(parent).digest("hex").slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH - "pi-fork:".length);
|
|
155
|
+
return `pi-fork:${digest}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
export interface BuildPiArgsInput {
|
|
150
159
|
parentSessionId?: string;
|
|
160
|
+
forkCacheKey?: string;
|
|
151
161
|
baseArgs: string[];
|
|
152
162
|
task: string;
|
|
153
163
|
sessionEnabled: boolean;
|
|
@@ -170,6 +180,11 @@ export interface BuildPiArgsInput {
|
|
|
170
180
|
cwd?: string;
|
|
171
181
|
promptFileStem?: string;
|
|
172
182
|
intercomSessionName?: string;
|
|
183
|
+
/** Human-readable display name for the child session (agent + task excerpt,
|
|
184
|
+
* derived by the caller). Passed to the child as PI_SUBAGENT_SESSION_NAME;
|
|
185
|
+
* the prompt runtime applies it via pi.setSessionName unless an intercom
|
|
186
|
+
* target takes precedence. */
|
|
187
|
+
sessionName?: string;
|
|
173
188
|
orchestratorIntercomTarget?: string;
|
|
174
189
|
runId?: string;
|
|
175
190
|
childAgentName?: string;
|
|
@@ -206,6 +221,8 @@ export interface BuildPiArgsInput {
|
|
|
206
221
|
*/
|
|
207
222
|
taskDelivery?: SubagentTaskDelivery;
|
|
208
223
|
waitToolEnabled?: boolean;
|
|
224
|
+
waitToolDefaultTimeoutMs?: number;
|
|
225
|
+
allowNestedSubagents?: boolean;
|
|
209
226
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
210
227
|
thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
|
|
211
228
|
extensionBindings?: ExtensionBindings;
|
|
@@ -283,6 +300,7 @@ function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fas
|
|
|
283
300
|
|
|
284
301
|
export interface ResolvePiLaunchToolPlanInput {
|
|
285
302
|
tools?: string[];
|
|
303
|
+
allowNestedSubagents?: boolean;
|
|
286
304
|
extensions?: string[];
|
|
287
305
|
subagentOnlyExtensions?: string[];
|
|
288
306
|
mcpDirectTools?: string[];
|
|
@@ -481,7 +499,10 @@ export function resolvePiLaunchToolPlan(
|
|
|
481
499
|
? ["read", ...requestedBuiltinTools]
|
|
482
500
|
: requestedBuiltinTools
|
|
483
501
|
).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
|
|
484
|
-
const fanoutAuthorized = declaredBuiltinTools.includes("subagent")
|
|
502
|
+
const fanoutAuthorized = declaredBuiltinTools.includes("subagent") || (
|
|
503
|
+
input.allowNestedSubagents === true &&
|
|
504
|
+
(!allowedToolSet || allowedToolSet.has("subagent"))
|
|
505
|
+
);
|
|
485
506
|
const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
|
|
486
507
|
? []
|
|
487
508
|
: (input.tools ?? []).filter(
|
|
@@ -496,8 +517,13 @@ export function resolvePiLaunchToolPlan(
|
|
|
496
517
|
throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
|
|
497
518
|
}
|
|
498
519
|
const resolvedMcpSelections = mcpResolution.selections;
|
|
520
|
+
const resolvedMcpNames = new Set(resolvedMcpSelections.map((selection) => selection.name));
|
|
521
|
+
const legacyMcpNameCounts = countLegacyUnderscoreMcpToolNames(resolvedMcpSelections);
|
|
499
522
|
const effectiveMcpSelections = resolvedMcpSelections.filter(
|
|
500
|
-
(selection) =>
|
|
523
|
+
(selection) =>
|
|
524
|
+
!allowedToolSet ||
|
|
525
|
+
allowedToolSet.has(selection.name) ||
|
|
526
|
+
isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
|
|
501
527
|
);
|
|
502
528
|
const effectiveMcpTools = effectiveMcpSelections.map(
|
|
503
529
|
(selection) => selection.name,
|
|
@@ -652,6 +678,36 @@ export function resolvePiLaunchToolPlan(
|
|
|
652
678
|
};
|
|
653
679
|
}
|
|
654
680
|
|
|
681
|
+
// Capability ceilings persisted before #1685 may still name hyphenated MCP server prefixes with underscores.
|
|
682
|
+
function countLegacyUnderscoreMcpToolNames(selections: readonly ResolvedMcpDirectToolSelection[]): Map<string, number> {
|
|
683
|
+
const counts = new Map<string, number>();
|
|
684
|
+
for (const selection of selections) {
|
|
685
|
+
const legacyName = legacyUnderscoreMcpToolName(selection);
|
|
686
|
+
if (legacyName !== selection.name) counts.set(legacyName, (counts.get(legacyName) ?? 0) + 1);
|
|
687
|
+
}
|
|
688
|
+
return counts;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function isLegacyUnderscoreMcpToolAllowed(
|
|
692
|
+
selection: ResolvedMcpDirectToolSelection,
|
|
693
|
+
allowedToolSet: ReadonlySet<string>,
|
|
694
|
+
resolvedMcpNames: ReadonlySet<string>,
|
|
695
|
+
legacyMcpNameCounts: ReadonlyMap<string, number>,
|
|
696
|
+
): boolean {
|
|
697
|
+
const legacyName = legacyUnderscoreMcpToolName(selection);
|
|
698
|
+
return legacyMcpNameCounts.get(legacyName) === 1 && !resolvedMcpNames.has(legacyName) && allowedToolSet.has(legacyName);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function legacyUnderscoreMcpToolName(selection: ResolvedMcpDirectToolSelection): string {
|
|
702
|
+
const slash = selection.selector.indexOf("/");
|
|
703
|
+
if (slash < 1) return selection.name;
|
|
704
|
+
const toolName = selection.selector.slice(slash + 1);
|
|
705
|
+
const suffix = `_${toolName}`;
|
|
706
|
+
if (!selection.name.endsWith(suffix)) return selection.name;
|
|
707
|
+
const prefix = selection.name.slice(0, -suffix.length);
|
|
708
|
+
return `${prefix.replace(/-/g, "_")}${suffix}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
655
711
|
/** Escape XML-significant characters in a string for safe attribute interpolation. */
|
|
656
712
|
function escapeXmlAttr(value: string): string {
|
|
657
713
|
return value
|
|
@@ -684,6 +740,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
684
740
|
|
|
685
741
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
686
742
|
tools: input.tools,
|
|
743
|
+
allowNestedSubagents: input.allowNestedSubagents,
|
|
687
744
|
extensions: input.extensions,
|
|
688
745
|
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
689
746
|
mcpDirectTools: input.mcpDirectTools,
|
|
@@ -795,6 +852,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
795
852
|
if (input.waitToolEnabled !== undefined) {
|
|
796
853
|
env[WAIT_TOOL_ENABLED_ENV] = input.waitToolEnabled ? "true" : "false";
|
|
797
854
|
}
|
|
855
|
+
if (input.waitToolDefaultTimeoutMs !== undefined) {
|
|
856
|
+
env[WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV] = String(input.waitToolDefaultTimeoutMs);
|
|
857
|
+
}
|
|
798
858
|
const inheritedNestedRoute = Boolean(
|
|
799
859
|
process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] &&
|
|
800
860
|
process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] &&
|
|
@@ -878,6 +938,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
878
938
|
if (input.intercomSessionName) {
|
|
879
939
|
env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
|
|
880
940
|
}
|
|
941
|
+
if (input.sessionName?.trim()) {
|
|
942
|
+
env.PI_SUBAGENT_SESSION_NAME = input.sessionName.trim();
|
|
943
|
+
}
|
|
881
944
|
if (input.orchestratorIntercomTarget) {
|
|
882
945
|
env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget;
|
|
883
946
|
}
|
|
@@ -956,6 +1019,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
956
1019
|
|
|
957
1020
|
env[SUBAGENT_PARENT_SESSION_ENV] =
|
|
958
1021
|
input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
|
|
1022
|
+
env[SUBAGENT_FORK_CACHE_KEY_ENV] = input.forkCacheKey?.trim() || undefined;
|
|
959
1023
|
|
|
960
1024
|
return {
|
|
961
1025
|
args,
|
|
@@ -8,6 +8,17 @@ export interface SingleOutputSnapshot {
|
|
|
8
8
|
exists: boolean;
|
|
9
9
|
mtimeMs?: number;
|
|
10
10
|
size?: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function missingFileStatError(error: unknown): boolean {
|
|
15
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
16
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function snapshotFromStatError(error: unknown): SingleOutputSnapshot {
|
|
20
|
+
if (missingFileStatError(error)) return { exists: false };
|
|
21
|
+
return { exists: false, error: error instanceof Error ? error.message : String(error) };
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
/**
|
|
@@ -161,23 +172,67 @@ export function validateFileOnlyOutputMode(outputMode: OutputMode | undefined, o
|
|
|
161
172
|
return undefined;
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
export function resolveSingleOutputClaimPath(outputPath: string): string {
|
|
176
|
+
let existing = path.resolve(outputPath);
|
|
177
|
+
const missingSegments: string[] = [];
|
|
178
|
+
while (!fs.existsSync(existing)) {
|
|
179
|
+
missingSegments.unshift(path.basename(existing));
|
|
180
|
+
const parent = path.dirname(existing);
|
|
181
|
+
if (parent === existing) break;
|
|
182
|
+
existing = parent;
|
|
183
|
+
}
|
|
184
|
+
return path.join(fs.realpathSync(existing), ...missingSegments);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function outputClaimError(outputPath: string, expectedClaimPath: string | undefined): string | undefined {
|
|
188
|
+
if (expectedClaimPath && resolveSingleOutputClaimPath(outputPath) !== expectedClaimPath) return "Output path changed after it was claimed.";
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
164
192
|
export function captureSingleOutputSnapshot(outputPath: string | undefined): SingleOutputSnapshot | undefined {
|
|
165
193
|
if (!outputPath) return undefined;
|
|
166
194
|
try {
|
|
167
195
|
const stat = fs.statSync(outputPath);
|
|
168
196
|
return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
169
|
-
} catch {
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return snapshotFromStatError(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function inspectSingleOutputChange(
|
|
203
|
+
outputPath: string,
|
|
204
|
+
beforeRun: SingleOutputSnapshot | undefined,
|
|
205
|
+
): { changed: boolean; error?: string } {
|
|
206
|
+
if (beforeRun?.error) return { changed: false, error: beforeRun.error };
|
|
207
|
+
try {
|
|
208
|
+
const stat = fs.statSync(outputPath);
|
|
209
|
+
return {
|
|
210
|
+
changed: !beforeRun?.exists || stat.mtimeMs !== beforeRun.mtimeMs || stat.size !== beforeRun.size,
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (missingFileStatError(error)) return { changed: false };
|
|
214
|
+
return { changed: false, error: error instanceof Error ? error.message : String(error) };
|
|
172
215
|
}
|
|
173
216
|
}
|
|
174
217
|
|
|
218
|
+
export function hasSingleOutputChangedSinceSnapshot(
|
|
219
|
+
outputPath: string | undefined,
|
|
220
|
+
beforeRun: SingleOutputSnapshot | undefined,
|
|
221
|
+
): boolean | undefined {
|
|
222
|
+
if (!outputPath) return undefined;
|
|
223
|
+
const inspected = inspectSingleOutputChange(outputPath, beforeRun);
|
|
224
|
+
return inspected.error ? undefined : inspected.changed;
|
|
225
|
+
}
|
|
226
|
+
|
|
175
227
|
function persistSingleOutput(
|
|
176
228
|
outputPath: string | undefined,
|
|
177
229
|
fullOutput: string,
|
|
178
|
-
|
|
230
|
+
expectedClaimPath?: string,
|
|
231
|
+
): { savedPath?: string; error?: string; fatalError?: boolean } {
|
|
179
232
|
if (!outputPath) return {};
|
|
180
233
|
try {
|
|
234
|
+
const claimError = outputClaimError(outputPath, expectedClaimPath);
|
|
235
|
+
if (claimError) return { error: claimError, fatalError: true };
|
|
181
236
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
182
237
|
fs.writeFileSync(outputPath, fullOutput, "utf-8");
|
|
183
238
|
return { savedPath: outputPath };
|
|
@@ -190,26 +245,21 @@ export function resolveSingleOutput(
|
|
|
190
245
|
outputPath: string | undefined,
|
|
191
246
|
fallbackOutput: string,
|
|
192
247
|
beforeRun: SingleOutputSnapshot | undefined,
|
|
193
|
-
|
|
248
|
+
expectedClaimPath?: string,
|
|
249
|
+
): { fullOutput: string; savedPath?: string; saveError?: string; fatalError?: boolean } {
|
|
194
250
|
if (!outputPath) return { fullOutput: fallbackOutput };
|
|
251
|
+
const claimError = outputClaimError(outputPath, expectedClaimPath);
|
|
252
|
+
if (claimError) return { fullOutput: fallbackOutput, saveError: claimError, fatalError: true };
|
|
195
253
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
} catch (error) {
|
|
203
|
-
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
204
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
205
|
-
return {
|
|
206
|
-
fullOutput: fallbackOutput,
|
|
207
|
-
saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
254
|
+
const changedSinceStart = inspectSingleOutputChange(outputPath, beforeRun);
|
|
255
|
+
if (changedSinceStart.error) {
|
|
256
|
+
return {
|
|
257
|
+
fullOutput: fallbackOutput,
|
|
258
|
+
saveError: `Failed to inspect output file: ${changedSinceStart.error}`,
|
|
259
|
+
};
|
|
210
260
|
}
|
|
211
261
|
|
|
212
|
-
if (changedSinceStart) {
|
|
262
|
+
if (changedSinceStart.changed) {
|
|
213
263
|
try {
|
|
214
264
|
return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
|
|
215
265
|
} catch (error) {
|
|
@@ -220,9 +270,9 @@ export function resolveSingleOutput(
|
|
|
220
270
|
}
|
|
221
271
|
}
|
|
222
272
|
|
|
223
|
-
const save = persistSingleOutput(outputPath, fallbackOutput);
|
|
273
|
+
const save = persistSingleOutput(outputPath, fallbackOutput, expectedClaimPath);
|
|
224
274
|
if (save.savedPath) return { fullOutput: fallbackOutput, savedPath: save.savedPath };
|
|
225
|
-
return { fullOutput: fallbackOutput, saveError: save.error };
|
|
275
|
+
return { fullOutput: fallbackOutput, saveError: save.error, ...(save.fatalError ? { fatalError: true } : {}) };
|
|
226
276
|
}
|
|
227
277
|
|
|
228
278
|
export function finalizeSingleOutput(params: {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { BeforeProviderRequestEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
|
|
6
6
|
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
7
7
|
import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, PERMISSION_POLICY_ENV } from "./permissions.ts";
|
|
8
8
|
import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
|
|
9
|
-
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
9
|
+
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_FORK_CACHE_KEY_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
10
10
|
import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
11
11
|
import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
12
12
|
import {
|
|
@@ -32,6 +32,9 @@ import { drainOutstandingWork } from "../background/auto-drain.ts";
|
|
|
32
32
|
const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
|
|
33
33
|
const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
|
|
34
34
|
export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
|
|
35
|
+
/** Human-readable child display name (agent + task excerpt) set by the parent
|
|
36
|
+
* at launch; applied via pi.setSessionName when no intercom target exists. */
|
|
37
|
+
export const SUBAGENT_SESSION_NAME_ENV = "PI_SUBAGENT_SESSION_NAME";
|
|
35
38
|
const STEERING_LEGACY_SETTLE_FALLBACK_MS = 1000;
|
|
36
39
|
const STEERING_SAFETY_POLL_INTERVAL_MS = 5000;
|
|
37
40
|
|
|
@@ -288,10 +291,27 @@ const PORTABLE_TOOL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
|
288
291
|
const MAX_PORTABLE_TOOL_ID_LENGTH = 64;
|
|
289
292
|
const COMPOSITE_TOOL_ID_APIS = new Set([
|
|
290
293
|
"azure-openai-responses",
|
|
294
|
+
"cursor-native",
|
|
295
|
+
"openai-completions",
|
|
296
|
+
"openai-responses",
|
|
297
|
+
]);
|
|
298
|
+
const PROMPT_CACHE_KEY_APIS = new Set([
|
|
299
|
+
"azure-openai-responses",
|
|
300
|
+
"openai-codex-responses",
|
|
291
301
|
"openai-completions",
|
|
292
302
|
"openai-responses",
|
|
293
303
|
]);
|
|
294
304
|
|
|
305
|
+
export function rewriteForkCacheProviderRequest(event: BeforeProviderRequestEvent, ctx?: Pick<ExtensionContext, "model">): unknown {
|
|
306
|
+
const forkCacheKey = process.env[SUBAGENT_FORK_CACHE_KEY_ENV]?.trim();
|
|
307
|
+
if (!forkCacheKey || !PROMPT_CACHE_KEY_APIS.has(ctx?.model?.api ?? "")) return undefined;
|
|
308
|
+
if (!event || typeof event !== "object") return undefined;
|
|
309
|
+
const payload = event.payload;
|
|
310
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
311
|
+
if (typeof (payload as { prompt_cache_key?: unknown }).prompt_cache_key !== "string") return undefined;
|
|
312
|
+
return { ...payload, prompt_cache_key: forkCacheKey };
|
|
313
|
+
}
|
|
314
|
+
|
|
295
315
|
function portableToolId(id: string): string {
|
|
296
316
|
if (PORTABLE_TOOL_ID_PATTERN.test(id) && id.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return id;
|
|
297
317
|
const encoded = `tool_${Buffer.from(id).toString("base64url") || "empty"}`;
|
|
@@ -671,7 +691,7 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
671
691
|
registerPermissionGate(pi);
|
|
672
692
|
registerToolBudget(pi, decodeToolBudgetEnv(process.env[TOOL_BUDGET_ENV], { allowZero: process.env[TOOL_BUDGET_ZERO_AUTH_ENV] === "1" }));
|
|
673
693
|
registerChildWatchdog(pi);
|
|
674
|
-
const
|
|
694
|
+
const waitToolConfig = resolveWaitToolConfig();
|
|
675
695
|
const waitState = {
|
|
676
696
|
baseCwd: "",
|
|
677
697
|
currentSessionId: null,
|
|
@@ -686,7 +706,7 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
686
706
|
watcherRestartTimer: null,
|
|
687
707
|
resultFileCoalescer: { schedule: () => false, clear: () => {} },
|
|
688
708
|
} as unknown as SubagentState;
|
|
689
|
-
if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState,
|
|
709
|
+
if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, waitToolConfig.enabled, undefined, waitToolConfig.defaultTimeoutMs);
|
|
690
710
|
let nativeSupervisorClientRegistered = false;
|
|
691
711
|
const registerNativeSupervisorClientOnce = (): void => {
|
|
692
712
|
if (nativeSupervisorClientRegistered) return;
|
|
@@ -744,6 +764,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
744
764
|
});
|
|
745
765
|
}
|
|
746
766
|
|
|
767
|
+
onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx));
|
|
768
|
+
|
|
747
769
|
onRuntimeEvent("context", (event: unknown, ctx?: ExtensionContext) => {
|
|
748
770
|
if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages)) return undefined;
|
|
749
771
|
const messages = stripParentOnlySubagentMessages(event.messages, {
|
|
@@ -756,9 +778,14 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
756
778
|
onRuntimeEvent("before_agent_start", async (event: unknown) => {
|
|
757
779
|
if (!event || typeof event !== "object" || !("systemPrompt" in event) || typeof event.systemPrompt !== "string") return undefined;
|
|
758
780
|
registerNativeSupervisorClientOnce();
|
|
781
|
+
// The intercom target is a routing address and always wins; the display
|
|
782
|
+
// name (agent + task excerpt, computed by the parent at launch) only
|
|
783
|
+
// applies when the bridge is not addressing this child.
|
|
759
784
|
const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
|
|
760
|
-
|
|
761
|
-
|
|
785
|
+
const displaySessionName = process.env[SUBAGENT_SESSION_NAME_ENV]?.trim();
|
|
786
|
+
const childSessionName = intercomSessionName || displaySessionName;
|
|
787
|
+
if (childSessionName && typeof pi.setSessionName === "function") {
|
|
788
|
+
pi.setSessionName(childSessionName);
|
|
762
789
|
}
|
|
763
790
|
|
|
764
791
|
const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
|
|
@@ -13,6 +13,21 @@ export interface WorkflowGraphBuildInput {
|
|
|
13
13
|
dynamicGroupStatuses?: Record<number, { status: WorkflowNodeStatus; error?: string; acceptance?: SingleResult["acceptance"] }>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** Return displayable workflow stages while hiding structural parallel groups and host monitors. */
|
|
17
|
+
export function workflowGraphStageNodes(graph: WorkflowGraphSnapshot | undefined): WorkflowGraphNode[] {
|
|
18
|
+
if (!graph?.nodes?.length) return [];
|
|
19
|
+
const stages: WorkflowGraphNode[] = [];
|
|
20
|
+
const visit = (node: WorkflowGraphNode): void => {
|
|
21
|
+
if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
|
|
22
|
+
for (const child of node.children ?? []) visit(child);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (node.kind !== "host-step") stages.push(node);
|
|
26
|
+
};
|
|
27
|
+
for (const node of graph.nodes) visit(node);
|
|
28
|
+
return stages;
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undefined {
|
|
17
32
|
switch (status) {
|
|
18
33
|
case "complete":
|