T16 — Zod schema validation với custom error format
Khi model gửi args sai schema, error message phải là instruction to retry: "Please rewrite the input with valid arguments. Errors: file_path: Required; limit: expected number" — không phải thông báo lỗi chung chung.
Tổng quan Tool Design
Tại sao quan trọng. Model thỉnh thoảng gửi tool args sai schema — missing field, wrong type, giá trị out of range. Nếu error message là
"Invalid input" hoặc Zod default verbose error, model không biết cụ thể cần sửa gì ở đâu. Message dạng instruction "Please rewrite the input with valid arguments. Errors: file_path: Required; limit: expected number, got string" → model có đủ context để retry đúng ngay lần đầu, không cần vòng lặp hỏi thêm.
Double-duty của Zod schema: Schema không chỉ validate — nó còn được dùng để generate JSON Schema cho model (qua AI SDK), làm TypeScript type, và làm documentation. Một schema, ba công dụng.
Phân tích code chi tiết Anatomy
Validation với custom error message
tool/tool.ts — validateArgs với custom format
TS
{`
function validateArgs<P extends z.ZodType>(
parameters: P,
raw: unknown,
): z.infer<P> {
const parsed = parameters.safeParse(raw)
if (!parsed.success) {
// Format error thành instruction cho model
const issues = parsed.error.issues
.map((i) => `${i.path.join(".")}: ${i.message}`)
.join("; ")
throw new ToolArgError(
`Please rewrite the input with valid arguments. Errors: ${issues}`,
)
}
return parsed.data
}
`}Schema definition — explicit descriptions
tool/edit.ts — schema với .describe() cho model
TS
{`
const EditArgs = z.object({
file_path: z.string()
.min(1)
.describe("Absolute path to the file to edit"),
old_string: z.string()
.describe("Exact string to find in the file (must match exactly)"),
new_string: z.string()
.describe("String to replace old_string with"),
// Optional fields với default
create_if_missing: z.boolean()
.optional()
.default(false)
.describe("Create file if it does not exist"),
})
// Error khi model gửi thiếu old_string:
// "Please rewrite the input with valid arguments.
// Errors: old_string: Required"
`}ToolArgError vs generic Error
ToolArgError — signal cho agent loop
TS
{`
class ToolArgError extends Error {
readonly _tag = "ToolArgError"
constructor(message: string) {
super(message)
this.name = "ToolArgError"
}
}
// Agent loop xử lý ToolArgError khác với runtime error:
// - ToolArgError: gửi error message về model → model retry với args đúng
// - RuntimeError: log, notify user, không retry tự động
`}Tương tác với kỹ thuật khác Interaction
- T14 (Effect lazy tool init): Validation xảy ra bên trong
execute()— Effect propagateToolArgErrorqua error channel, agent loop catch và gửi về model. - T1 (ReAct loop): Khi tool trả về ToolArgError, loop không exit — xem đây là tool result (dạng error), model nhận message và retry args trong bước tiếp theo.
- T6 (Doom loop): Nếu model liên tục retry với args sai (không sửa được), doom loop detection sẽ trigger sau 3 lần cùng tool + args → hỏi user.
Failure modes Failure
1. Model không sửa được
Đôi khi model giữ nguyên args và chỉ thêm comment giải thích trong text response — không thật sự sửa args. Đây là failure mode của model, không phải harness. Doom loop detection (T6) giải quyết bằng cách hỏi user.
2. Zod error quá verbose
Schema lồng sâu (nested object, discriminated union) tạo ra Zod errors rất dài, có thể chiếm nhiều token. Cần format error thành format ngắn gọn hơn, chỉ giữ field path và message chính.
Fix: Giới hạn issues chỉ lấy top-5, truncate từng message ở 100 chars. Không cần model biết toàn bộ Zod stacktrace.
So sánh với các harness khác Compare
| Harness | Tool arg validation | Error format cho model |
|---|---|---|
| opencode | Zod safeParse + custom ToolArgError message | "Please rewrite the input with valid arguments. Errors: ..." |
| Claude Code | Zod tương tự | Instruction dạng tương tự |
| Aider | Pydantic (Python) | Pydantic error default — dài hơn cần thiết |
| Cline | JSON Schema validate + throw | Generic error message |
| OpenHarness | Pydantic v2 validation | Python validation error, ít instructional hơn |
Implementation recipe Recipe
TS
{`
import { z } from "zod"
class ToolArgError extends Error {
readonly _tag = "ToolArgError" as const
}
function validateArgs<T extends z.ZodType>(schema: T, raw: unknown): z.infer<T> {
const result = schema.safeParse(raw)
if (!result.success) {
// Lấy top 5 issues, format ngắn gọn
const issues = result.error.issues
.slice(0, 5)
.map(i => {
const path = i.path.length ? i.path.join(".") + ": " : ""
return path + i.message
})
.join("; ")
throw new ToolArgError(
`Please rewrite the tool input with valid arguments. Errors: ${issues}`
)
}
return result.data
}
// Dùng trong tool execute:
async function execute(rawArgs: unknown) {
const args = validateArgs(MyToolSchema, rawArgs)
// ... tiếp tục với args đã validated và typed
}
`}