pi-subagents 0.66.0 → 0.68.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 +138 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +43 -15
- package/docs/configuration.md +67 -23
- package/docs/extension-api.md +38 -19
- package/docs/missions.md +10 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +22 -12
- package/docs/standalone-background.md +59 -0
- package/docs/tool-reference.md +33 -20
- package/docs/watchdog.md +39 -10
- package/docs/workflows.md +37 -13
- package/index.ts +5 -2
- package/inspector-runner.mjs +2 -2
- package/package.json +4 -2
- package/prompts/parallel-review.md +1 -1
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +32 -0
- package/skills/pi-subagents/SKILL.md +32 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +11 -9
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +190 -71
- package/src/agents/builtin-names.ts +1 -0
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +94 -59
- package/src/api/required-child-extensions.ts +6 -0
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +66 -4
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +62 -39
- package/src/extension/public-execution.ts +7 -5
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +81 -81
- package/src/extension/tool-description.ts +30 -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 +44 -31
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +348 -176
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +17 -12
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +23 -10
- package/src/runs/background/binary-bootstrap.ts +38 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +105 -7
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +81 -33
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +12 -33
- package/src/runs/background/runner-child-launch.ts +6 -1
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +51 -18
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +441 -304
- package/src/runs/background/subagent-wait.ts +176 -28
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +134 -248
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +374 -178
- 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 +38 -11
- package/src/runs/shared/async-status-projection.ts +127 -33
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +28 -5
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +8 -1
- package/src/runs/shared/child-session.ts +127 -52
- package/src/runs/shared/child-tool-plan.ts +142 -11
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -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 +27 -4
- package/src/runs/shared/worktree.ts +45 -15
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +68 -8
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +74 -30
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -7
- package/src/slash/subagents-admin.ts +24 -13
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +168 -37
- package/src/watchdog/child-status.ts +28 -28
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +21 -1
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +10 -2
- package/src/watchdog/register-main.ts +39 -35
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +123 -74
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +100 -27
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +3 -0
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +5 -3
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +279 -10
- package/src/workflows/workflow-checklist.ts +2 -2
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/runner-server-preload.mjs +0 -13
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -2,6 +2,26 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createOwnedProcessTreeController } from "../background/owned-process-tree.ts";
|
|
3
3
|
import type { ProcessTreeTerminal } from "../../shared/types.ts";
|
|
4
4
|
|
|
5
|
+
function terminateCommandTree(pid: number, controller: ReturnType<typeof createOwnedProcessTreeController>): Promise<ProcessTreeTerminal> {
|
|
6
|
+
if (process.platform !== "win32") return controller.terminate();
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let settled = false;
|
|
9
|
+
const fallback = () => {
|
|
10
|
+
if (settled) return;
|
|
11
|
+
settled = true;
|
|
12
|
+
void controller.terminate().then(resolve);
|
|
13
|
+
};
|
|
14
|
+
const cleanup = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
15
|
+
cleanup.once("error", fallback);
|
|
16
|
+
cleanup.once("close", (status) => {
|
|
17
|
+
if (settled) return;
|
|
18
|
+
if (status !== 0) return fallback();
|
|
19
|
+
settled = true;
|
|
20
|
+
resolve({ state: "observed", mechanism: "windows-taskkill", pid, verifiedAt: Date.now() });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
export interface SetupCommandOptions {
|
|
6
26
|
cwd?: string;
|
|
7
27
|
env?: NodeJS.ProcessEnv;
|
|
@@ -19,6 +39,7 @@ export interface SetupCommandOptions {
|
|
|
19
39
|
|
|
20
40
|
export interface SetupCommandResult {
|
|
21
41
|
stdout: string;
|
|
42
|
+
stdoutBuffer: Buffer;
|
|
22
43
|
stderr: string;
|
|
23
44
|
status: number | null;
|
|
24
45
|
signal: NodeJS.Signals | null;
|
|
@@ -46,7 +67,7 @@ export async function runSetupCommand(
|
|
|
46
67
|
options: SetupCommandOptions,
|
|
47
68
|
): Promise<SetupCommandResult> {
|
|
48
69
|
const result: SetupCommandResult = {
|
|
49
|
-
stdout: "", stderr: "", status: null, signal: null, outputIncomplete: false,
|
|
70
|
+
stdout: "", stdoutBuffer: Buffer.alloc(0), stderr: "", status: null, signal: null, outputIncomplete: false,
|
|
50
71
|
};
|
|
51
72
|
const maxBuffer = options.maxBuffer ?? 1024 * 1024;
|
|
52
73
|
if (!Number.isSafeInteger(maxBuffer) || maxBuffer <= 0) throw new Error("Invalid setup command maxBuffer");
|
|
@@ -84,7 +105,8 @@ export async function runSetupCommand(
|
|
|
84
105
|
let directSettled = false;
|
|
85
106
|
const releaseUnknownIO = () => {
|
|
86
107
|
if (!directSettled || result.processTree?.state !== "unknown") return;
|
|
87
|
-
|
|
108
|
+
const detail = result.processTree.diagnostic ? `: ${result.processTree.diagnostic}` : "";
|
|
109
|
+
result.error = commandError(`Worktree setup process tree settlement is unverified${detail}`, "PROCESS_TREE_UNVERIFIED");
|
|
88
110
|
result.outputIncomplete = true;
|
|
89
111
|
// Only local I/O is released; unknown descendant ownership remains retained.
|
|
90
112
|
child.stdin.destroy();
|
|
@@ -101,7 +123,7 @@ export async function runSetupCommand(
|
|
|
101
123
|
releaseUnknownIO();
|
|
102
124
|
return;
|
|
103
125
|
}
|
|
104
|
-
termination = tree
|
|
126
|
+
termination = terminateCommandTree(result.pid!, tree).then((proof) => {
|
|
105
127
|
result.processTree = proof;
|
|
106
128
|
releaseUnknownIO();
|
|
107
129
|
return proof;
|
|
@@ -177,7 +199,8 @@ export async function runSetupCommand(
|
|
|
177
199
|
if (termination) result.processTree = await termination;
|
|
178
200
|
const cancelled = cancellation();
|
|
179
201
|
if (cancelled) fail(cancelled);
|
|
180
|
-
result.
|
|
202
|
+
result.stdoutBuffer = Buffer.concat(stdout);
|
|
203
|
+
result.stdout = result.stdoutBuffer.toString("utf8");
|
|
181
204
|
result.stderr = Buffer.concat(stderr).toString("utf8");
|
|
182
205
|
return result;
|
|
183
206
|
} finally {
|
|
@@ -18,6 +18,8 @@ const WORKTREE_NAMING_COMPONENT_MAX_BYTES = 96;
|
|
|
18
18
|
const WORKTREE_NAMING_LABEL_MAX_BYTES = 256;
|
|
19
19
|
const WORKTREE_NAMING_BRANCH_MAX_BYTES = 256;
|
|
20
20
|
const WORKTREE_COMMAND_OUTPUT_MAX_BYTES = 128 * 1024;
|
|
21
|
+
const WORKTRUNK_COMMAND = process.platform === "win32" ? "git" : "wt";
|
|
22
|
+
const WORKTRUNK_ARG_PREFIX = process.platform === "win32" ? ["wt"] : [];
|
|
21
23
|
export const MACHINE_DIFF_OPTIONS = ["--no-color", "--no-ext-diff", "--no-textconv", "--default-prefix", "--line-prefix=", "--no-relative"] as const;
|
|
22
24
|
const MACHINE_PATCH_OPTIONS = [...MACHINE_DIFF_OPTIONS, "--binary"] as const;
|
|
23
25
|
const PATCH_VALIDATION_OPTIONS = ["apply", "--check", "--cached", "--reverse", "--binary", "--whitespace=nowarn"] as const;
|
|
@@ -178,7 +180,7 @@ export interface WorktreeSetupProgress {
|
|
|
178
180
|
setup: WorktreeSetup;
|
|
179
181
|
attempts: Array<{ index: number; branch: string; path?: string; validated: boolean; command?: WorktreeSetupProgress["command"]; hookCommand?: WorktreeSetupProgress["command"] }>;
|
|
180
182
|
phase: string;
|
|
181
|
-
command?: { command: string; args: string[]; pid?: number; processGroupId?: number; result?: Omit<SetupCommandResult, "stdout" | "stderr"> };
|
|
183
|
+
command?: { command: string; args: string[]; pid?: number; processGroupId?: number; result?: Omit<SetupCommandResult, "stdout" | "stdoutBuffer" | "stderr"> };
|
|
182
184
|
unknown?: string;
|
|
183
185
|
cleanup?: WorktreeCleanupReport;
|
|
184
186
|
}
|
|
@@ -243,7 +245,7 @@ class SetupTransaction {
|
|
|
243
245
|
...options, signal: this.options.signal, deadlineAt: this.options.deadlineAt,
|
|
244
246
|
onSpawn: (process) => { Object.assign(this.progress.command!, process); this.publish(); },
|
|
245
247
|
});
|
|
246
|
-
const { stdout: _stdout, stderr: _stderr, ...metadata } = result;
|
|
248
|
+
const { stdout: _stdout, stdoutBuffer: _stdoutBuffer, stderr: _stderr, ...metadata } = result;
|
|
247
249
|
this.progress.command.result = metadata;
|
|
248
250
|
if (result.processTree?.state === "unknown") this.unknown(result.error ?? "Command tree settlement unverified");
|
|
249
251
|
this.publish();
|
|
@@ -339,11 +341,9 @@ export function validateWorktreePatchRepresentsCurrentWorktree(worktreePath: str
|
|
|
339
341
|
return undefined;
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
async function
|
|
344
|
+
async function probeWorktreeSource(tx: Pick<SetupTransaction, "git" | "gitChecked">, cwd: string): Promise<string> {
|
|
343
345
|
const repoCheck = await tx.git(cwd, ["rev-parse", "--is-inside-work-tree"], [0, 128]);
|
|
344
346
|
if (repoCheck.status !== 0 || repoCheck.stdout.trim() !== "true") throw new Error("worktree isolation requires a git repository");
|
|
345
|
-
const rawPrefix = (await tx.gitChecked(cwd, ["rev-parse", "--show-prefix"])).trim();
|
|
346
|
-
const cwdRelative = rawPrefix ? path.normalize(rawPrefix.replace(/[\\/]+$/, "")) : "";
|
|
347
347
|
const toplevel = (await tx.gitChecked(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
348
348
|
|
|
349
349
|
// pi-subagents writes durable runtime state under .pi/subagents/ by default;
|
|
@@ -352,6 +352,26 @@ async function resolveRepoState(tx: SetupTransaction, cwd: string, requestedBase
|
|
|
352
352
|
if (status.trim().length > 0) {
|
|
353
353
|
throw new Error("worktree isolation requires a clean git working tree. Commit or stash changes first.");
|
|
354
354
|
}
|
|
355
|
+
return toplevel;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Read-only admission check; allocation repeats it because source state can change. */
|
|
359
|
+
export async function preflightWorktreeSource(cwd: string, options: Pick<SetupCommandOptions, "signal" | "deadlineAt"> = {}): Promise<void> {
|
|
360
|
+
const git = async (cwd: string, args: string[], acceptedExitCodes: readonly number[] = [0]): Promise<SetupCommandResult> => {
|
|
361
|
+
const result = await runSetupCommand("git", ["-C", cwd, ...args], { ...options, acceptedExitCodes });
|
|
362
|
+
if (result.error) throw result.error;
|
|
363
|
+
if (result.outputIncomplete || result.status === null || !acceptedExitCodes.includes(result.status)) {
|
|
364
|
+
throw new Error(result.stderr.trim().slice(0, 2000) || "Worktree source probe failed");
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
};
|
|
368
|
+
await probeWorktreeSource({ git, gitChecked: async (cwd, args) => (await git(cwd, args)).stdout }, cwd);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function resolveRepoState(tx: SetupTransaction, cwd: string, requestedBaseRef: string | undefined): Promise<RepoState> {
|
|
372
|
+
const toplevel = await probeWorktreeSource(tx, cwd);
|
|
373
|
+
const rawPrefix = (await tx.gitChecked(cwd, ["rev-parse", "--show-prefix"])).trim();
|
|
374
|
+
const cwdRelative = rawPrefix ? path.normalize(rawPrefix.replace(/[\\/]+$/, "")) : "";
|
|
355
375
|
|
|
356
376
|
const baseRef = normalizeWorktreeBaseRef(requestedBaseRef) ?? DEFAULT_WORKTREE_BASE_REF;
|
|
357
377
|
let baseCommit: string;
|
|
@@ -526,6 +546,11 @@ function hasConfiguredWorktreeBaseDir(baseDir: string | undefined): boolean {
|
|
|
526
546
|
: (process.env.PI_SUBAGENTS_WORKTREE_DIR?.trim().length ?? 0) > 0;
|
|
527
547
|
}
|
|
528
548
|
|
|
549
|
+
function isInsidePiExtensionsDirectory(targetPath: string): boolean {
|
|
550
|
+
const extensionsDir = normalizeComparableCwd(path.join(getAgentDir(), "extensions"));
|
|
551
|
+
return isPathInside(extensionsDir, normalizeComparableCwd(targetPath));
|
|
552
|
+
}
|
|
553
|
+
|
|
529
554
|
interface WorktrunkCapability {
|
|
530
555
|
available: boolean;
|
|
531
556
|
reason?: string;
|
|
@@ -533,7 +558,7 @@ interface WorktrunkCapability {
|
|
|
533
558
|
|
|
534
559
|
function runWorktrunk(args: string[], cwd?: string): WorktreeCommandResult {
|
|
535
560
|
try {
|
|
536
|
-
const result = spawnSync(
|
|
561
|
+
const result = spawnSync(WORKTRUNK_COMMAND, [...WORKTRUNK_ARG_PREFIX, ...args], {
|
|
537
562
|
cwd,
|
|
538
563
|
encoding: "utf-8",
|
|
539
564
|
windowsHide: true,
|
|
@@ -578,7 +603,7 @@ export function resolveWorktreeProvider(requested: WorktreeProvider | undefined,
|
|
|
578
603
|
return "native";
|
|
579
604
|
}
|
|
580
605
|
|
|
581
|
-
async function resolveSetupProvider(tx: SetupTransaction, requested: WorktreeProvider | undefined, baseDir
|
|
606
|
+
async function resolveSetupProvider(tx: SetupTransaction, requested: WorktreeProvider | undefined, baseDir: string | undefined, repoRoot: string): Promise<ManagedWorktreeProvider> {
|
|
582
607
|
const selection = requested ?? DEFAULT_WORKTREE_PROVIDER;
|
|
583
608
|
if (selection !== "auto" && selection !== "native" && selection !== "worktrunk") throw new Error('worktree provider must be "auto", "native", or "worktrunk"');
|
|
584
609
|
if (selection === "native") return "native";
|
|
@@ -586,13 +611,14 @@ async function resolveSetupProvider(tx: SetupTransaction, requested: WorktreePro
|
|
|
586
611
|
if (selection === "worktrunk") throw new Error("worktreeProvider='worktrunk' cannot be combined with worktreeBaseDir or PI_SUBAGENTS_WORKTREE_DIR");
|
|
587
612
|
return "native";
|
|
588
613
|
}
|
|
614
|
+
if (selection === "auto" && isInsidePiExtensionsDirectory(repoRoot)) return "native";
|
|
589
615
|
let reason: string | undefined;
|
|
590
616
|
try {
|
|
591
617
|
const probeExitCodes = Array.from({ length: 256 }, (_, code) => code);
|
|
592
|
-
const version = await tx.command(
|
|
618
|
+
const version = await tx.command(WORKTRUNK_COMMAND, [...WORKTRUNK_ARG_PREFIX, "--version"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
|
|
593
619
|
if (version.status !== 0 || !/\b(?:wt\s+)?v?(\d+\.\d+(?:\.\d+)?)\b/i.test(version.stdout.trim())) reason = "Worktrunk is unavailable or returned an invalid version";
|
|
594
620
|
else {
|
|
595
|
-
const help = await tx.command(
|
|
621
|
+
const help = await tx.command(WORKTRUNK_COMMAND, [...WORKTRUNK_ARG_PREFIX, "switch", "--help"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
|
|
596
622
|
const missing = ["--create", "--base", "--no-cd", "--no-hooks", "--format"].filter((flag) => !`${help.stdout}\n${help.stderr}`.includes(flag));
|
|
597
623
|
if (help.status !== 0 || missing.length) reason = `Worktrunk switch capability unavailable: ${missing.join(", ")}`;
|
|
598
624
|
}
|
|
@@ -616,11 +642,13 @@ export function shouldDeferWorktreeCwd(requested: WorktreeProvider | undefined,
|
|
|
616
642
|
* to the repository. Managed leaves always nest one level deeper under the
|
|
617
643
|
* project folder (`basename(repoRoot)`).
|
|
618
644
|
*/
|
|
619
|
-
function resolveWorktreeDedicatedRoot(configuredBaseDir: string | undefined, repoRoot: string): string {
|
|
645
|
+
function resolveWorktreeDedicatedRoot(configuredBaseDir: string | undefined, repoRoot: string, relocateExtensionRepo = true): string {
|
|
620
646
|
const rawBaseDir = configuredBaseDir ?? process.env.PI_SUBAGENTS_WORKTREE_DIR;
|
|
621
647
|
let expanded: string;
|
|
622
648
|
if (rawBaseDir === undefined || (configuredBaseDir === undefined && !rawBaseDir.trim())) {
|
|
623
|
-
expanded =
|
|
649
|
+
expanded = relocateExtensionRepo && isInsidePiExtensionsDirectory(repoRoot)
|
|
650
|
+
? path.join(getAgentDir(), "worktrees")
|
|
651
|
+
: path.join(path.dirname(repoRoot), "worktrees");
|
|
624
652
|
} else {
|
|
625
653
|
const trimmed = rawBaseDir.trim();
|
|
626
654
|
if (!trimmed) throw new Error("worktree base directory cannot be empty");
|
|
@@ -942,7 +970,7 @@ async function createWorktrunkWorktree(
|
|
|
942
970
|
const naming = buildWorktreeNaming({ runId, index, agent: agents?.[index], label: labels?.[index], task: tasks?.[index], branchPrefix });
|
|
943
971
|
const args = ["-C", toplevel, "switch", "--create", naming.requestedBranch, "--base", baseCommit, "--no-cd", "--no-hooks", "--format", "json"];
|
|
944
972
|
tx.attempt(index, naming.requestedBranch);
|
|
945
|
-
const result = await tx.command(
|
|
973
|
+
const result = await tx.command(WORKTRUNK_COMMAND, [...WORKTRUNK_ARG_PREFIX, ...args], { cwd: toplevel, maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES });
|
|
946
974
|
tx.progress.phase = "validation";
|
|
947
975
|
try {
|
|
948
976
|
const output = parseWorktrunkSwitchOutput(result.stdout);
|
|
@@ -1267,7 +1295,7 @@ async function compensateSetup(tx: SetupTransaction): Promise<WorktreeCleanupRep
|
|
|
1267
1295
|
deadlineAt: tx.options.deadlineAt, acceptedExitCodes,
|
|
1268
1296
|
onSpawn: (process) => { Object.assign(tx.progress.command!, process); tx.publish(); },
|
|
1269
1297
|
});
|
|
1270
|
-
const { stdout: _stdout, stderr: _stderr, ...metadata } = result;
|
|
1298
|
+
const { stdout: _stdout, stdoutBuffer: _stdoutBuffer, stderr: _stderr, ...metadata } = result;
|
|
1271
1299
|
tx.progress.command.result = metadata;
|
|
1272
1300
|
if (result.processTree?.state === "unknown") tx.unknown(result.error ?? "Rollback command settlement unverified");
|
|
1273
1301
|
tx.publish();
|
|
@@ -1331,9 +1359,11 @@ async function allocateWorktrees(tx: SetupTransaction, cwd: string, runId: strin
|
|
|
1331
1359
|
tx.progress.setup.cwd = repo.toplevel;
|
|
1332
1360
|
tx.progress.setup.baseCommit = repo.baseCommit;
|
|
1333
1361
|
const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
|
|
1334
|
-
const provider = await resolveSetupProvider(tx, options?.provider, options?.baseDir);
|
|
1362
|
+
const provider = await resolveSetupProvider(tx, options?.provider, options?.baseDir, repo.toplevel);
|
|
1335
1363
|
const branchPrefix = normalizeWorktreeBranchPrefix(options?.branchPrefix);
|
|
1336
|
-
const dedicatedRoot = provider === "native"
|
|
1364
|
+
const dedicatedRoot = provider === "native"
|
|
1365
|
+
? resolveWorktreeDedicatedRoot(options?.baseDir, repo.toplevel, (options?.provider ?? DEFAULT_WORKTREE_PROVIDER) === "auto")
|
|
1366
|
+
: undefined;
|
|
1337
1367
|
const worktrees = tx.progress.setup.worktrees;
|
|
1338
1368
|
|
|
1339
1369
|
try {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Anthropic prices a cache write by the TTL it is asked for: 1.25x base input
|
|
5
|
+
* for the 5m window, 2x for the 1h one. Child sessions are short-lived and
|
|
6
|
+
* rarely idle, so a 1h window they never claim is a flat surcharge on every
|
|
7
|
+
* write. Setting this to `short` keeps that surcharge off children while the
|
|
8
|
+
* parent keeps `long`, the same split Claude Code makes between its main
|
|
9
|
+
* conversation and its subagents.
|
|
10
|
+
*
|
|
11
|
+
* Unset means children inherit the parent's retention, which is the behaviour
|
|
12
|
+
* before this setting existed.
|
|
13
|
+
*/
|
|
14
|
+
export function childCacheRetention(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
15
|
+
return env.PI_SUBAGENT_CACHE_RETENTION || undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Launch-environment form for spawned children. Empty when unset, so the child
|
|
20
|
+
* inherits the parent's `PI_CACHE_RETENTION` rather than having it cleared.
|
|
21
|
+
*/
|
|
22
|
+
export function childCacheRetentionEnv(env?: NodeJS.ProcessEnv): { PI_CACHE_RETENTION?: string } {
|
|
23
|
+
const retention = childCacheRetention(env);
|
|
24
|
+
return retention ? { PI_CACHE_RETENTION: retention } : {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pi resolves retention per request as `options.env?.[name] || process.env[name]`,
|
|
29
|
+
* so a per-call env beats the process-wide one. Wrapping the session's own
|
|
30
|
+
* stream function keeps this scoped to one child, with no shared-state race
|
|
31
|
+
* against a parent turn streaming concurrently.
|
|
32
|
+
*/
|
|
33
|
+
export function pinChildCacheRetention(agent: { streamFunction: StreamFn } | undefined, env?: NodeJS.ProcessEnv): void {
|
|
34
|
+
if (!agent?.streamFunction) return;
|
|
35
|
+
const retention = childCacheRetention(env);
|
|
36
|
+
if (!retention) return;
|
|
37
|
+
const base = agent.streamFunction;
|
|
38
|
+
agent.streamFunction = (model, context, options) =>
|
|
39
|
+
base(model, context, {
|
|
40
|
+
...options,
|
|
41
|
+
env: { ...(options?.env ?? {}), PI_CACHE_RETENTION: retention },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// Forking a parent transcript into a child must drop Anthropic's signed and redacted
|
|
2
|
+
// thinking blocks: a thinking signature is bound to the session that produced it and
|
|
3
|
+
// cannot be replayed into a branch. Stripping them is required; disabling the child's
|
|
4
|
+
// thinking is not. Pi >= 0.85.0 recovers from signed-thinking mismatches on the
|
|
5
|
+
// transport, so a sanitized transcript is safe to resume with thinking enabled and the
|
|
6
|
+
// child keeps the level it asked for, reasoning fresh from its first turn.
|
|
2
7
|
import * as fs from "node:fs";
|
|
3
8
|
import * as path from "node:path";
|
|
4
9
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { findModelInfo, type ModelInfo } from "./model-info.ts";
|
|
6
10
|
|
|
7
11
|
type SubagentExecutionContext = "fresh" | "fork";
|
|
8
12
|
|
|
@@ -19,7 +23,6 @@ interface BranchSessionEntry {
|
|
|
19
23
|
api?: string;
|
|
20
24
|
model?: string;
|
|
21
25
|
};
|
|
22
|
-
thinkingLevel?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
interface BranchSessionManager {
|
|
@@ -39,20 +42,11 @@ interface ForkContextResolverOptions {
|
|
|
39
42
|
openSession?: (path: string, sessionDir?: string) => BranchSessionManager;
|
|
40
43
|
/** Rewrite a created fork before its path can be used to spawn a child. */
|
|
41
44
|
pruneSession?: (sessionFile: string) => Promise<void>;
|
|
42
|
-
/** Decide per child index whether a sanitized transcript must also disable the child's
|
|
43
|
-
* thinking. Defaults to true (the pre-existing conservative behavior) when omitted. */
|
|
44
|
-
forceThinkingOffForIndex?: (index: number) => boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface ForkContextResolution {
|
|
48
|
-
sessionFile: string;
|
|
49
|
-
thinkingOverride?: "off";
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
interface ForkContextResolver {
|
|
53
48
|
prepareSessionForIndex(index?: number): Promise<void>;
|
|
54
49
|
sessionFileForIndex(index?: number): string | undefined;
|
|
55
|
-
thinkingOverrideForIndex(index?: number): "off" | undefined;
|
|
56
50
|
}
|
|
57
51
|
|
|
58
52
|
export function resolveSubagentContext(value: unknown): SubagentExecutionContext {
|
|
@@ -101,20 +95,6 @@ export function canPreferForkFromSnapshot(input: PreferredForkSnapshot): boolean
|
|
|
101
95
|
}
|
|
102
96
|
}
|
|
103
97
|
|
|
104
|
-
/** Decide whether a resolved child model uses Anthropic's provider or message API, which
|
|
105
|
-
* requires the sanitized fork to disable thinking. Unknown models stay conservative. */
|
|
106
|
-
export function forkedChildRequiresThinkingOff(
|
|
107
|
-
model: string | undefined,
|
|
108
|
-
availableModels?: ModelInfo[],
|
|
109
|
-
preferredProvider?: string,
|
|
110
|
-
): boolean {
|
|
111
|
-
if (!model) return true;
|
|
112
|
-
const info = findModelInfo(model, availableModels, preferredProvider);
|
|
113
|
-
if (!info) return true;
|
|
114
|
-
return info.provider.toLowerCase() === "anthropic"
|
|
115
|
-
|| info.api?.toLowerCase() === "anthropic-messages";
|
|
116
|
-
}
|
|
117
|
-
|
|
118
98
|
function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"], block: unknown): boolean {
|
|
119
99
|
if (!message || !block || typeof block !== "object" || !("type" in block)) return false;
|
|
120
100
|
const provider = typeof message.provider === "string" ? message.provider.toLowerCase() : "";
|
|
@@ -128,28 +108,6 @@ function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"],
|
|
|
128
108
|
return record.redacted === true || (typeof signature === "string" && signature.length > 0);
|
|
129
109
|
}
|
|
130
110
|
|
|
131
|
-
function createEntryId(entries: BranchSessionEntry[]): string {
|
|
132
|
-
const ids = new Set(entries.map((entry) => entry.id).filter((id): id is string => typeof id === "string"));
|
|
133
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
134
|
-
const id = randomUUID().slice(0, 8);
|
|
135
|
-
if (!ids.has(id)) return id;
|
|
136
|
-
}
|
|
137
|
-
return randomUUID();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function appendThinkingOffEntry(entries: BranchSessionEntry[]): void {
|
|
141
|
-
const last = entries[entries.length - 1];
|
|
142
|
-
if (last?.type === "thinking_level_change" && last.thinkingLevel === "off") return;
|
|
143
|
-
const parent = [...entries].reverse().find((entry) => typeof entry.id === "string");
|
|
144
|
-
entries.push({
|
|
145
|
-
type: "thinking_level_change",
|
|
146
|
-
id: createEntryId(entries),
|
|
147
|
-
parentId: parent?.id ?? null,
|
|
148
|
-
timestamp: new Date().toISOString(),
|
|
149
|
-
thinkingLevel: "off",
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
111
|
function sanitizeUnsafeThinkingBlocks(entries: BranchSessionEntry[]): boolean {
|
|
154
112
|
let sanitized = false;
|
|
155
113
|
for (const entry of entries) {
|
|
@@ -183,7 +141,6 @@ export function createForkContextResolver(
|
|
|
183
141
|
return {
|
|
184
142
|
prepareSessionForIndex: async () => {},
|
|
185
143
|
sessionFileForIndex: () => undefined,
|
|
186
|
-
thinkingOverrideForIndex: () => undefined,
|
|
187
144
|
};
|
|
188
145
|
}
|
|
189
146
|
|
|
@@ -217,12 +174,12 @@ export function createForkContextResolver(
|
|
|
217
174
|
path.basename(parentSessionFile, ".jsonl"),
|
|
218
175
|
"forks",
|
|
219
176
|
);
|
|
220
|
-
const
|
|
177
|
+
const cachedSessionFiles = new Map<number, string>();
|
|
221
178
|
const preparedIndexes = new Set<number>();
|
|
222
179
|
const preparationPromises = new Map<number, Promise<void>>();
|
|
223
180
|
|
|
224
|
-
const resolveFork = (index = 0):
|
|
225
|
-
const cached =
|
|
181
|
+
const resolveFork = (index = 0): string => {
|
|
182
|
+
const cached = cachedSessionFiles.get(index);
|
|
226
183
|
if (cached) return cached;
|
|
227
184
|
try {
|
|
228
185
|
if (!fs.existsSync(parentSessionFile)) {
|
|
@@ -233,34 +190,23 @@ export function createForkContextResolver(
|
|
|
233
190
|
if (!sessionFile) {
|
|
234
191
|
throw new Error("Session manager did not return a forked session file.");
|
|
235
192
|
}
|
|
236
|
-
const forceThinkingOff = (sanitized: boolean): boolean =>
|
|
237
|
-
sanitized && (options.forceThinkingOffForIndex?.(index) ?? true);
|
|
238
|
-
let thinkingOverride: "off" | undefined;
|
|
239
193
|
if (!fs.existsSync(sessionFile)) {
|
|
240
194
|
const header = sourceManager.getHeader?.();
|
|
241
195
|
const entries = sourceManager.getEntries?.();
|
|
242
196
|
if (!header || !entries) {
|
|
243
197
|
throw new Error(`Session manager returned a forked session file that does not exist and cannot be persisted by fallback: ${sessionFile}`);
|
|
244
198
|
}
|
|
245
|
-
|
|
246
|
-
appendThinkingOffEntry(entries);
|
|
247
|
-
thinkingOverride = "off";
|
|
248
|
-
}
|
|
199
|
+
sanitizeUnsafeThinkingBlocks(entries);
|
|
249
200
|
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
250
201
|
fs.writeFileSync(sessionFile, `${[header, ...entries].map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
251
202
|
} else {
|
|
252
203
|
const entries = readSessionEntries(sessionFile);
|
|
253
204
|
if (sanitizeUnsafeThinkingBlocks(entries)) {
|
|
254
|
-
if (forceThinkingOff(true)) {
|
|
255
|
-
appendThinkingOffEntry(entries);
|
|
256
|
-
thinkingOverride = "off";
|
|
257
|
-
}
|
|
258
205
|
fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
259
206
|
}
|
|
260
207
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return resolution;
|
|
208
|
+
cachedSessionFiles.set(index, sessionFile);
|
|
209
|
+
return sessionFile;
|
|
264
210
|
} catch (error) {
|
|
265
211
|
const cause = error instanceof Error ? error : new Error(String(error));
|
|
266
212
|
throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
|
|
@@ -269,11 +215,11 @@ export function createForkContextResolver(
|
|
|
269
215
|
|
|
270
216
|
return {
|
|
271
217
|
async prepareSessionForIndex(index = 0): Promise<void> {
|
|
272
|
-
const
|
|
218
|
+
const sessionFile = resolveFork(index);
|
|
273
219
|
if (!options.pruneSession || preparedIndexes.has(index)) return;
|
|
274
220
|
let preparation = preparationPromises.get(index);
|
|
275
221
|
if (!preparation) {
|
|
276
|
-
preparation = options.pruneSession(
|
|
222
|
+
preparation = options.pruneSession(sessionFile).then(() => {
|
|
277
223
|
preparedIndexes.add(index);
|
|
278
224
|
});
|
|
279
225
|
preparationPromises.set(index, preparation);
|
|
@@ -284,10 +230,7 @@ export function createForkContextResolver(
|
|
|
284
230
|
if (options.pruneSession && !preparedIndexes.has(index)) {
|
|
285
231
|
throw new Error(`Pruned fork session ${index} was used before pruning completed.`);
|
|
286
232
|
}
|
|
287
|
-
return resolveFork(index)
|
|
288
|
-
},
|
|
289
|
-
thinkingOverrideForIndex(index = 0): "off" | undefined {
|
|
290
|
-
return resolveFork(index).thinkingOverride;
|
|
233
|
+
return resolveFork(index);
|
|
291
234
|
},
|
|
292
235
|
};
|
|
293
236
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
4
|
+
import type { PiLaunchToolPlan } from "../runs/shared/child-tool-plan.ts";
|
|
4
5
|
import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
|
|
5
6
|
|
|
6
|
-
export const AGENT_DEFINITION_PROJECTION_VERSION =
|
|
7
|
-
|
|
7
|
+
export const AGENT_DEFINITION_PROJECTION_VERSION = 2 as const;
|
|
8
|
+
// v2: the Intercom bridge prompt and tools are part of the binding on every
|
|
9
|
+
// path, and the bridge text no longer names the parent session.
|
|
10
|
+
export const LAUNCH_BINDING_PROJECTION_VERSION = 2 as const;
|
|
8
11
|
|
|
9
12
|
function stableJson(value: unknown): string {
|
|
10
13
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -47,7 +50,6 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
47
50
|
inheritSkills: agent.inheritSkills,
|
|
48
51
|
model: agent.model,
|
|
49
52
|
modelProvider: agent.modelProvider,
|
|
50
|
-
fallbackModels: agent.fallbackModels,
|
|
51
53
|
fast: agent.fast,
|
|
52
54
|
thinking: agent.thinking,
|
|
53
55
|
tools: agent.tools,
|
|
@@ -60,6 +62,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
60
62
|
skills: agent.skills,
|
|
61
63
|
skillPath: agent.skillPath,
|
|
62
64
|
output: agent.output,
|
|
65
|
+
outputSchema: agent.outputSchema,
|
|
63
66
|
defaultReads: agent.defaultReads,
|
|
64
67
|
defaultProgress: agent.defaultProgress,
|
|
65
68
|
defaultContext: agent.defaultContext,
|
|
@@ -75,8 +78,9 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
/** Digest of the parsed definition; a runtime overlay that already captured it wins over re-hashing the overlaid copy. */
|
|
78
82
|
export function agentDefinitionDigest(agent: AgentConfig): string {
|
|
79
|
-
return stableJsonDigest(projectAgentDefinition(agent));
|
|
83
|
+
return agent.definitionDigest ?? stableJsonDigest(projectAgentDefinition(agent));
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
export interface LaunchBindingInput {
|
|
@@ -84,7 +88,6 @@ export interface LaunchBindingInput {
|
|
|
84
88
|
/** Caller task; runtime acceptance/output task annotations are explicitly outside the preflight-known subset. */
|
|
85
89
|
task?: string;
|
|
86
90
|
model?: string;
|
|
87
|
-
modelCandidates?: string[];
|
|
88
91
|
fast?: boolean;
|
|
89
92
|
thinking?: string;
|
|
90
93
|
systemPrompt?: string | null;
|
|
@@ -110,9 +113,7 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
|
|
|
110
113
|
version: LAUNCH_BINDING_PROJECTION_VERSION,
|
|
111
114
|
definitionDigest: input.definitionDigest,
|
|
112
115
|
taskDigest: input.task === undefined ? undefined : stableJsonDigest(input.task),
|
|
113
|
-
|
|
114
|
-
// this set makes retries correlate to the same preflight binding.
|
|
115
|
-
modelCandidates: input.modelCandidates,
|
|
116
|
+
model: input.model,
|
|
116
117
|
fast: input.fast,
|
|
117
118
|
thinking: input.thinking,
|
|
118
119
|
systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : stableJsonDigest(input.systemPrompt),
|
|
@@ -136,3 +137,62 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
|
|
|
136
137
|
export function launchBindingDigest(input: LaunchBindingInput): string {
|
|
137
138
|
return stableJsonDigest(projectLaunchBinding(input));
|
|
138
139
|
}
|
|
140
|
+
|
|
141
|
+
type LaunchBindingPromptMode = Pick<LaunchBindingInput, "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills">;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Who the child is: the launched agent, or the identity a persisted step
|
|
145
|
+
* already captured when a runner recomputes the binding per model attempt.
|
|
146
|
+
*/
|
|
147
|
+
export type LaunchBindingIdentity =
|
|
148
|
+
| { agent: AgentConfig }
|
|
149
|
+
| ({ definitionDigest: string } & LaunchBindingPromptMode);
|
|
150
|
+
|
|
151
|
+
export type LaunchBindingSource = LaunchBindingIdentity
|
|
152
|
+
& Pick<LaunchBindingInput, "fast" | "thinking" | "skills" | "outputPath" | "outputMode" | "structuredOutputSchema" | "extensionBindings">
|
|
153
|
+
& {
|
|
154
|
+
task: string;
|
|
155
|
+
model?: string;
|
|
156
|
+
/** Effective child system prompt before runtime acceptance prose. */
|
|
157
|
+
systemPrompt: string;
|
|
158
|
+
toolPlan: Pick<PiLaunchToolPlan, "effectiveToolAllowlist" | "excludeTools" | "extensionArgs" | "effectiveMcpTools">;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export interface LaunchBinding {
|
|
162
|
+
definitionDigest: string;
|
|
163
|
+
launchContractDigest: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Assemble launch identity from resolved preflight or execution inputs.
|
|
168
|
+
* The stable projection omits undefined optional fields.
|
|
169
|
+
*/
|
|
170
|
+
export function resolveLaunchBinding(source: LaunchBindingSource): LaunchBinding {
|
|
171
|
+
const identity = "agent" in source ? {
|
|
172
|
+
definitionDigest: agentDefinitionDigest(source.agent),
|
|
173
|
+
systemPromptMode: source.agent.systemPromptMode,
|
|
174
|
+
inheritProjectContext: source.agent.inheritProjectContext,
|
|
175
|
+
inheritGlobalContext: source.agent.inheritGlobalContext,
|
|
176
|
+
inheritSkills: source.agent.inheritSkills,
|
|
177
|
+
} : source;
|
|
178
|
+
return {
|
|
179
|
+
definitionDigest: identity.definitionDigest,
|
|
180
|
+
launchContractDigest: launchBindingDigest({
|
|
181
|
+
...identity,
|
|
182
|
+
task: source.task,
|
|
183
|
+
model: source.model,
|
|
184
|
+
fast: source.fast,
|
|
185
|
+
thinking: source.thinking || undefined,
|
|
186
|
+
systemPrompt: source.systemPrompt,
|
|
187
|
+
skills: source.skills,
|
|
188
|
+
tools: source.toolPlan.effectiveToolAllowlist,
|
|
189
|
+
excludeTools: source.toolPlan.excludeTools.length > 0 ? source.toolPlan.excludeTools : undefined,
|
|
190
|
+
extensions: source.toolPlan.extensionArgs,
|
|
191
|
+
mcpDirectTools: source.toolPlan.effectiveMcpTools,
|
|
192
|
+
outputPath: source.outputPath || undefined,
|
|
193
|
+
outputMode: source.outputMode,
|
|
194
|
+
structuredOutputSchema: source.structuredOutputSchema,
|
|
195
|
+
extensionBindings: source.extensionBindings || undefined,
|
|
196
|
+
}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
const OPENCODE_HOST = "opencode.ai";
|
|
4
|
+
|
|
5
|
+
function matchesOpenCodeHost(baseUrl: string): boolean {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(baseUrl).hostname === OPENCODE_HOST;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* OpenCode session-routing headers for internal subagent model calls.
|
|
15
|
+
*
|
|
16
|
+
* Pi's own session path emits these from coding-agent's provider-attribution
|
|
17
|
+
* merge, but subagent-internal calls (watchdog review, permission arbiter,
|
|
18
|
+
* task-mutation arbiter, prompt audit) stream through bare Agents that bypass
|
|
19
|
+
* that path. Without them OpenCode falls back to client-IP affinity and loses
|
|
20
|
+
* prompt-cache routing (see pi issue #4847). Returns undefined for every other
|
|
21
|
+
* provider so non-OpenCode requests stay byte-identical.
|
|
22
|
+
*/
|
|
23
|
+
export function opencodeSessionHeaders(
|
|
24
|
+
model: Pick<Model<Api>, "provider" | "baseUrl">,
|
|
25
|
+
sessionId: string | undefined,
|
|
26
|
+
): ProviderHeaders | undefined {
|
|
27
|
+
if (!sessionId) return undefined;
|
|
28
|
+
if (model.provider !== "opencode" && model.provider !== "opencode-go" && !matchesOpenCodeHost(model.baseUrl)) return undefined;
|
|
29
|
+
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
|
30
|
+
}
|
|
@@ -5,7 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import type { TextContent } from "@earendil-works/pi-ai";
|
|
6
6
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
7
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { resolveModelCandidate } from "../runs/shared/model-
|
|
8
|
+
import { resolveModelCandidate } from "../runs/shared/model-resolution.ts";
|
|
9
9
|
import { splitKnownThinkingSuffix, toModelInfo } from "./model-info.ts";
|
|
10
10
|
import type { ForkContextConfig } from "./types.ts";
|
|
11
11
|
|