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
@@ -0,0 +1,131 @@
import type { Bindings, ExecutionGraph } from '../types';
import { ExecutionError, WorkflowPaused } from '../types';
import { GraphExecutor } from '../graph-executor';
import { graphSchema } from '../lib/schemas';
import { createComponentLoader } from '../lib/component-loader';
import { writeEvaluation, updateComponentStats } from './execution-evaluator';
import { parseTriplets } from './triplet-parser';
import { searchNodes } from './search-nodes';
import { buildExecutionGraph } from './graph-builder';
export async function handleCypherSearch(
triplets: unknown[],
env: Bindings,
): Promise<{ nodes: Record<string, unknown>; cypher: unknown; missing: string[] }> {
const parsed = parseTriplets(triplets);
if (!parsed) {
throw new Error('無法解析任何節點');
}
const { nodeResults } = searchNodes(parsed);
const graph = buildExecutionGraph(parsed, nodeResults, 'cypher-search-result', 'Cypher Search Result');
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: [] };
}
export async function handleCypherExecute(
triplets: unknown[],
context: Record<string, unknown> | undefined,
graphId: string,
graphName: string,
config: Record<string, Record<string, unknown>> | undefined,
env: Bindings,
waitUntil: (promise: Promise<void>) => void,
apiKey?: string,
): Promise<{
success: boolean;
data?: unknown;
error?: string;
trace?: unknown;
duration_ms: number;
graph?: ExecutionGraph;
// resumable workflow: 節點 pending 時回 paused(不算 success 也不算 fail
paused?: boolean;
task_id?: string;
run_id?: string;
paused_node_id?: string;
}> {
const parsed = parseTriplets(triplets as unknown[]);
if (!parsed) {
throw new Error('無法解析任何節點');
}
const { nodeResults } = searchNodes(parsed, config);
const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config);
const parseResult = graphSchema.safeParse(graph);
if (!parseResult.success) {
throw new Error('圖定義產生失敗');
}
const loader = createComponentLoader(env);
const executor = new GraphExecutor(loader, undefined, env, apiKey);
const start = Date.now();
try {
const result = await executor.execute(parseResult.data as ExecutionGraph, context ?? {}, env.EXEC_CONTEXT);
const duration_ms = Date.now() - start;
// 非同步記錄統計(Phase 7 補充 analytics,目前為 no-op
const componentId = graph.nodes.find(n => n.componentId)?.componentId ?? graphId;
const runId = `${graphId}-${Date.now()}`;
waitUntil(writeEvaluation(env, {
run_id: runId,
workflow_id: graphId,
component_id: componentId,
verdict: 'success',
duration_ms,
evaluated_at: Date.now(),
}));
waitUntil(updateComponentStats(env, componentId, 'success', duration_ms));
return { success: true, data: result.data, trace: result.trace, duration_ms, graph };
} catch (err) {
const duration_ms = Date.now() - start;
// Resumable workflow: 節點回 pending → 回 paused 結構,不算成功也不算失敗
// SDD: resumable-workflow/design.md
if (err instanceof WorkflowPaused) {
return {
success: true,
paused: true,
task_id: err.task_id,
run_id: err.run_id,
paused_node_id: err.paused_node_id,
trace: err.trace_so_far,
duration_ms,
graph,
};
}
const errMsg = err instanceof Error ? err.message : String(err);
const componentId = graph.nodes.find(n => n.componentId)?.componentId ?? graphId;
const runId = `${graphId}-${Date.now()}`;
waitUntil(writeEvaluation(env, {
run_id: runId,
workflow_id: graphId,
component_id: componentId,
verdict: 'failed',
duration_ms,
error_message: errMsg.slice(0, 200),
evaluated_at: Date.now(),
}));
waitUntil(updateComponentStats(env, componentId, 'failed', duration_ms));
if (err instanceof ExecutionError) {
const traceFormatted = err.trace.map(s => ({
node: s.nodeId,
status: s.error ? 'failed' : 'success',
...(s.error ? { error: s.error } : {}),
}));
throw new Error(JSON.stringify({
success: false,
error: errMsg,
failed_node: err.failed_node,
failed_input: err.failed_input,
trace: traceFormatted,
duration_ms,
}));
}
throw err;
}
}