Skip to content

Commit 2793502

Browse files
authored
refactor(config): migrate agent.ts Info to Effect Schema (#23237)
1 parent 9f7bd02 commit 2793502

1 file changed

Lines changed: 98 additions & 81 deletions

File tree

packages/opencode/src/config/agent.ts

Lines changed: 98 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
export * as ConfigAgent from "./agent"
22

3-
import { Log } from "../util"
3+
import { Schema } from "effect"
44
import z from "zod"
5+
import { Bus } from "@/bus"
6+
import { zod, ZodOverride } from "@/util/effect-zod"
7+
import { Log } from "../util"
58
import { NamedError } from "@opencode-ai/shared/util/error"
69
import { Glob } from "@opencode-ai/shared/util/glob"
7-
import { Bus } from "@/bus"
810
import { configEntryNameFromPath } from "./entry-name"
911
import { InvalidError } from "./error"
1012
import * as ConfigMarkdown from "./markdown"
@@ -13,89 +15,104 @@ import { ConfigPermission } from "./permission"
1315

1416
const log = Log.create({ service: "config" })
1517

16-
export const Info = z
17-
.object({
18-
model: ConfigModelID.zod.optional(),
19-
variant: z
20-
.string()
21-
.optional()
22-
.describe("Default model variant for this agent (applies only when using the agent's configured model)."),
23-
temperature: z.number().optional(),
24-
top_p: z.number().optional(),
25-
prompt: z.string().optional(),
26-
tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
27-
disable: z.boolean().optional(),
28-
description: z.string().optional().describe("Description of when to use the agent"),
29-
mode: z.enum(["subagent", "primary", "all"]).optional(),
30-
hidden: z
31-
.boolean()
32-
.optional()
33-
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
34-
options: z.record(z.string(), z.any()).optional(),
35-
color: z
36-
.union([
37-
z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
38-
z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
39-
])
40-
.optional()
41-
.describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
42-
steps: z
43-
.number()
44-
.int()
45-
.positive()
46-
.optional()
47-
.describe("Maximum number of agentic iterations before forcing text-only response"),
48-
maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
49-
permission: ConfigPermission.Info.optional(),
50-
})
51-
.catchall(z.any())
52-
.transform((agent, _ctx) => {
53-
const knownKeys = new Set([
54-
"name",
55-
"model",
56-
"variant",
57-
"prompt",
58-
"description",
59-
"temperature",
60-
"top_p",
61-
"mode",
62-
"hidden",
63-
"color",
64-
"steps",
65-
"maxSteps",
66-
"options",
67-
"permission",
68-
"disable",
69-
"tools",
70-
])
71-
72-
const options: Record<string, unknown> = { ...agent.options }
73-
for (const [key, value] of Object.entries(agent)) {
74-
if (!knownKeys.has(key)) options[key] = value
75-
}
18+
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
7619

77-
const permission: ConfigPermission.Info = {}
78-
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
79-
const action = enabled ? "allow" : "deny"
80-
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
81-
permission.edit = action
82-
continue
83-
}
84-
permission[tool] = action
85-
}
86-
Object.assign(permission, agent.permission)
20+
const Color = Schema.Union([
21+
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
22+
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
23+
])
24+
25+
// ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)`
26+
// shape lives outside the Effect Schema type system), so the walker reaches it
27+
// via ZodOverride rather than a pure Schema reference. This preserves the
28+
// `$ref: PermissionConfig` emitted in openapi.json.
29+
const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
30+
31+
const AgentSchema = Schema.StructWithRest(
32+
Schema.Struct({
33+
model: Schema.optional(ConfigModelID),
34+
variant: Schema.optional(Schema.String).annotate({
35+
description: "Default model variant for this agent (applies only when using the agent's configured model).",
36+
}),
37+
temperature: Schema.optional(Schema.Number),
38+
top_p: Schema.optional(Schema.Number),
39+
prompt: Schema.optional(Schema.String),
40+
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
41+
description: "@deprecated Use 'permission' field instead",
42+
}),
43+
disable: Schema.optional(Schema.Boolean),
44+
description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
45+
mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
46+
hidden: Schema.optional(Schema.Boolean).annotate({
47+
description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
48+
}),
49+
options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
50+
color: Schema.optional(Color).annotate({
51+
description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
52+
}),
53+
steps: Schema.optional(PositiveInt).annotate({
54+
description: "Maximum number of agentic iterations before forcing text-only response",
55+
}),
56+
maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
57+
permission: Schema.optional(PermissionRef),
58+
}),
59+
[Schema.Record(Schema.String, Schema.Any)],
60+
)
8761

88-
const steps = agent.steps ?? agent.maxSteps
62+
const KNOWN_KEYS = new Set([
63+
"name",
64+
"model",
65+
"variant",
66+
"prompt",
67+
"description",
68+
"temperature",
69+
"top_p",
70+
"mode",
71+
"hidden",
72+
"color",
73+
"steps",
74+
"maxSteps",
75+
"options",
76+
"permission",
77+
"disable",
78+
"tools",
79+
])
8980

90-
return { ...agent, options, permission, steps } as typeof agent & {
91-
options?: Record<string, unknown>
92-
permission?: ConfigPermission.Info
93-
steps?: number
81+
// Post-parse normalisation:
82+
// - Promote any unknown-but-present keys into `options` so they survive the
83+
// round-trip in a well-known field.
84+
// - Translate the deprecated `tools: { name: boolean }` map into the new
85+
// `permission` shape (write-adjacent tools collapse into `permission.edit`).
86+
// - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias.
87+
const normalize = (agent: z.infer<typeof Info>) => {
88+
const options: Record<string, unknown> = { ...agent.options }
89+
for (const [key, value] of Object.entries(agent)) {
90+
if (!KNOWN_KEYS.has(key)) options[key] = value
91+
}
92+
93+
const permission: ConfigPermission.Info = {}
94+
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
95+
const action = enabled ? "allow" : "deny"
96+
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
97+
permission.edit = action
98+
continue
9499
}
95-
})
96-
.meta({
97-
ref: "AgentConfig",
98-
})
100+
permission[tool] = action
101+
}
102+
globalThis.Object.assign(permission, agent.permission)
103+
104+
return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps }
105+
}
106+
107+
export const Info = zod(AgentSchema)
108+
.transform(normalize)
109+
.meta({ ref: "AgentConfig" }) as unknown as z.ZodType<
110+
Omit<z.infer<ReturnType<typeof zod<typeof AgentSchema>>>, "options" | "permission" | "steps"> & {
111+
options?: Record<string, unknown>
112+
permission?: ConfigPermission.Info
113+
steps?: number
114+
}
115+
>
99116
export type Info = z.infer<typeof Info>
100117

101118
export async function load(dir: string) {

0 commit comments

Comments
 (0)