arcrun — AI workflow execution engine (clean history)

Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。

此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在
richblack/arcrun 與本地 backup 分支)。含:
- acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe)
- recipe push 把關(資料外流提醒 + 打通檢查)
- 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1)
- CLI / cypher-executor / registry / 完整 SDD

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-03 15:52:38 +08:00
commit 922a57fe34
485 changed files with 89356 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
/**
* Recipe expander:把 prompt_recipe 展開成 claude_api 的實際 input
* SDD: matrix/arcrun/.agents/specs/recipe-system/design.md §2.2 + Phase 2.1
*
* 流程:
* 1. loadPromptRecipe 取定義
* 2. fragments → 用 KBDB API 抓 block content
* 3. inputs → 從 workflow context 取值 + 跑 transform
* 4. 套進 prompt_assembly.system / .user 的 {{var}} 模板
* 5. 回傳 { prompt, model, output_format, output_required_fields }
*/
import { loadPromptRecipe, RecipeLoadError } from './recipe-loader';
import { applyTransform } from './recipe-transforms';
import type { Fragment, RecipeInput } from './prompt-recipe-schema';
type ExpanderEnv = {
RECIPES: { get: (key: string) => Promise<string | null> };
KBDB_BASE_URL?: string;
};
export interface ExpandedRecipe {
prompt: string; // user promptsystem + user 用 \n\n--- system ---\n 分隔)
model: 'haiku' | 'sonnet' | 'opus';
output_format: 'text' | 'json';
output_required_fields?: string[];
}
/** 從 path 取嵌套值,例如 "ctx.read_drafts.blocks" / "loop.item" */
function getByPath(ctx: Record<string, unknown>, path: string): unknown {
const parts = path.split('.');
let cur: unknown = ctx;
for (const p of parts) {
if (cur === null || cur === undefined) return undefined;
if (typeof cur !== 'object') return undefined;
cur = (cur as Record<string, unknown>)[p];
}
return cur;
}
/** {{var}} 模板替換(top-level vars 物件) */
function interpolate(template: string, vars: Record<string, string>): string {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (vars[key] !== undefined ? vars[key] : `{{${key}}}`));
}
async function fetchKbdbBlock(
env: ExpanderEnv,
apiKey: string,
fragment: Extract<Fragment, { source: 'kbdb_block' }>,
): Promise<unknown> {
const base = (env.KBDB_BASE_URL ?? 'https://kbdb.finally.click').replace(/\/$/, '');
let url: string;
if (fragment.block_id) {
url = `${base}/blocks/${encodeURIComponent(fragment.block_id)}`;
} else {
url = `${base}/blocks?page_name=${encodeURIComponent(fragment.block_page_name!)}&limit=1`;
}
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
if (!res.ok) throw new Error(`KBDB fragment 抓取失敗 (${res.status}): ${url}`);
const data = (await res.json()) as Record<string, unknown>;
// page_name 模式回 {blocks:[]}block_id 模式直接回 block 物件
const block: Record<string, unknown> = fragment.block_id
? data
: ((data.blocks as unknown[])?.[0] as Record<string, unknown>) ?? {};
if (!block) throw new Error(`KBDB block 不存在: ${fragment.block_id ?? fragment.block_page_name}`);
const fieldVal = block[fragment.field];
if (fieldVal === undefined) throw new Error(`block 缺欄位 "${fragment.field}"`);
return fieldVal;
}
async function resolveFragment(
env: ExpanderEnv,
apiKey: string,
frag: Fragment,
): Promise<{ var: string; value: unknown }> {
if (frag.source === 'kv') {
const val = await env.RECIPES.get(frag.key);
if (val === null) throw new Error(`KV 找不到 key: ${frag.key}`);
return { var: frag.var, value: val };
}
return { var: frag.var, value: await fetchKbdbBlock(env, apiKey, frag) };
}
function resolveInput(input: RecipeInput, ctx: Record<string, unknown>): { var: string; value: unknown } {
let val = getByPath(ctx, input.from);
const beforeDefault = val;
if (val === undefined) val = input.default;
try {
if (input.transform) val = applyTransform(val, input.transform);
return { var: input.var, value: val };
} catch (e) {
// 把 path 跟原值放進錯誤訊息,方便 debug recipe
const valType = Array.isArray(beforeDefault) ? `array(${beforeDefault.length})`
: beforeDefault === undefined ? 'undefined(default applied)'
: typeof beforeDefault;
throw new Error(`${e instanceof Error ? e.message : String(e)} [path=${input.from}, type=${valType}]`);
}
}
/** 主入口:展開 recipe → 組 prompt */
export async function expandPromptRecipe(
recipeRef: string,
ctx: Record<string, unknown>,
env: ExpanderEnv,
apiKey: string, // KBDB partner key(從 workflow auth 來)
): Promise<ExpandedRecipe> {
const recipe = await loadPromptRecipe(recipeRef, env.RECIPES);
const vars: Record<string, string> = {};
for (const frag of recipe.fragments) {
const { var: name, value } = await resolveFragment(env, apiKey, frag);
vars[name] = typeof value === 'string' ? value : JSON.stringify(value);
}
for (const inp of recipe.inputs) {
const { var: name, value } = resolveInput(inp, ctx);
vars[name] = typeof value === 'string' ? value : JSON.stringify(value);
}
const system = interpolate(recipe.prompt_assembly.system, vars);
const user = interpolate(recipe.prompt_assembly.user, vars);
// claude_api 容器目前吃單一 prompt 字串 → system + user 用分隔線拼
const prompt = `${system}\n\n--- USER ---\n\n${user}`;
return {
prompt,
model: recipe.model,
output_format: recipe.output.format,
output_required_fields: recipe.output.required_fields,
};
}
export { RecipeLoadError };