pi-subagents 0.59.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 +37 -0
- package/docs/tool-reference.md +4 -1
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- 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 +47 -6
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +34 -27
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +154 -25
- package/src/api/shared-types.ts +2 -0
- package/src/extension/public-execution.ts +1 -0
- package/src/extension/schemas.ts +1 -0
- package/src/extension/tool-description.ts +8 -4
- package/src/runs/background/async-execution.ts +2 -2
- package/src/runs/background/async-job-tracker.ts +3 -0
- package/src/runs/background/async-status.ts +45 -2
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/run-status.ts +13 -2
- package/src/runs/background/subagent-runner.ts +5 -1
- package/src/runs/background/subagent-wait.ts +10 -2
- package/src/runs/background/wait-completions.ts +3 -0
- package/src/runs/foreground/execution.ts +11 -2
- package/src/runs/foreground/subagent-executor.ts +98 -1
- package/src/runs/shared/async-status-projection.ts +138 -4
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/pi-args.ts +47 -1
- package/src/runs/shared/single-output.ts +45 -18
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
- package/src/runs/shared/workflow-graph.ts +15 -0
- package/src/shared/types.ts +34 -1
- package/src/tui/fleet-status.ts +11 -3
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +597 -112
- package/src/watchdog/change-signature.ts +40 -1
- package/src/workflows/host-command.ts +6 -1
- package/src/workflows/scripted-workflow.ts +53 -2
|
@@ -2,6 +2,7 @@ import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-t
|
|
|
2
2
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
3
3
|
import type { AsyncJobState, AsyncJobStep, HostStepFreshnessV1, HostStepMonitorKind, HostStepNodeV1, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../../shared/types.ts";
|
|
4
4
|
import { HOST_STEP_MAX_COUNT, HOST_STEP_MAX_DETAIL_CHARS, HOST_STEP_MAX_LABEL_CHARS, HOST_STEP_MAX_PROVIDER_CHARS, HOST_STEP_MAX_REASON_CHARS, HOST_STEP_MAX_REF_CHARS, HOST_STEP_MAX_ROLE_CHARS, HOST_STEP_MAX_TARGET_CHARS, hostStepReportName, parseHostStepNode, validHostStepNodes } from "./host-step-status.ts";
|
|
5
|
+
import { workflowGraphStageNodes } from "./workflow-graph.ts";
|
|
5
6
|
|
|
6
7
|
export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
|
|
7
8
|
export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
|
|
@@ -15,6 +16,21 @@ const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
|
|
|
15
16
|
export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
|
|
16
17
|
export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
|
|
17
18
|
|
|
19
|
+
const ASYNC_STATUS_SNAPSHOT_STATES: Record<AsyncStatusSnapshotState, true> = {
|
|
20
|
+
queued: true,
|
|
21
|
+
running: true,
|
|
22
|
+
complete: true,
|
|
23
|
+
failed: true,
|
|
24
|
+
partial: true,
|
|
25
|
+
paused: true,
|
|
26
|
+
stopped: true,
|
|
27
|
+
rejected: true,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function isAsyncStatusSnapshotState(value: string): value is AsyncStatusSnapshotState {
|
|
31
|
+
return Object.hasOwn(ASYNC_STATUS_SNAPSHOT_STATES, value);
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
export interface AsyncStatusSnapshotActivityV1 {
|
|
19
35
|
state?: string;
|
|
20
36
|
currentTool?: string;
|
|
@@ -87,6 +103,7 @@ export interface AsyncStatusWorkflowRow {
|
|
|
87
103
|
state: AsyncJobStep["status"] | HostStepState | "planned";
|
|
88
104
|
/** Present only for typed host-owned monitor rows; legacy child rows omit it. */
|
|
89
105
|
kind?: HostStepMonitorKind;
|
|
106
|
+
context?: AsyncJobStep["context"];
|
|
90
107
|
modelThinking?: string;
|
|
91
108
|
activity?: string;
|
|
92
109
|
startedAt?: number;
|
|
@@ -155,7 +172,8 @@ function publicCount(value: unknown): number | undefined {
|
|
|
155
172
|
function normalizeState(value: string): AsyncStatusSnapshotState {
|
|
156
173
|
if (value === "completed") return "complete";
|
|
157
174
|
if (value === "pending") return "queued";
|
|
158
|
-
|
|
175
|
+
if (isAsyncStatusSnapshotState(value)) return value;
|
|
176
|
+
return "partial";
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
function terminalState(state: AsyncStatusSnapshotState): boolean {
|
|
@@ -230,6 +248,16 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
|
|
|
230
248
|
return node;
|
|
231
249
|
}
|
|
232
250
|
|
|
251
|
+
function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
252
|
+
const step: AsyncJobStep = {
|
|
253
|
+
agent: node.agent ?? node.label,
|
|
254
|
+
status: workflowGraphStepStatus(node.status),
|
|
255
|
+
workflowKey: node.id,
|
|
256
|
+
label: node.label,
|
|
257
|
+
};
|
|
258
|
+
return projectStep(step, node.flatIndex ?? index, depth, ctx);
|
|
259
|
+
}
|
|
260
|
+
|
|
233
261
|
function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
234
262
|
const state = normalizeState(child.state);
|
|
235
263
|
const startedAt = publicTime(child.startedAt);
|
|
@@ -310,9 +338,14 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
|
|
|
310
338
|
};
|
|
311
339
|
if (ctx.caps.maxDepth > 0) {
|
|
312
340
|
const stepChildren = job.steps?.map((step, index) => projectStep(step, step.index ?? index, 1, ctx)) ?? [];
|
|
341
|
+
const loadedKeys = new Set(job.steps?.flatMap((step) => step.workflowKey ? [step.workflowKey] : []) ?? []);
|
|
342
|
+
const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
343
|
+
const graphChildren = graphStages
|
|
344
|
+
.filter((graphNode) => !loadedKeys.has(graphNode.id))
|
|
345
|
+
.map((graphNode, index) => projectWorkflowGraphNode(graphNode, index, 1, ctx));
|
|
313
346
|
const nestedChildren = job.nestedChildren?.map((child, index) => projectNestedRun(child, index, 1, ctx)) ?? [];
|
|
314
347
|
const hostStepChildren = validHostStepList(job.hostSteps).map((hostStep) => projectHostStep(hostStep, ctx));
|
|
315
|
-
const ordinaryChildren = [...stepChildren, ...nestedChildren];
|
|
348
|
+
const ordinaryChildren = [...stepChildren, ...graphChildren, ...nestedChildren];
|
|
316
349
|
const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
|
|
317
350
|
const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
|
|
318
351
|
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
@@ -320,7 +353,10 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
|
|
|
320
353
|
ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
|
|
321
354
|
if (bounded.length) node.children = bounded;
|
|
322
355
|
} else {
|
|
323
|
-
|
|
356
|
+
const loadedKeys = new Set(job.steps?.flatMap((step) => step.workflowKey ? [step.workflowKey] : []) ?? []);
|
|
357
|
+
const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
358
|
+
const graphCount = graphStages.filter((graphNode) => !loadedKeys.has(graphNode.id)).length;
|
|
359
|
+
ctx.omitted.children += (job.steps?.length ?? 0) + graphCount + (job.nestedChildren?.length ?? 0) + validHostStepList(job.hostSteps).length;
|
|
324
360
|
}
|
|
325
361
|
return node;
|
|
326
362
|
}
|
|
@@ -392,6 +428,50 @@ function isWorkflowPreflight(value: readonly HostStepNodeV1[] | WorkflowGraphSna
|
|
|
392
428
|
return value !== undefined && !Array.isArray(value) && "lanes" in value;
|
|
393
429
|
}
|
|
394
430
|
|
|
431
|
+
function isWorkflowGraph(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowGraphSnapshot {
|
|
432
|
+
return value !== undefined && !Array.isArray(value) && "nodes" in value;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function workflowGraphRowState(status: WorkflowGraphSnapshot["nodes"][number]["status"]): AsyncStatusWorkflowRow["state"] {
|
|
436
|
+
switch (status) {
|
|
437
|
+
case "pending":
|
|
438
|
+
return "planned";
|
|
439
|
+
case "completed":
|
|
440
|
+
return "complete";
|
|
441
|
+
case "detached":
|
|
442
|
+
return "paused";
|
|
443
|
+
default:
|
|
444
|
+
return status;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function workflowGraphStepStatus(status: WorkflowGraphSnapshot["nodes"][number]["status"]): AsyncJobStep["status"] {
|
|
449
|
+
switch (status) {
|
|
450
|
+
case "completed":
|
|
451
|
+
return "complete";
|
|
452
|
+
case "detached":
|
|
453
|
+
return "paused";
|
|
454
|
+
default:
|
|
455
|
+
return status;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function workflowGraphRowName(node: WorkflowGraphSnapshot["nodes"][number]): string {
|
|
460
|
+
const key = publicText(node.id, "stage", HOST_STEP_MAX_LABEL_CHARS);
|
|
461
|
+
const label = publicOptionalText(node.label, HOST_STEP_MAX_LABEL_CHARS);
|
|
462
|
+
const phase = publicOptionalText(node.phase, HOST_STEP_MAX_LABEL_CHARS);
|
|
463
|
+
const agent = publicOptionalText(node.agent, HOST_STEP_MAX_LABEL_CHARS);
|
|
464
|
+
return `${phase ? `${phase}: ` : ""}${key}${label && label !== node.id ? ` · ${label}` : ""}${agent ? ` (${agent})` : ""}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
|
|
468
|
+
return {
|
|
469
|
+
name: workflowGraphRowName(node),
|
|
470
|
+
state: workflowGraphRowState(node.status),
|
|
471
|
+
...(preflight ? { preflight } : {}),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
395
475
|
/** Project loaded workflow child facts plus stored preflight hints into compact rows. */
|
|
396
476
|
export function projectAsyncWorkflowRows(
|
|
397
477
|
steps: readonly AsyncJobStep[] | undefined,
|
|
@@ -399,9 +479,62 @@ export function projectAsyncWorkflowRows(
|
|
|
399
479
|
preflightOverride?: WorkflowPreflightV1,
|
|
400
480
|
): AsyncStatusWorkflowRow[] {
|
|
401
481
|
const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined);
|
|
402
|
-
const
|
|
482
|
+
const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined;
|
|
483
|
+
const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? undefined : hostStepsOrPreflight;
|
|
403
484
|
const loaded = steps ?? [];
|
|
404
485
|
const declared = new Map<string, WorkflowPreflightLaneV1>();
|
|
486
|
+
if (graph) {
|
|
487
|
+
const loadedIndexesByKey = new Map<string, number[]>();
|
|
488
|
+
for (const [index, step] of loaded.entries()) {
|
|
489
|
+
if (!step.workflowKey) continue;
|
|
490
|
+
const indexes = loadedIndexesByKey.get(step.workflowKey) ?? [];
|
|
491
|
+
indexes.push(index);
|
|
492
|
+
loadedIndexesByKey.set(step.workflowKey, indexes);
|
|
493
|
+
}
|
|
494
|
+
const consumed = new Set<number>();
|
|
495
|
+
const childRows: AsyncStatusWorkflowRow[] = [];
|
|
496
|
+
for (const lane of preflight?.lanes ?? []) declared.set(lane.key, lane);
|
|
497
|
+
const graphStages = workflowGraphStageNodes(graph);
|
|
498
|
+
const graphKeys = new Set(graphStages.map((node) => node.id));
|
|
499
|
+
const graphPhaseByNodeId = new Map<string, string>();
|
|
500
|
+
for (const phase of graph.phases) {
|
|
501
|
+
for (const nodeId of phase.nodeIds) {
|
|
502
|
+
if (graphKeys.has(nodeId) && !graphPhaseByNodeId.has(nodeId)) graphPhaseByNodeId.set(nodeId, phase.title);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const graphLaneKeys = new Set(graphPhaseByNodeId.values());
|
|
506
|
+
const preflightForNode = (node: WorkflowGraphSnapshot["nodes"][number]): WorkflowPreflightLaneV1 | undefined => {
|
|
507
|
+
const phaseTitle = graphPhaseByNodeId.get(node.id);
|
|
508
|
+
return declared.get(node.id) ?? (node.phase ? declared.get(node.phase) : undefined) ?? (phaseTitle ? declared.get(phaseTitle) : undefined);
|
|
509
|
+
};
|
|
510
|
+
for (const node of graphStages) {
|
|
511
|
+
const indexes = (loadedIndexesByKey.get(node.id) ?? []).filter((index) => !consumed.has(index));
|
|
512
|
+
if (indexes.length > 0) {
|
|
513
|
+
for (const index of indexes) {
|
|
514
|
+
consumed.add(index);
|
|
515
|
+
childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, preflightForNode(node)));
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
childRows.push(projectWorkflowGraphRow(node, preflightForNode(node)));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
for (const lane of preflight?.lanes ?? []) {
|
|
522
|
+
if (graphKeys.has(lane.key) || graphLaneKeys.has(lane.key)) continue;
|
|
523
|
+
const indexes = (loadedIndexesByKey.get(lane.key) ?? []).filter((index) => !consumed.has(index));
|
|
524
|
+
if (indexes.length > 0) {
|
|
525
|
+
for (const index of indexes) {
|
|
526
|
+
consumed.add(index);
|
|
527
|
+
childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, lane));
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
childRows.push({ name: lane.key, state: "planned", preflight: lane });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const [index, step] of loaded.entries()) {
|
|
534
|
+
if (!consumed.has(index)) childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? declared.get(step.workflowKey) : undefined));
|
|
535
|
+
}
|
|
536
|
+
return [...childRows, ...validHostStepList(graph).map(hostStepRow)];
|
|
537
|
+
}
|
|
405
538
|
const loadedIndexByKey = new Map<string, number>();
|
|
406
539
|
for (const [index, step] of loaded.entries()) {
|
|
407
540
|
if (step.workflowKey !== undefined && !loadedIndexByKey.has(step.workflowKey)) loadedIndexByKey.set(step.workflowKey, index);
|
|
@@ -431,6 +564,7 @@ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
|
|
|
431
564
|
return {
|
|
432
565
|
name: workflowStepName(step, index),
|
|
433
566
|
state: step.status,
|
|
567
|
+
...(step.context ? { context: step.context } : {}),
|
|
434
568
|
...(modelThinking ? { modelThinking } : {}),
|
|
435
569
|
...(activity ? { activity } : {}),
|
|
436
570
|
...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
|
|
@@ -125,11 +125,8 @@ export function formatUnresolvedMcpDirectToolSelectors(selectors: readonly strin
|
|
|
125
125
|
|
|
126
126
|
function getServerPrefix(serverName: string, mode: McpToolPrefix): string {
|
|
127
127
|
if (mode === "none") return "";
|
|
128
|
-
if (mode === "short")
|
|
129
|
-
|
|
130
|
-
return short || "mcp";
|
|
131
|
-
}
|
|
132
|
-
return serverName.replace(/-/g, "_");
|
|
128
|
+
if (mode === "short") return serverName.replace(/-?mcp$/i, "") || "mcp";
|
|
129
|
+
return serverName;
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
function formatToolName(toolName: string, serverName: string, prefix: McpToolPrefix): string {
|
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import type { ArtifactPaths, TimeoutRecoverySummary, TrackedMutationEvidence, TrackedMutationFingerprint, TrackedMutationSnapshot } from "../../shared/types.ts";
|
|
6
|
+
import type { ArtifactPaths, TimeoutRecoveryProjection, TimeoutRecoverySummary, TrackedMutationEvidence, TrackedMutationFingerprint, TrackedMutationSnapshot } from "../../shared/types.ts";
|
|
7
7
|
|
|
8
8
|
const MAX_TRACKED_PATHS = 500;
|
|
9
9
|
const MAX_HASH_BYTES = 1024 * 1024;
|
|
@@ -15,7 +15,7 @@ function gitArguments(args: string[]): string[] {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function gitOutput(cwd: string, args: string[], maxBuffer = MAX_HASH_BYTES): string {
|
|
18
|
-
return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
|
|
18
|
+
return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer, windowsHide: true });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function splitNul(output: string): string[] {
|
|
@@ -26,7 +26,7 @@ function hashLargeDiff(cwd: string, relativePath: string): string {
|
|
|
26
26
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tracked-diff-"));
|
|
27
27
|
const diffPath = path.join(tempDir, "diff.patch");
|
|
28
28
|
try {
|
|
29
|
-
execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore" });
|
|
29
|
+
execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore", windowsHide: true });
|
|
30
30
|
const hash = createHash("sha256");
|
|
31
31
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
32
32
|
const fd = fs.openSync(diffPath, "r");
|
|
@@ -110,9 +110,48 @@ function formatPathList(paths: string[]): string {
|
|
|
110
110
|
return paths.length > MAX_TIMEOUT_FILES ? `${shown}, ... (${paths.length - MAX_TIMEOUT_FILES} more)` : shown;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** Keep status and completion details to bounded routing evidence, not raw output or effects. */
|
|
114
|
+
export function projectTimeoutRecovery(value: unknown): TimeoutRecoveryProjection | undefined {
|
|
115
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
116
|
+
const source = value as Record<string, unknown>;
|
|
117
|
+
const termination = source.termination === "timed-out" || source.termination === "stopped" ? source.termination : undefined;
|
|
118
|
+
if (!termination || !Array.isArray(source.changedFiles)) return undefined;
|
|
119
|
+
const allChangedFiles = source.changedFiles.filter((file): file is string => typeof file === "string");
|
|
120
|
+
const changedFiles = allChangedFiles.slice(0, MAX_TIMEOUT_FILES);
|
|
121
|
+
const reportStatus = source.reportStatus === "missing"
|
|
122
|
+
|| source.reportStatus === "written"
|
|
123
|
+
|| source.reportStatus === "not-requested"
|
|
124
|
+
|| source.reportStatus === "unknown"
|
|
125
|
+
? source.reportStatus
|
|
126
|
+
: undefined;
|
|
127
|
+
return {
|
|
128
|
+
termination,
|
|
129
|
+
changedFiles,
|
|
130
|
+
...(source.truncated === true || allChangedFiles.length > changedFiles.length ? { truncated: true } : {}),
|
|
131
|
+
...(source.recoveryNeeded === true ? { recoveryNeeded: true } : {}),
|
|
132
|
+
...(source.reason === "timed-out-with-dirty-worktree" ? { reason: source.reason } : {}),
|
|
133
|
+
...(reportStatus ? { reportStatus } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Render only the bounded recovery route needed by a parent/operator. */
|
|
138
|
+
export function formatTimeoutRecoveryLines(value: unknown, indent = ""): string[] {
|
|
139
|
+
const recovery = projectTimeoutRecovery(value);
|
|
140
|
+
if (!recovery?.recoveryNeeded) return [];
|
|
141
|
+
const changedFiles = recovery.changedFiles.length > 0
|
|
142
|
+
? `${recovery.changedFiles.join(", ")}${recovery.truncated ? ", …" : ""}`
|
|
143
|
+
: "none";
|
|
144
|
+
const changedFileCount = recovery.changedFiles.length > 0 ? `${recovery.changedFiles.length}${recovery.truncated ? "+" : ""}` : "0";
|
|
145
|
+
return [
|
|
146
|
+
`${indent}Recovery needed: review the diff and artifacts before resuming or launching dependent stages.`,
|
|
147
|
+
`${indent}Recovery evidence: requested report: ${recovery.reportStatus ?? "unknown"}; changed tracked files: ${changedFiles} (${changedFileCount}); classification: ${recovery.reason ?? recovery.termination}`,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
export function buildTimeoutRecoverySummary(input: {
|
|
114
152
|
termination: "timed-out" | "stopped";
|
|
115
153
|
evidence: TrackedMutationEvidence;
|
|
154
|
+
requiredOutputMissing?: boolean;
|
|
116
155
|
currentTool?: string;
|
|
117
156
|
currentToolArgs?: string;
|
|
118
157
|
currentPath?: string;
|
|
@@ -122,11 +161,19 @@ export function buildTimeoutRecoverySummary(input: {
|
|
|
122
161
|
}): TimeoutRecoverySummary {
|
|
123
162
|
const warning = "Inspect partial changes before retrying or resuming the child.";
|
|
124
163
|
const changedFiles = input.evidence.changedFiles.slice(0, MAX_TIMEOUT_FILES);
|
|
164
|
+
let reportStatus: "missing" | "written" | "not-requested" = "not-requested";
|
|
165
|
+
if (input.requiredOutputMissing === true) reportStatus = "missing";
|
|
166
|
+
else if (input.requiredOutputMissing === false) reportStatus = "written";
|
|
167
|
+
const recoveryNeeded = input.termination === "timed-out"
|
|
168
|
+
&& reportStatus === "missing"
|
|
169
|
+
&& input.evidence.changedFiles.length > 0;
|
|
125
170
|
const lines = [
|
|
126
171
|
"Recovery summary:",
|
|
127
172
|
`- termination: ${input.termination}`,
|
|
128
173
|
`- changed tracked files: ${input.evidence.unavailable ? `unavailable (${input.evidence.unavailable})` : formatPathList(input.evidence.changedFiles)}`,
|
|
129
174
|
];
|
|
175
|
+
if (input.requiredOutputMissing !== undefined) lines.push(`- requested report: ${reportStatus}`);
|
|
176
|
+
if (recoveryNeeded) lines.push("- Recovery needed: review the diff and artifacts before resuming or launching dependent stages.");
|
|
130
177
|
if (input.currentTool) lines.push(`- active tool: ${input.currentTool}${input.currentToolArgs ? ` — ${input.currentToolArgs}` : ""}`);
|
|
131
178
|
if (input.currentPath) lines.push(`- active path: ${input.currentPath}`);
|
|
132
179
|
if (input.sessionFile) lines.push(`- session file: ${input.sessionFile}`);
|
|
@@ -138,6 +185,8 @@ export function buildTimeoutRecoverySummary(input: {
|
|
|
138
185
|
termination: input.termination,
|
|
139
186
|
changedFiles,
|
|
140
187
|
truncated: input.evidence.changedFiles.length > changedFiles.length || input.evidence.truncated || undefined,
|
|
188
|
+
...(recoveryNeeded ? { recoveryNeeded: true, reason: "timed-out-with-dirty-worktree" as const } : {}),
|
|
189
|
+
reportStatus,
|
|
141
190
|
currentTool: input.currentTool,
|
|
142
191
|
currentToolArgs: input.currentToolArgs,
|
|
143
192
|
currentPath: input.currentPath,
|
|
@@ -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;
|
|
@@ -507,8 +517,13 @@ export function resolvePiLaunchToolPlan(
|
|
|
507
517
|
throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
|
|
508
518
|
}
|
|
509
519
|
const resolvedMcpSelections = mcpResolution.selections;
|
|
520
|
+
const resolvedMcpNames = new Set(resolvedMcpSelections.map((selection) => selection.name));
|
|
521
|
+
const legacyMcpNameCounts = countLegacyUnderscoreMcpToolNames(resolvedMcpSelections);
|
|
510
522
|
const effectiveMcpSelections = resolvedMcpSelections.filter(
|
|
511
|
-
(selection) =>
|
|
523
|
+
(selection) =>
|
|
524
|
+
!allowedToolSet ||
|
|
525
|
+
allowedToolSet.has(selection.name) ||
|
|
526
|
+
isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
|
|
512
527
|
);
|
|
513
528
|
const effectiveMcpTools = effectiveMcpSelections.map(
|
|
514
529
|
(selection) => selection.name,
|
|
@@ -663,6 +678,36 @@ export function resolvePiLaunchToolPlan(
|
|
|
663
678
|
};
|
|
664
679
|
}
|
|
665
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
|
+
|
|
666
711
|
/** Escape XML-significant characters in a string for safe attribute interpolation. */
|
|
667
712
|
function escapeXmlAttr(value: string): string {
|
|
668
713
|
return value
|
|
@@ -974,6 +1019,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
974
1019
|
|
|
975
1020
|
env[SUBAGENT_PARENT_SESSION_ENV] =
|
|
976
1021
|
input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
|
|
1022
|
+
env[SUBAGENT_FORK_CACHE_KEY_ENV] = input.forkCacheKey?.trim() || undefined;
|
|
977
1023
|
|
|
978
1024
|
return {
|
|
979
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
|
/**
|
|
@@ -183,12 +194,36 @@ export function captureSingleOutputSnapshot(outputPath: string | undefined): Sin
|
|
|
183
194
|
try {
|
|
184
195
|
const stat = fs.statSync(outputPath);
|
|
185
196
|
return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
186
|
-
} catch {
|
|
187
|
-
|
|
188
|
-
|
|
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) };
|
|
189
215
|
}
|
|
190
216
|
}
|
|
191
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
|
+
|
|
192
227
|
function persistSingleOutput(
|
|
193
228
|
outputPath: string | undefined,
|
|
194
229
|
fullOutput: string,
|
|
@@ -216,23 +251,15 @@ export function resolveSingleOutput(
|
|
|
216
251
|
const claimError = outputClaimError(outputPath, expectedClaimPath);
|
|
217
252
|
if (claimError) return { fullOutput: fallbackOutput, saveError: claimError, fatalError: true };
|
|
218
253
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch (error) {
|
|
226
|
-
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
227
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
228
|
-
return {
|
|
229
|
-
fullOutput: fallbackOutput,
|
|
230
|
-
saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
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
|
+
};
|
|
233
260
|
}
|
|
234
261
|
|
|
235
|
-
if (changedSinceStart) {
|
|
262
|
+
if (changedSinceStart.changed) {
|
|
236
263
|
try {
|
|
237
264
|
return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
|
|
238
265
|
} catch (error) {
|
|
@@ -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 {
|
|
@@ -295,6 +295,22 @@ const COMPOSITE_TOOL_ID_APIS = new Set([
|
|
|
295
295
|
"openai-completions",
|
|
296
296
|
"openai-responses",
|
|
297
297
|
]);
|
|
298
|
+
const PROMPT_CACHE_KEY_APIS = new Set([
|
|
299
|
+
"azure-openai-responses",
|
|
300
|
+
"openai-codex-responses",
|
|
301
|
+
"openai-completions",
|
|
302
|
+
"openai-responses",
|
|
303
|
+
]);
|
|
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
|
+
}
|
|
298
314
|
|
|
299
315
|
function portableToolId(id: string): string {
|
|
300
316
|
if (PORTABLE_TOOL_ID_PATTERN.test(id) && id.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return id;
|
|
@@ -748,6 +764,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
748
764
|
});
|
|
749
765
|
}
|
|
750
766
|
|
|
767
|
+
onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx));
|
|
768
|
+
|
|
751
769
|
onRuntimeEvent("context", (event: unknown, ctx?: ExtensionContext) => {
|
|
752
770
|
if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages)) return undefined;
|
|
753
771
|
const messages = stripParentOnlySubagentMessages(event.messages, {
|
|
@@ -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":
|