Przeglądaj źródła

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

Kit Langton 1 dzień temu
rodzic
commit
2793502db2
1 zmienionych plików z 98 dodań i 81 usunięć
  1. 98 81
      packages/opencode/src/config/agent.ts

+ 98 - 81
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) {