summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-17 23:16:24 -0400
committerGitHub <[email protected]>2026-04-18 03:16:24 +0000
commit2793502db20ffc88c18fdec91133ac157d470461 (patch)
tree0c69912dd27affc17ca67e5525a46c66f72b5a4a
parent9f7bd0246c14620758865513ceddfe0a768bec2e (diff)
downloadopencode-2793502db20ffc88c18fdec91133ac157d470461.tar.gz
opencode-2793502db20ffc88c18fdec91133ac157d470461.zip
refactor(config): migrate agent.ts Info to Effect Schema (#23237)
-rw-r--r--packages/opencode/src/config/agent.ts179
1 files changed, 98 insertions, 81 deletions
diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts
index 9053b19fc..32dbc7410 100644
--- a/packages/opencode/src/config/agent.ts
+++ b/packages/opencode/src/config/agent.ts
@@ -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) {