feat: package Pi extensions as Bun workspaces
This commit is contained in:
@@ -0,0 +1,871 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import test from "node:test";
|
||||
import { promisify } from "node:util";
|
||||
import type {
|
||||
DefaultResourceLoader,
|
||||
ExtensionContext,
|
||||
SettingsManager,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { emptyUsage } from "../_workflows/model.ts";
|
||||
import type {
|
||||
RunAgentOptions,
|
||||
ThinkingLevel,
|
||||
WorkflowModel,
|
||||
} from "../_workflows/runner.ts";
|
||||
import { DEFAULT_WORKFLOW_CONFIG, type WorkflowRun } from "./domain.ts";
|
||||
import { WorkflowEngine, type WorkspaceManager } from "./engine.ts";
|
||||
import {
|
||||
PiAssignmentExecutor,
|
||||
ProcessAssignmentScheduler,
|
||||
resolveWorkflowModel,
|
||||
} from "./execution.ts";
|
||||
import { loadAgentProfiles } from "./profiles.ts";
|
||||
import { WorkflowStore } from "./store.ts";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
async function withTempDir(run: (directory: string) => Promise<void>) {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), "workflow-execution-"));
|
||||
try {
|
||||
await run(directory);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function model(provider: string, id: string) {
|
||||
return {
|
||||
provider,
|
||||
id,
|
||||
contextWindow: 128_000,
|
||||
} as unknown as WorkflowModel;
|
||||
}
|
||||
|
||||
function registry(models: WorkflowModel[]) {
|
||||
return {
|
||||
find(provider: string, id: string) {
|
||||
return models.find((candidate) => candidate.provider === provider && candidate.id === id);
|
||||
},
|
||||
getAll() {
|
||||
return models;
|
||||
},
|
||||
} as unknown as ExtensionContext["modelRegistry"];
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
let resolve: (() => void) | undefined;
|
||||
const promise = new Promise<void>((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
return { promise, resolve: () => resolve?.() };
|
||||
}
|
||||
|
||||
test("process scheduler enforces aggregate and per-run capacity with queued cancellation", async () => {
|
||||
const scheduler = new ProcessAssignmentScheduler();
|
||||
scheduler.lowerCeiling(2);
|
||||
const waits = [deferred(), deferred(), deferred(), deferred()];
|
||||
const controllers = waits.map(() => new AbortController());
|
||||
let active = 0;
|
||||
let maximum = 0;
|
||||
const tasks = waits.map((wait, index) =>
|
||||
scheduler.schedule({
|
||||
runId: index < 3 ? "run-a" : "run-b",
|
||||
runLimit: 2,
|
||||
signal: controllers[index].signal,
|
||||
task: async () => {
|
||||
active++;
|
||||
maximum = Math.max(maximum, active);
|
||||
try {
|
||||
await wait.promise;
|
||||
return index;
|
||||
} finally {
|
||||
active--;
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
const cancelled = tasks[2].catch((error: unknown) => error);
|
||||
assert.deepEqual(scheduler.snapshot(), {
|
||||
ceiling: 2,
|
||||
inUse: 2,
|
||||
running: 0,
|
||||
queued: 2,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(scheduler.snapshot(), {
|
||||
ceiling: 2,
|
||||
inUse: 2,
|
||||
running: 2,
|
||||
queued: 2,
|
||||
});
|
||||
controllers[2].abort(new Error("cancel queued"));
|
||||
assert.match(String(await cancelled), /cancel queued/);
|
||||
waits[0].resolve();
|
||||
waits[1].resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
waits[3].resolve();
|
||||
assert.deepEqual(await Promise.all([tasks[0], tasks[1], tasks[3]]), [0, 1, 3]);
|
||||
assert.equal(maximum, 2);
|
||||
assert.deepEqual(scheduler.snapshot(), {
|
||||
ceiling: 2,
|
||||
inUse: 0,
|
||||
running: 0,
|
||||
queued: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("exclusive mutation reservations serialize a run without blocking other runs", async () => {
|
||||
const scheduler = new ProcessAssignmentScheduler();
|
||||
scheduler.lowerCeiling(3);
|
||||
const waits = [deferred(), deferred(), deferred(), deferred()];
|
||||
const started: number[] = [];
|
||||
const schedule = (index: number, runId: string, exclusive = false) =>
|
||||
scheduler.schedule({
|
||||
runId,
|
||||
runLimit: 3,
|
||||
exclusive,
|
||||
signal: new AbortController().signal,
|
||||
async task() {
|
||||
started.push(index);
|
||||
await waits[index].promise;
|
||||
},
|
||||
});
|
||||
const firstRead = schedule(0, "run-a");
|
||||
const mutation = schedule(1, "run-a", true);
|
||||
const laterRead = schedule(2, "run-a");
|
||||
const otherRun = schedule(3, "run-b");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(started, [0, 3]);
|
||||
waits[0].resolve();
|
||||
await firstRead;
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(started, [0, 3, 1]);
|
||||
waits[1].resolve();
|
||||
await mutation;
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(started, [0, 3, 1, 2]);
|
||||
waits[2].resolve();
|
||||
waits[3].resolve();
|
||||
await Promise.all([laterRead, otherRun]);
|
||||
});
|
||||
|
||||
test("running cancellation releases a scheduler reservation exactly once", async () => {
|
||||
const scheduler = new ProcessAssignmentScheduler();
|
||||
const controller = new AbortController();
|
||||
let reserved = false;
|
||||
let started = false;
|
||||
const task = scheduler.schedule({
|
||||
runId: "run-a",
|
||||
runLimit: 1,
|
||||
signal: controller.signal,
|
||||
async onReserved() {
|
||||
reserved = true;
|
||||
},
|
||||
async task(signal) {
|
||||
started = true;
|
||||
return new Promise<never>((_resolve, reject) => {
|
||||
signal.addEventListener("abort", () => reject(signal.reason), {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
controller.abort(new Error("cancel running"));
|
||||
await assert.rejects(task, /cancel running/);
|
||||
assert.equal(reserved, true);
|
||||
assert.equal(started, true);
|
||||
assert.deepEqual(scheduler.snapshot(), {
|
||||
ceiling: 32,
|
||||
inUse: 0,
|
||||
running: 0,
|
||||
queued: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("model resolution is exact, inherited-provider aware, and ambiguity safe", () => {
|
||||
const inherited = model("provider-a", "parent");
|
||||
const sharedA = model("provider-a", "shared");
|
||||
const sharedB = model("provider-b", "shared");
|
||||
const models = registry([inherited, sharedA, sharedB]);
|
||||
assert.equal(resolveWorkflowModel(models, {}, inherited), inherited);
|
||||
assert.throws(
|
||||
() => resolveWorkflowModel(registry([]), {}, inherited),
|
||||
/Unknown inherited model/,
|
||||
);
|
||||
assert.equal(resolveWorkflowModel(models, { model: "shared" }, inherited), sharedA);
|
||||
assert.equal(
|
||||
resolveWorkflowModel(models, { provider: "provider-b", model: "shared" }, inherited),
|
||||
sharedB,
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveWorkflowModel(models, { model: "shared" }, undefined),
|
||||
/multiple providers/,
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveWorkflowModel(models, { provider: "provider-a" }, inherited),
|
||||
/requires a model/,
|
||||
);
|
||||
});
|
||||
|
||||
test("the immutable process ceiling caps aggregate reservations at 32", async () => {
|
||||
const scheduler = new ProcessAssignmentScheduler();
|
||||
const waits = Array.from({ length: 40 }, deferred);
|
||||
let maximum = 0;
|
||||
let active = 0;
|
||||
const tasks = waits.map((wait, index) =>
|
||||
scheduler.schedule({
|
||||
runId: `run-${index % 4}`,
|
||||
runLimit: 32,
|
||||
signal: new AbortController().signal,
|
||||
task: async () => {
|
||||
active++;
|
||||
maximum = Math.max(maximum, active);
|
||||
try {
|
||||
await wait.promise;
|
||||
} finally {
|
||||
active--;
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.equal(scheduler.snapshot().inUse, 32);
|
||||
assert.equal(scheduler.snapshot().queued, 8);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
waits.forEach((wait) => wait.resolve());
|
||||
await Promise.all(tasks);
|
||||
assert.equal(maximum, 32);
|
||||
assert.equal(scheduler.snapshot().inUse, 0);
|
||||
});
|
||||
|
||||
test("production executor resolves trust and creates fresh structured child runs", async () => {
|
||||
await withTempDir(async (directory) => {
|
||||
const profiles = (
|
||||
await loadAgentProfiles({ cwd: directory, projectTrusted: false })
|
||||
).profiles;
|
||||
const profile = profiles.find((candidate) => candidate.roles.includes("discover"));
|
||||
assert.ok(profile);
|
||||
const inherited = model("provider-a", "parent");
|
||||
const models = registry([inherited]);
|
||||
const scheduler = new ProcessAssignmentScheduler();
|
||||
let resources = 0;
|
||||
let sessions = 0;
|
||||
const trustedChildren: string[] = [];
|
||||
const observedTools: string[][] = [];
|
||||
const observedCustomTools: string[][] = [];
|
||||
const fakeResources = {
|
||||
loader: {} as DefaultResourceLoader,
|
||||
settingsManager: {} as SettingsManager,
|
||||
};
|
||||
const fakeRunAgent = async (options: RunAgentOptions) => {
|
||||
sessions++;
|
||||
observedTools.push(options.tools ?? []);
|
||||
observedCustomTools.push(
|
||||
options.customTools?.map((tool) => tool.name) ?? [],
|
||||
);
|
||||
options.onSessionCreated?.({
|
||||
childSessionId: `child-${sessions}`,
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
output: "",
|
||||
structured: {
|
||||
questions: [],
|
||||
candidatePaths: ["src/index.ts"],
|
||||
languages: ["typescript"],
|
||||
tags: ["backend"],
|
||||
riskTags: [],
|
||||
},
|
||||
aborted: false,
|
||||
usage: emptyUsage(),
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
childSessionId: `child-${sessions}`,
|
||||
contextWindow: inherited.contextWindow,
|
||||
transcript: [{ role: "assistant" as const, text: "done" }],
|
||||
};
|
||||
};
|
||||
const executor = new PiAssignmentExecutor({
|
||||
modelRegistry: models,
|
||||
parentCwd: directory,
|
||||
parentProjectTrusted: true,
|
||||
parentModel: inherited,
|
||||
parentThinkingLevel: "medium" as ThinkingLevel,
|
||||
concurrencyCeiling: 4,
|
||||
runConcurrency: 2,
|
||||
scheduler,
|
||||
runAgent: fakeRunAgent,
|
||||
async createResources() {
|
||||
resources++;
|
||||
return fakeResources;
|
||||
},
|
||||
resolveProjectTrust(options) {
|
||||
trustedChildren.push(options.childCwd);
|
||||
return false;
|
||||
},
|
||||
});
|
||||
const makeRequest = (runId: string) => ({
|
||||
runId,
|
||||
role: "discover" as const,
|
||||
request: "Inspect the repository",
|
||||
profile,
|
||||
facts: {
|
||||
stage: "discover" as const,
|
||||
taskKind: "feature" as const,
|
||||
requestTerms: ["inspect"],
|
||||
candidatePaths: [],
|
||||
languages: [],
|
||||
tags: [],
|
||||
riskTags: [],
|
||||
},
|
||||
decisions: [],
|
||||
cwd: path.join(directory, "worktree"),
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
const first = await executor.run(makeRequest("run-a"));
|
||||
const second = await executor.run(makeRequest("run-b"));
|
||||
await executor.run({
|
||||
...makeRequest("run-c"),
|
||||
allowedReadTools: ["grep"],
|
||||
});
|
||||
assert.equal(first.kind, "workflow-assignment-result");
|
||||
assert.equal(first.metadata.childSessionId, "child-1");
|
||||
assert.equal(second.metadata.childSessionId, "child-2");
|
||||
assert.equal(resources, 3);
|
||||
assert.equal(sessions, 3);
|
||||
assert.deepEqual(trustedChildren, [
|
||||
path.join(directory, "worktree"),
|
||||
path.join(directory, "worktree"),
|
||||
path.join(directory, "worktree"),
|
||||
]);
|
||||
assert.ok(observedTools.every((tools) => tools.includes("structured_output")));
|
||||
assert.equal(observedTools[2]?.includes("read"), false);
|
||||
assert.equal(observedTools[2]?.includes("grep"), true);
|
||||
assert.deepEqual(observedCustomTools[2], ["grep"]);
|
||||
|
||||
const failedExecutor = new PiAssignmentExecutor({
|
||||
modelRegistry: models,
|
||||
parentCwd: directory,
|
||||
parentProjectTrusted: true,
|
||||
parentModel: inherited,
|
||||
parentThinkingLevel: "medium" as ThinkingLevel,
|
||||
concurrencyCeiling: 4,
|
||||
runConcurrency: 2,
|
||||
scheduler: new ProcessAssignmentScheduler(),
|
||||
async createResources() {
|
||||
return fakeResources;
|
||||
},
|
||||
async runAgent(options) {
|
||||
options.onSessionCreated?.({
|
||||
childSessionId: "child-failed",
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
output: "partial",
|
||||
error: "provider failed",
|
||||
aborted: false,
|
||||
usage: emptyUsage(),
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
childSessionId: "child-failed",
|
||||
contextWindow: inherited.contextWindow,
|
||||
transcript: [{ role: "assistant" as const, text: "partial" }],
|
||||
};
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
failedExecutor.run(makeRequest("run-failed")),
|
||||
(error: unknown) => {
|
||||
assert.ok(error instanceof Error);
|
||||
assert.equal(error.message, "provider failed");
|
||||
assert.equal(
|
||||
(error as Error & { assignmentMetadata?: { childSessionId?: string } })
|
||||
.assignmentMetadata?.childSessionId,
|
||||
"child-failed",
|
||||
);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("mutating children receive guarded file tools without process execution", async () => {
|
||||
await withTempDir(async (directory) => {
|
||||
await execFileAsync("git", ["init", "-q", directory]);
|
||||
await execFileAsync("git", ["-C", directory, "config", "user.email", "test@example.com"]);
|
||||
await execFileAsync("git", ["-C", directory, "config", "user.name", "Test User"]);
|
||||
await mkdir(path.join(directory, "src"), { recursive: true });
|
||||
await writeFile(path.join(directory, "src", "existing.ts"), "export {};\n", "utf8");
|
||||
await execFileAsync("git", ["-C", directory, "add", "."]);
|
||||
await execFileAsync("git", ["-C", directory, "commit", "-qm", "initial"]);
|
||||
const profiles = (
|
||||
await loadAgentProfiles({ cwd: directory, projectTrusted: false })
|
||||
).profiles;
|
||||
const profile = profiles.find((candidate) => candidate.roles.includes("implement"));
|
||||
assert.ok(profile);
|
||||
const standardWorker = {
|
||||
...profile,
|
||||
name: "worker",
|
||||
source: "standard-package" as const,
|
||||
tools: [...(profile.tools ?? []), "bash", "subagent"],
|
||||
};
|
||||
const inherited = model("provider-a", "parent");
|
||||
const executor = new PiAssignmentExecutor({
|
||||
modelRegistry: registry([inherited]),
|
||||
parentCwd: directory,
|
||||
parentProjectTrusted: false,
|
||||
parentModel: inherited,
|
||||
parentThinkingLevel: "medium" as ThinkingLevel,
|
||||
concurrencyCeiling: 2,
|
||||
runConcurrency: 1,
|
||||
scheduler: new ProcessAssignmentScheduler(),
|
||||
async createResources() {
|
||||
return {
|
||||
loader: {} as DefaultResourceLoader,
|
||||
settingsManager: {} as SettingsManager,
|
||||
};
|
||||
},
|
||||
async runAgent(options) {
|
||||
assert.deepEqual(options.tools, [
|
||||
"read",
|
||||
"grep",
|
||||
"find",
|
||||
"ls",
|
||||
"edit",
|
||||
"write",
|
||||
"structured_output",
|
||||
]);
|
||||
assert.equal(options.tools?.includes("bash"), false);
|
||||
const writeTool = options.customTools?.find((tool) => tool.name === "write");
|
||||
assert.ok(writeTool);
|
||||
await writeTool.execute(
|
||||
"write-1",
|
||||
{ path: "src/generated.ts", content: "export const generated = true;\n" },
|
||||
options.signal ?? new AbortController().signal,
|
||||
undefined,
|
||||
{} as ExtensionContext,
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
output: "",
|
||||
structured: {
|
||||
plan: "# Plan",
|
||||
uiImpact: "none",
|
||||
uiImpactEvidence: ["No user-facing change"],
|
||||
affectedUiSurfaces: [],
|
||||
assignments: [],
|
||||
},
|
||||
aborted: false,
|
||||
usage: emptyUsage(),
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
childSessionId: "mutating-child",
|
||||
contextWindow: inherited.contextWindow,
|
||||
transcript: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
const result = await executor.run({
|
||||
runId: "run-mutation",
|
||||
role: "design",
|
||||
request: "Generate an owned file",
|
||||
profile: standardWorker,
|
||||
facts: {
|
||||
stage: "design",
|
||||
taskKind: "feature",
|
||||
requestTerms: ["generate"],
|
||||
candidatePaths: ["src/generated.ts"],
|
||||
languages: ["typescript"],
|
||||
tags: [],
|
||||
riskTags: [],
|
||||
},
|
||||
decisions: [],
|
||||
cwd: directory,
|
||||
signal: new AbortController().signal,
|
||||
mutates: true,
|
||||
ownedPaths: ["src"],
|
||||
});
|
||||
assert.deepEqual(result.metadata.gitAfter?.changedPaths, ["src/generated.ts"]);
|
||||
|
||||
const prototypePath = path.join(directory, "prototypes", "candidate-1");
|
||||
await mkdir(prototypePath, { recursive: true });
|
||||
await writeFile(path.join(prototypePath, "route.ts"), "prototype\n", "utf8");
|
||||
const cleanupExecutor = new PiAssignmentExecutor({
|
||||
modelRegistry: registry([inherited]),
|
||||
parentCwd: directory,
|
||||
parentProjectTrusted: false,
|
||||
parentModel: inherited,
|
||||
parentThinkingLevel: "medium" as ThinkingLevel,
|
||||
concurrencyCeiling: 2,
|
||||
runConcurrency: 1,
|
||||
scheduler: new ProcessAssignmentScheduler(),
|
||||
async createResources() {
|
||||
return {
|
||||
loader: {} as DefaultResourceLoader,
|
||||
settingsManager: {} as SettingsManager,
|
||||
};
|
||||
},
|
||||
async runAgent(options) {
|
||||
assert.equal(options.tools?.includes("workflow_remove"), true);
|
||||
const removeTool = options.customTools?.find(
|
||||
(tool) => tool.name === "workflow_remove",
|
||||
);
|
||||
assert.ok(removeTool);
|
||||
await removeTool.execute(
|
||||
"remove-prototype",
|
||||
{ path: "prototypes/candidate-1" },
|
||||
options.signal ?? new AbortController().signal,
|
||||
undefined,
|
||||
{} as ExtensionContext,
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
output: "",
|
||||
structured: {
|
||||
summary: "Removed prototype-only route",
|
||||
changedPaths: ["prototypes/candidate-1"],
|
||||
focusedTests: [],
|
||||
},
|
||||
aborted: false,
|
||||
usage: emptyUsage(),
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
childSessionId: "cleanup-child",
|
||||
contextWindow: inherited.contextWindow,
|
||||
transcript: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
await cleanupExecutor.run({
|
||||
runId: "run-cleanup",
|
||||
role: "remediate",
|
||||
request: "Remove prototype-only routes",
|
||||
profile,
|
||||
facts: {
|
||||
stage: "implement",
|
||||
taskKind: "feature",
|
||||
requestTerms: ["cleanup"],
|
||||
candidatePaths: ["prototypes/candidate-1"],
|
||||
languages: ["typescript"],
|
||||
tags: ["frontend"],
|
||||
riskTags: [],
|
||||
},
|
||||
decisions: [],
|
||||
cwd: directory,
|
||||
signal: new AbortController().signal,
|
||||
mutates: true,
|
||||
ownedPaths: ["prototypes/candidate-1"],
|
||||
});
|
||||
await assert.rejects(access(prototypePath));
|
||||
|
||||
const reviewProfile = profiles.find((candidate) => candidate.roles.includes("review"));
|
||||
assert.ok(reviewProfile);
|
||||
const reviewRequest = {
|
||||
runId: "run-review",
|
||||
role: "review" as const,
|
||||
request: "Review the feature",
|
||||
profile: reviewProfile,
|
||||
facts: {
|
||||
stage: "review" as const,
|
||||
taskKind: "feature" as const,
|
||||
requestTerms: ["review"],
|
||||
candidatePaths: ["src/generated.ts"],
|
||||
languages: ["typescript"],
|
||||
tags: [],
|
||||
riskTags: [],
|
||||
},
|
||||
decisions: [],
|
||||
cwd: directory,
|
||||
signal: new AbortController().signal,
|
||||
captureGitState: true,
|
||||
approvedPlan: "# Approved plan",
|
||||
verificationEvidence: {
|
||||
commands: [],
|
||||
focusedBehavioralTests: true,
|
||||
verifierSucceeded: true,
|
||||
},
|
||||
reviewDiff: "a😀b",
|
||||
};
|
||||
const reviewExecutor = (
|
||||
readCompleteDiff: boolean,
|
||||
readCompletePlan = readCompleteDiff,
|
||||
) =>
|
||||
new PiAssignmentExecutor({
|
||||
modelRegistry: registry([inherited]),
|
||||
parentCwd: directory,
|
||||
parentProjectTrusted: false,
|
||||
parentModel: inherited,
|
||||
parentThinkingLevel: "medium" as ThinkingLevel,
|
||||
concurrencyCeiling: 2,
|
||||
runConcurrency: 1,
|
||||
scheduler: new ProcessAssignmentScheduler(),
|
||||
async createResources() {
|
||||
return {
|
||||
loader: {} as DefaultResourceLoader,
|
||||
settingsManager: {} as SettingsManager,
|
||||
};
|
||||
},
|
||||
async runAgent(options) {
|
||||
assert.equal(options.tools?.includes("workflow_diff"), true);
|
||||
assert.equal(options.tools?.includes("workflow_plan"), true);
|
||||
assert.equal(
|
||||
options.customTools?.some((tool) => tool.name === "workflow_diff"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
options.customTools?.some((tool) => tool.name === "workflow_plan"),
|
||||
true,
|
||||
);
|
||||
const diffTool = options.customTools?.find(
|
||||
(tool) => tool.name === "workflow_diff",
|
||||
);
|
||||
assert.ok(diffTool);
|
||||
const executeTool = diffTool.execute as unknown as (
|
||||
id: string,
|
||||
params: { offset: number; limit: number },
|
||||
) => Promise<{ content: Array<{ type: string; text: string }> }>;
|
||||
const unicodePage = await executeTool("unicode-page", {
|
||||
offset: 1,
|
||||
limit: 1,
|
||||
});
|
||||
assert.equal(unicodePage.content[0]?.text, "😀");
|
||||
return {
|
||||
ok: true,
|
||||
output: "",
|
||||
structured: {
|
||||
specification: { verdict: "approved", findings: [] },
|
||||
standards: { verdict: "approved", findings: [] },
|
||||
},
|
||||
aborted: false,
|
||||
usage: emptyUsage(),
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
childSessionId: "review-child",
|
||||
contextWindow: inherited.contextWindow,
|
||||
transcript: [
|
||||
...(readCompletePlan
|
||||
? [
|
||||
{
|
||||
role: "tool" as const,
|
||||
name: "workflow_plan",
|
||||
toolCallId: "plan-call",
|
||||
text: '{"offset":0,"limit":49152}',
|
||||
},
|
||||
{
|
||||
role: "toolResult" as const,
|
||||
name: "workflow_plan",
|
||||
toolCallId: "plan-call",
|
||||
text: "plan page",
|
||||
isError: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(readCompleteDiff
|
||||
? [
|
||||
{
|
||||
role: "tool" as const,
|
||||
name: "workflow_diff",
|
||||
toolCallId: "diff-call",
|
||||
text: '{"offset":0,"limit":49152}',
|
||||
},
|
||||
{
|
||||
role: "toolResult" as const,
|
||||
name: "workflow_diff",
|
||||
toolCallId: "diff-call",
|
||||
text: "diff page",
|
||||
isError: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
await reviewExecutor(true).run(reviewRequest);
|
||||
await assert.rejects(
|
||||
reviewExecutor(false).run(reviewRequest),
|
||||
/did not read the complete workflow diff/,
|
||||
);
|
||||
await assert.rejects(
|
||||
reviewExecutor(true, false).run(reviewRequest),
|
||||
/did not read the complete approved workflow plan/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("production executor drives Discover and Design through Plan Approval", async () => {
|
||||
await withTempDir(async (directory) => {
|
||||
const profiles = (
|
||||
await loadAgentProfiles({ cwd: directory, projectTrusted: false })
|
||||
).profiles;
|
||||
const profileName = (role: "implement" | "verify" | "review") => {
|
||||
const profile = profiles.find((candidate) => candidate.roles.includes(role));
|
||||
assert.ok(profile);
|
||||
return profile.name;
|
||||
};
|
||||
const inherited = model("provider-a", "parent");
|
||||
const models = registry([inherited]);
|
||||
let sessions = 0;
|
||||
const fakeRunAgent = async (options: RunAgentOptions) => {
|
||||
sessions++;
|
||||
options.onSessionCreated?.({
|
||||
childSessionId: `pipeline-child-${sessions}`,
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
});
|
||||
const structured = options.prompt.includes("Assignment role: discover")
|
||||
? {
|
||||
questions: [],
|
||||
candidatePaths: ["src/index.ts"],
|
||||
languages: ["typescript"],
|
||||
tags: ["backend"],
|
||||
riskTags: [],
|
||||
}
|
||||
: {
|
||||
plan: "# Plan\n\nImplement and verify the feature.",
|
||||
uiImpact: "none",
|
||||
uiImpactEvidence: ["No user-facing change"],
|
||||
affectedUiSurfaces: [],
|
||||
acceptanceCriteria: ["Feature behavior passes focused tests"],
|
||||
verificationCommands: [
|
||||
{
|
||||
executable: "npm",
|
||||
args: ["test"],
|
||||
ownedPaths: [],
|
||||
kind: "test",
|
||||
},
|
||||
],
|
||||
assignments: [
|
||||
{
|
||||
role: "implement",
|
||||
profileName: profileName("implement"),
|
||||
rationale: "Implement the approved scope",
|
||||
mutates: true,
|
||||
ownedPaths: ["src"],
|
||||
},
|
||||
{
|
||||
role: "verify",
|
||||
profileName: profileName("verify"),
|
||||
rationale: "Run focused verification",
|
||||
mutates: false,
|
||||
ownedPaths: [],
|
||||
},
|
||||
{
|
||||
role: "review",
|
||||
profileName: profileName("review"),
|
||||
rationale: "Review independently",
|
||||
mutates: false,
|
||||
ownedPaths: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
return {
|
||||
ok: true,
|
||||
output: "",
|
||||
structured,
|
||||
aborted: false,
|
||||
usage: emptyUsage(),
|
||||
provider: inherited.provider,
|
||||
model: inherited.id,
|
||||
thinkingLevel: "medium" as ThinkingLevel,
|
||||
childSessionId: `pipeline-child-${sessions}`,
|
||||
contextWindow: inherited.contextWindow,
|
||||
transcript: [{ role: "assistant" as const, text: "done" }],
|
||||
};
|
||||
};
|
||||
const executor = new PiAssignmentExecutor({
|
||||
modelRegistry: models,
|
||||
parentCwd: directory,
|
||||
parentProjectTrusted: false,
|
||||
parentModel: inherited,
|
||||
parentThinkingLevel: "medium" as ThinkingLevel,
|
||||
concurrencyCeiling: 4,
|
||||
runConcurrency: 2,
|
||||
scheduler: new ProcessAssignmentScheduler(),
|
||||
runAgent: fakeRunAgent,
|
||||
async createResources() {
|
||||
return {
|
||||
loader: {} as DefaultResourceLoader,
|
||||
settingsManager: {} as SettingsManager,
|
||||
};
|
||||
},
|
||||
});
|
||||
const workspace: WorkspaceManager = {
|
||||
async prepare(options) {
|
||||
return {
|
||||
sourceRoot: options.sourceRoot,
|
||||
worktreeRoot: path.join(options.sourceRoot, ".worktrees"),
|
||||
worktreePath: path.join(options.sourceRoot, ".worktrees", options.runId),
|
||||
branch: `feature/${options.featureName}`,
|
||||
baseHead: "abc123",
|
||||
branchExists: false,
|
||||
sourceChanges: [],
|
||||
sourceChangesOmitted: 0,
|
||||
dirtySourceAllowed: false,
|
||||
};
|
||||
},
|
||||
async create(plan): Promise<WorkflowRun["workspace"]> {
|
||||
return {
|
||||
state: "ready",
|
||||
sourceRoot: plan.sourceRoot,
|
||||
root: plan.worktreePath,
|
||||
branch: plan.branch,
|
||||
baseHead: plan.baseHead,
|
||||
};
|
||||
},
|
||||
async reconcile(current) {
|
||||
return current;
|
||||
},
|
||||
};
|
||||
let id = 0;
|
||||
const engine = new WorkflowEngine({
|
||||
store: new WorkflowStore(path.join(directory, "state")),
|
||||
workspace,
|
||||
executor,
|
||||
sourceRoot: directory,
|
||||
parentSessionId: "session-1",
|
||||
effectiveConfig: structuredClone(DEFAULT_WORKFLOW_CONFIG),
|
||||
profiles,
|
||||
now: () => 10_000,
|
||||
createId: () => `pipeline-${++id}`,
|
||||
processId: 100,
|
||||
isProcessAlive: () => true,
|
||||
});
|
||||
const result = await engine.execute({
|
||||
action: "start",
|
||||
request: "Build the feature",
|
||||
featureName: "build-feature",
|
||||
idempotencyKey: "pipeline-start",
|
||||
});
|
||||
assert.equal(result.openGate?.kind, "plan-approval");
|
||||
assert.equal(sessions, 2);
|
||||
assert.deepEqual(
|
||||
result.assignments
|
||||
.filter((assignment) => assignment.childSessionId)
|
||||
.map((assignment) => assignment.childSessionId),
|
||||
["pipeline-child-1", "pipeline-child-2"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.assignments
|
||||
.filter((assignment) => assignment.status === "queued")
|
||||
.map((assignment) => assignment.role),
|
||||
["implement", "verify", "review"],
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user