|
|
@@ -1,10 +1,12 @@
|
|
|
export * as ConfigAgent from "./agent"
|
|
|
|
|
|
-import { Log } from "../util"
|
|
|
+import { Schema } from "effect"
|
|
|
import z from "zod"
|
|
|
+import { Bus } from "@/bus"
|
|
|
+import { zod, ZodOverride } from "@/util/effect-zod"
|
|
|
+import { Log } from "../util"
|
|
|
import { NamedError } from "@opencode-ai/shared/util/error"
|
|
|
import { Glob } from "@opencode-ai/shared/util/glob"
|
|
|
-import { Bus } from "@/bus"
|
|
|
import { configEntryNameFromPath } from "./entry-name"
|
|
|
import { InvalidError } from "./error"
|
|
|
import * as ConfigMarkdown from "./markdown"
|
|
|
@@ -13,89 +15,104 @@ import { ConfigPermission } from "./permission"
|
|
|
|
|
|
const log = Log.create({ service: "config" })
|
|
|
|
|
|
-export const Info = z
|
|
|
- .object({
|
|
|
- model: ConfigModelID.zod.optional(),
|
|
|
- variant: z
|
|
|
- .string()
|
|
|
- .optional()
|
|
|
- .describe("Default model variant for this agent (applies only when using the agent's configured model)."),
|
|
|
- temperature: z.number().optional(),
|
|
|
- top_p: z.number().optional(),
|
|
|
- prompt: z.string().optional(),
|
|
|
- tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
|
|
|
- disable: z.boolean().optional(),
|
|
|
- description: z.string().optional().describe("Description of when to use the agent"),
|
|
|
- mode: z.enum(["subagent", "primary", "all"]).optional(),
|
|
|
- hidden: z
|
|
|
- .boolean()
|
|
|
- .optional()
|
|
|
- .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
|
|
|
- options: z.record(z.string(), z.any()).optional(),
|
|
|
- color: z
|
|
|
- .union([
|
|
|
- z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
|
|
|
- z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
|
|
- ])
|
|
|
- .optional()
|
|
|
- .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
|
|
|
- steps: z
|
|
|
- .number()
|
|
|
- .int()
|
|
|
- .positive()
|
|
|
- .optional()
|
|
|
- .describe("Maximum number of agentic iterations before forcing text-only response"),
|
|
|
- maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
|
|
|
- permission: ConfigPermission.Info.optional(),
|
|
|
- })
|
|
|
- .catchall(z.any())
|
|
|
- .transform((agent, _ctx) => {
|
|
|
- const knownKeys = new Set([
|
|
|
- "name",
|
|
|
- "model",
|
|
|
- "variant",
|
|
|
- "prompt",
|
|
|
- "description",
|
|
|
- "temperature",
|
|
|
- "top_p",
|
|
|
- "mode",
|
|
|
- "hidden",
|
|
|
- "color",
|
|
|
- "steps",
|
|
|
- "maxSteps",
|
|
|
- "options",
|
|
|
- "permission",
|
|
|
- "disable",
|
|
|
- "tools",
|
|
|
- ])
|
|
|
-
|
|
|
- const options: Record<string, unknown> = { ...agent.options }
|
|
|
- for (const [key, value] of Object.entries(agent)) {
|
|
|
- if (!knownKeys.has(key)) options[key] = value
|
|
|
- }
|
|
|
+const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
|
|
|
|
|
- const permission: ConfigPermission.Info = {}
|
|
|
- for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
|
|
|
- const action = enabled ? "allow" : "deny"
|
|
|
- if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
|
|
- permission.edit = action
|
|
|
- continue
|
|
|
- }
|
|
|
- permission[tool] = action
|
|
|
- }
|
|
|
- Object.assign(permission, agent.permission)
|
|
|
+const Color = Schema.Union([
|
|
|
+ Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
|
|
+ Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
|
|
+])
|
|
|
+
|
|
|
+// ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)`
|
|
|
+// shape lives outside the Effect Schema type system), so the walker reaches it
|
|
|
+// via ZodOverride rather than a pure Schema reference. This preserves the
|
|
|
+// `$ref: PermissionConfig` emitted in openapi.json.
|
|
|
+const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
|
|
|
+
|
|
|
+const AgentSchema = Schema.StructWithRest(
|
|
|
+ Schema.Struct({
|
|
|
+ model: Schema.optional(ConfigModelID),
|
|
|
+ variant: Schema.optional(Schema.String).annotate({
|
|
|
+ description: "Default model variant for this agent (applies only when using the agent's configured model).",
|
|
|
+ }),
|
|
|
+ temperature: Schema.optional(Schema.Number),
|
|
|
+ top_p: Schema.optional(Schema.Number),
|
|
|
+ prompt: Schema.optional(Schema.String),
|
|
|
+ tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
|
|
|
+ description: "@deprecated Use 'permission' field instead",
|
|
|
+ }),
|
|
|
+ disable: Schema.optional(Schema.Boolean),
|
|
|
+ description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
|
|
|
+ mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
|
|
|
+ hidden: Schema.optional(Schema.Boolean).annotate({
|
|
|
+ description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
|
|
|
+ }),
|
|
|
+ options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
|
|
+ color: Schema.optional(Color).annotate({
|
|
|
+ description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
|
|
|
+ }),
|
|
|
+ steps: Schema.optional(PositiveInt).annotate({
|
|
|
+ description: "Maximum number of agentic iterations before forcing text-only response",
|
|
|
+ }),
|
|
|
+ maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
|
|
|
+ permission: Schema.optional(PermissionRef),
|
|
|
+ }),
|
|
|
+ [Schema.Record(Schema.String, Schema.Any)],
|
|
|
+)
|
|
|
|
|
|
- const steps = agent.steps ?? agent.maxSteps
|
|
|
+const KNOWN_KEYS = new Set([
|
|
|
+ "name",
|
|
|
+ "model",
|
|
|
+ "variant",
|
|
|
+ "prompt",
|
|
|
+ "description",
|
|
|
+ "temperature",
|
|
|
+ "top_p",
|
|
|
+ "mode",
|
|
|
+ "hidden",
|
|
|
+ "color",
|
|
|
+ "steps",
|
|
|
+ "maxSteps",
|
|
|
+ "options",
|
|
|
+ "permission",
|
|
|
+ "disable",
|
|
|
+ "tools",
|
|
|
+])
|
|
|
|
|
|
- return { ...agent, options, permission, steps } as typeof agent & {
|
|
|
- options?: Record<string, unknown>
|
|
|
- permission?: ConfigPermission.Info
|
|
|
- steps?: number
|
|
|
+// Post-parse normalisation:
|
|
|
+// - Promote any unknown-but-present keys into `options` so they survive the
|
|
|
+// round-trip in a well-known field.
|
|
|
+// - Translate the deprecated `tools: { name: boolean }` map into the new
|
|
|
+// `permission` shape (write-adjacent tools collapse into `permission.edit`).
|
|
|
+// - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias.
|
|
|
+const normalize = (agent: z.infer<typeof Info>) => {
|
|
|
+ const options: Record<string, unknown> = { ...agent.options }
|
|
|
+ for (const [key, value] of Object.entries(agent)) {
|
|
|
+ if (!KNOWN_KEYS.has(key)) options[key] = value
|
|
|
+ }
|
|
|
+
|
|
|
+ const permission: ConfigPermission.Info = {}
|
|
|
+ for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
|
|
|
+ const action = enabled ? "allow" : "deny"
|
|
|
+ if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
|
|
+ permission.edit = action
|
|
|
+ continue
|
|
|
}
|
|
|
- })
|
|
|
- .meta({
|
|
|
- ref: "AgentConfig",
|
|
|
- })
|
|
|
+ permission[tool] = action
|
|
|
+ }
|
|
|
+ globalThis.Object.assign(permission, agent.permission)
|
|
|
+
|
|
|
+ return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps }
|
|
|
+}
|
|
|
+
|
|
|
+export const Info = zod(AgentSchema)
|
|
|
+ .transform(normalize)
|
|
|
+ .meta({ ref: "AgentConfig" }) as unknown as z.ZodType<
|
|
|
+ Omit<z.infer<ReturnType<typeof zod<typeof AgentSchema>>>, "options" | "permission" | "steps"> & {
|
|
|
+ options?: Record<string, unknown>
|
|
|
+ permission?: ConfigPermission.Info
|
|
|
+ steps?: number
|
|
|
+ }
|
|
|
+>
|
|
|
export type Info = z.infer<typeof Info>
|
|
|
|
|
|
export async function load(dir: string) {
|