+62
-62
@@ -1,4 +1,5 @@
|
||||
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
|
||||
import type { DeepMutable } from "@opencode/plugin/promise/types";
|
||||
import type { ModelInfo } from "@opencode/client";
|
||||
import { z } from "zod";
|
||||
import { settings, workerNames } from "./config.js";
|
||||
@@ -7,18 +8,38 @@ import { discover, modelKey } from "./provider.js";
|
||||
import { descriptions, routingGuide } from "./routing.js";
|
||||
import { Skill } from "@opencode/schema/skill";
|
||||
import { AbsolutePath } from "@opencode/schema/schema";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import antiOverengineering from "../skills/anti-overengineering.md" with { type: "text" };
|
||||
import taskPlanning from "../skills/task-planning.md" with { type: "text" };
|
||||
import { AgentsRpc } from "./agents-rpc.js";
|
||||
import { matches } from "./wildcard.js";
|
||||
|
||||
// Permission actions use wildcards, not filesystem globs.
|
||||
function matches(value: string, pattern: string) {
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
||||
.replace(/\*/g, ".*")
|
||||
.replace(/\?/g, ".");
|
||||
return new RegExp(`^${escaped}$`, "s").test(value);
|
||||
// Assignments use "" as unset.
|
||||
function assigned(value: string | undefined): value is string {
|
||||
return value !== undefined && value !== "";
|
||||
}
|
||||
|
||||
/** Points the agent at the available model, using the variant or a reasoning-effort fallback. */
|
||||
function useModel(
|
||||
agent: DeepMutable<Agent.Info>,
|
||||
assignment: ReturnType<typeof model>,
|
||||
available: ModelInfo,
|
||||
) {
|
||||
const hasVariant =
|
||||
assignment.variant !== undefined &&
|
||||
available.variants.some(({ id }) => id === assignment.variant);
|
||||
agent.model = {
|
||||
providerID: Provider.ID.make(available.providerID),
|
||||
id: Model.ID.make(available.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.variant);
|
||||
agent.request = {
|
||||
settings:
|
||||
hasVariant || assignment.variant === undefined
|
||||
? {}
|
||||
: { reasoningEffort: assignment.variant },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
}
|
||||
|
||||
const reading = [
|
||||
@@ -40,12 +61,12 @@ export default Plugin.define({
|
||||
id: "stanp-opencode.agents",
|
||||
async setup(context) {
|
||||
const { agents } = settings.parse(context.options);
|
||||
if (!workerNames.some((name) => agents?.[name])) return;
|
||||
if (!workerNames.some((name) => assigned(agents?.[name]))) return;
|
||||
const assignments: Partial<Record<(typeof workerNames)[number], ReturnType<typeof model>>> =
|
||||
{};
|
||||
for (const name of workerNames) {
|
||||
const reference = agents?.[name];
|
||||
if (reference) assignments[name] = model(reference, `agents ${name}`);
|
||||
if (assigned(reference)) assignments[name] = model(reference, `agents ${name}`);
|
||||
else if (!name.startsWith("deep-")) throw new Error(`agents ${name} is required`);
|
||||
}
|
||||
let models = new Map<string, ModelInfo>();
|
||||
@@ -106,13 +127,14 @@ export default Plugin.define({
|
||||
["task-planning", taskPlanning],
|
||||
] as const) {
|
||||
if (editor.get(name)) continue;
|
||||
const description = content.match(/^description: "?(.+?)"?$/m)?.[1];
|
||||
if (!description) throw new Error(`${name} skill has no description`);
|
||||
const description = content.match(/^description: "?(.+?)"?$/mu)?.[1];
|
||||
if (description === undefined || description === "")
|
||||
throw new Error(`${name} skill has no description`);
|
||||
editor.add({
|
||||
id: Skill.ID.make(name),
|
||||
name: Skill.Name.make(name),
|
||||
description,
|
||||
path: AbsolutePath.make(fileURLToPath(import.meta.url)),
|
||||
path: AbsolutePath.make(import.meta.filename),
|
||||
content,
|
||||
});
|
||||
}
|
||||
@@ -123,22 +145,20 @@ export default Plugin.define({
|
||||
.list()
|
||||
.map((tool) => tool.id)
|
||||
.filter((id) => id.startsWith("codegraph_"))
|
||||
.sort();
|
||||
.toSorted();
|
||||
if (JSON.stringify(next) === JSON.stringify(codegraphTools)) return;
|
||||
codegraphTools = next;
|
||||
if (agentTransformReady)
|
||||
queueMicrotask(() => {
|
||||
void context.agent
|
||||
.reload()
|
||||
.catch((error) =>
|
||||
console.error("stanp-opencode agent reload failed", error),
|
||||
);
|
||||
void context.agent.reload().catch((error) => {
|
||||
console.error("stanp-opencode agent reload failed", error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const availableFor = (assignment: { providerID: string; id: string }) =>
|
||||
models.get(modelKey(assignment.providerID, assignment.id));
|
||||
await context.agent.transform((editor) => {
|
||||
const availableFor = (assignment: { providerID: string; id: string }) =>
|
||||
models.get(modelKey(assignment.providerID, assignment.id));
|
||||
const deepLo = assignments["deep-lo"];
|
||||
const deep =
|
||||
deepLo !== undefined &&
|
||||
@@ -154,59 +174,37 @@ export default Plugin.define({
|
||||
active.clear();
|
||||
for (const name of workers) active.add(name);
|
||||
if (workers.length === 0 || coordinate === undefined) return;
|
||||
for (const name of workers) {
|
||||
const assignment = assignments[name]!;
|
||||
const available = availableFor(assignment)!;
|
||||
const configureWorker = (
|
||||
name: (typeof workerNames)[number],
|
||||
assignment: ReturnType<typeof model>,
|
||||
available: ModelInfo,
|
||||
) => {
|
||||
editor.update(name, (agent) => {
|
||||
const hasVariant =
|
||||
assignment.variant !== undefined &&
|
||||
available.variants.some(({ id }) => id === assignment.variant);
|
||||
agent.name = Agent.Name.make(name);
|
||||
agent.mode = "subagent";
|
||||
agent.hidden = false;
|
||||
agent.description = descriptions[name];
|
||||
agent.model = {
|
||||
providerID: Provider.ID.make(available.providerID),
|
||||
id: Model.ID.make(available.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.variant!);
|
||||
agent.request = {
|
||||
settings:
|
||||
hasVariant || assignment.variant === undefined
|
||||
? {}
|
||||
: { reasoningEffort: assignment.variant },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
useModel(agent, assignment, available);
|
||||
agent.permissions = [
|
||||
...structuredClone(inherited),
|
||||
{ action: "subagent", resource: "*", effect: "deny" },
|
||||
];
|
||||
});
|
||||
};
|
||||
for (const name of workers) {
|
||||
const assignment = assignments[name];
|
||||
const available = assignment && availableFor(assignment);
|
||||
if (assignment && available) configureWorker(name, assignment, available);
|
||||
}
|
||||
const available = availableFor(coordinate);
|
||||
if (!available) return;
|
||||
editor.update("coordinate", (agent) => {
|
||||
const available = availableFor(coordinate)!;
|
||||
const hasVariant =
|
||||
coordinate.variant !== undefined &&
|
||||
available.variants.some(({ id }) => id === coordinate.variant);
|
||||
agent.name = Agent.Name.make("coordinate");
|
||||
agent.mode = "primary";
|
||||
agent.hidden = false;
|
||||
agent.description =
|
||||
"Read, plan, and coordinate specialist workers; never implement directly.";
|
||||
agent.model = {
|
||||
providerID: Provider.ID.make(available.providerID),
|
||||
id: Model.ID.make(available.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.variant!);
|
||||
agent.request = {
|
||||
settings:
|
||||
hasVariant || coordinate.variant === undefined
|
||||
? {}
|
||||
: { reasoningEffort: coordinate.variant },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
useModel(agent, coordinate, available);
|
||||
agent.system = routingGuide(deep);
|
||||
agent.permissions = [
|
||||
{ action: "*", resource: "*", effect: "deny" },
|
||||
@@ -242,18 +240,20 @@ export default Plugin.define({
|
||||
}
|
||||
await context.session.hook("context", (event) => {
|
||||
if (workerNames.some((name) => name === event.agent)) {
|
||||
delete event.tools.subagent;
|
||||
delete event.tools["subagent"];
|
||||
return;
|
||||
}
|
||||
const tool = event.tools.subagent;
|
||||
const tool = event.tools["subagent"];
|
||||
if (event.agent === "coordinate" && workers.length === 0) {
|
||||
delete event.tools.subagent;
|
||||
delete event.tools["subagent"];
|
||||
return;
|
||||
}
|
||||
if (event.agent !== "coordinate" || !tool) return;
|
||||
tool.description = `Delegate a self-contained assignment to one of these workers:\n${workers.map((name) => `- ${name}: ${descriptions[name]}`).join("\n")}`;
|
||||
// The executable definition remains native; constrain only this request's schema.
|
||||
const properties = z.record(z.string(), z.unknown()).parse(tool.input.properties ?? {});
|
||||
const properties = z
|
||||
.record(z.string(), z.unknown())
|
||||
.parse(tool.input["properties"] ?? {});
|
||||
tool.input = {
|
||||
...tool.input,
|
||||
properties: { ...properties, agent: { type: "string", enum: workers } },
|
||||
@@ -273,7 +273,7 @@ export default Plugin.define({
|
||||
// Commands can create child sessions without going through the subagent tool.
|
||||
await context.session.hook("prompt", async (event) => {
|
||||
const child = await context.session.get({ sessionID: event.sessionID });
|
||||
if (!child.parentID) return;
|
||||
if (child.parentID === undefined || child.parentID === "") return;
|
||||
const parent = await context.session.get({ sessionID: child.parentID });
|
||||
if (child.agent === "coordinate" && parent.agent !== "multi-work")
|
||||
throw new Error("coordinate cannot run as a subagent without Multi-work");
|
||||
|
||||
Reference in New Issue
Block a user