pi-subagents 0.61.0 → 0.62.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 +24 -1
- package/docs/agents.md +6 -2
- package/docs/configuration.md +3 -3
- package/docs/extension-api.md +2 -2
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +2 -2
- package/install.mjs +0 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +3 -4
- package/src/agents/agent-management.ts +19 -0
- package/src/agents/agent-serializer.ts +3 -0
- package/src/agents/agents.ts +16 -2
- package/src/agents/runtime-agent-registry.ts +5 -1
- package/src/api/preflight.ts +4 -0
- package/src/extension/public-execution.ts +1 -0
- package/src/extension/schemas.ts +6 -2
- package/src/extension/tool-description.ts +1 -1
- package/src/runs/background/active-async-capacity.ts +26 -7
- package/src/runs/background/async-execution.ts +47 -16
- package/src/runs/background/async-resume.ts +3 -2
- package/src/runs/background/process-terminal.ts +16 -0
- package/src/runs/background/scheduled-runs.ts +63 -6
- package/src/runs/background/steering.ts +4 -1
- package/src/runs/background/subagent-runner.ts +14 -7
- package/src/runs/background/wait-tool.ts +1 -7
- package/src/runs/foreground/execution.ts +14 -7
- package/src/runs/foreground/subagent-executor.ts +3 -2
- package/src/runs/shared/acceptance.ts +70 -9
- package/src/runs/shared/capability-ceiling.ts +1 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +2 -6
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +24 -12
- package/src/runs/shared/pi-spawn.ts +69 -35
- package/src/runs/shared/structured-output.ts +33 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/src/runs/shared/task-intent.ts +17 -6
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/shared/atomic-json.ts +3 -1
- package/src/shared/fork-context.ts +0 -12
- package/src/shared/fork-session-cwd.ts +27 -0
- package/src/shared/launch-contract.ts +3 -0
- package/src/shared/types.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/slash/subagents-admin.ts +2 -0
|
@@ -5,17 +5,21 @@ import * as path from "node:path";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
7
7
|
import type { JsonSchemaObject } from "../../shared/types.ts";
|
|
8
|
+
import type { ResolvedAcceptanceReportMode } from "./acceptance.ts";
|
|
8
9
|
|
|
9
10
|
export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA";
|
|
10
11
|
export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE";
|
|
11
12
|
export const STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE";
|
|
13
|
+
export const STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED";
|
|
12
14
|
export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
|
|
15
|
+
export const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
|
|
13
16
|
|
|
14
17
|
export interface StructuredOutputRuntime {
|
|
15
18
|
schema: JsonSchemaObject;
|
|
16
19
|
schemaPath: string;
|
|
17
20
|
outputPath: string;
|
|
18
21
|
acceptanceReportPath?: string;
|
|
22
|
+
acceptanceReportRequired?: boolean;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"] as const;
|
|
@@ -61,14 +65,14 @@ function rewriteLocalJsonPointerRefs(schema: unknown, pointerPrefix: string, inh
|
|
|
61
65
|
return rewritten;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
export function createStructuredOutputToolParameters(schema: JsonSchemaObject, options: { acceptanceReport?:
|
|
68
|
+
export function createStructuredOutputToolParameters(schema: JsonSchemaObject, options: { acceptanceReport?: "optional" | "required" } = {}): JsonSchemaObject {
|
|
65
69
|
return {
|
|
66
70
|
type: "object",
|
|
67
71
|
properties: {
|
|
68
72
|
value: rewriteLocalJsonPointerRefs(schema, "#/properties/value"),
|
|
69
73
|
...(options.acceptanceReport ? { acceptanceReport: { type: "object" } } : {}),
|
|
70
74
|
},
|
|
71
|
-
required: ["value"],
|
|
75
|
+
required: ["value", ...(options.acceptanceReport === "required" ? ["acceptanceReport"] : [])],
|
|
72
76
|
additionalProperties: false,
|
|
73
77
|
};
|
|
74
78
|
}
|
|
@@ -129,7 +133,7 @@ export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"):
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string, options: {
|
|
136
|
+
export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string, options: { acceptanceReport?: ResolvedAcceptanceReportMode } = {}): StructuredOutputRuntime {
|
|
133
137
|
assertJsonSchemaObject(schema);
|
|
134
138
|
const rootDir = baseDir ?? os.tmpdir();
|
|
135
139
|
fs.mkdirSync(rootDir, { recursive: true });
|
|
@@ -137,7 +141,14 @@ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?
|
|
|
137
141
|
const schemaPath = path.join(dir, "schema.json");
|
|
138
142
|
const outputPath = path.join(dir, "output.json");
|
|
139
143
|
fs.writeFileSync(schemaPath, JSON.stringify(schema), { mode: 0o600 });
|
|
140
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
schema,
|
|
146
|
+
schemaPath,
|
|
147
|
+
outputPath,
|
|
148
|
+
...(options.acceptanceReport && options.acceptanceReport !== "off"
|
|
149
|
+
? { acceptanceReportPath: path.join(dir, "acceptance-report.json"), acceptanceReportRequired: options.acceptanceReport === "required" }
|
|
150
|
+
: {}),
|
|
151
|
+
};
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
export async function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): Promise<{ status: "valid" } | { status: "invalid"; message: string }> {
|
|
@@ -178,14 +189,30 @@ export async function readStructuredOutput(runtime: StructuredOutputRuntime): Pr
|
|
|
178
189
|
}
|
|
179
190
|
|
|
180
191
|
export function readStructuredOutputAcceptanceReport(runtime: StructuredOutputRuntime): { value?: unknown; error?: string } {
|
|
181
|
-
if (!runtime.acceptanceReportPath
|
|
192
|
+
if (!runtime.acceptanceReportPath) return {};
|
|
193
|
+
if (!fs.existsSync(runtime.acceptanceReportPath)) {
|
|
194
|
+
return runtime.acceptanceReportRequired ? { error: MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR } : {};
|
|
195
|
+
}
|
|
182
196
|
try {
|
|
183
|
-
return { value: JSON.parse(fs.readFileSync(runtime.acceptanceReportPath, "utf-8")) };
|
|
197
|
+
return { value: JSON.parse(fs.readFileSync(runtime.acceptanceReportPath, "utf-8")) as unknown };
|
|
184
198
|
} catch (error) {
|
|
185
199
|
return { error: `Failed to read structured output acceptance report: ${error instanceof Error ? error.message : String(error)}` };
|
|
186
200
|
}
|
|
187
201
|
}
|
|
188
202
|
|
|
203
|
+
export function clearStructuredOutputCaptures(runtime: StructuredOutputRuntime): string | undefined {
|
|
204
|
+
let cleanupError: string | undefined;
|
|
205
|
+
for (const filePath of [runtime.outputPath, runtime.acceptanceReportPath]) {
|
|
206
|
+
if (!filePath) continue;
|
|
207
|
+
try {
|
|
208
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
cleanupError ??= `Failed to clear stale structured output capture ${filePath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return cleanupError;
|
|
214
|
+
}
|
|
215
|
+
|
|
189
216
|
export function cleanupStructuredOutputRuntime(runtime: StructuredOutputRuntime | undefined): void {
|
|
190
217
|
if (!runtime) return;
|
|
191
218
|
try {
|
|
@@ -8,7 +8,8 @@ import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, P
|
|
|
8
8
|
import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
|
|
9
9
|
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_FORK_CACHE_KEY_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
10
10
|
import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
11
|
-
import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
11
|
+
import { createStructuredOutputToolParameters, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
12
|
+
import { validateAcceptanceReport } from "./acceptance.ts";
|
|
12
13
|
import {
|
|
13
14
|
CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
|
|
14
15
|
formatChildToolDiagnostic,
|
|
@@ -729,10 +730,14 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
729
730
|
});
|
|
730
731
|
const structuredOutputPath = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
|
|
731
732
|
const structuredAcceptanceReportPath = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV];
|
|
733
|
+
const structuredAcceptanceReportRequired = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] === "1";
|
|
732
734
|
const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
|
|
733
735
|
if (structuredOutputPath && structuredSchemaPath) {
|
|
734
736
|
const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
|
|
735
|
-
const
|
|
737
|
+
const acceptanceReportMode = structuredAcceptanceReportPath
|
|
738
|
+
? structuredAcceptanceReportRequired ? "required" as const : "optional" as const
|
|
739
|
+
: undefined;
|
|
740
|
+
const parameters = createStructuredOutputToolParameters(schema, { acceptanceReport: acceptanceReportMode });
|
|
736
741
|
const registerTool = pi.registerTool as unknown as (tool: {
|
|
737
742
|
name: string;
|
|
738
743
|
label: string;
|
|
@@ -750,11 +755,23 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
750
755
|
if (validation.status === "invalid") {
|
|
751
756
|
throw new Error(`Structured output validation failed: ${validation.message}`);
|
|
752
757
|
}
|
|
758
|
+
if (structuredAcceptanceReportRequired && params.acceptanceReport === undefined) {
|
|
759
|
+
throw new Error(MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR);
|
|
760
|
+
}
|
|
761
|
+
if (structuredAcceptanceReportRequired && params.acceptanceReport !== undefined) {
|
|
762
|
+
const acceptanceValidation = validateAcceptanceReport(params.acceptanceReport, "acceptanceReport");
|
|
763
|
+
if (!acceptanceValidation.report) {
|
|
764
|
+
throw new Error(`Invalid structured output acceptance report: ${acceptanceValidation.errors.join("; ")}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
753
767
|
fs.mkdirSync(path.dirname(structuredOutputPath), { recursive: true });
|
|
754
|
-
fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
|
|
755
768
|
if (structuredAcceptanceReportPath && params.acceptanceReport !== undefined) {
|
|
769
|
+
fs.mkdirSync(path.dirname(structuredAcceptanceReportPath), { recursive: true });
|
|
756
770
|
fs.writeFileSync(structuredAcceptanceReportPath, JSON.stringify(params.acceptanceReport), { mode: 0o600 });
|
|
771
|
+
} else if (structuredAcceptanceReportPath && fs.existsSync(structuredAcceptanceReportPath)) {
|
|
772
|
+
fs.unlinkSync(structuredAcceptanceReportPath);
|
|
757
773
|
}
|
|
774
|
+
fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
|
|
758
775
|
return {
|
|
759
776
|
content: [{ type: "text", text: "Structured output captured." }],
|
|
760
777
|
details: { path: structuredOutputPath },
|
|
@@ -34,13 +34,22 @@ const REVIEWER_REQUIRED_EDIT_PATTERNS = [
|
|
|
34
34
|
/\bmake\s+(?:the\s+)?code\s+changes\b/i,
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
-
// The prohibition's object ends at punctuation
|
|
38
|
-
// (but/and/then), so a follow-on clause like "but
|
|
39
|
-
// the text for write-intent testing instead of
|
|
40
|
-
|
|
37
|
+
// The prohibition's object ends at punctuation, a serialized line separator,
|
|
38
|
+
// or a coordinating word (but/and/then), so a follow-on clause like "but
|
|
39
|
+
// implement the fix" stays in the text for write-intent testing instead of
|
|
40
|
+
// being swallowed as the object.
|
|
41
|
+
// Accept serialized line separators too: workflow prompts can carry literal
|
|
42
|
+
// `\\n`/`\\r\\n` between clauses instead of decoded newlines.
|
|
43
|
+
const NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b((?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
|
|
41
44
|
|
|
42
45
|
/** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
|
|
43
46
|
const GENERIC_PROHIBITION_OBJECT = /^\s*(?:(?:any|all|the|these|those|your|our|existing|project|product|source|sources|config|configs|repo|repository)[\s/,-]*)*(?:files?|code|codebase|sources?|anything|repo(?:sitory)?)?\s*$/i;
|
|
47
|
+
const SCOPED_PROHIBITION_CONTINUATION = /^(?:\\(?:r\\n|n)|\r?\n)\s*(?:in|inside|under|within|outside|except|other\s+than|besides)\b/i;
|
|
48
|
+
const OUTPUT_INSTRUCTION_CONTINUATION = /^(?:\\(?:r\\n|n)|\r?\n)\s*(?:in|inside|within)\s+(?:(?:(?:your|the)\s+)(?:final\s+)?|final\s+)(?:output|response|report|answer|reply|message|summary|findings?|analysis|recommendations?)(?:\s*\/\s*(?:output|response|report|answer|reply|message|summary|findings?|analysis|recommendations?))*\b(?![\\/])/i;
|
|
49
|
+
|
|
50
|
+
function hasScopedProhibitionContinuation(text: string): boolean {
|
|
51
|
+
return SCOPED_PROHIBITION_CONTINUATION.test(text) && !OUTPUT_INSTRUCTION_CONTINUATION.test(text);
|
|
52
|
+
}
|
|
44
53
|
|
|
45
54
|
const SCOPED_NO_EDIT_CONSTRAINT_PATTERNS = [
|
|
46
55
|
/\bdo not edit files?\s+outside\b/i,
|
|
@@ -135,11 +144,13 @@ function analyzeNoEditProhibitions(taskText: string): NoEditProhibitionAnalysis
|
|
|
135
144
|
|| NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText));
|
|
136
145
|
let blanket = present;
|
|
137
146
|
let strippedText = stripPatterns(taskText, [...REVIEW_ONLY_PATTERNS, ...NO_TOOL_INTENT_PATTERNS]);
|
|
138
|
-
strippedText = strippedText.replace(new RegExp(NO_EDIT_PROHIBITION_PATTERN.source, NO_EDIT_PROHIBITION_PATTERN.flags), (
|
|
147
|
+
strippedText = strippedText.replace(new RegExp(NO_EDIT_PROHIBITION_PATTERN.source, NO_EDIT_PROHIBITION_PATTERN.flags), (match, object: string, offset: number, source: string) => {
|
|
139
148
|
present = true;
|
|
140
|
-
if (GENERIC_PROHIBITION_OBJECT.test(object)) blanket = true;
|
|
149
|
+
if (GENERIC_PROHIBITION_OBJECT.test(object) && !hasScopedProhibitionContinuation(source.slice(offset + match.length))) blanket = true;
|
|
141
150
|
return " ";
|
|
142
151
|
});
|
|
152
|
+
// Restore boundaries after stripping a prohibition from serialized prompts.
|
|
153
|
+
strippedText = strippedText.replace(/\\(?:r\\n|n)/g, "\n");
|
|
143
154
|
return { present, blanket, strippedText };
|
|
144
155
|
}
|
|
145
156
|
|
|
@@ -16,7 +16,7 @@ export const DEFAULT_FAST_TOOL_TIMEOUT_TOOLS = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
18
|
/** Tools whose normal job can be to wait for a person or another run. */
|
|
19
|
-
export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait"
|
|
19
|
+
export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait"]);
|
|
20
20
|
|
|
21
21
|
// Backward-compatible export name for existing callers/tests.
|
|
22
22
|
export const TOOL_TIMEOUT_ALLOWLIST = TOOL_TIMEOUT_EXEMPT_TOOLS;
|
|
@@ -15,6 +15,7 @@ type AtomicJsonWriterOptions = {
|
|
|
15
15
|
mode?: number;
|
|
16
16
|
retryRenameErrors?: boolean;
|
|
17
17
|
retryDirectoryErrors?: boolean;
|
|
18
|
+
ignoreCleanupErrorAfterSuccess?: boolean;
|
|
18
19
|
retryDelaysMs?: readonly number[];
|
|
19
20
|
wait?: (delayMs: number) => void;
|
|
20
21
|
};
|
|
@@ -46,6 +47,7 @@ export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (
|
|
|
46
47
|
const mode = options.mode;
|
|
47
48
|
const retryRenameErrors = options.retryRenameErrors ?? process.platform === "win32";
|
|
48
49
|
const retryDirectoryErrors = options.retryDirectoryErrors ?? retryRenameErrors;
|
|
50
|
+
const ignoreCleanupErrorAfterSuccess = options.ignoreCleanupErrorAfterSuccess ?? false;
|
|
49
51
|
const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;
|
|
50
52
|
const renameRetryDelaysMs = retryRenameErrors ? retryDelaysMs : [];
|
|
51
53
|
const directoryRetryDelaysMs = retryDirectoryErrors ? retryDelaysMs : [];
|
|
@@ -71,7 +73,7 @@ export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (
|
|
|
71
73
|
} catch (cleanupError) {
|
|
72
74
|
// Preserve the write/rename failure: cleanup is best effort and must
|
|
73
75
|
// not hide the error callers need to classify or report.
|
|
74
|
-
if (writeError === undefined) throw cleanupError;
|
|
76
|
+
if (writeError === undefined && !ignoreCleanupErrorAfterSuccess) throw cleanupError;
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
};
|
|
@@ -174,18 +174,6 @@ function readSessionEntries(sessionFile: string): BranchSessionEntry[] {
|
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
/** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
|
|
178
|
-
export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
|
|
179
|
-
const entries = readSessionEntries(sessionFile);
|
|
180
|
-
const header = entries[0];
|
|
181
|
-
if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
|
|
182
|
-
const resolvedCwd = path.resolve(cwd);
|
|
183
|
-
const effectiveCwd = fs.realpathSync.native(resolvedCwd);
|
|
184
|
-
if (header.cwd === effectiveCwd) return;
|
|
185
|
-
header.cwd = effectiveCwd;
|
|
186
|
-
fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
177
|
export function createForkContextResolver(
|
|
190
178
|
sessionManager: ForkableSessionManager,
|
|
191
179
|
requestedContext: unknown,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
interface SessionEntry {
|
|
5
|
+
type?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
|
|
11
|
+
export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
|
|
12
|
+
const lines = fs.readFileSync(sessionFile, "utf-8").split("\n").filter((line) => line.trim().length > 0);
|
|
13
|
+
const entries = lines.map((line, index) => {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(line) as SessionEntry;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
18
|
+
throw new Error(`Unable to inspect forked session ${sessionFile}: invalid JSONL on line ${index + 1}: ${cause.message}`, { cause });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const header = entries[0];
|
|
22
|
+
if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
|
|
23
|
+
const effectiveCwd = fs.realpathSync.native(path.resolve(cwd));
|
|
24
|
+
if (header.cwd === effectiveCwd) return;
|
|
25
|
+
header.cwd = effectiveCwd;
|
|
26
|
+
fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
27
|
+
}
|
|
@@ -51,6 +51,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
51
51
|
fast: agent.fast,
|
|
52
52
|
thinking: agent.thinking,
|
|
53
53
|
tools: agent.tools,
|
|
54
|
+
excludeTools: agent.excludeTools,
|
|
54
55
|
allowNestedSubagents: agent.allowNestedSubagents,
|
|
55
56
|
mcpDirectTools: agent.mcpDirectTools,
|
|
56
57
|
extensions: agent.extensions,
|
|
@@ -93,6 +94,7 @@ export interface LaunchBindingInput {
|
|
|
93
94
|
inheritSkills: boolean;
|
|
94
95
|
skills?: string[];
|
|
95
96
|
tools?: string[];
|
|
97
|
+
excludeTools?: string[];
|
|
96
98
|
extensions?: string[];
|
|
97
99
|
subagentOnlyExtensions?: string[];
|
|
98
100
|
mcpDirectTools?: string[];
|
|
@@ -120,6 +122,7 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
|
|
|
120
122
|
inheritSkills: input.inheritSkills,
|
|
121
123
|
skills: input.skills,
|
|
122
124
|
tools: input.tools,
|
|
125
|
+
excludeTools: input.excludeTools,
|
|
123
126
|
extensions: input.extensions,
|
|
124
127
|
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
125
128
|
mcpDirectTools: input.mcpDirectTools,
|
package/src/shared/types.ts
CHANGED
|
@@ -780,6 +780,7 @@ export interface SteeringRecoveryDescriptor {
|
|
|
780
780
|
thinking?: string;
|
|
781
781
|
thinkingCeiling?: ThinkingLevel;
|
|
782
782
|
tools?: string[];
|
|
783
|
+
excludeTools?: string[];
|
|
783
784
|
allowNestedSubagents?: boolean;
|
|
784
785
|
extensions?: string[];
|
|
785
786
|
subagentOnlyExtensions?: string[];
|
|
@@ -994,6 +995,7 @@ export interface AcceptanceReviewGate {
|
|
|
994
995
|
|
|
995
996
|
export interface AcceptanceConfig {
|
|
996
997
|
level?: AcceptanceLevel;
|
|
998
|
+
report?: "on" | "off";
|
|
997
999
|
criteria?: Array<string | AcceptanceGate>;
|
|
998
1000
|
evidence?: AcceptanceEvidenceKind[];
|
|
999
1001
|
verify?: AcceptanceVerifyCommand[];
|
|
@@ -1342,7 +1344,7 @@ export interface AgentCapabilityRow {
|
|
|
1342
1344
|
restrictionSources?: string[];
|
|
1343
1345
|
aliases?: string[];
|
|
1344
1346
|
runner: { type: "pi" } | { type: "external-cli"; adapter?: string; capabilities: ExternalCliCapabilities } | { type: "external-job"; provider: string; available?: boolean; capabilities: ExternalJobRunnerStatus["capabilities"] };
|
|
1345
|
-
tools: { ambient: boolean; names: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
|
|
1347
|
+
tools: { ambient: boolean; names: string[]; excludeTools?: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
|
|
1346
1348
|
model?: { value?: string; fallbackModels?: string[]; thinking?: string | false };
|
|
1347
1349
|
execution?: { defaultAsync?: boolean; timeoutMs?: number };
|
|
1348
1350
|
output?: { path?: string; mode?: OutputMode };
|
|
@@ -417,7 +417,7 @@ function detailsFromSessionEntry(entry: unknown): Details | undefined {
|
|
|
417
417
|
}
|
|
418
418
|
if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
|
|
419
419
|
const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
|
|
420
|
-
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait"
|
|
420
|
+
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait")) return undefined;
|
|
421
421
|
return isSubagentDetails(message.details) ? message.details : undefined;
|
|
422
422
|
}
|
|
423
423
|
|
|
@@ -97,6 +97,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
97
97
|
systemPrompt: agent.systemPrompt,
|
|
98
98
|
...(agent.skills !== undefined ? { skills: [...agent.skills] } : {}),
|
|
99
99
|
...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
|
|
100
|
+
...(agent.excludeTools !== undefined ? { excludeTools: [...agent.excludeTools] } : {}),
|
|
100
101
|
...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
|
|
101
102
|
...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
|
|
102
103
|
...(agent.mutationTools !== undefined ? { mutationTools: [...agent.mutationTools] } : {}),
|
|
@@ -188,6 +189,7 @@ function metadataFor(agent: AgentConfig): string {
|
|
|
188
189
|
if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
|
|
189
190
|
if (agent.thinking !== undefined) lines.push(`Thinking: ${agent.thinking === false ? "off" : agent.thinking}`);
|
|
190
191
|
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
192
|
+
if (agent.excludeTools?.length) lines.push(`Excluded tools: ${agent.excludeTools.join(", ")}`);
|
|
191
193
|
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
192
194
|
lines.push(`System prompt mode: ${agent.systemPromptMode}`);
|
|
193
195
|
lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
|