refactor(config): migrate agent.ts Info to Effect Schema (#23237)

This commit is contained in:
Kit Langton
2026-04-18 03:16:24 +00:00
committed by GitHub
parent 9f7bd0246c
commit 2793502db2
+66 -49
View File
@@ -1,10 +1,12 @@
export * as ConfigAgent from "./agent" export * as ConfigAgent from "./agent"
import { Log } from "../util" import { Schema } from "effect"
import z from "zod" 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 { NamedError } from "@opencode-ai/shared/util/error"
import { Glob } from "@opencode-ai/shared/util/glob" import { Glob } from "@opencode-ai/shared/util/glob"
import { Bus } from "@/bus"
import { configEntryNameFromPath } from "./entry-name" import { configEntryNameFromPath } from "./entry-name"
import { InvalidError } from "./error" import { InvalidError } from "./error"
import * as ConfigMarkdown from "./markdown" import * as ConfigMarkdown from "./markdown"
@@ -13,44 +15,51 @@ import { ConfigPermission } from "./permission"
const log = Log.create({ service: "config" }) const log = Log.create({ service: "config" })
export const Info = z const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
.object({
model: ConfigModelID.zod.optional(), const Color = Schema.Union([
variant: z Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
.string() Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
.optional() ])
.describe("Default model variant for this agent (applies only when using the agent's configured model)."),
temperature: z.number().optional(), // ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)`
top_p: z.number().optional(), // shape lives outside the Effect Schema type system), so the walker reaches it
prompt: z.string().optional(), // via ZodOverride rather than a pure Schema reference. This preserves the
tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"), // `$ref: PermissionConfig` emitted in openapi.json.
disable: z.boolean().optional(), const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
description: z.string().optional().describe("Description of when to use the agent"),
mode: z.enum(["subagent", "primary", "all"]).optional(), const AgentSchema = Schema.StructWithRest(
hidden: z Schema.Struct({
.boolean() model: Schema.optional(ConfigModelID),
.optional() variant: Schema.optional(Schema.String).annotate({
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"), description: "Default model variant for this agent (applies only when using the agent's configured model).",
options: z.record(z.string(), z.any()).optional(), }),
color: z temperature: Schema.optional(Schema.Number),
.union([ top_p: Schema.optional(Schema.Number),
z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"), prompt: Schema.optional(Schema.String),
z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
]) description: "@deprecated Use 'permission' field instead",
.optional() }),
.describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"), disable: Schema.optional(Schema.Boolean),
steps: z description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
.number() mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
.int() hidden: Schema.optional(Schema.Boolean).annotate({
.positive() description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
.optional() }),
.describe("Maximum number of agentic iterations before forcing text-only response"), options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."), color: Schema.optional(Color).annotate({
permission: ConfigPermission.Info.optional(), description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
}) }),
.catchall(z.any()) steps: Schema.optional(PositiveInt).annotate({
.transform((agent, _ctx) => { description: "Maximum number of agentic iterations before forcing text-only response",
const knownKeys = new Set([ }),
maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
permission: Schema.optional(PermissionRef),
}),
[Schema.Record(Schema.String, Schema.Any)],
)
const KNOWN_KEYS = new Set([
"name", "name",
"model", "model",
"variant", "variant",
@@ -67,11 +76,18 @@ export const Info = z
"permission", "permission",
"disable", "disable",
"tools", "tools",
]) ])
// 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 } const options: Record<string, unknown> = { ...agent.options }
for (const [key, value] of Object.entries(agent)) { for (const [key, value] of Object.entries(agent)) {
if (!knownKeys.has(key)) options[key] = value if (!KNOWN_KEYS.has(key)) options[key] = value
} }
const permission: ConfigPermission.Info = {} const permission: ConfigPermission.Info = {}
@@ -83,19 +99,20 @@ export const Info = z
} }
permission[tool] = action permission[tool] = action
} }
Object.assign(permission, agent.permission) globalThis.Object.assign(permission, agent.permission)
const steps = agent.steps ?? agent.maxSteps return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps }
}
return { ...agent, options, permission, steps } as typeof agent & { 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> options?: Record<string, unknown>
permission?: ConfigPermission.Info permission?: ConfigPermission.Info
steps?: number steps?: number
} }
}) >
.meta({
ref: "AgentConfig",
})
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
export async function load(dir: string) { export async function load(dir: string) {