refactor(config): migrate agent.ts Info to Effect Schema (#23237)
This commit is contained in:
@@ -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,89 +15,104 @@ 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(),
|
|
||||||
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 }
|
const Color = Schema.Union([
|
||||||
for (const [key, value] of Object.entries(agent)) {
|
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
||||||
if (!knownKeys.has(key)) options[key] = value
|
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 KNOWN_KEYS = new Set([
|
||||||
|
"name",
|
||||||
|
"model",
|
||||||
|
"variant",
|
||||||
|
"prompt",
|
||||||
|
"description",
|
||||||
|
"temperature",
|
||||||
|
"top_p",
|
||||||
|
"mode",
|
||||||
|
"hidden",
|
||||||
|
"color",
|
||||||
|
"steps",
|
||||||
|
"maxSteps",
|
||||||
|
"options",
|
||||||
|
"permission",
|
||||||
|
"disable",
|
||||||
|
"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 }
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
permission[tool] = action
|
||||||
|
}
|
||||||
|
globalThis.Object.assign(permission, agent.permission)
|
||||||
|
|
||||||
const permission: ConfigPermission.Info = {}
|
return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps }
|
||||||
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 steps = agent.steps ?? agent.maxSteps
|
export const Info = zod(AgentSchema)
|
||||||
|
.transform(normalize)
|
||||||
return { ...agent, options, permission, steps } as typeof agent & {
|
.meta({ ref: "AgentConfig" }) as unknown as z.ZodType<
|
||||||
options?: Record<string, unknown>
|
Omit<z.infer<ReturnType<typeof zod<typeof AgentSchema>>>, "options" | "permission" | "steps"> & {
|
||||||
permission?: ConfigPermission.Info
|
options?: Record<string, unknown>
|
||||||
steps?: number
|
permission?: ConfigPermission.Info
|
||||||
}
|
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user