feat: package Pi extensions as Bun workspaces
This commit is contained in:
@@ -0,0 +1,817 @@
|
||||
import type { Api, Model } from "@earendil-works/pi-ai";
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import {
|
||||
createEditToolDefinition,
|
||||
createFindToolDefinition,
|
||||
createGrepToolDefinition,
|
||||
createLsToolDefinition,
|
||||
createReadToolDefinition,
|
||||
createWriteToolDefinition,
|
||||
defineTool,
|
||||
type DefaultResourceLoader,
|
||||
type ExtensionContext,
|
||||
type SettingsManager,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import {
|
||||
createChildResources,
|
||||
resolveStandaloneChildProjectTrust,
|
||||
} from "@_jb/pi-shared/child-session";
|
||||
import {
|
||||
runAgent,
|
||||
type AgentOutcome,
|
||||
type RunAgentOptions,
|
||||
type ThinkingLevel,
|
||||
type WorkflowModel,
|
||||
} from "../_workflows/runner.ts";
|
||||
import { STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION } from "../_workflows/prompt.ts";
|
||||
import type {
|
||||
AssignmentExecutionError,
|
||||
AssignmentExecutionMetadata,
|
||||
AssignmentExecutionResult,
|
||||
AssignmentRequest,
|
||||
AssignmentExecutor,
|
||||
} from "./engine.ts";
|
||||
import { GitStateProbe, MutationScope } from "./workspace.ts";
|
||||
|
||||
const PROCESS_MAX_ASSIGNMENTS = 32;
|
||||
|
||||
interface ScheduledAssignment {
|
||||
runId: string;
|
||||
runLimit: number;
|
||||
exclusive: boolean;
|
||||
signal: AbortSignal;
|
||||
start: () => void;
|
||||
reject: (error: Error) => void;
|
||||
state: "queued" | "reserved" | "running" | "settled";
|
||||
onAbort: () => void;
|
||||
}
|
||||
|
||||
function abortError(signal: AbortSignal) {
|
||||
return signal.reason instanceof Error
|
||||
? signal.reason
|
||||
: new Error("Assignment was cancelled");
|
||||
}
|
||||
|
||||
/** Process-wide capacity with synchronous reservation and cancellation-safe release. */
|
||||
export class ProcessAssignmentScheduler {
|
||||
private ceiling = PROCESS_MAX_ASSIGNMENTS;
|
||||
private inUse = 0;
|
||||
private running = 0;
|
||||
private readonly runUsage = new Map<string, number>();
|
||||
private readonly exclusiveRuns = new Set<string>();
|
||||
private readonly queue: ScheduledAssignment[] = [];
|
||||
|
||||
lowerCeiling(ceiling: number) {
|
||||
if (!Number.isInteger(ceiling) || ceiling < 1 || ceiling > PROCESS_MAX_ASSIGNMENTS) {
|
||||
throw new Error(`Assignment ceiling must be between 1 and ${PROCESS_MAX_ASSIGNMENTS}`);
|
||||
}
|
||||
this.ceiling = Math.min(this.ceiling, ceiling);
|
||||
this.drain();
|
||||
}
|
||||
|
||||
schedule<T>(options: {
|
||||
runId: string;
|
||||
runLimit: number;
|
||||
signal: AbortSignal;
|
||||
exclusive?: boolean;
|
||||
onReserved?: () => Promise<void>;
|
||||
task: (signal: AbortSignal) => Promise<T>;
|
||||
}): Promise<T> {
|
||||
if (!Number.isInteger(options.runLimit) || options.runLimit < 1) {
|
||||
return Promise.reject(new Error("Workflow Run assignment limit must be positive"));
|
||||
}
|
||||
if (options.signal.aborted) return Promise.reject(abortError(options.signal));
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const entry: ScheduledAssignment = {
|
||||
runId: options.runId,
|
||||
runLimit: options.runLimit,
|
||||
signal: options.signal,
|
||||
exclusive: options.exclusive ?? false,
|
||||
state: "queued",
|
||||
reject,
|
||||
onAbort: () => {
|
||||
if (entry.state !== "queued") return;
|
||||
entry.state = "settled";
|
||||
const index = this.queue.indexOf(entry);
|
||||
if (index >= 0) this.queue.splice(index, 1);
|
||||
options.signal.removeEventListener("abort", entry.onAbort);
|
||||
reject(abortError(options.signal));
|
||||
},
|
||||
start: () => {
|
||||
queueMicrotask(async () => {
|
||||
if (entry.state !== "reserved") return;
|
||||
entry.state = "running";
|
||||
this.running++;
|
||||
try {
|
||||
if (options.signal.aborted) throw abortError(options.signal);
|
||||
await options.onReserved?.();
|
||||
if (options.signal.aborted) throw abortError(options.signal);
|
||||
resolve(await options.task(options.signal));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
this.running--;
|
||||
this.release(entry);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
options.signal.addEventListener("abort", entry.onAbort, { once: true });
|
||||
this.queue.push(entry);
|
||||
this.drain();
|
||||
});
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
return {
|
||||
ceiling: this.ceiling,
|
||||
inUse: this.inUse,
|
||||
running: this.running,
|
||||
queued: this.queue.filter((entry) => entry.state === "queued").length,
|
||||
};
|
||||
}
|
||||
|
||||
private drain() {
|
||||
while (this.inUse < this.ceiling) {
|
||||
const index = this.queue.findIndex((entry, entryIndex) => {
|
||||
if (entry.state !== "queued") return false;
|
||||
const usage = this.runUsage.get(entry.runId) ?? 0;
|
||||
if (entry.exclusive) return usage === 0;
|
||||
if (this.exclusiveRuns.has(entry.runId) || usage >= entry.runLimit) {
|
||||
return false;
|
||||
}
|
||||
return !this.queue
|
||||
.slice(0, entryIndex)
|
||||
.some(
|
||||
(queued) =>
|
||||
queued.state === "queued" &&
|
||||
queued.runId === entry.runId &&
|
||||
queued.exclusive,
|
||||
);
|
||||
});
|
||||
if (index < 0) return;
|
||||
const entry = this.queue.splice(index, 1)[0];
|
||||
if (!entry || entry.signal.aborted) {
|
||||
entry?.onAbort();
|
||||
continue;
|
||||
}
|
||||
entry.state = "reserved";
|
||||
this.inUse++;
|
||||
this.runUsage.set(entry.runId, (this.runUsage.get(entry.runId) ?? 0) + 1);
|
||||
if (entry.exclusive) this.exclusiveRuns.add(entry.runId);
|
||||
entry.start();
|
||||
}
|
||||
}
|
||||
|
||||
private release(entry: ScheduledAssignment) {
|
||||
if (entry.state === "settled") return;
|
||||
entry.state = "settled";
|
||||
entry.signal.removeEventListener("abort", entry.onAbort);
|
||||
this.inUse--;
|
||||
if (entry.exclusive) this.exclusiveRuns.delete(entry.runId);
|
||||
const remaining = (this.runUsage.get(entry.runId) ?? 1) - 1;
|
||||
if (remaining === 0) this.runUsage.delete(entry.runId);
|
||||
else this.runUsage.set(entry.runId, remaining);
|
||||
this.drain();
|
||||
}
|
||||
}
|
||||
|
||||
export const processAssignmentScheduler = new ProcessAssignmentScheduler();
|
||||
|
||||
export function resolveWorkflowModel(
|
||||
registry: ExtensionContext["modelRegistry"],
|
||||
profile: { provider?: string; model?: string },
|
||||
inherited: WorkflowModel | undefined,
|
||||
): Model<Api> | undefined {
|
||||
if (profile.provider && !profile.model) {
|
||||
throw new Error(`Agent Profile provider ${profile.provider} requires a model`);
|
||||
}
|
||||
if (!profile.model) {
|
||||
if (!inherited) return undefined;
|
||||
const found = registry.find(inherited.provider, inherited.id);
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
`Unknown inherited model ${inherited.provider}/${inherited.id}`,
|
||||
);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
if (profile.provider) {
|
||||
const found = registry.find(profile.provider, profile.model);
|
||||
if (!found) throw new Error(`Unknown model ${profile.provider}/${profile.model}`);
|
||||
return found;
|
||||
}
|
||||
const slash = profile.model.indexOf("/");
|
||||
if (slash > 0) {
|
||||
const provider = profile.model.slice(0, slash);
|
||||
const model = profile.model.slice(slash + 1);
|
||||
const found = registry.find(provider, model);
|
||||
if (!found) throw new Error(`Unknown model ${profile.model}`);
|
||||
return found;
|
||||
}
|
||||
if (inherited) {
|
||||
const found = registry.find(inherited.provider, profile.model);
|
||||
if (found) return found;
|
||||
}
|
||||
const matches = registry.getAll().filter((model) => model.id === profile.model);
|
||||
if (matches.length === 1) return matches[0];
|
||||
if (matches.length > 1) {
|
||||
throw new Error(
|
||||
`Model ${profile.model} exists in multiple providers; use provider/model`,
|
||||
);
|
||||
}
|
||||
throw new Error(`Unknown model ${profile.model}`);
|
||||
}
|
||||
|
||||
const DISCOVERY_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
"questions",
|
||||
"candidatePaths",
|
||||
"languages",
|
||||
"tags",
|
||||
"riskTags",
|
||||
],
|
||||
properties: {
|
||||
questions: {
|
||||
type: "array",
|
||||
maxItems: 20,
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["id", "prompt"],
|
||||
properties: {
|
||||
id: { type: "string", maxLength: 160 },
|
||||
prompt: { type: "string", maxLength: 2000 },
|
||||
},
|
||||
},
|
||||
},
|
||||
candidatePaths: { type: "array", items: { type: "string" } },
|
||||
languages: { type: "array", items: { type: "string" } },
|
||||
tags: { type: "array", items: { type: "string" } },
|
||||
riskTags: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const DESIGN_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
"plan",
|
||||
"uiImpact",
|
||||
"uiImpactEvidence",
|
||||
"affectedUiSurfaces",
|
||||
"assignments",
|
||||
"acceptanceCriteria",
|
||||
"verificationCommands",
|
||||
],
|
||||
properties: {
|
||||
plan: { type: "string" },
|
||||
uiImpact: { enum: ["none", "minor", "non-trivial"] },
|
||||
uiImpactEvidence: { type: "array", items: { type: "string" } },
|
||||
affectedUiSurfaces: { type: "array", items: { type: "string" } },
|
||||
acceptanceCriteria: { type: "array", items: { type: "string" } },
|
||||
verificationCommands: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["executable", "args", "ownedPaths", "kind"],
|
||||
properties: {
|
||||
executable: { type: "string" },
|
||||
args: { type: "array", items: { type: "string" } },
|
||||
ownedPaths: { type: "array", items: { type: "string" } },
|
||||
kind: {
|
||||
enum: ["test", "typecheck", "lint", "build", "browser", "generation"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
assignments: {
|
||||
type: "array",
|
||||
minItems: 3,
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["role", "profileName", "rationale", "mutates", "ownedPaths"],
|
||||
properties: {
|
||||
role: { enum: ["prototype", "implement", "verify", "review"] },
|
||||
profileName: { type: "string" },
|
||||
rationale: { type: "string" },
|
||||
mutates: { type: "boolean" },
|
||||
ownedPaths: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const PROTOTYPE_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
"path",
|
||||
"route",
|
||||
"structuralRationale",
|
||||
"responsiveChecks",
|
||||
"browserEvidence",
|
||||
"launchInstructions",
|
||||
],
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
route: { type: "string" },
|
||||
structuralRationale: { type: "string" },
|
||||
responsiveChecks: { type: "array", items: { type: "string" } },
|
||||
browserEvidence: { type: "array", items: { type: "string" } },
|
||||
launchInstructions: { type: "string" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const IMPLEMENTATION_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["summary", "changedPaths", "focusedTests"],
|
||||
properties: {
|
||||
summary: { type: "string" },
|
||||
changedPaths: { type: "array", items: { type: "string" } },
|
||||
focusedTests: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const VERIFICATION_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["succeeded", "focusedBehavioralTests", "summary"],
|
||||
properties: {
|
||||
succeeded: { type: "boolean" },
|
||||
focusedBehavioralTests: { type: "boolean" },
|
||||
summary: { type: "string" },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const REVIEW_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["specification", "standards"],
|
||||
properties: {
|
||||
specification: { $ref: "#/$defs/axis" },
|
||||
standards: { $ref: "#/$defs/axis" },
|
||||
},
|
||||
$defs: {
|
||||
axis: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["verdict", "findings"],
|
||||
properties: {
|
||||
verdict: { enum: ["approved", "changes_requested"] },
|
||||
findings: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["severity", "message"],
|
||||
properties: {
|
||||
severity: { type: "string" },
|
||||
message: { type: "string" },
|
||||
path: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
function schemaForRole(role: AssignmentRequest["role"]) {
|
||||
if (role === "discover") return DISCOVERY_SCHEMA;
|
||||
if (role === "design") return DESIGN_SCHEMA;
|
||||
if (role === "prototype") return PROTOTYPE_SCHEMA;
|
||||
if (role === "verify") return VERIFICATION_SCHEMA;
|
||||
if (role === "review") return REVIEW_SCHEMA;
|
||||
return IMPLEMENTATION_SCHEMA;
|
||||
}
|
||||
|
||||
function pagedTextTool(options: {
|
||||
name: "workflow_diff" | "workflow_plan";
|
||||
label: string;
|
||||
description: string;
|
||||
contents: string;
|
||||
}) {
|
||||
const characters = [...options.contents];
|
||||
return defineTool({
|
||||
name: options.name,
|
||||
label: options.label,
|
||||
description: options.description,
|
||||
parameters: Type.Object({
|
||||
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 48 * 1024 })),
|
||||
}),
|
||||
async execute(_id, params) {
|
||||
const offset = params.offset ?? 0;
|
||||
const limit = params.limit ?? 48 * 1024;
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: characters.slice(offset, offset + limit).join(""),
|
||||
},
|
||||
],
|
||||
details: { offset, limit, totalCharacters: characters.length },
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function readCompletePagedText(
|
||||
transcript: AssignmentExecutionMetadata["transcript"],
|
||||
toolName: "workflow_diff" | "workflow_plan",
|
||||
totalCharacters: number,
|
||||
) {
|
||||
if (totalCharacters === 0) return true;
|
||||
const entries = transcript.filter(
|
||||
(
|
||||
entry,
|
||||
): entry is {
|
||||
role: string;
|
||||
name?: string;
|
||||
text: string;
|
||||
toolCallId?: string;
|
||||
isError?: boolean;
|
||||
} =>
|
||||
typeof entry === "object" &&
|
||||
entry !== null &&
|
||||
"role" in entry &&
|
||||
typeof entry.role === "string" &&
|
||||
"text" in entry &&
|
||||
typeof entry.text === "string",
|
||||
);
|
||||
const successfulCallIds = new Set(
|
||||
entries
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.role === "toolResult" &&
|
||||
entry.name === toolName &&
|
||||
!entry.isError &&
|
||||
typeof entry.toolCallId === "string",
|
||||
)
|
||||
.map((entry) => entry.toolCallId!),
|
||||
);
|
||||
const intervals = entries
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.role === "tool" &&
|
||||
typeof entry.toolCallId === "string" &&
|
||||
successfulCallIds.has(entry.toolCallId) &&
|
||||
entry.name === toolName,
|
||||
)
|
||||
.map((entry) => {
|
||||
try {
|
||||
const input = JSON.parse(entry.text) as { offset?: unknown; limit?: unknown };
|
||||
const offset = input.offset === undefined ? 0 : input.offset;
|
||||
const limit = input.limit === undefined ? 48 * 1024 : input.limit;
|
||||
if (
|
||||
typeof offset !== "number" ||
|
||||
!Number.isInteger(offset) ||
|
||||
offset < 0 ||
|
||||
typeof limit !== "number" ||
|
||||
!Number.isInteger(limit) ||
|
||||
limit < 1 ||
|
||||
limit > 48 * 1024
|
||||
) {
|
||||
return [0, 0] as const;
|
||||
}
|
||||
return [offset, Math.min(totalCharacters, offset + limit)] as const;
|
||||
} catch {
|
||||
return [0, 0] as const;
|
||||
}
|
||||
})
|
||||
.sort((left, right) => left[0] - right[0]);
|
||||
let covered = 0;
|
||||
for (const [start, end] of intervals) {
|
||||
if (start > covered) return false;
|
||||
covered = Math.max(covered, end);
|
||||
if (covered >= totalCharacters) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function assignmentPrompt(request: AssignmentRequest) {
|
||||
const plannerCandidates = request.plannerCandidates?.map((entry) => ({
|
||||
role: entry.role,
|
||||
candidates: entry.candidates
|
||||
.filter((candidate) => candidate.eligible)
|
||||
.map((candidate) => ({
|
||||
name: candidate.profileName,
|
||||
digest: candidate.profileDigest,
|
||||
specificity: candidate.specificity,
|
||||
})),
|
||||
}));
|
||||
return [
|
||||
`Assignment role: ${request.role}`,
|
||||
`Workflow request: ${request.request}`,
|
||||
`Repository facts: ${JSON.stringify(request.facts)}`,
|
||||
`Accepted decisions: ${JSON.stringify(request.decisions.map((item) => item.value))}`,
|
||||
...(plannerCandidates
|
||||
? [`Eligible planner candidates: ${JSON.stringify(plannerCandidates)}`]
|
||||
: []),
|
||||
...(request.approvedPlan
|
||||
? request.role === "verify" || request.role === "review"
|
||||
? [
|
||||
`Approved plan: ${[...request.approvedPlan].length} characters available through workflow_plan. Read the complete plan.`,
|
||||
]
|
||||
: [`Approved plan:\n${request.approvedPlan}`]
|
||||
: []),
|
||||
...(request.verificationEvidence
|
||||
? [
|
||||
`Persisted verification evidence: ${JSON.stringify(request.verificationEvidence)}`,
|
||||
]
|
||||
: []),
|
||||
...(request.reviewDiff
|
||||
? [
|
||||
`Review diff: ${[...request.reviewDiff].length} characters available through workflow_diff. Review the complete scope.`,
|
||||
]
|
||||
: []),
|
||||
"Follow the active Agent Profile instructions. Return only through structured_output.",
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
interface ChildResources {
|
||||
loader: DefaultResourceLoader;
|
||||
settingsManager: SettingsManager;
|
||||
}
|
||||
|
||||
export interface PiAssignmentExecutorOptions {
|
||||
modelRegistry: ExtensionContext["modelRegistry"];
|
||||
parentCwd: string;
|
||||
parentProjectTrusted: boolean;
|
||||
parentModel?: WorkflowModel;
|
||||
parentThinkingLevel: ThinkingLevel;
|
||||
concurrencyCeiling: number;
|
||||
runConcurrency: number;
|
||||
scheduler?: ProcessAssignmentScheduler;
|
||||
runAgent?: (options: RunAgentOptions) => Promise<AgentOutcome>;
|
||||
createResources?: (options: {
|
||||
cwd: string;
|
||||
projectTrusted: boolean;
|
||||
appendSystemPrompt: string[];
|
||||
}) => Promise<ChildResources>;
|
||||
resolveProjectTrust?: typeof resolveStandaloneChildProjectTrust;
|
||||
}
|
||||
|
||||
export class PiAssignmentExecutor implements AssignmentExecutor {
|
||||
readonly reservesCapacity = true;
|
||||
private readonly options: PiAssignmentExecutorOptions;
|
||||
private readonly scheduler: ProcessAssignmentScheduler;
|
||||
|
||||
constructor(options: PiAssignmentExecutorOptions) {
|
||||
this.options = options;
|
||||
this.scheduler = options.scheduler ?? processAssignmentScheduler;
|
||||
this.scheduler.lowerCeiling(options.concurrencyCeiling);
|
||||
}
|
||||
|
||||
async run(request: AssignmentRequest): Promise<AssignmentExecutionResult> {
|
||||
const model = resolveWorkflowModel(
|
||||
this.options.modelRegistry,
|
||||
request.profile,
|
||||
this.options.parentModel,
|
||||
) as WorkflowModel | undefined;
|
||||
const thinkingLevel =
|
||||
(request.profile.effort as ThinkingLevel | undefined) ??
|
||||
this.options.parentThinkingLevel;
|
||||
const resolveTrust =
|
||||
this.options.resolveProjectTrust ?? resolveStandaloneChildProjectTrust;
|
||||
const projectTrusted = resolveTrust({
|
||||
parentCwd: this.options.parentCwd,
|
||||
childCwd: request.cwd,
|
||||
parentTrusted: this.options.parentProjectTrusted,
|
||||
});
|
||||
const createResources = this.options.createResources ?? createChildResources;
|
||||
const resources = await createResources({
|
||||
cwd: request.cwd,
|
||||
projectTrusted,
|
||||
appendSystemPrompt: [
|
||||
request.profile.instructions,
|
||||
STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
|
||||
],
|
||||
});
|
||||
const execute = this.options.runAgent ?? runAgent;
|
||||
const mutationScope = request.mutates
|
||||
? await MutationScope.create(request.cwd, request.ownedPaths ?? [])
|
||||
: undefined;
|
||||
const gitProbe =
|
||||
mutationScope || request.captureGitState ? new GitStateProbe() : undefined;
|
||||
const evidenceTools = [
|
||||
...(request.reviewDiff
|
||||
? [
|
||||
pagedTextTool({
|
||||
name: "workflow_diff",
|
||||
label: "Workflow Diff",
|
||||
description:
|
||||
"Read the complete review diff in bounded character ranges.",
|
||||
contents: request.reviewDiff,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(request.approvedPlan &&
|
||||
(request.role === "verify" || request.role === "review")
|
||||
? [
|
||||
pagedTextTool({
|
||||
name: "workflow_plan",
|
||||
label: "Workflow Plan",
|
||||
description:
|
||||
"Read the complete approved plan in bounded character ranges.",
|
||||
contents: request.approvedPlan,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const allowedReadTools = new Set(
|
||||
request.allowedReadTools ?? ["read", "grep", "find", "ls"],
|
||||
);
|
||||
const auditedReadTools = [
|
||||
createReadToolDefinition(request.cwd),
|
||||
createGrepToolDefinition(request.cwd),
|
||||
createFindToolDefinition(request.cwd),
|
||||
createLsToolDefinition(request.cwd),
|
||||
].filter((tool) => allowedReadTools.has(tool.name));
|
||||
const mutationTools = mutationScope
|
||||
? [
|
||||
createEditToolDefinition(request.cwd, {
|
||||
operations: {
|
||||
async readFile(absolutePath) {
|
||||
return readFile(await mutationScope.assertWritePath(absolutePath));
|
||||
},
|
||||
async writeFile(absolutePath, content) {
|
||||
await mutationScope.writeFile(absolutePath, content);
|
||||
},
|
||||
async access(absolutePath) {
|
||||
await access(await mutationScope.assertWritePath(absolutePath));
|
||||
},
|
||||
},
|
||||
}),
|
||||
createWriteToolDefinition(request.cwd, {
|
||||
operations: {
|
||||
async writeFile(absolutePath, content) {
|
||||
await mutationScope.writeFile(absolutePath, content);
|
||||
},
|
||||
async mkdir(directory) {
|
||||
await mutationScope.mkdir(directory);
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
const removalTools =
|
||||
mutationScope && request.role === "remediate"
|
||||
? [
|
||||
defineTool({
|
||||
name: "workflow_remove",
|
||||
label: "Remove Owned Prototype Path",
|
||||
description:
|
||||
"Remove a prototype-only file or directory inside Assignment ownership.",
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
async execute(_id, params) {
|
||||
await mutationScope.remove(params.path);
|
||||
return {
|
||||
content: [
|
||||
{ type: "text" as const, text: `Removed ${params.path}` },
|
||||
],
|
||||
details: { path: params.path },
|
||||
};
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
return this.scheduler.schedule({
|
||||
runId: request.runId,
|
||||
runLimit: request.runConcurrency ?? this.options.runConcurrency,
|
||||
signal: request.signal,
|
||||
exclusive: request.mutates,
|
||||
onReserved: request.onReserved,
|
||||
task: async (signal) => {
|
||||
let created: AssignmentExecutionMetadata | undefined;
|
||||
const gitBefore = gitProbe
|
||||
? await gitProbe.capture(mutationScope?.root ?? request.cwd)
|
||||
: undefined;
|
||||
const outcome = await execute({
|
||||
prompt: assignmentPrompt(request),
|
||||
schema: schemaForRole(request.role),
|
||||
model,
|
||||
thinkingLevel,
|
||||
cwd: request.cwd,
|
||||
loader: resources.loader,
|
||||
settingsManager: resources.settingsManager,
|
||||
modelRegistry: this.options.modelRegistry,
|
||||
signal,
|
||||
tools: request.mutates
|
||||
? [
|
||||
...(request.profile.tools ?? []).filter((tool) =>
|
||||
(["edit", "write"].includes(tool) ||
|
||||
allowedReadTools.has(tool)),
|
||||
),
|
||||
...evidenceTools.map((tool) => tool.name),
|
||||
...removalTools.map((tool) => tool.name),
|
||||
"structured_output",
|
||||
]
|
||||
: [
|
||||
...(request.profile.tools ?? []).filter((tool) =>
|
||||
allowedReadTools.has(tool),
|
||||
),
|
||||
...evidenceTools.map((tool) => tool.name),
|
||||
"structured_output",
|
||||
],
|
||||
customTools: [
|
||||
...auditedReadTools,
|
||||
...mutationTools,
|
||||
...removalTools,
|
||||
...evidenceTools,
|
||||
],
|
||||
onSessionCreated(metadata) {
|
||||
created = {
|
||||
childSessionId: metadata.childSessionId,
|
||||
provider: metadata.provider,
|
||||
model: metadata.model,
|
||||
effort: metadata.thinkingLevel,
|
||||
contextCapacity: undefined,
|
||||
transcript: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
const gitAfter = gitProbe
|
||||
? await gitProbe.capture(mutationScope?.root ?? request.cwd)
|
||||
: undefined;
|
||||
let safetyError: Error | undefined;
|
||||
if (
|
||||
request.role === "review" &&
|
||||
request.reviewDiff !== undefined &&
|
||||
!readCompletePagedText(
|
||||
outcome.transcript,
|
||||
"workflow_diff",
|
||||
[...request.reviewDiff].length,
|
||||
)
|
||||
) {
|
||||
safetyError = new Error(
|
||||
"Review Assignment did not read the complete workflow diff",
|
||||
);
|
||||
}
|
||||
if (
|
||||
!safetyError &&
|
||||
request.approvedPlan !== undefined &&
|
||||
(request.role === "verify" || request.role === "review") &&
|
||||
!readCompletePagedText(
|
||||
outcome.transcript,
|
||||
"workflow_plan",
|
||||
[...request.approvedPlan].length,
|
||||
)
|
||||
) {
|
||||
safetyError = new Error(
|
||||
"Assignment did not read the complete approved workflow plan",
|
||||
);
|
||||
}
|
||||
if (!safetyError && gitProbe && gitBefore && gitAfter) {
|
||||
try {
|
||||
gitProbe.assertIdentity(gitBefore, gitAfter);
|
||||
const touched = gitProbe.changedSince(gitBefore, gitAfter);
|
||||
const outOfScope = mutationScope
|
||||
? touched.filter((changed) => !mutationScope.owns(changed))
|
||||
: touched;
|
||||
if (outOfScope.length > 0) {
|
||||
throw new Error(
|
||||
request.captureGitState && !mutationScope
|
||||
? `Read-only Assignment changed the repository: ${outOfScope.join(", ")}`
|
||||
: `Assignment changed paths outside ownership: ${outOfScope.join(", ")}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
safetyError = error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
}
|
||||
const metadata: AssignmentExecutionMetadata = {
|
||||
childSessionId: outcome.childSessionId ?? created?.childSessionId,
|
||||
provider: outcome.provider ?? created?.provider,
|
||||
model: outcome.model ?? created?.model,
|
||||
effort: outcome.thinkingLevel ?? created?.effort ?? thinkingLevel,
|
||||
contextCapacity: outcome.contextWindow,
|
||||
transcript: outcome.transcript,
|
||||
gitBefore,
|
||||
gitAfter,
|
||||
};
|
||||
if (safetyError || !outcome.ok) {
|
||||
const error = new Error(
|
||||
safetyError?.message ?? outcome.error ?? "Pi Assignment failed",
|
||||
) as AssignmentExecutionError;
|
||||
error.assignmentMetadata = metadata;
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
kind: "workflow-assignment-result",
|
||||
output: outcome.structured,
|
||||
metadata,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user