← opencode report

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.
Nhóm: C — Tool DesignFile: tool/tool.ts · Lines 86–96ID: C.4 / T16Status: Stable

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 propagate ToolArgError qua 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

HarnessTool arg validationError format cho model
opencodeZod safeParse + custom ToolArgError message"Please rewrite the input with valid arguments. Errors: ..."
Claude CodeZod tương tựInstruction dạng tương tự
AiderPydantic (Python)Pydantic error default — dài hơn cần thiết
ClineJSON Schema validate + throwGeneric error message
OpenHarnessPydantic v2 validationPython 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
}
`}

Tham khảo Refs