feat(arcrun): recipe system + resumable workflow + component registry canon
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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
// Backfill component metadata 進 registry index
|
||||
// SDD: matrix/arcrun/.agents/specs/component-registry-canon/design.md Phase 1
|
||||
//
|
||||
// 用法:
|
||||
// node scripts/backfill-index.mjs --dry-run # 看會送什麼
|
||||
// node scripts/backfill-index.mjs # 真的灌
|
||||
//
|
||||
// 流程:
|
||||
// 1. 掃 ../components/*/component.contract.yaml
|
||||
// 2. 解析 YAML(用 zero-dep 簡易 parser,contract 是 well-formed YAML)
|
||||
// 3. 對每個 contract POST registry.arcrun.dev/components/index-only
|
||||
// 4. 印 success / already_indexed / fail 統計
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const COMPONENTS_DIR = join(__dirname, '..', 'components');
|
||||
const REGISTRY_URL = process.env.REGISTRY_URL ?? 'https://registry.arcrun.dev';
|
||||
const DRY_RUN = process.argv.includes('--dry-run');
|
||||
|
||||
// YAML 是 well-formed contract.yaml,用 js-yaml 解析最穩
|
||||
async function parseYaml(text) {
|
||||
const { load } = await import('js-yaml');
|
||||
return load(text);
|
||||
}
|
||||
|
||||
function listComponents() {
|
||||
return readdirSync(COMPONENTS_DIR)
|
||||
.filter((name) => {
|
||||
const p = join(COMPONENTS_DIR, name);
|
||||
return statSync(p).isDirectory();
|
||||
})
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function readContract(name) {
|
||||
const path = join(COMPONENTS_DIR, name, 'component.contract.yaml');
|
||||
const text = readFileSync(path, 'utf-8');
|
||||
return parseYaml(text);
|
||||
}
|
||||
|
||||
async function postIndexOnly(contract) {
|
||||
const res = await fetch(`${REGISTRY_URL}/components/index-only`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ contract }),
|
||||
});
|
||||
const body = await res.text();
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(body);
|
||||
} catch {
|
||||
parsed = { raw: body };
|
||||
}
|
||||
return { status: res.status, body: parsed };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== arcrun Component Registry Backfill ===');
|
||||
console.log(`Registry: ${REGISTRY_URL}`);
|
||||
console.log(`Mode: ${DRY_RUN ? 'DRY RUN' : 'LIVE'}`);
|
||||
console.log();
|
||||
|
||||
const names = listComponents();
|
||||
console.log(`Found ${names.length} components in ${COMPONENTS_DIR}`);
|
||||
console.log();
|
||||
|
||||
const stats = { created: 0, already: 0, fail: 0 };
|
||||
|
||||
for (const name of names) {
|
||||
let contract;
|
||||
try {
|
||||
contract = await readContract(name);
|
||||
} catch (e) {
|
||||
console.log(` ✗ ${name.padEnd(28)} READ FAIL: ${e.message}`);
|
||||
stats.fail++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const cid = contract.canonical_id ?? '(no canonical_id)';
|
||||
const ver = contract.version ?? '(no version)';
|
||||
|
||||
if (DRY_RUN) {
|
||||
console.log(` → ${name.padEnd(28)} ${cid} ${ver}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const { status, body } = await postIndexOnly(contract);
|
||||
if (status === 200 && body.already_indexed) {
|
||||
console.log(` = ${name.padEnd(28)} ${cid} ${ver} [already]`);
|
||||
stats.already++;
|
||||
} else if (status === 201) {
|
||||
console.log(` ✓ ${name.padEnd(28)} ${cid} ${ver} [${body.component_hash_id}]`);
|
||||
stats.created++;
|
||||
} else {
|
||||
console.log(` ✗ ${name.padEnd(28)} ${cid} ${ver} HTTP ${status}: ${JSON.stringify(body).slice(0, 200)}`);
|
||||
stats.fail++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ✗ ${name.padEnd(28)} POST FAIL: ${e.message}`);
|
||||
stats.fail++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log('=== Summary ===');
|
||||
console.log(`Created: ${stats.created}`);
|
||||
console.log(`Already indexed: ${stats.already}`);
|
||||
console.log(`Failed: ${stats.fail}`);
|
||||
process.exit(stats.fail > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('Fatal:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# 單一 component 註冊到 registry index
|
||||
# SDD: matrix/arcrun/.agents/specs/component-registry-canon/design.md Phase 2
|
||||
#
|
||||
# 用法:
|
||||
# bash scripts/register-component.sh <component_name>
|
||||
# REGISTRY_URL=https://registry.arcrun.dev bash scripts/register-component.sh kbdb_ingest
|
||||
#
|
||||
# CI deploy 流程內也使用同樣邏輯(見 .github/workflows/deploy.yml 的 Register step)
|
||||
# 此 script 是「本地 / hook 一致性」的 SSOT,CI 改邏輯時 script 跟著改
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
REGISTRY_URL="${REGISTRY_URL:-https://registry.arcrun.dev}"
|
||||
COMPONENT_NAME="${1:-}"
|
||||
|
||||
if [[ -z "$COMPONENT_NAME" ]]; then
|
||||
echo "Usage: $0 <component_name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
COMPONENT_DIR="$SCRIPT_DIR/../components/$COMPONENT_NAME"
|
||||
CONTRACT="$COMPONENT_DIR/component.contract.yaml"
|
||||
|
||||
if [[ ! -f "$CONTRACT" ]]; then
|
||||
echo "::warning::no component.contract.yaml at $COMPONENT_DIR" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 -c "import yaml" 2>/dev/null || {
|
||||
echo "需要 python3 + pyyaml" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
contract_json=$(python3 -c "
|
||||
import yaml, json
|
||||
with open('$CONTRACT') as f:
|
||||
c = yaml.safe_load(f)
|
||||
print(json.dumps({'contract': c}))
|
||||
") || {
|
||||
echo "::warning::無法解析 $CONTRACT" >&2
|
||||
exit 0
|
||||
}
|
||||
|
||||
echo "Registering $COMPONENT_NAME to $REGISTRY_URL ..."
|
||||
http_code=$(curl -s -o /tmp/reg-response.json -w "%{http_code}" \
|
||||
-X POST "$REGISTRY_URL/components/index-only" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$contract_json")
|
||||
|
||||
if [[ "$http_code" =~ ^(200|201)$ ]]; then
|
||||
echo "✓ $COMPONENT_NAME registered (HTTP $http_code)"
|
||||
cat /tmp/reg-response.json
|
||||
echo
|
||||
else
|
||||
echo "::warning::Registry 註冊失敗 HTTP $http_code" >&2
|
||||
cat /tmp/reg-response.json || true
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user