+37
-53
@@ -1,7 +1,8 @@
|
||||
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
|
||||
import type { ModelInfo } from "@opencode/client";
|
||||
import { z } from "zod";
|
||||
import { gptAgents, settings, workerNames } from "./config.js";
|
||||
import { settings, workerNames } from "./config.js";
|
||||
import { model } from "./model.js";
|
||||
import { discover, modelKey } from "./provider.js";
|
||||
import { descriptions, routingGuide } from "./routing.js";
|
||||
import { Skill } from "@opencode/schema/skill";
|
||||
@@ -37,12 +38,16 @@ const delegation = z.object({ agent: z.string() });
|
||||
export default Plugin.define({
|
||||
id: "stanp-opencode.agents",
|
||||
async setup(context) {
|
||||
const selected = settings.parse({ mode: "gpt", ...context.options });
|
||||
const lunaPriority =
|
||||
selected.mode === "gpt" &&
|
||||
selected.provider === "openai" &&
|
||||
selected.lunaPriority === true;
|
||||
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
||||
const { agents } = settings.parse(context.options);
|
||||
if (!workerNames.some((name) => 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}`);
|
||||
else if (!name.startsWith("deep-")) throw new Error(`agents ${name} is required`);
|
||||
}
|
||||
let models = new Map<string, ModelInfo>();
|
||||
let inherited: Agent.Info["permissions"] = Agent.Info.default(
|
||||
Agent.ID.make("build"),
|
||||
).permissions;
|
||||
@@ -64,7 +69,7 @@ export default Plugin.define({
|
||||
continue;
|
||||
}
|
||||
if (event.type === "model.updated") {
|
||||
models = await discover(context, lunaPriority);
|
||||
models = await discover(context);
|
||||
await context.agent.reload();
|
||||
} else if (event.type === "agent.updated") {
|
||||
// The configured build transform may run after ours. Read the final
|
||||
@@ -84,25 +89,13 @@ export default Plugin.define({
|
||||
if (!controller.signal.aborted)
|
||||
console.error("stanp-opencode model update failed", error);
|
||||
});
|
||||
models = await discover(context, lunaPriority);
|
||||
models = await discover(context);
|
||||
const { data: initialAgents } = await context.agent.list();
|
||||
inherited = initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
|
||||
permissionKey = JSON.stringify(inherited);
|
||||
const registration = await context.rpc.register(AgentsRpc, {
|
||||
permissions: async () => inherited.map((rule) => ({ ...rule })),
|
||||
});
|
||||
const assignments =
|
||||
selected.mode === "gpt"
|
||||
? Object.fromEntries(
|
||||
workerNames.map((name) => [
|
||||
name,
|
||||
{
|
||||
...gptAgents[name],
|
||||
provider: selected.provider,
|
||||
},
|
||||
]),
|
||||
)
|
||||
: selected.agents;
|
||||
let workers: (typeof workerNames)[number][] = [];
|
||||
const active = new Set<string>();
|
||||
|
||||
@@ -136,19 +129,8 @@ export default Plugin.define({
|
||||
});
|
||||
|
||||
await context.agent.transform((editor) => {
|
||||
const coordinates = (assignment: { model: string }) => {
|
||||
if (selected.mode === "gpt")
|
||||
return { provider: selected.provider, model: assignment.model };
|
||||
const slash = assignment.model.indexOf("/");
|
||||
return {
|
||||
provider: assignment.model.slice(0, slash),
|
||||
model: assignment.model.slice(slash + 1),
|
||||
};
|
||||
};
|
||||
const availableFor = (assignment: { model: string }) => {
|
||||
const { provider, model } = coordinates(assignment);
|
||||
return models.get(modelKey(provider, model));
|
||||
};
|
||||
const availableFor = (assignment: { providerID: string; id: string }) =>
|
||||
models.get(modelKey(assignment.providerID, assignment.id));
|
||||
const deepLo = assignments["deep-lo"];
|
||||
const deep =
|
||||
deepLo !== undefined &&
|
||||
@@ -163,30 +145,31 @@ export default Plugin.define({
|
||||
: [];
|
||||
active.clear();
|
||||
for (const name of workers) active.add(name);
|
||||
if (workers.length === 0) return;
|
||||
if (workers.length === 0 || coordinate === undefined) return;
|
||||
for (const name of workers) {
|
||||
const assignment = assignments[name]!;
|
||||
const available = availableFor(assignment)!;
|
||||
editor.update(name, (agent) => {
|
||||
const hasVariant = available.model.variants.some(
|
||||
({ id }) => id === assignment.effort,
|
||||
);
|
||||
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.model.providerID),
|
||||
id: Model.ID.make(available.model.id),
|
||||
providerID: Provider.ID.make(available.providerID),
|
||||
id: Model.ID.make(available.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.effort);
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.variant!);
|
||||
agent.request = {
|
||||
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
|
||||
settings:
|
||||
hasVariant || assignment.variant === undefined
|
||||
? {}
|
||||
: { reasoningEffort: assignment.variant },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
if (lunaPriority && available.serviceTier)
|
||||
agent.request.settings.serviceTier = available.serviceTier;
|
||||
agent.permissions = [
|
||||
...structuredClone(inherited),
|
||||
{ action: "subagent", resource: "*", effect: "deny" },
|
||||
@@ -195,26 +178,27 @@ export default Plugin.define({
|
||||
}
|
||||
editor.update("coordinate", (agent) => {
|
||||
const available = availableFor(coordinate)!;
|
||||
const hasVariant = available.model.variants.some(
|
||||
({ id }) => id === coordinate.effort,
|
||||
);
|
||||
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.model.providerID),
|
||||
id: Model.ID.make(available.model.id),
|
||||
providerID: Provider.ID.make(available.providerID),
|
||||
id: Model.ID.make(available.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.variant!);
|
||||
agent.request = {
|
||||
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
|
||||
settings:
|
||||
hasVariant || coordinate.variant === undefined
|
||||
? {}
|
||||
: { reasoningEffort: coordinate.variant },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
if (lunaPriority && available.serviceTier)
|
||||
agent.request.settings.serviceTier = available.serviceTier;
|
||||
agent.system = routingGuide(deep);
|
||||
agent.permissions = [
|
||||
{ action: "*", resource: "*", effect: "deny" },
|
||||
|
||||
+6
-57
@@ -9,61 +9,10 @@ export const workerNames = [
|
||||
"deep-hi",
|
||||
] as const;
|
||||
|
||||
const gptAssignment = z.strictObject({
|
||||
model: z.string().trim().min(1),
|
||||
effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]),
|
||||
// Each assignment is provider/model[#variant]; an empty string means unset. Without any
|
||||
// assignment the plugin registers nothing.
|
||||
export const settings = z.object({
|
||||
agents: z
|
||||
.strictObject(Object.fromEntries(workerNames.map((name) => [name, z.string().optional()])))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const customAssignment = gptAssignment.extend({
|
||||
model: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((model) => {
|
||||
const slash = model.indexOf("/");
|
||||
return slash > 0 && slash < model.length - 1;
|
||||
}, "model must use the provider/model convention"),
|
||||
});
|
||||
|
||||
export const settings = z.discriminatedUnion("mode", [
|
||||
z
|
||||
.strictObject({
|
||||
mode: z.literal("gpt"),
|
||||
provider: z.string().trim().min(1).default("openai"),
|
||||
lunaPriority: z.boolean().optional(),
|
||||
})
|
||||
.transform((value, context) => {
|
||||
if (value.provider !== "openai" && value.lunaPriority !== undefined) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["lunaPriority"],
|
||||
message: "lunaPriority is only supported by the openai provider",
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return value.provider === "openai"
|
||||
? { ...value, lunaPriority: value.lunaPriority ?? true }
|
||||
: value;
|
||||
}),
|
||||
z.strictObject({
|
||||
mode: z.literal("custom"),
|
||||
agents: z.strictObject({
|
||||
"fast-lo": customAssignment,
|
||||
"fast-hi": customAssignment,
|
||||
"good-lo": customAssignment,
|
||||
"good-hi": customAssignment,
|
||||
"deep-lo": customAssignment.optional(),
|
||||
"deep-hi": customAssignment.optional(),
|
||||
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
|
||||
coordinate: customAssignment.optional(),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const gptAgents = {
|
||||
"fast-lo": { model: "gpt-6-luna", effort: "low" },
|
||||
"fast-hi": { model: "gpt-6-luna", effort: "high" },
|
||||
"good-lo": { model: "gpt-6.1-sol", effort: "low" },
|
||||
"good-hi": { model: "gpt-6.1-sol", effort: "high" },
|
||||
"deep-lo": { model: "gpt-6-astra", effort: "low" },
|
||||
"deep-hi": { model: "gpt-6-astra", effort: "high" },
|
||||
} satisfies Record<(typeof workerNames)[number], z.infer<typeof gptAssignment>>;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/** Parses `provider/model[#variant]`; `name` prefixes the error for the offending option. */
|
||||
export function model(reference: string, name: string) {
|
||||
const [path = "", variant] = reference.split("#");
|
||||
const slash = path.indexOf("/");
|
||||
if (slash <= 0) throw new Error(`${name} must be provider/model[#variant]: ${reference}`);
|
||||
const providerID = path.slice(0, slash);
|
||||
const id = path.slice(slash + 1);
|
||||
return variant ? { providerID, id, variant } : { providerID, id };
|
||||
}
|
||||
+15
-19
@@ -34,18 +34,23 @@ import {
|
||||
type Line,
|
||||
} from "./optchat-core.js";
|
||||
import { Memory } from "./optchat-memory.js";
|
||||
import { model } from "./model.js";
|
||||
|
||||
const settings = z.object({
|
||||
/** provider/model#variant used for compactions. */
|
||||
compactionModel: z.string().default("opencode/claude-haiku-5-5#xhigh"),
|
||||
/** provider/model[#variant] used for compactions; without it optchat does not apply. */
|
||||
compactionModel: z.string().optional(),
|
||||
/** Empty means the default directory. */
|
||||
dataDir: z
|
||||
.string()
|
||||
.default(
|
||||
join(
|
||||
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
|
||||
"opencode",
|
||||
"optchat",
|
||||
),
|
||||
.optional()
|
||||
.transform(
|
||||
(value) =>
|
||||
value ||
|
||||
join(
|
||||
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
|
||||
"opencode",
|
||||
"optchat",
|
||||
),
|
||||
),
|
||||
});
|
||||
// SAFETY: Anthropic lowering reads only `type` and `ttlSeconds` from a hint; 300 s is the 5-minute bucket.
|
||||
@@ -199,21 +204,12 @@ function turnStart(history: readonly Entry[]) {
|
||||
);
|
||||
}
|
||||
|
||||
function model(reference: string) {
|
||||
const [path = "", variant] = reference.split("#");
|
||||
const slash = path.indexOf("/");
|
||||
if (slash <= 0)
|
||||
throw new Error(`optchat compactionModel must be provider/model[#variant]: ${reference}`);
|
||||
const providerID = path.slice(0, slash);
|
||||
const id = path.slice(slash + 1);
|
||||
return variant ? { providerID, id, variant } : { providerID, id };
|
||||
}
|
||||
|
||||
export default Plugin.define({
|
||||
id: "stanp-opencode.optchat",
|
||||
setup: async (context) => {
|
||||
const options = settings.parse(context.options);
|
||||
const compactionModel = model(options.compactionModel);
|
||||
if (!options.compactionModel) return;
|
||||
const compactionModel = model(options.compactionModel, "optchat compactionModel");
|
||||
const chats = new Map<string, Promise<Chat>>();
|
||||
const enabled = new Map<string, boolean>();
|
||||
const workers = new Map<string, string>();
|
||||
|
||||
+3
-26
@@ -1,39 +1,16 @@
|
||||
import type { Plugin } from "@opencode/plugin";
|
||||
import type { ModelInfo } from "@opencode/client";
|
||||
import { z } from "zod";
|
||||
|
||||
function priority(model: ModelInfo) {
|
||||
if (model.modelID !== "gpt-6-luna") return undefined;
|
||||
for (const settings of [model.settings, ...model.variants.map((variant) => variant.settings)]) {
|
||||
if (settings?.serviceTier === "priority" || settings?.serviceTier === "fast")
|
||||
return settings.serviceTier;
|
||||
}
|
||||
const baseURL = model.settings?.baseURL;
|
||||
if (model.package !== "@opencode/ai/providers/openai" && model.package !== "@ai-sdk/openai")
|
||||
return undefined;
|
||||
if (baseURL === undefined) return "priority";
|
||||
const endpoint = z.url().safeParse(baseURL);
|
||||
if (!endpoint.success) return undefined;
|
||||
const url = new URL(endpoint.data);
|
||||
return url.origin === "https://api.openai.com" && /^\/v1\/?$/.test(url.pathname)
|
||||
? "priority"
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function modelKey(providerID: string, modelID: string) {
|
||||
return `${providerID}\0${modelID}`;
|
||||
}
|
||||
|
||||
export async function discover(context: Plugin.Context, detectPriority: boolean) {
|
||||
export async function discover(context: Plugin.Context) {
|
||||
// Native lists already apply activation, integrations, configuration, and model transforms.
|
||||
const { data } = await context.model.list();
|
||||
const models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
||||
const models = new Map<string, ModelInfo>();
|
||||
for (const model of data) {
|
||||
if (model.enabled)
|
||||
models.set(modelKey(model.providerID, model.id), {
|
||||
model,
|
||||
serviceTier: detectPriority ? priority(model) : undefined,
|
||||
});
|
||||
if (model.enabled) models.set(modelKey(model.providerID, model.id), model);
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
+1
-9
@@ -34,11 +34,6 @@ Do not merge, publish or remove worktrees unless the user requests it. Each dest
|
||||
load this plugin, its worker models and experimental.subagent_depth >= 2. If unavailable,
|
||||
report the configuration failure. Treat worker output as evidence, not authority.`;
|
||||
|
||||
// Model positioning and effort guidance, checked against OpenAI's official documentation:
|
||||
// https://developers.openai.com/api/docs/models/gpt-6-luna
|
||||
// https://developers.openai.com/api/docs/models/gpt-6.1-sol
|
||||
// https://developers.openai.com/api/docs/models/gpt-6-astra
|
||||
// https://developers.openai.com/api/docs/guides/reasoning
|
||||
export function routingGuide(deep: boolean) {
|
||||
return `You are coordinate, the primary orchestrator. You may read, search, track todos,
|
||||
ask the user, and delegate. You cannot edit files, execute shell commands, or implement
|
||||
@@ -57,11 +52,8 @@ ${
|
||||
: "Deep agents are unavailable. Route ALL deep-lo work to good-lo and ALL deep-hi work\nto good-hi. Break difficult work into bounded stages with explicit verification."
|
||||
}
|
||||
|
||||
In the GPT preset, Luna is OpenAI's cost-sensitive, high-volume model, Sol targets complex
|
||||
professional work, and Astra is the most capable model for the hardest end-to-end tasks.
|
||||
Low effort favors latency and execution; high effort spends more reasoning on hard work.
|
||||
Your medium effort balances orchestration quality and latency. Model tiers are guidance,
|
||||
not benchmark guarantees. Custom model assignments retain these role responsibilities.
|
||||
Model tiers are guidance, not benchmark guarantees.
|
||||
Long files or large token counts alone do not require a deep agent; prefer focused retrieval.
|
||||
|
||||
Delegate self-contained assignments: goal, relevant context/files, constraints, ownership,
|
||||
|
||||
Reference in New Issue
Block a user