497f92a268
Three new platform capabilities + one component (kbdb_get) to enable
real AI workflow execution through cypher binding YAML.
## Recipe System (容器 + Recipe 模式)
SDD: .agents/specs/recipe-system/
- prompt_recipe schema (Zod): fragments + inputs + assembly + output
- recipe-expander.ts: expand recipe ref → real prompt by fetching KBDB blocks
+ pulling context fields with transforms (pluck_content / extract_field / etc)
- 7 transform whitelist: json_array / to_string / join / markdown_list /
extract_field / first / pluck_content
- graph-executor hooks: detect node.data.recipe → expand → inject into ctx
- output JSON parser (with markdown fence stripping for Claude-wrapped JSON)
- Stored in RECIPES KV under prompt_recipe:{name}
## Resumable Workflow (webhook callback resume)
SDD: .agents/specs/resumable-workflow/
- WorkflowPaused class + paused-runs.ts (persist/load/consume in EXEC_CONTEXT KV, 24h TTL)
- graph-executor: detect {pending:true, task_id} → persist state → throw WorkflowPaused
- cypher-handlers: catch → return {success:true, paused:true, task_id, run_id}
- POST /workflows/resume route: consume KV state → resumeFromPaused()
- Auto-inject callback_url for claude_api nodes (PUBLIC_BASE_URL or default cypher.arcrun.dev)
- claude_api/main.go: forward callback_url to Mira daemon, default timeout 25s→120s
- Idempotent (consume = load+delete)
## Component Registry Canon
SDD: .agents/specs/component-registry-canon/
- Add POST /components/index-only endpoint (metadata-only, no wasm/sandbox)
- Backfill script (mjs): scan registry/components/*/contract.yaml → submit to KV
- register-component.sh: SSOT for local + CI hook (deploy.yml change in next commit)
- Drop R2 dead storage from submitComponent + types + wrangler
- Schema relaxed: category enum + auth/ai/platform; cold_start 50→500ms; size 2→8MB
## kbdb_get component
- registry/components/kbdb_get/: TinyGo WASM, two modes (block_id / page_name list)
- .component-builds/kbdb_get/: WASI shim worker (kbdb-get.arcrun.dev)
End-to-end validation: AI uses MCP execute_workflow with recipe ref →
cypher-executor expands prompt from KBDB schema/skill blocks + drafts →
claude_api calls Mira daemon → daemon callback fires resume route →
workflow continues. Verified with real 2KB+ Karpathy LLM Wiki draft.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
/**
|
||
* arcrun WASM 零件 Worker (kbdb_get)
|
||
*
|
||
* POST / → JSON input → WASM (WASI preview1 stdin/stdout) → JSON output
|
||
*
|
||
* host function: http_request(用於呼叫 KBDB API)
|
||
* SDD: polaris/mira/.agents/specs/mira-app/design.md §6 / tasks.md §5.3
|
||
*/
|
||
|
||
import componentWasm from '../component.wasm' assert { type: 'webassembly' };
|
||
import { Hono } from 'hono';
|
||
import { cors } from 'hono/cors';
|
||
import { createWasiShim, type WasiHostFunctions } from '../../../cypher-executor/src/lib/wasi-shim';
|
||
|
||
const app = new Hono();
|
||
app.use('*', cors());
|
||
|
||
app.get('/', (c) => c.json({ ok: true, component: 'kbdb_get' }));
|
||
|
||
app.post('/', async (c) => {
|
||
let input: unknown;
|
||
try {
|
||
input = await c.req.json();
|
||
} catch {
|
||
return c.json({ success: false, error: 'request body must be JSON' }, 400);
|
||
}
|
||
|
||
try {
|
||
const result = await runWasm(input);
|
||
return c.json(result);
|
||
} catch (e) {
|
||
return c.json(
|
||
{ success: false, error: e instanceof Error ? e.message : String(e) },
|
||
500,
|
||
);
|
||
}
|
||
});
|
||
|
||
export default app;
|
||
|
||
// ── WASM runner ──────────────────────────────────────────────────────────────
|
||
|
||
async function runWasm(input: unknown): Promise<unknown> {
|
||
const hostFunctions: WasiHostFunctions = {
|
||
http_request: async (url, method, headersJson, body) => {
|
||
const headers: Record<string, string> = {};
|
||
if (headersJson) {
|
||
try {
|
||
const parsed = JSON.parse(headersJson);
|
||
if (parsed && typeof parsed === 'object') {
|
||
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
||
if (typeof v === 'string') headers[k] = v;
|
||
}
|
||
}
|
||
} catch {
|
||
// ignore header parse errors
|
||
}
|
||
}
|
||
const init: RequestInit = { method, headers };
|
||
if (body && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
|
||
init.body = body;
|
||
}
|
||
const res = await fetch(url, init);
|
||
return await res.text();
|
||
},
|
||
};
|
||
|
||
const shim = createWasiShim(JSON.stringify(input), hostFunctions);
|
||
|
||
const instance = await WebAssembly.instantiate(
|
||
componentWasm as WebAssembly.Module,
|
||
shim.imports,
|
||
);
|
||
shim.setMemory(instance.exports.memory as WebAssembly.Memory);
|
||
await shim.run(instance);
|
||
|
||
const stdout = shim.getStdout().trim();
|
||
const stderr = shim.getStderr().trim();
|
||
if (stderr) console.error('[kbdb_get wasm stderr]', stderr);
|
||
if (!stdout) throw new Error('WASM component produced no output');
|
||
return JSON.parse(stdout);
|
||
}
|