fix: deep custom config should be optional #17
@@ -41,7 +41,7 @@ Both installers use `~/.config/opencode`, respect `XDG_CONFIG_HOME`, and accept
|
||||
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc).
|
||||
|
||||
For an installed plugin, use `./plugins/stanp-opencode` as the plugin package path. In GPT mode with the `openai` provider, set top-level `lunaPriority` to `false` to avoid requesting priority pricing. Other providers do not accept this option. Custom top-level and per-agent `provider` and `lunaPriority` fields are invalid.
|
||||
Custom configurations require the six worker assignments and use `provider/model` model names.
|
||||
Custom configurations require the four `fast-*` and `good-*` assignments; `deep-lo` and `deep-hi` are optional. Assignments use `provider/model` model names and require both `model` and `effort` when supplied. If either deep assignment is omitted, both deep workers are disabled and orchestration uses `good-lo`.
|
||||
The legacy optional `coordinate` assignment is accepted for compatibility but ignored. Orchestration
|
||||
inherits `deep-lo`, or `good-lo` when the configured `deep-lo` model is unavailable.
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
`multi-work` is a **primary-only** agent for coordinating independent work in
|
||||
the current Herdr root checkout or new Herdr-managed Git worktrees. Like
|
||||
`coordinate`, it uses the configured `deep-lo` model and effort, falling back
|
||||
to `good-lo` when `deep-lo` is unavailable. No extra custom model assignment is
|
||||
to `good-lo` when `deep-lo` is unavailable or either custom deep assignment is omitted. No extra custom model assignment is
|
||||
required.
|
||||
|
||||
```text
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
// Required for multi-work -> coordinate -> workers. Apply in worktrees too.
|
||||
// Multi-work and coordinate use deep-lo, falling back to good-lo when unavailable.
|
||||
"experimental": { "subagent_depth": 2 },
|
||||
// All six worker assignments are required. Models use provider/model names.
|
||||
// An unavailable deep-lo omits deep workers and uses good-lo for orchestration.
|
||||
// Fast and good assignments are required; both deep assignments are optional.
|
||||
// Omitting either deep assignment disables both deep workers and uses good-lo.
|
||||
// Models use provider/model names; each assignment needs model and effort.
|
||||
"plugins": [
|
||||
{
|
||||
"package": "stanp-opencode",
|
||||
|
||||
+2
-2
@@ -57,8 +57,8 @@ export const settings = z.discriminatedUnion("mode", [
|
||||
"fast-hi": customAssignment,
|
||||
"good-lo": customAssignment,
|
||||
"good-hi": customAssignment,
|
||||
"deep-lo": customAssignment,
|
||||
"deep-hi": customAssignment,
|
||||
"deep-lo": customAssignment.optional(),
|
||||
"deep-hi": customAssignment.optional(),
|
||||
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
|
||||
coordinate: customAssignment.optional(),
|
||||
}),
|
||||
|
||||
+10
-7
@@ -204,17 +204,20 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
||||
model: assignment.model.slice(slash + 1),
|
||||
};
|
||||
};
|
||||
const availableFor = (
|
||||
assignment: (typeof assignments)[(typeof workerNames)[number]],
|
||||
) => {
|
||||
const availableFor = (assignment: { model: string }) => {
|
||||
const { provider, model } = coordinates(assignment);
|
||||
return models.get(modelKey(provider, model));
|
||||
};
|
||||
const deep = availableFor(assignments["deep-lo"]) !== undefined;
|
||||
const deepLo = assignments["deep-lo"];
|
||||
const deep =
|
||||
deepLo !== undefined &&
|
||||
assignments["deep-hi"] !== undefined &&
|
||||
availableFor(deepLo) !== undefined;
|
||||
const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-"));
|
||||
const coordinate = assignments[deep ? "deep-lo" : "good-lo"];
|
||||
const coordinate = deep ? deepLo : assignments["good-lo"];
|
||||
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
|
||||
(assignment) => availableFor(assignment) !== undefined,
|
||||
(assignment) =>
|
||||
assignment !== undefined && availableFor(assignment) !== undefined,
|
||||
)
|
||||
? candidates
|
||||
: [];
|
||||
@@ -222,7 +225,7 @@ const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permission
|
||||
for (const name of workers) active.add(name);
|
||||
if (workers.length === 0) return;
|
||||
for (const name of workers) {
|
||||
const assignment = assignments[name];
|
||||
const assignment = assignments[name]!;
|
||||
const available = availableFor(assignment)!;
|
||||
editor.update(name, (agent) => {
|
||||
const hasVariant = available.model.variants.some(
|
||||
|
||||
@@ -439,6 +439,26 @@ test("orchestration inherits deep-lo and falls back to good-lo with available wo
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("custom deep assignments are optional and a partial deep tier falls back to good-lo", async () => {
|
||||
for (const omitted of [["deep-lo", "deep-hi"], ["deep-lo"], ["deep-hi"]] as const) {
|
||||
const agents: Partial<ReturnType<typeof customAssignments>> = customAssignments();
|
||||
for (const name of omitted) delete agents[name];
|
||||
const result = await host(allModels, { mode: "custom", agents });
|
||||
expect(result.agents.get("coordinate")?.model).toMatchObject({
|
||||
id: "gpt-6-sol",
|
||||
variant: "low",
|
||||
});
|
||||
expect(result.agents.get("coordinate")?.system).toContain("Deep agents are unavailable");
|
||||
for (const name of ["fast-lo", "fast-hi", "good-lo", "good-hi"])
|
||||
expect(result.agents.has(name)).toBe(true);
|
||||
expect(result.agents.has("deep-lo")).toBe(false);
|
||||
expect(result.agents.has("deep-hi")).toBe(false);
|
||||
expect((await result.tools("coordinate")).subagent?.input.properties).toMatchObject({
|
||||
agent: { type: "string", enum: ["fast-lo", "fast-hi", "good-lo", "good-hi"] },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("registers the embedded anti-overengineering skill and makes it available to coordinate", async () => {
|
||||
const result = await host();
|
||||
const skill = result.skills.get("anti-overengineering")!;
|
||||
@@ -499,6 +519,23 @@ test("custom assignments resolve duplicate model IDs by provider and refresh pro
|
||||
|
||||
test("custom configuration requires qualified models and rejects obsolete fields", () => {
|
||||
expect(() => settings.parse({ mode: "custom", agents: customAssignments() })).not.toThrow();
|
||||
for (const omitted of [["deep-lo", "deep-hi"], ["deep-lo"], ["deep-hi"]] as const) {
|
||||
const agents: Partial<ReturnType<typeof customAssignments>> = customAssignments();
|
||||
for (const name of omitted) delete agents[name];
|
||||
expect(() => settings.parse({ mode: "custom", agents })).not.toThrow();
|
||||
}
|
||||
for (const name of ["fast-lo", "fast-hi", "good-lo", "good-hi"] as const) {
|
||||
const agents: Partial<ReturnType<typeof customAssignments>> = customAssignments();
|
||||
delete agents[name];
|
||||
expect(() => settings.parse({ mode: "custom", agents })).toThrow();
|
||||
}
|
||||
for (const name of ["deep-lo", "deep-hi"] as const)
|
||||
expect(() =>
|
||||
settings.parse({
|
||||
mode: "custom",
|
||||
agents: { ...customAssignments(), [name]: { model: "test/model" } },
|
||||
}),
|
||||
).toThrow();
|
||||
for (const model of ["bare", "/model", "provider/"])
|
||||
expect(() =>
|
||||
settings.parse({
|
||||
|
||||
Reference in New Issue
Block a user