feat: Phase 2 — Thread lifecycle, execution engine, worker, CLI

- types.ts: START/END, RoleMeta, ThreadContext, Role, Moderator, WorkflowDefinition
- engine.ts: executeThread with JSONL persistence + AbortSignal
- worker.ts: per-bundle process, TCP IPC, kill individual threads
- CLI: run/ps/kill/threads/thread/thread rm commands
- 32 tests pass, biome clean

小橘 <xiaoju@shazhou.work>
This commit is contained in:
2026-05-06 04:59:54 +00:00
parent 01e930df8f
commit 7582a88d6b
46 changed files with 2829 additions and 167 deletions
+77
View File
@@ -0,0 +1,77 @@
import { readFile, stat } from "node:fs/promises";
import { basename, resolve } from "node:path";
import {
err,
hashWorkflowBundleBytes,
ok,
type Result,
readWorkflowRegistry,
registerWorkflowVersion,
validateWorkflowBundle,
writeWorkflowRegistry,
} from "@uncaged/workflow";
import { storeWorkflowBundleCopy } from "./bundle-store.js";
import { validateCliWorkflowName } from "./workflow-name.js";
export async function cmdAdd(
storageRoot: string,
name: string,
filePath: string,
): Promise<Result<{ hash: string }, string>> {
const nameOk = validateCliWorkflowName(name);
if (!nameOk.ok) {
return nameOk;
}
let resolvedPath: string;
try {
resolvedPath = resolve(filePath);
await stat(resolvedPath);
} catch {
return err(`bundle file not found: ${filePath}`);
}
let source: string;
try {
source = await readFile(resolvedPath, "utf8");
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return err(`failed to read bundle: ${message}`);
}
const validated = validateWorkflowBundle({
filePath: resolvedPath,
source,
});
if (!validated.ok) {
return validated;
}
const encoder = new TextEncoder();
const bytes = encoder.encode(source);
const hash = hashWorkflowBundleBytes(bytes);
const stored = await storeWorkflowBundleCopy(storageRoot, hash, resolvedPath, source);
if (!stored.ok) {
return stored;
}
const reg = await readWorkflowRegistry(storageRoot);
if (!reg.ok) {
return err(reg.error.message);
}
const next = registerWorkflowVersion(reg.value, name, hash, Date.now());
const written = await writeWorkflowRegistry(storageRoot, next);
if (!written.ok) {
return err(written.error.message);
}
return ok({ hash });
}
export function formatAddSuccess(name: string, filePath: string, hash: string): string {
return `registered workflow "${name}" from ${basename(filePath)} as ${hash}`;
}