pi-subagents 0.66.0 → 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 +68 -0
- package/README.md +4 -3
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +6 -3
- package/docs/configuration.md +7 -5
- package/docs/extension-api.md +33 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +1 -1
- package/docs/observability.md +4 -4
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +18 -8
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +26 -12
- package/inspector-runner.mjs +2 -2
- package/package.json +1 -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 +7 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agents.ts +6 -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 +2 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +20 -8
- package/src/extension/public-execution.ts +4 -2
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +67 -78
- package/src/extension/tool-description.ts +29 -82
- 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 +22 -13
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/auto-drain.ts +6 -3
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +31 -1
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/run-child-session.ts +61 -4
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +11 -3
- package/src/runs/background/runner-child-launch.ts +2 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +47 -31
- package/src/runs/background/subagent-wait.ts +51 -8
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/execution.ts +44 -31
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +110 -100
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +5 -2
- package/src/runs/shared/async-status-projection.ts +4 -0
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch.ts +12 -2
- 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 +33 -2
- package/src/runs/shared/child-tool-plan.ts +122 -3
- package/src/runs/shared/completion-guard.ts +5 -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/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-fallback.ts +2 -0
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
- 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.ts +42 -12
- 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 +4 -1
- 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 +16 -14
- package/src/tui/render.ts +149 -28
- 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/scripted-workflow.ts +237 -7
- package/src/workflows/workflow-checklist.ts +2 -2
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
5
|
+
import { agentDefinitionDigest } from "../shared/launch-contract.ts";
|
|
5
6
|
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
|
|
6
7
|
import { getAgentDir } from "../shared/utils.ts";
|
|
7
8
|
|
|
@@ -18,9 +19,13 @@ function defaultSubagentConfigDir(agentDir = defaultAgentDir()): string {
|
|
|
18
19
|
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
19
20
|
export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
|
|
20
21
|
export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
22
|
+
const ORCHESTRATOR_TARGET_PLACEHOLDER = "{orchestratorTarget}";
|
|
23
|
+
// The default template must stay session-independent: the child reads the
|
|
24
|
+
// supervisor target from its runtime config, and a prompt that names the
|
|
25
|
+
// parent session would make the launch digest vary per session (#2127).
|
|
21
26
|
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the supervisor in normal assistant text.
|
|
22
27
|
|
|
23
|
-
Use contact_supervisor first. It resolves the supervisor session
|
|
28
|
+
Use contact_supervisor first. It resolves the supervisor session and run metadata automatically.
|
|
24
29
|
- Need a decision, blocked, approval, or product/API/scope ambiguity: contact_supervisor({ reason: "need_decision", message: "<question>" })
|
|
25
30
|
- Need structured supervisor input rather than a freeform reply: contact_supervisor({ reason: "interview_request", message: "<what input is needed>", interview: { title: "...", questions: [] } })
|
|
26
31
|
- After contact_supervisor with reason "need_decision" or "interview_request", stay alive and continue only after the reply arrives. Do not finish your final response with a choose-one question.
|
|
@@ -36,6 +41,32 @@ export interface IntercomBridgeState {
|
|
|
36
41
|
orchestratorTarget?: string;
|
|
37
42
|
extensionDir: string;
|
|
38
43
|
instruction: string;
|
|
44
|
+
/** True when the instruction template names the supervisor session, which ties the child prompt to the parent session. */
|
|
45
|
+
interpolatesOrchestratorTarget: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type IntercomBridgeConfigValidation =
|
|
49
|
+
| { ok: true; value: IntercomBridgeConfig }
|
|
50
|
+
| { ok: false; error: string };
|
|
51
|
+
|
|
52
|
+
/** Validates untrusted bridge config from descriptors or delegation requests; `label` prefixes each error. */
|
|
53
|
+
export function validateIntercomBridgeConfig({ value, label }: { value: unknown; label: string }): IntercomBridgeConfigValidation {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, error: `${label} must be an object.` };
|
|
55
|
+
const bridge = value as Record<string, unknown>;
|
|
56
|
+
for (const field of Object.keys(bridge)) {
|
|
57
|
+
if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") return { ok: false, error: `${label}.${field} is not supported.` };
|
|
58
|
+
}
|
|
59
|
+
if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") return { ok: false, error: `${label}.mode is invalid.` };
|
|
60
|
+
if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") return { ok: false, error: `${label}.instructionFile must be a string.` };
|
|
61
|
+
if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") return { ok: false, error: `${label}.resultDelivery must be a boolean.` };
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
value: {
|
|
65
|
+
...(bridge.mode !== undefined ? { mode: bridge.mode as IntercomBridgeMode } : {}),
|
|
66
|
+
...(bridge.instructionFile !== undefined ? { instructionFile: bridge.instructionFile as string } : {}),
|
|
67
|
+
...(bridge.resultDelivery !== undefined ? { resultDelivery: bridge.resultDelivery as boolean } : {}),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
export interface IntercomBridgeDiagnostic {
|
|
@@ -114,7 +145,7 @@ function resolveInstructionTemplate(instructionFile: string, settingsDir: string
|
|
|
114
145
|
}
|
|
115
146
|
|
|
116
147
|
function buildIntercomBridgeInstruction(orchestratorTarget: string, template: string): string {
|
|
117
|
-
const instruction = template.replaceAll(
|
|
148
|
+
const instruction = template.replaceAll(ORCHESTRATOR_TARGET_PLACEHOLDER, orchestratorTarget).trim();
|
|
118
149
|
if (instruction.startsWith(INTERCOM_BRIDGE_MARKER)) return instruction;
|
|
119
150
|
return `${INTERCOM_BRIDGE_MARKER}\n${instruction}`;
|
|
120
151
|
}
|
|
@@ -149,24 +180,34 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
|
|
|
149
180
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
150
181
|
const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
|
|
151
182
|
const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir(agentDir));
|
|
152
|
-
const defaultInstruction = buildIntercomBridgeInstruction(
|
|
153
|
-
orchestratorTarget || "{orchestratorTarget}",
|
|
154
|
-
DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
|
|
155
|
-
);
|
|
156
183
|
const reason = inactiveReason(mode, input.context, orchestratorTarget);
|
|
157
184
|
if (reason || !orchestratorTarget) {
|
|
158
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
active: false,
|
|
187
|
+
mode,
|
|
188
|
+
resultDelivery: config.resultDelivery,
|
|
189
|
+
extensionDir: NATIVE_INTERCOM_EXTENSION_DIR,
|
|
190
|
+
instruction: buildIntercomBridgeInstruction(ORCHESTRATOR_TARGET_PLACEHOLDER, DEFAULT_INTERCOM_BRIDGE_TEMPLATE),
|
|
191
|
+
interpolatesOrchestratorTarget: false,
|
|
192
|
+
};
|
|
159
193
|
}
|
|
194
|
+
const template = resolveInstructionTemplate(config.instructionFile, settingsDir);
|
|
160
195
|
return {
|
|
161
196
|
active: true,
|
|
162
197
|
mode,
|
|
163
198
|
resultDelivery: config.resultDelivery,
|
|
164
199
|
orchestratorTarget,
|
|
165
200
|
extensionDir: NATIVE_INTERCOM_EXTENSION_DIR,
|
|
166
|
-
instruction: buildIntercomBridgeInstruction(orchestratorTarget,
|
|
201
|
+
instruction: buildIntercomBridgeInstruction(orchestratorTarget, template),
|
|
202
|
+
interpolatesOrchestratorTarget: template.includes(ORCHESTRATOR_TARGET_PLACEHOLDER),
|
|
167
203
|
};
|
|
168
204
|
}
|
|
169
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Rewrites the launch prompt and tools for an active bridge. The parsed
|
|
208
|
+
* definition digest is captured first so launch identity keeps describing the
|
|
209
|
+
* agent file rather than this runtime overlay.
|
|
210
|
+
*/
|
|
170
211
|
export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
|
|
171
212
|
if (!bridge.active || !bridge.orchestratorTarget) return agent;
|
|
172
213
|
|
|
@@ -185,6 +226,7 @@ export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomB
|
|
|
185
226
|
if (tools === agent.tools && systemPrompt === agent.systemPrompt) return agent;
|
|
186
227
|
return {
|
|
187
228
|
...agent,
|
|
229
|
+
definitionDigest: agentDefinitionDigest(agent),
|
|
188
230
|
tools,
|
|
189
231
|
systemPrompt,
|
|
190
232
|
};
|
|
@@ -83,6 +83,8 @@ interface IntercomParams {
|
|
|
83
83
|
type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string>) => fs.FSWatcher;
|
|
84
84
|
|
|
85
85
|
interface NativeSupervisorChannelDeps {
|
|
86
|
+
/** Owned live/final-drain mailboxes. Only a completed poll retires the snapshot, never a demand probe. */
|
|
87
|
+
getChannelDirs?: () => { dirs: string[]; retire?: () => void };
|
|
86
88
|
/** Retained scheduled states for the current runtime owner, never foreign owners. */
|
|
87
89
|
getCurrentOwnerStates?: () => Iterable<SubagentState>;
|
|
88
90
|
platform?: NodeJS.Platform;
|
|
@@ -286,18 +288,18 @@ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRe
|
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
function listRequestFiles(): Array<{ channelDir: string; file: string }> {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
291
|
+
function listRequestFiles(channelDirs?: string[]): Array<{ channelDir: string; file: string }> {
|
|
292
|
+
if (!channelDirs) {
|
|
293
|
+
try {
|
|
294
|
+
channelDirs = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true })
|
|
295
|
+
.filter(entry => entry.isDirectory()).map(entry => path.join(SUPERVISOR_CHANNEL_ROOT, entry.name));
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
296
300
|
}
|
|
297
301
|
const files: Array<{ channelDir: string; file: string }> = [];
|
|
298
|
-
for (const
|
|
299
|
-
if (!entry.isDirectory()) continue;
|
|
300
|
-
const channelDir = path.join(SUPERVISOR_CHANNEL_ROOT, entry.name);
|
|
302
|
+
for (const channelDir of channelDirs) {
|
|
301
303
|
const requestsDir = path.join(channelDir, REQUESTS_DIR);
|
|
302
304
|
let requestEntries: fs.Dirent[];
|
|
303
305
|
try {
|
|
@@ -606,6 +608,7 @@ function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, Pendin
|
|
|
606
608
|
}
|
|
607
609
|
|
|
608
610
|
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
|
|
611
|
+
registerTools: () => void;
|
|
609
612
|
start: () => void;
|
|
610
613
|
activateTransport: () => void;
|
|
611
614
|
findPendingAsks: (target: { runId: string; agent: string; childIndex: number }) => string[];
|
|
@@ -681,10 +684,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
681
684
|
let started = false;
|
|
682
685
|
let lastStaleCleanupAt = 0;
|
|
683
686
|
const platform = deps.platform ?? process.platform;
|
|
684
|
-
const useNativeWatcher = () => shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
|
|
687
|
+
const useNativeWatcher = () => !deps.getChannelDirs && shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
|
|
685
688
|
const hasTransportDemand = () => {
|
|
686
689
|
if (pending.size > 0) return true;
|
|
687
690
|
if (state.foregroundControls.size > 0) return true;
|
|
691
|
+
if (deps.getChannelDirs?.().dirs.length) return true;
|
|
688
692
|
if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
|
|
689
693
|
for (const ownerState of deps.getCurrentOwnerStates?.() ?? []) {
|
|
690
694
|
for (const job of ownerState.asyncJobs.values()) {
|
|
@@ -699,6 +703,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
699
703
|
};
|
|
700
704
|
|
|
701
705
|
const cleanupStaleChannelsIfDue = (): void => {
|
|
706
|
+
if (deps.getChannelDirs) return; // The root owns global retention cleanup.
|
|
702
707
|
const nowMs = Date.now();
|
|
703
708
|
if (nowMs - lastStaleCleanupAt < STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS) return;
|
|
704
709
|
lastStaleCleanupAt = nowMs;
|
|
@@ -714,7 +719,8 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
714
719
|
// Only display notifications require a live UI context, not request registration.
|
|
715
720
|
refreshPendingRequests(pending, state, observeRequestLifecycle, runState);
|
|
716
721
|
const now = Date.now();
|
|
717
|
-
|
|
722
|
+
const channels = deps.getChannelDirs?.();
|
|
723
|
+
for (const { channelDir, file } of listRequestFiles(channels?.dirs)) {
|
|
718
724
|
if (seenFiles.has(file)) continue;
|
|
719
725
|
const request = parseRequestFile(file, channelDir);
|
|
720
726
|
if (!request || !requestMatchesOwner(request, state)) continue;
|
|
@@ -769,13 +775,14 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
769
775
|
if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
|
|
770
776
|
}
|
|
771
777
|
}
|
|
778
|
+
channels?.retire?.();
|
|
772
779
|
};
|
|
773
780
|
|
|
774
781
|
const startPolling = (): void => {
|
|
775
782
|
if (poller) return;
|
|
776
783
|
poller = timers.setInterval(() => {
|
|
777
784
|
poll();
|
|
778
|
-
if (!useNativeWatcher() && platform === "darwin" && !hasTransportDemand()) {
|
|
785
|
+
if (!useNativeWatcher() && (platform === "darwin" || deps.getChannelDirs) && !hasTransportDemand()) {
|
|
779
786
|
if (poller) timers.clearInterval(poller);
|
|
780
787
|
poller = undefined;
|
|
781
788
|
}
|
|
@@ -830,6 +837,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
830
837
|
};
|
|
831
838
|
|
|
832
839
|
return {
|
|
840
|
+
registerTools: registerParentTools,
|
|
833
841
|
activateTransport: () => {
|
|
834
842
|
if (!started) return;
|
|
835
843
|
poll();
|
|
@@ -857,6 +865,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
857
865
|
started = true;
|
|
858
866
|
registerParentTools();
|
|
859
867
|
poll();
|
|
868
|
+
if (deps.getChannelDirs) return; // Child polling starts only after a descendant launch.
|
|
860
869
|
try {
|
|
861
870
|
fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
|
|
862
871
|
if (!useNativeWatcher()) {
|
|
@@ -280,6 +280,10 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncSt
|
|
|
280
280
|
if (!childStatus) return { state: "retained", reason: `async workflow child ${label} status is missing or unreadable` };
|
|
281
281
|
if (!terminalState(childStatus.state)) return { state: "retained", reason: `async workflow child ${label} is still ${childStatus.state}` };
|
|
282
282
|
if (!childStatus.processTerminal?.runnerProcessInstanceId) return { state: "retained", reason: `async workflow child ${label} has no runner process identity` };
|
|
283
|
+
if (childStatus.processTerminal.state === "not-started"
|
|
284
|
+
&& childStatus.processTerminal.runId === step.runId
|
|
285
|
+
&& typeof childStatus.error === "string"
|
|
286
|
+
&& childStatus.error) continue;
|
|
283
287
|
const proof = readProcessTerminal(childDir, {
|
|
284
288
|
runId: step.runId,
|
|
285
289
|
runnerProcessInstanceId: childStatus.processTerminal.runnerProcessInstanceId,
|
|
@@ -10,23 +10,22 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
|
|
13
|
-
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
14
13
|
import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
14
|
+
import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.ts";
|
|
15
15
|
import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
|
|
16
16
|
import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
|
|
17
|
-
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
|
|
18
|
-
import {
|
|
17
|
+
import { applyThinkingSuffix, getHostBuiltinToolNames, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
|
|
18
|
+
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
19
19
|
import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
|
|
20
20
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
21
21
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
22
22
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
23
|
-
import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
23
|
+
import { PI_CODING_AGENT_PACKAGE, resolveBunPiExecutable, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
24
24
|
import { JITI_ALIAS_ENV, resolveHostPeerAliases } from "./runner-aliases.ts";
|
|
25
25
|
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
26
26
|
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
27
27
|
import { backgroundProcessOptions } from "../shared/background-process-options.ts";
|
|
28
|
-
import {
|
|
29
|
-
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
28
|
+
import { normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
30
29
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
31
30
|
import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
|
|
32
31
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
@@ -80,7 +79,7 @@ import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
|
|
|
80
79
|
import { statusStepDescription } from "./chain-append.ts";
|
|
81
80
|
import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
|
|
82
81
|
import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
83
|
-
import {
|
|
82
|
+
import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
|
|
84
83
|
import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
|
|
85
84
|
import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
|
|
86
85
|
import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
|
|
@@ -542,15 +541,20 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
542
541
|
const cwdError = preflightLaunchCwd(requestedCwd, cwd);
|
|
543
542
|
if (cwdError) return { error: cwdError };
|
|
544
543
|
|
|
545
|
-
|
|
544
|
+
// The compiled host exposes its SDK only through Pi's extension loader.
|
|
545
|
+
const binaryHost = resolveBunPiExecutable();
|
|
546
|
+
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
547
|
+
const bootstrap = path.join(path.dirname(runner), "binary-bootstrap.ts");
|
|
548
|
+
if (binaryHost && !fs.existsSync(bootstrap)) return { error: `Background runner bootstrap not found: ${bootstrap}` };
|
|
549
|
+
if (!binaryHost && !jitiCliPath) {
|
|
546
550
|
return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
|
|
547
551
|
}
|
|
548
|
-
if (!piPackageRoot) {
|
|
549
|
-
return { error: `Background children require
|
|
552
|
+
if (!binaryHost && !piPackageRoot) {
|
|
553
|
+
return { error: `Background children require a supported standalone Pi host or the installed npm package (${PI_CODING_AGENT_PACKAGE}); neither is available.` };
|
|
550
554
|
}
|
|
551
|
-
const hostPeerAliases = resolveHostPeerAliases(piPackageRoot);
|
|
555
|
+
const hostPeerAliases = !binaryHost && piPackageRoot ? resolveHostPeerAliases(piPackageRoot) : { aliases: {}, missing: [] };
|
|
552
556
|
if (hostPeerAliases.missing.length > 0) {
|
|
553
|
-
return { error: `Background children require
|
|
557
|
+
return { error: `Background children require the host npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}.` };
|
|
554
558
|
}
|
|
555
559
|
|
|
556
560
|
fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
|
|
@@ -560,8 +564,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
560
564
|
const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
|
|
561
565
|
const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}) };
|
|
562
566
|
writePrivateAtomicJson(cfgPath, launchConfig);
|
|
563
|
-
const
|
|
564
|
-
const nodeCommand = resolveNodeExecutable();
|
|
567
|
+
const command = binaryHost ?? resolveNodeExecutable();
|
|
565
568
|
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
|
|
566
569
|
const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
|
|
567
570
|
const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
|
|
@@ -587,17 +590,23 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
587
590
|
stdoutFd = fs.openSync(logPaths.stdoutPath, "a");
|
|
588
591
|
stderrFd = fs.openSync(logPaths.stderrPath, "a");
|
|
589
592
|
}
|
|
590
|
-
const preload = hostPeerAliases.
|
|
591
|
-
? ["--import", new URL("../../../runner-
|
|
593
|
+
const preload = Object.keys(hostPeerAliases.aliases).length > 0
|
|
594
|
+
? ["--import", new URL("../../../runner-peer-preload.mjs", import.meta.url).href]
|
|
592
595
|
: [];
|
|
593
|
-
const
|
|
596
|
+
const args = binaryHost
|
|
597
|
+
? ["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session", "--mode", "rpc", "--extension", bootstrap]
|
|
598
|
+
: [...preload, jitiCliPath!, runner, cfgPath];
|
|
599
|
+
const proc = spawn(command, args, {
|
|
594
600
|
cwd,
|
|
595
601
|
...backgroundProcessOptions(),
|
|
596
602
|
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
597
603
|
env: {
|
|
598
604
|
...omitExtensionBindingsEnv(process.env),
|
|
599
|
-
[PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
|
|
600
|
-
|
|
605
|
+
[PI_CODING_AGENT_PACKAGE_ROOT_ENV]: binaryHost ? undefined : piPackageRoot,
|
|
606
|
+
// npm must override inherited bundled layouts (#2071); binaries retain release assets.
|
|
607
|
+
PI_PACKAGE_DIR: binaryHost ? process.env.PI_PACKAGE_DIR : piPackageRoot,
|
|
608
|
+
[JITI_ALIAS_ENV]: binaryHost ? undefined : JSON.stringify(hostPeerAliases.aliases),
|
|
609
|
+
PI_SUBAGENT_RUNNER_CONFIG: binaryHost ? cfgPath : undefined,
|
|
601
610
|
},
|
|
602
611
|
});
|
|
603
612
|
closeFd(stdoutFd);
|
|
@@ -891,22 +900,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
891
900
|
);
|
|
892
901
|
if (missingSkills.includes("pi-subagents")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
|
|
893
902
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const injection = buildSkillInjection(resolvedSkills);
|
|
897
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
898
|
-
}
|
|
899
|
-
const memoryInjection = buildAgentMemoryInjection(a, stepCwd);
|
|
900
|
-
if (memoryInjection) {
|
|
901
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
|
|
902
|
-
}
|
|
903
|
-
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: stepCwd, agentName: a.name });
|
|
903
|
+
// A namespaced parallel output is injected by the runner, not the prompt.
|
|
904
|
+
const systemPrompt = buildEffectiveSystemPrompt({ agent: a, resolvedSkills, cwd: stepCwd, ...(!namespaceOutputPath && outputPath ? { outputPath } : {}) });
|
|
904
905
|
|
|
905
906
|
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false, undefined, readExistenceCwd);
|
|
906
907
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
907
908
|
if (behavior.progress) progressInstructionCreated = true;
|
|
908
909
|
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
|
|
909
|
-
if (!namespaceOutputPath) systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, a);
|
|
910
910
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
911
911
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
912
912
|
let taskTemplate = s.task ?? "{previous}";
|
|
@@ -963,6 +963,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
963
963
|
const launchRuleError = applyWatchdogLaunchRules({ cwd: stepCwd, agent: a.name, model: modelCandidates[0] ?? model, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
|
|
964
964
|
if (launchRuleError) throw new AsyncStartValidationError(launchRuleError);
|
|
965
965
|
const fast = s.fast ?? params.fast ?? a.fast;
|
|
966
|
+
const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
|
|
966
967
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
967
968
|
tools: a.tools,
|
|
968
969
|
excludeTools: a.excludeTools,
|
|
@@ -981,6 +982,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
981
982
|
agentName: a.name,
|
|
982
983
|
permissionRules,
|
|
983
984
|
runtimeSnapshotHost: ctx.pi,
|
|
985
|
+
hostAvailableBuiltins,
|
|
984
986
|
});
|
|
985
987
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
986
988
|
if (externalRunner && permissionRules) {
|
|
@@ -1377,6 +1379,7 @@ export function executeAsyncChain(
|
|
|
1377
1379
|
deadlineAt,
|
|
1378
1380
|
globalConcurrencyLimit: params.globalConcurrencyLimit,
|
|
1379
1381
|
runFanoutBudget,
|
|
1382
|
+
hostAvailableBuiltins: getHostBuiltinToolNames(ctx.pi),
|
|
1380
1383
|
workflowGraph,
|
|
1381
1384
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
1382
1385
|
...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
|
|
@@ -1636,16 +1639,6 @@ export function executeAsyncSingle(
|
|
|
1636
1639
|
agentConfig.filePath ? path.dirname(agentConfig.filePath) : runnerCwd,
|
|
1637
1640
|
);
|
|
1638
1641
|
if (missingSkills.includes("pi-subagents")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
|
|
1639
|
-
let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
|
|
1640
|
-
if (resolvedSkills.length > 0) {
|
|
1641
|
-
const injection = buildSkillInjection(resolvedSkills);
|
|
1642
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
1643
|
-
}
|
|
1644
|
-
const memoryInjection = buildAgentMemoryInjection(agentConfig, runnerCwd);
|
|
1645
|
-
if (memoryInjection) {
|
|
1646
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
|
|
1647
|
-
}
|
|
1648
|
-
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: runnerCwd, agentName: agentConfig.name });
|
|
1649
1642
|
|
|
1650
1643
|
const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
|
|
1651
1644
|
const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
|
|
@@ -1668,7 +1661,7 @@ export function executeAsyncSingle(
|
|
|
1668
1661
|
|
|
1669
1662
|
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
1670
1663
|
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
|
|
1671
|
-
systemPrompt =
|
|
1664
|
+
const systemPrompt = buildEffectiveSystemPrompt({ agent: agentConfig, resolvedSkills, cwd: runnerCwd, ...(outputPath ? { outputPath } : {}) });
|
|
1672
1665
|
const outputMode = params.outputMode ?? agentConfig.outputMode ?? "inline";
|
|
1673
1666
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
1674
1667
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
@@ -1759,6 +1752,7 @@ export function executeAsyncSingle(
|
|
|
1759
1752
|
return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
|
|
1760
1753
|
}
|
|
1761
1754
|
}
|
|
1755
|
+
const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
|
|
1762
1756
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
1763
1757
|
tools: agentConfig.tools,
|
|
1764
1758
|
excludeTools: agentConfig.excludeTools,
|
|
@@ -1777,6 +1771,7 @@ export function executeAsyncSingle(
|
|
|
1777
1771
|
agentName: agentConfig.name,
|
|
1778
1772
|
permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
|
|
1779
1773
|
runtimeSnapshotHost: ctx.pi,
|
|
1774
|
+
hostAvailableBuiltins,
|
|
1780
1775
|
});
|
|
1781
1776
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
1782
1777
|
if (!externalRunner) {
|
|
@@ -1793,24 +1788,17 @@ export function executeAsyncSingle(
|
|
|
1793
1788
|
});
|
|
1794
1789
|
if (contractError) return formatAsyncStartError("single", contractError);
|
|
1795
1790
|
}
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1791
|
+
const fast = params.fast ?? agentConfig.fast;
|
|
1792
|
+
const launchThinking = resolveEffectiveThinking(model, effectiveThinking);
|
|
1793
|
+
const { definitionDigest, launchContractDigest } = resolveLaunchBinding({
|
|
1794
|
+
agent: agentConfig,
|
|
1798
1795
|
task,
|
|
1799
|
-
...(model ? { model } : {}),
|
|
1800
1796
|
modelCandidates,
|
|
1801
|
-
...(
|
|
1802
|
-
...(
|
|
1803
|
-
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1797
|
+
...(fast !== undefined ? { fast } : {}),
|
|
1798
|
+
...(launchThinking ? { thinking: launchThinking } : {}),
|
|
1804
1799
|
systemPrompt,
|
|
1805
|
-
systemPromptMode: agentConfig.systemPromptMode,
|
|
1806
|
-
inheritProjectContext: agentConfig.inheritProjectContext,
|
|
1807
|
-
inheritGlobalContext: agentConfig.inheritGlobalContext,
|
|
1808
|
-
inheritSkills: agentConfig.inheritSkills,
|
|
1809
1800
|
skills: resolvedSkills.map((skill) => skill.name),
|
|
1810
|
-
|
|
1811
|
-
...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
1812
|
-
extensions: toolPlan.extensionArgs,
|
|
1813
|
-
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
1801
|
+
toolPlan,
|
|
1814
1802
|
...(outputPath ? { outputPath } : {}),
|
|
1815
1803
|
outputMode,
|
|
1816
1804
|
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
@@ -1939,7 +1927,7 @@ export function executeAsyncSingle(
|
|
|
1939
1927
|
waitToolEnabled: params.waitToolEnabled,
|
|
1940
1928
|
waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
|
|
1941
1929
|
...(params.agentContract ? { agentContract: params.agentContract } : {}),
|
|
1942
|
-
definitionDigest
|
|
1930
|
+
definitionDigest,
|
|
1943
1931
|
launchBindingTask: task,
|
|
1944
1932
|
launchContractDigest,
|
|
1945
1933
|
...(extensionBindings ? { extensionBindings } : {}),
|
|
@@ -1988,6 +1976,7 @@ export function executeAsyncSingle(
|
|
|
1988
1976
|
launchContractDigest,
|
|
1989
1977
|
launchResolvedExtensions,
|
|
1990
1978
|
runFanoutBudget,
|
|
1979
|
+
hostAvailableBuiltins,
|
|
1991
1980
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
1992
1981
|
...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
|
|
1993
1982
|
...(lane ? { lane } : {}),
|
|
@@ -15,6 +15,7 @@ import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/paral
|
|
|
15
15
|
import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
|
|
16
16
|
import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
17
17
|
import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
|
|
18
|
+
import { validateIntercomBridgeConfig } from "../../intercom/intercom-bridge.ts";
|
|
18
19
|
import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
|
|
19
20
|
|
|
20
21
|
export interface AsyncResumeParams {
|
|
@@ -318,7 +319,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
318
319
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
319
320
|
const parsed = value as Record<string, unknown>;
|
|
320
321
|
const allowedFields = new Set([
|
|
321
|
-
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
322
|
+
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "fast", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
322
323
|
"subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
323
324
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
324
325
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
@@ -352,6 +353,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
352
353
|
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
353
354
|
if (parsed.context !== undefined && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
|
|
354
355
|
if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
|
|
356
|
+
if (parsed.fast !== undefined && typeof parsed.fast !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': fast must be a boolean.`);
|
|
355
357
|
if (parsed.modelOrigin !== undefined && parsed.modelOrigin !== "explicit" && parsed.modelOrigin !== "inherited" && parsed.modelOrigin !== "configured") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOrigin must be 'explicit', 'inherited', or 'configured'.`);
|
|
356
358
|
if (parsed.modelOrigin === undefined && parsed.model !== undefined) parsed.modelOrigin = parsed.modelOverrideFromParent ? "inherited" : "configured";
|
|
357
359
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
@@ -411,14 +413,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
411
413
|
if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
|
|
412
414
|
}
|
|
413
415
|
if (parsed.intercomBridge !== undefined) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
for (const field of Object.keys(bridge)) {
|
|
417
|
-
if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
|
|
418
|
-
}
|
|
419
|
-
if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.mode is invalid.`);
|
|
420
|
-
if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
|
|
421
|
-
if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
|
|
416
|
+
const bridge = validateIntercomBridgeConfig({ value: parsed.intercomBridge, label: "intercomBridge" });
|
|
417
|
+
if (!bridge.ok) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${bridge.error}`);
|
|
422
418
|
}
|
|
423
419
|
if (parsed.controlConfig !== undefined) {
|
|
424
420
|
if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
|
|
@@ -24,14 +24,17 @@ function resultText(value: AgentToolResult<Details>): string {
|
|
|
24
24
|
return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function hasOutstandingWork(sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
|
|
27
|
+
function hasOutstandingWork(state: SubagentState, sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
|
|
28
28
|
const asyncRuns = listAsyncRuns(DIRS.async, {
|
|
29
29
|
states: ["queued", "running"],
|
|
30
30
|
sessionId,
|
|
31
31
|
resultsDir: DIRS.results,
|
|
32
32
|
now: () => nowMs,
|
|
33
33
|
}, observation?.status);
|
|
34
|
-
|
|
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;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/** Drain all work owned by the current headless session, including work added while draining. */
|
|
@@ -44,7 +47,7 @@ export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: Re
|
|
|
44
47
|
const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
|
|
45
48
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
|
|
46
49
|
const deadlineAt = now() + timeoutMs;
|
|
47
|
-
const hasWork = deps.hasWork ?? (
|
|
50
|
+
const hasWork = deps.hasWork ?? ((id: string, time: number) => hasOutstandingWork(deps.state, id, time, observation));
|
|
48
51
|
const wait = deps.wait ?? waitForSubagents;
|
|
49
52
|
|
|
50
53
|
while (true) {
|
|
@@ -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
|
+
}
|
|
@@ -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 {
|