pi-subagents 0.65.1 → 0.67.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 +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
4
|
import { previewDisplayText } from "../../shared/display-text.ts";
|
|
5
5
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
6
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type
|
|
6
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNode, type HostStepState, type LaunchResolvedChildExtensions, type RuntimeAcknowledgedChildExtensions, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TimeoutRecoveryProjection, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorktreeNaming, type WorkflowPreflight, type WorkflowGraphSnapshot } from "../../shared/types.ts";
|
|
7
7
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
8
8
|
import { readStatus } from "../../shared/utils.ts";
|
|
9
9
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
@@ -25,6 +25,7 @@ import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSumma
|
|
|
25
25
|
import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
|
|
26
26
|
import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
|
|
27
27
|
import { formatWorkflowChecklistText, projectWorkflowChecklist } from "../../workflows/workflow-checklist.ts";
|
|
28
|
+
import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
|
|
28
29
|
|
|
29
30
|
interface AsyncRunStepSummary {
|
|
30
31
|
index: number;
|
|
@@ -82,8 +83,8 @@ interface AsyncRunStepSummary {
|
|
|
82
83
|
effects?: AsyncJobStep["effects"];
|
|
83
84
|
processTerminal?: AsyncJobStep["processTerminal"];
|
|
84
85
|
timeoutRecovery?: TimeoutRecoveryProjection;
|
|
85
|
-
launchResolvedExtensions?:
|
|
86
|
-
runtimeAcknowledgedExtensions?:
|
|
86
|
+
launchResolvedExtensions?: LaunchResolvedChildExtensions;
|
|
87
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
87
88
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
88
89
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
89
90
|
children?: NestedRunSummary[];
|
|
@@ -122,7 +123,7 @@ export interface AsyncRunSummary {
|
|
|
122
123
|
chainStepCount?: number;
|
|
123
124
|
pendingAppends?: number;
|
|
124
125
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
125
|
-
hostSteps?:
|
|
126
|
+
hostSteps?: HostStepNode[];
|
|
126
127
|
workflowGraph?: AsyncStatus["workflowGraph"];
|
|
127
128
|
steps: AsyncRunStepSummary[];
|
|
128
129
|
sessionDir?: string;
|
|
@@ -135,8 +136,8 @@ export interface AsyncRunSummary {
|
|
|
135
136
|
nestedWarnings?: string[];
|
|
136
137
|
processTerminal?: AsyncStatus["processTerminal"];
|
|
137
138
|
runFanoutBudget?: AsyncStatus["runFanoutBudget"];
|
|
138
|
-
launchResolvedExtensions?:
|
|
139
|
-
runtimeAcknowledgedExtensions?:
|
|
139
|
+
launchResolvedExtensions?: LaunchResolvedChildExtensions;
|
|
140
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
140
141
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
141
142
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
142
143
|
parentWorkflowRunId?: string;
|
|
@@ -144,7 +145,7 @@ export interface AsyncRunSummary {
|
|
|
144
145
|
lane?: AsyncStatus["lane"];
|
|
145
146
|
workflow?: Details["workflow"];
|
|
146
147
|
workflowChildren?: Details["workflowChildren"];
|
|
147
|
-
preflight?:
|
|
148
|
+
preflight?: WorkflowPreflight;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
interface AsyncRunListOptions {
|
|
@@ -481,7 +482,7 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
481
482
|
});
|
|
482
483
|
}
|
|
483
484
|
|
|
484
|
-
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
485
|
+
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}, observeStatus?: RawDrainStatusObserver): AsyncRunSummary[] {
|
|
485
486
|
let entries: string[];
|
|
486
487
|
const activeEntries = new Set<string>();
|
|
487
488
|
const wantsActive = options.states === undefined || options.states.some(isActiveAsyncState);
|
|
@@ -508,6 +509,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
508
509
|
indexed.add(entry);
|
|
509
510
|
activeEntries.add(entry);
|
|
510
511
|
} else {
|
|
512
|
+
observeStatus?.(null);
|
|
511
513
|
updateActiveRunIndex(path.join(asyncDirRoot, entry), "failed");
|
|
512
514
|
}
|
|
513
515
|
}
|
|
@@ -519,6 +521,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
519
521
|
}
|
|
520
522
|
} catch (error) {
|
|
521
523
|
if (isNotFoundError(error)) return [];
|
|
524
|
+
observeStatus?.(null);
|
|
522
525
|
throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
|
|
523
526
|
cause: error instanceof Error ? error : undefined,
|
|
524
527
|
});
|
|
@@ -544,14 +547,17 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
544
547
|
try {
|
|
545
548
|
const reconciliation = options.reconcile === false
|
|
546
549
|
? undefined
|
|
547
|
-
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
550
|
+
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, observeStatus);
|
|
548
551
|
status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
552
|
+
if (options.reconcile === false) observeStatus?.(status);
|
|
549
553
|
} catch (error) {
|
|
554
|
+
observeStatus?.(null);
|
|
550
555
|
if (!activeEntries.has(entry) || !isAsyncStatusIsolationError(asyncDir, error)) throw error;
|
|
551
556
|
isolateCorruptActiveRun(asyncDir, entry, error, options.now);
|
|
552
557
|
continue;
|
|
553
558
|
}
|
|
554
559
|
if (!status) {
|
|
560
|
+
observeStatus?.(null);
|
|
555
561
|
if (activeEntries.has(entry)) updateActiveRunIndex(asyncDir, "failed");
|
|
556
562
|
continue;
|
|
557
563
|
}
|
|
@@ -573,6 +579,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
573
579
|
nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
|
|
574
580
|
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
575
581
|
} catch (error) {
|
|
582
|
+
observeStatus?.(null);
|
|
576
583
|
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
577
584
|
}
|
|
578
585
|
}
|
|
@@ -580,6 +587,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
580
587
|
try {
|
|
581
588
|
summary = statusToSummary(asyncDir, status, nestedWarnings, nestedRoute);
|
|
582
589
|
} catch (error) {
|
|
590
|
+
observeStatus?.(null);
|
|
583
591
|
if (!activeEntries.has(entry) || !isAsyncStatusIsolationError(asyncDir, error)) throw error;
|
|
584
592
|
isolateCorruptActiveRun(asyncDir, entry, error, options.now);
|
|
585
593
|
continue;
|
|
@@ -2,6 +2,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import { snapshotBackgroundWork } from "../../api/background-work.ts";
|
|
3
3
|
import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { listAsyncRuns } from "./async-status.ts";
|
|
5
|
+
import type { ReadonlyDrainObservation } from "../shared/readonly-drain-observation.ts";
|
|
5
6
|
import { waitForSubagents, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -23,46 +24,59 @@ function resultText(value: AgentToolResult<Details>): string {
|
|
|
23
24
|
return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function hasOutstandingWork(sessionId: string, nowMs: number): boolean {
|
|
27
|
+
function hasOutstandingWork(state: SubagentState, sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
|
|
27
28
|
const asyncRuns = listAsyncRuns(DIRS.async, {
|
|
28
29
|
states: ["queued", "running"],
|
|
29
30
|
sessionId,
|
|
30
31
|
resultsDir: DIRS.results,
|
|
31
32
|
now: () => nowMs,
|
|
32
|
-
});
|
|
33
|
-
|
|
33
|
+
}, observation?.status);
|
|
34
|
+
const detachedForeground = [...(state.foregroundRuns?.values() ?? [])].some((run) =>
|
|
35
|
+
run.sessionId === sessionId && run.children.some((child) => child.status === "detached")
|
|
36
|
+
);
|
|
37
|
+
return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0 || detachedForeground;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/** Drain all work owned by the current headless session, including work added while draining. */
|
|
37
|
-
export async function drainOutstandingWork(deps: AutoDrainDeps): Promise<void> {
|
|
41
|
+
export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: ReadonlyDrainObservation): Promise<void> {
|
|
38
42
|
const sessionId = deps.state.currentSessionId;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
observation?.begin(sessionId, !deps.hasWork && !deps.wait && !deps.now);
|
|
44
|
+
try {
|
|
45
|
+
if (!sessionId) throw new Error("Cannot auto-drain background work without an active session identity.");
|
|
46
|
+
const now = deps.now ?? Date.now;
|
|
47
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
|
|
48
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
|
|
49
|
+
const deadlineAt = now() + timeoutMs;
|
|
50
|
+
const hasWork = deps.hasWork ?? ((id: string, time: number) => hasOutstandingWork(deps.state, id, time, observation));
|
|
51
|
+
const wait = deps.wait ?? waitForSubagents;
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
while (true) {
|
|
54
|
+
const work = hasWork(sessionId, now());
|
|
55
|
+
observation?.predicate(work);
|
|
56
|
+
if (!work) break;
|
|
57
|
+
const remainingMs = deadlineAt - now();
|
|
58
|
+
if (remainingMs <= 0) {
|
|
59
|
+
throw new Error(`Auto-drain timed out after ${timeoutMs}ms with background work still active in session '${sessionId}'.`);
|
|
60
|
+
}
|
|
61
|
+
const waitResult = await wait(
|
|
62
|
+
{ all: true, timeoutMs: remainingMs },
|
|
63
|
+
undefined,
|
|
64
|
+
{
|
|
65
|
+
state: deps.state,
|
|
66
|
+
events: deps.events,
|
|
67
|
+
now,
|
|
68
|
+
stopOnAttention: false,
|
|
69
|
+
failOnFailedRuns: true,
|
|
70
|
+
failOnAttention: true,
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
if (waitResult.isError) {
|
|
74
|
+
throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "bg_wait returned an error without details"}.`);
|
|
75
|
+
}
|
|
66
76
|
}
|
|
77
|
+
observation?.complete();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
observation?.deny();
|
|
80
|
+
throw error;
|
|
67
81
|
}
|
|
68
82
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as sdk from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { runConfiguredSubagent, type SubagentRunConfig } from "./subagent-runner.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pi's extension loader supplies the embedded SDK. Bare Bun/Node cannot replace
|
|
8
|
+
* it: an apparent import success can be an auto-installed, different SDK.
|
|
9
|
+
* Await the configured runner inside extension initialization so the outer Pi
|
|
10
|
+
* session/RPC loop never starts. Exit only after shared disposal/lease release.
|
|
11
|
+
* Derived from xz-dev's PR #2049, commit 910807bfefcf9ee41d73fa25ec86dcd75ab8f4b2.
|
|
12
|
+
*/
|
|
13
|
+
export default async function runBinaryBootstrap(): Promise<never> {
|
|
14
|
+
const configPath = process.env.PI_SUBAGENT_RUNNER_CONFIG;
|
|
15
|
+
delete process.env.PI_SUBAGENT_RUNNER_CONFIG;
|
|
16
|
+
try {
|
|
17
|
+
if (!configPath || !path.isAbsolute(configPath)) throw new Error("Missing absolute PI_SUBAGENT_RUNNER_CONFIG path");
|
|
18
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as SubagentRunConfig;
|
|
19
|
+
if (!config || typeof config.id !== "string" || typeof config.asyncDir !== "string" || !Array.isArray(config.steps)) {
|
|
20
|
+
throw new Error("Invalid binary runner configuration");
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
fs.unlinkSync(configPath);
|
|
24
|
+
} catch {
|
|
25
|
+
// Temp-config cleanup is best effort, as in the Node entrypoint.
|
|
26
|
+
}
|
|
27
|
+
await runConfiguredSubagent(config, { loadPiCodingAgent: async () => sdk });
|
|
28
|
+
process.exit(0);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("Subagent binary runner error:", error);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -32,6 +32,7 @@ export interface ImportedAsyncRootResult {
|
|
|
32
32
|
structuredOutputSchemaPath?: string;
|
|
33
33
|
acceptance?: AcceptanceLedger;
|
|
34
34
|
artifactPaths?: ArtifactPaths;
|
|
35
|
+
savedOutputPath?: string;
|
|
35
36
|
outputSaveError?: string;
|
|
36
37
|
transcriptPath?: string;
|
|
37
38
|
transcriptError?: string;
|
|
@@ -71,6 +72,7 @@ interface AsyncResultFile {
|
|
|
71
72
|
structuredOutputSchemaPath?: string;
|
|
72
73
|
acceptance?: AcceptanceLedger;
|
|
73
74
|
artifactPaths?: ArtifactPaths;
|
|
75
|
+
savedOutputPath?: string;
|
|
74
76
|
outputSaveError?: string;
|
|
75
77
|
transcriptPath?: string;
|
|
76
78
|
transcriptError?: string;
|
|
@@ -121,6 +123,11 @@ function selectedStatusStep(status: AsyncStatus | null, index: number): NonNulla
|
|
|
121
123
|
|
|
122
124
|
function isTerminalStatus(status: AsyncStatus | null, index: number): boolean {
|
|
123
125
|
if (!status) return false;
|
|
126
|
+
// A workflow-owned single runner publishes its result after completing its
|
|
127
|
+
// step. Keep that window open while root process proof is absent or pending;
|
|
128
|
+
// explicit terminal/unavailable proof retains the existing step fallback.
|
|
129
|
+
if (status.mode === "single" && status.parentWorkflowRunId
|
|
130
|
+
&& (!status.processTerminal || status.processTerminal.state === "pending")) return TERMINAL_STATES.has(status.state);
|
|
124
131
|
const step = selectedStatusStep(status, index);
|
|
125
132
|
if (step && TERMINAL_STEP_STATUSES.has(step.status)) return true;
|
|
126
133
|
return TERMINAL_STATES.has(status.state);
|
|
@@ -229,6 +236,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
229
236
|
...(child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath } : {}),
|
|
230
237
|
...(child?.acceptance ?? step?.acceptance ? { acceptance: child?.acceptance ?? step?.acceptance } : {}),
|
|
231
238
|
...(child?.artifactPaths ? { artifactPaths: child.artifactPaths } : {}),
|
|
239
|
+
...(child?.savedOutputPath ? { savedOutputPath: child.savedOutputPath } : {}),
|
|
232
240
|
...(child?.outputSaveError ? { outputSaveError: child.outputSaveError } : {}),
|
|
233
241
|
...(child?.transcriptPath ?? step?.transcriptPath ? { transcriptPath: child?.transcriptPath ?? step?.transcriptPath } : {}),
|
|
234
242
|
...(child?.transcriptError ? { transcriptError: child.transcriptError } : {}),
|
|
@@ -275,28 +275,36 @@ function parseSteerRequest(raw: unknown): SteerRequest | undefined {
|
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
export function consumeSteerRequestsFromDir(dir: string, fsImpl: Pick<typeof fs, "existsSync" | "rmSync" | "readdirSync" | "readFileSync"> = fs): SteerRequest[] {
|
|
279
|
-
if (!fsImpl.existsSync(dir)) return [];
|
|
278
|
+
export function consumeSteerRequestsFromDir(dir: string, fsImpl: Pick<typeof fs, "existsSync" | "rmSync" | "readdirSync" | "readFileSync"> = fs, onError: (error: unknown) => void = () => {}): SteerRequest[] {
|
|
280
279
|
let entries: string[];
|
|
281
280
|
try {
|
|
282
281
|
entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort();
|
|
283
|
-
} catch {
|
|
282
|
+
} catch (error) {
|
|
284
283
|
// Leave requests in place so the periodic poll can retry the scan.
|
|
284
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") onError(error);
|
|
285
285
|
return [];
|
|
286
286
|
}
|
|
287
287
|
const requests: SteerRequest[] = [];
|
|
288
288
|
for (const entry of entries) {
|
|
289
289
|
const requestPath = path.join(dir, entry);
|
|
290
290
|
let parsed: SteerRequest | undefined;
|
|
291
|
+
let text: string;
|
|
292
|
+
try {
|
|
293
|
+
text = fsImpl.readFileSync(requestPath, "utf-8");
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") onError(error);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
291
298
|
try {
|
|
292
|
-
parsed = parseSteerRequest(JSON.parse(
|
|
299
|
+
parsed = parseSteerRequest(JSON.parse(text));
|
|
293
300
|
} catch {
|
|
294
301
|
parsed = undefined;
|
|
295
302
|
}
|
|
296
303
|
try {
|
|
297
304
|
fsImpl.rmSync(requestPath, { recursive: true });
|
|
298
|
-
} catch {
|
|
305
|
+
} catch (error) {
|
|
299
306
|
// Already removed by a concurrent check — do not execute it twice.
|
|
307
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") onError(error);
|
|
300
308
|
continue;
|
|
301
309
|
}
|
|
302
310
|
if (parsed) requests.push(parsed);
|
|
@@ -304,8 +312,8 @@ export function consumeSteerRequestsFromDir(dir: string, fsImpl: Pick<typeof fs,
|
|
|
304
312
|
return requests.sort((left, right) => left.ts - right.ts || left.id.localeCompare(right.id));
|
|
305
313
|
}
|
|
306
314
|
|
|
307
|
-
export function consumeSteerRequests(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "rmSync" | "readdirSync" | "readFileSync"> = fs): SteerRequest[] {
|
|
308
|
-
return consumeSteerRequestsFromDir(steerRequestsDir(asyncDir), fsImpl);
|
|
315
|
+
export function consumeSteerRequests(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "rmSync" | "readdirSync" | "readFileSync"> = fs, onError?: (error: unknown) => void): SteerRequest[] {
|
|
316
|
+
return consumeSteerRequestsFromDir(steerRequestsDir(asyncDir), fsImpl, onError);
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
export function queueRevivalBrief(asyncDir: string, request: SteerRequest): string {
|
|
@@ -387,17 +395,26 @@ function parseStopRequest(raw: unknown): StopRequest | undefined {
|
|
|
387
395
|
function consumeStopRequestFile(
|
|
388
396
|
requestPath: string,
|
|
389
397
|
fsImpl: Pick<typeof fs, "rmSync" | "readFileSync">,
|
|
398
|
+
onError?: (error: unknown) => void,
|
|
390
399
|
): StopRequest | undefined {
|
|
400
|
+
let text: string;
|
|
401
|
+
try {
|
|
402
|
+
text = fsImpl.readFileSync(requestPath, "utf-8");
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) onError?.(error);
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
391
407
|
let request: StopRequest | undefined;
|
|
392
408
|
try {
|
|
393
|
-
request = parseStopRequest(JSON.parse(
|
|
409
|
+
request = parseStopRequest(JSON.parse(text));
|
|
394
410
|
} catch {
|
|
395
411
|
request = undefined;
|
|
396
412
|
}
|
|
397
413
|
try {
|
|
398
414
|
fsImpl.rmSync(requestPath, { force: true, recursive: true });
|
|
399
|
-
} catch {
|
|
400
|
-
//
|
|
415
|
+
} catch (error) {
|
|
416
|
+
// Execute only after successful removal.
|
|
417
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) onError?.(error);
|
|
401
418
|
return undefined;
|
|
402
419
|
}
|
|
403
420
|
return request;
|
|
@@ -406,6 +423,7 @@ function consumeStopRequestFile(
|
|
|
406
423
|
export function consumeStopRequestPayloads(
|
|
407
424
|
asyncDir: string,
|
|
408
425
|
fsImpl: Pick<typeof fs, "existsSync" | "rmSync" | "readdirSync" | "readFileSync"> = fs,
|
|
426
|
+
onError?: (error: unknown) => void,
|
|
409
427
|
): StopRequest[] {
|
|
410
428
|
const dir = stopRequestsDir(asyncDir);
|
|
411
429
|
const requests: StopRequest[] = [];
|
|
@@ -413,18 +431,19 @@ export function consumeStopRequestPayloads(
|
|
|
413
431
|
let entries: string[];
|
|
414
432
|
try {
|
|
415
433
|
entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort();
|
|
416
|
-
} catch {
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) onError?.(error);
|
|
417
436
|
entries = [];
|
|
418
437
|
}
|
|
419
438
|
for (const entry of entries) {
|
|
420
|
-
const request = consumeStopRequestFile(path.join(dir, entry), fsImpl);
|
|
439
|
+
const request = consumeStopRequestFile(path.join(dir, entry), fsImpl, onError);
|
|
421
440
|
if (request) requests.push(request);
|
|
422
441
|
}
|
|
423
442
|
}
|
|
424
443
|
|
|
425
444
|
const legacyPath = stopRequestPath(asyncDir);
|
|
426
445
|
if (fsImpl.existsSync(legacyPath)) {
|
|
427
|
-
const request = consumeStopRequestFile(legacyPath, fsImpl);
|
|
446
|
+
const request = consumeStopRequestFile(legacyPath, fsImpl, onError);
|
|
428
447
|
if (request) requests.push(request);
|
|
429
448
|
}
|
|
430
449
|
return requests.sort((left, right) => (left.ts ?? 0) - (right.ts ?? 0));
|
|
@@ -470,19 +489,21 @@ export function deliverStopRequest(input: {
|
|
|
470
489
|
requestAsyncStop(input.asyncDir, { ...(input.source ? { source: input.source } : {}), ...(input.targetIndex !== undefined ? { targetIndex: input.targetIndex } : {}), ...(input.childId ? { childId: input.childId } : {}) }, { now: input.now });
|
|
471
490
|
}
|
|
472
491
|
|
|
492
|
+
|
|
473
493
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
494
|
+
* Active owner: watch and consume only kinds with installed handlers.
|
|
495
|
+
* Uses `fs.watch` when available and starts interval polling
|
|
476
496
|
* only when native watching is unavailable or fails. Fires once per distinct
|
|
477
497
|
* request. Returns a disposer.
|
|
478
498
|
*/
|
|
479
499
|
export function watchAsyncControlInbox(
|
|
480
500
|
asyncDir: string,
|
|
481
501
|
opts: {
|
|
482
|
-
onInterrupt
|
|
502
|
+
onInterrupt?: () => void;
|
|
483
503
|
onTimeout?: () => void;
|
|
484
504
|
onStop?: (request: StopRequest) => void;
|
|
485
505
|
onSteer?: (request: SteerRequest) => void;
|
|
506
|
+
onError?: (error: unknown, phase: "install" | "scan" | "callback", request?: SteerRequest) => void;
|
|
486
507
|
pollIntervalMs?: number;
|
|
487
508
|
safetyPollIntervalMs?: number;
|
|
488
509
|
platform?: NodeJS.Platform;
|
|
@@ -493,22 +514,44 @@ export function watchAsyncControlInbox(
|
|
|
493
514
|
const fsImpl = opts.fs ?? fs;
|
|
494
515
|
const timers = opts.timers ?? { setInterval, clearInterval };
|
|
495
516
|
const dir = controlInboxDir(asyncDir);
|
|
517
|
+
const report = (error: unknown, phase: "install" | "scan" | "callback", request?: SteerRequest): void => {
|
|
518
|
+
try {
|
|
519
|
+
if (opts.onError) opts.onError(error, phase, request);
|
|
520
|
+
else console.error(`Control inbox ${phase} failed:`, error);
|
|
521
|
+
} catch (reportError) {
|
|
522
|
+
console.error("Control inbox error reporter failed:", reportError);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const dirs = [
|
|
526
|
+
...(opts.onInterrupt || opts.onTimeout || opts.onStop ? [dir] : []),
|
|
527
|
+
...(opts.onStop ? [stopRequestsDir(asyncDir)] : []),
|
|
528
|
+
...(opts.onSteer ? [steerRequestsDir(asyncDir)] : []),
|
|
529
|
+
];
|
|
530
|
+
if (dirs.length === 0) return () => {};
|
|
496
531
|
try {
|
|
497
|
-
fsImpl.mkdirSync(
|
|
498
|
-
} catch {
|
|
499
|
-
|
|
532
|
+
for (const target of dirs) fsImpl.mkdirSync(target, { recursive: true });
|
|
533
|
+
} catch (error) {
|
|
534
|
+
report(error, "install");
|
|
500
535
|
}
|
|
501
536
|
|
|
502
537
|
let disposed = false;
|
|
503
538
|
const check = (): void => {
|
|
504
539
|
if (disposed) return;
|
|
505
540
|
try {
|
|
506
|
-
for (const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
541
|
+
if (opts.onStop) for (const request of consumeStopRequestPayloads(asyncDir, fsImpl, (error) => report(error, "scan"))) {
|
|
542
|
+
try { opts.onStop(request); } catch (error) { report(error, "callback"); }
|
|
543
|
+
}
|
|
544
|
+
if (opts.onTimeout && consumeTimeoutRequest(asyncDir, fsImpl)) {
|
|
545
|
+
try { opts.onTimeout(); } catch (error) { report(error, "callback"); }
|
|
546
|
+
}
|
|
547
|
+
if (opts.onInterrupt && consumeInterruptRequest(asyncDir, fsImpl)) {
|
|
548
|
+
try { opts.onInterrupt(); } catch (error) { report(error, "callback"); }
|
|
549
|
+
}
|
|
550
|
+
if (opts.onSteer) for (const request of consumeSteerRequestsFromDir(steerRequestsDir(asyncDir), fsImpl, (error) => report(error, "scan"))) {
|
|
551
|
+
try { opts.onSteer(request); } catch (error) { report(error, "callback", request); }
|
|
552
|
+
}
|
|
553
|
+
} catch (error) {
|
|
554
|
+
report(error, "scan");
|
|
512
555
|
}
|
|
513
556
|
};
|
|
514
557
|
|
|
@@ -516,40 +559,31 @@ export function watchAsyncControlInbox(
|
|
|
516
559
|
check();
|
|
517
560
|
|
|
518
561
|
const watchers: fs.FSWatcher[] = [];
|
|
519
|
-
const watchedDirs = new Set<string>();
|
|
520
562
|
let interval: ReturnType<typeof setInterval> | undefined;
|
|
521
563
|
let safetyInterval: ReturnType<typeof setInterval> | undefined;
|
|
522
564
|
const startPolling = (): void => {
|
|
523
565
|
if (interval || disposed) return;
|
|
566
|
+
if (safetyInterval) { timers.clearInterval(safetyInterval); safetyInterval = undefined; }
|
|
524
567
|
interval = timers.setInterval(check, opts.pollIntervalMs ?? POLL_INTERVAL_MS);
|
|
525
568
|
interval.unref?.();
|
|
526
569
|
};
|
|
527
|
-
const startSafetyPolling = (): void => {
|
|
528
|
-
if (safetyInterval || disposed) return;
|
|
529
|
-
safetyInterval = timers.setInterval(check, opts.safetyPollIntervalMs ?? CONTROL_SAFETY_POLL_INTERVAL_MS);
|
|
530
|
-
safetyInterval.unref?.();
|
|
531
|
-
};
|
|
532
|
-
const watchDir = (target: string, create = false): void => {
|
|
533
|
-
if (disposed || watchedDirs.has(target)) return;
|
|
534
|
-
if (create) fsImpl.mkdirSync(target, { recursive: true });
|
|
535
|
-
const watcher = fsImpl.watch(resolveWatchPath(target, fsImpl.realpathSync.native), () => check());
|
|
536
|
-
watcher.on?.("error", startPolling);
|
|
537
|
-
watchers.push(watcher);
|
|
538
|
-
watchedDirs.add(target);
|
|
539
|
-
};
|
|
540
570
|
try {
|
|
541
571
|
if (shouldUseNativeFsWatch("runner-control-inbox", opts.platform)) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
572
|
+
for (const target of dirs) {
|
|
573
|
+
const watcher = fsImpl.watch(resolveWatchPath(target, fsImpl.realpathSync.native), check);
|
|
574
|
+
watcher.on?.("error", startPolling);
|
|
575
|
+
watchers.push(watcher);
|
|
576
|
+
}
|
|
577
|
+
if (!interval) {
|
|
578
|
+
safetyInterval = timers.setInterval(check, opts.safetyPollIntervalMs ?? CONTROL_SAFETY_POLL_INTERVAL_MS);
|
|
579
|
+
safetyInterval.unref?.();
|
|
580
|
+
}
|
|
546
581
|
} else {
|
|
547
582
|
startPolling();
|
|
548
583
|
}
|
|
549
584
|
} catch {
|
|
550
585
|
startPolling();
|
|
551
586
|
}
|
|
552
|
-
|
|
553
587
|
return () => {
|
|
554
588
|
if (disposed) return;
|
|
555
589
|
disposed = true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
-
import { safeTerminalText } from "../../shared/display-text.ts";
|
|
4
|
+
import { safeTerminalText, truncateDisplayText } from "../../shared/display-text.ts";
|
|
5
5
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
6
6
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
7
7
|
import {
|
|
@@ -23,6 +23,10 @@ import { isTrustedRecordedSessionFile } from "../../shared/session-file-trust.ts
|
|
|
23
23
|
const DEFAULT_TRANSCRIPT_LINES = 80;
|
|
24
24
|
const MAX_TRANSCRIPT_LINES = 500;
|
|
25
25
|
const TRANSCRIPT_TAIL_BYTES = 256 * 1024;
|
|
26
|
+
const MAX_TRANSCRIPT_LINE_CHARS = 2000;
|
|
27
|
+
const MAX_TRANSCRIPT_BODY_BYTES = 32 * 1024;
|
|
28
|
+
const TRANSCRIPT_LINE_OMISSION = "… [content omitted]";
|
|
29
|
+
const TRANSCRIPT_BODY_OMISSION = " … [earlier lines omitted]";
|
|
26
30
|
|
|
27
31
|
type ForegroundControl = SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
|
|
28
32
|
type ForegroundRun = NonNullable<SubagentState["foregroundRuns"]> extends Map<string, infer T> ? T : never;
|
|
@@ -494,7 +498,31 @@ function appendTranscriptBody(lines: string[], sourceLabel: string, sourceLines:
|
|
|
494
498
|
lines.push(" (no transcript lines available yet)");
|
|
495
499
|
return;
|
|
496
500
|
}
|
|
497
|
-
|
|
501
|
+
// Bound the rendered body, not the raw input: terminal escaping can expand it.
|
|
502
|
+
// Keep line prefixes (roles/tool names), then prefer whole recent lines. The
|
|
503
|
+
// byte budget includes indentation, separators and omission markers, but not
|
|
504
|
+
// the source label, run metadata, warnings or artifact paths above the body.
|
|
505
|
+
const body: string[] = [];
|
|
506
|
+
let bytes = 0;
|
|
507
|
+
for (let index = sourceLines.length - 1; index >= 0; index--) {
|
|
508
|
+
// Preserve assembled-line binary detection (including indentation). A
|
|
509
|
+
// binary placeholder replaces the whole line and must not be reindented.
|
|
510
|
+
const safe = safeTerminalText(` ${sourceLines[index]!}`);
|
|
511
|
+
const lineLimit = MAX_TRANSCRIPT_LINE_CHARS + 2;
|
|
512
|
+
const line = safe.length <= lineLimit
|
|
513
|
+
? safe
|
|
514
|
+
: truncateDisplayText(safe, lineLimit - TRANSCRIPT_LINE_OMISSION.length) + TRANSCRIPT_LINE_OMISSION;
|
|
515
|
+
const size = Buffer.byteLength(line) + 1;
|
|
516
|
+
if (bytes + size > MAX_TRANSCRIPT_BODY_BYTES) {
|
|
517
|
+
const markerBytes = Buffer.byteLength(TRANSCRIPT_BODY_OMISSION) + 1;
|
|
518
|
+
while (bytes + markerBytes > MAX_TRANSCRIPT_BODY_BYTES) bytes -= Buffer.byteLength(body.pop()!) + 1;
|
|
519
|
+
body.push(TRANSCRIPT_BODY_OMISSION);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
body.push(line);
|
|
523
|
+
bytes += size;
|
|
524
|
+
}
|
|
525
|
+
lines.push(...body.reverse());
|
|
498
526
|
}
|
|
499
527
|
|
|
500
528
|
export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string, options: TranscriptOptions = {}): string {
|