/** * prompt_recipe Zod schema * SDD: matrix/arcrun/.agents/specs/recipe-system/design.md §2.1 * * 平行於既有 auth_recipe / api_recipe,存 RECIPES KV (key: `prompt_recipe:{name}`) * 容器 + recipe 模式:claude_api 是容器,recipe 是配方 */ import { z } from 'zod'; // ── Transform 白名單 ────────────────────────────────────────────────────────── // 限制 transform 種類避免變 mini-DSL;超過範圍請寫零件 export const TRANSFORM_NAMES = [ 'json_array', // array → JSON.stringify 整體 'to_string', // 任意值 → String(x) 'join', // array → join(sep),sep 預設換行 'markdown_list', // array → "- a\n- b\n- c" 'extract_field', // array of object → 抽 field 後的 array(再可串其他 transform) 'first', // array → first element(取單一) 'pluck_content', // KBDB blocks array → 抽 content 後 join 雙換行(草稿合併常用) ] as const; /** transform 表示法:name 或 name:arg(如 extract_field:page_name) */ export const TransformSchema = z.string().regex(/^[a-z_]+(:.+)?$/, 'transform 必須為 name 或 name:arg 格式'); // ── Fragment:從 KBDB / KV 抓固定資料 ────────────────────────────────────────── export const KBDBBlockFragmentSchema = z.object({ var: z.string().min(1), // prompt template 內的變數名 source: z.literal('kbdb_block'), block_id: z.string().optional(), // 二擇一 block_page_name: z.string().optional(), // 比 block_id 穩定 field: z.string().default('content'), // 抓 block 的哪個欄位 }); export const KVFragmentSchema = z.object({ var: z.string().min(1), source: z.literal('kv'), key: z.string().min(1), }); // discriminatedUnion 對 refined zod object 不支援,故拆成驗證後 + 單獨檢查 block_id|page_name export const FragmentSchema = z.discriminatedUnion('source', [ KBDBBlockFragmentSchema, KVFragmentSchema, ]).superRefine((d, ctx) => { if (d.source === 'kbdb_block' && !d.block_id && !d.block_page_name) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'block_id 或 block_page_name 必填其一', }); } }); // ── Input:從 workflow context 取值(含 transform) ──────────────────────────── export const InputSchema = z.object({ var: z.string().min(1), from: z.string().min(1), // JSONPath-lite,如 "ctx.read_drafts.blocks" transform: TransformSchema.optional(), default: z.unknown().optional(), // from 取不到時的預設值(避免炸 prompt) }); // ── Prompt 組裝 ────────────────────────────────────────────────────────────── export const PromptAssemblySchema = z.object({ system: z.string().min(1), // 模板,可含 {{var}} user: z.string().min(1), }); // ── 輸出規格 ────────────────────────────────────────────────────────────────── export const OutputSpecSchema = z.object({ format: z.enum(['text', 'json']).default('text'), // 若 format=json,可選 schema 做 parse 後驗證(簡化版,列必填欄位即可) required_fields: z.array(z.string()).optional(), }); // ── 完整 prompt_recipe 定義 ──────────────────────────────────────────────────── export const PromptRecipeSchema = z.object({ kind: z.literal('prompt_recipe'), name: z.string().min(1).regex(/^[a-z][a-z0-9_]*$/, 'name 為 lowercase + underscore'), version: z.number().int().positive().default(1), description: z.string().optional(), model: z.enum(['haiku', 'sonnet', 'opus']).default('sonnet'), fragments: z.array(FragmentSchema).default([]), inputs: z.array(InputSchema).default([]), prompt_assembly: PromptAssemblySchema, output: OutputSpecSchema.default({ format: 'text' }), }); export type PromptRecipe = z.infer; export type Fragment = z.infer; export type RecipeInput = z.infer;