feat: multi-work agent
CI / verify (pull_request) Failing after 9s

This commit is contained in:
2026-09-17 21:09:19 +02:00
parent ba9fddad9d
commit 5cf0beb74b
20 changed files with 2907 additions and 299 deletions
+84 -16
View File
@@ -10,8 +10,9 @@ import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client";
import type { SessionContext } from "@opencode/plugin/promise/session";
import { z } from "zod";
import { gptAgents } from "../src/config.js";
import plugin from "../src/index.js";
import { basePlugin as plugin } from "../src/index.js";
import { playwrightMcpCommand } from "../src/playwright.js";
import { bindingKey } from "../src/multi-work.js";
const providerID = Provider.ID.make("test");
function model(id: string): ModelInfo {
@@ -63,6 +64,7 @@ async function host(
let before!: (event: { tool: string; agent: string; input: unknown }) => Promise<void> | void;
let parent = "build";
let child = "fast-lo";
let childDirectory = directory;
let defaultAgent = "build";
const registration = { async dispose() {} };
let transform!: (editor: AgentEditor) => void;
@@ -218,8 +220,13 @@ async function host(
},
async get({ sessionID }: { sessionID: string }) {
return sessionID === "child"
? { agent: child, parentID: "parent" }
: { agent: parent };
? {
agent: child,
projectID: "project-fixture",
parentID: "parent",
location: { directory: childDirectory },
}
: { agent: parent, projectID: "project-fixture", location: { directory } };
},
},
tool: {
@@ -299,6 +306,19 @@ async function host(
child = target;
for (const hook of sessionHooks.prompt ?? []) await hook({ sessionID: "child" });
},
bindCoordinate(ready: boolean, moved = true) {
childDirectory = moved ? `${directory}/worktree` : directory;
storage.set(bindingKey("child"), {
owner: "parent",
child: "child",
projectID: "project-fixture",
ready,
root: { directory, workspaceID: "w1", repo: `${directory}/.git` },
directory: `${directory}/worktree`,
workspaceID: "w2",
branch: "multi-work/test",
});
},
async tools(caller: string) {
const event: ModelContext = {
agent: Agent.ID.make(caller),
@@ -318,28 +338,52 @@ async function host(
};
}
test("missing required models disables registration; only GPT mode degrades without Astra", async () => {
test("orchestration inherits deep-lo and falls back to good-lo with available worker sets", async () => {
const full = await host();
expect(full.agents.has("deep-hi")).toBe(true);
expect(String(full.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
expect(String(full.agents.get("coordinate")?.model?.variant)).toBe("low");
const reduced = await host(allModels.slice(0, 2));
expect(reduced.agents.has("deep-hi")).toBe(false);
expect(String(reduced.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
expect(String(reduced.agents.get("coordinate")?.model?.variant)).toBe("low");
expect(reduced.agents.get("coordinate")?.system).toContain("Deep agents are unavailable");
expect((await host([])).agents.has("coordinate")).toBe(false);
expect((await host(allModels.slice(1))).agents.has("coordinate")).toBe(false);
const custom = await host([...allModels, model("local")], {
const customAgents = {
...gptAgents,
"good-lo": { model: "custom-good", effort: "high" as const },
"deep-lo": { model: "custom-deep", effort: "medium" as const },
};
const custom = await host(
[...allModels, reasoningModel("custom-good"), reasoningModel("custom-deep")],
{
mode: "custom",
provider: "test",
agents: customAgents,
},
);
expect(custom.agents.get("coordinate")?.model).toMatchObject({
id: "custom-deep",
variant: "medium",
});
const customFallback = await host([...allModels, reasoningModel("custom-good")], {
mode: "custom",
provider: "test",
agents: { ...gptAgents, coordinate: { model: "local", effort: "low" } },
agents: customAgents,
});
expect(String(custom.agents.get("coordinate")?.model?.id)).toBe("local");
expect(customFallback.agents.get("coordinate")?.model).toMatchObject({
id: "custom-good",
variant: "high",
});
expect(customFallback.agents.has("deep-lo")).toBe(false);
expect(
(
await host(allModels.slice(0, 2), {
await host(allModels, {
mode: "custom",
provider: "test",
agents: gptAgents,
agents: customAgents,
})
).agents.has("coordinate"),
).toBe(false);
@@ -619,6 +663,27 @@ test("manual calls and command-created child sessions cannot bypass delegation r
expect((await result.tools("build")).subagent?.description).toBe("Native subagents");
});
test("coordinate prompt admission requires both owned provenance and a completed move; multi-work cannot be a child", async () => {
const result = await host();
await expect(result.command("multi-work", "coordinate")).rejects.toThrow(
"ready worktree binding",
);
result.bindCoordinate(false);
await expect(result.command("multi-work", "coordinate")).rejects.toThrow(
"ready worktree binding",
);
result.bindCoordinate(true, false);
await expect(result.command("multi-work", "coordinate")).rejects.toThrow(
"ready worktree binding",
);
result.bindCoordinate(true);
await result.command("multi-work", "coordinate");
await expect(result.command("build", "coordinate")).rejects.toThrow("ready worktree binding");
await expect(result.command("multi-work", "fast-lo")).rejects.toThrow("local fast-lo");
for (const invoke of [result.delegate, result.command])
await expect(invoke("build", "multi-work")).rejects.toThrow("primary only");
});
test("reasoning settings and Luna priority use native advertised or official support", async () => {
const luna = {
...model("gpt-5.6-luna"),
@@ -650,12 +715,12 @@ test("reasoning settings and Luna priority use native advertised or official sup
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
});
test("coordinator defaults expose their effort variant without relying on it for requests", async () => {
test("coordinator uses inherited effort and ignores the legacy custom assignment", async () => {
for (const options of [{ provider: "test" }, { mode: "gpt", provider: "test" }] as const) {
const result = await host(allModels, options);
expect(result.agents.get("coordinate")?.model).toMatchObject({
id: "gpt-6-astra",
variant: "medium",
variant: "low",
});
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
}
@@ -663,7 +728,7 @@ test("coordinator defaults expose their effort variant without relying on it for
const fallback = await host(allModels.slice(0, 2));
expect(fallback.agents.get("coordinate")?.model).toMatchObject({
id: "gpt-5.6-sol",
variant: "medium",
variant: "low",
});
expect(fallback.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
@@ -672,8 +737,11 @@ test("coordinator defaults expose their effort variant without relying on it for
provider: "test",
agents: { ...gptAgents, coordinate: { model: "local", effort: "xhigh" } },
});
expect(custom.agents.get("coordinate")?.model?.variant).toBeUndefined();
expect(custom.agents.get("coordinate")?.request.settings.reasoningEffort).toBe("xhigh");
expect(custom.agents.get("coordinate")?.model).toMatchObject({
id: "gpt-6-astra",
variant: "low",
});
expect(custom.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
});
test("the current host model API filters discovery and reloads changed availability", async () => {
@@ -685,12 +753,12 @@ test("the current host model API filters discovery and reloads changed availabil
const result = await host([]);
await result.refresh(allModels);
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("medium");
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("low");
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
await result.refresh(allModels.slice(0, 2));
expect(result.agents.has("deep-lo")).toBe(false);
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("medium");
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("low");
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
await result.refresh([]);
expect(result.agents.has("coordinate")).toBe(false);