d63d58ccb5
- Move 15 old workflow-* packages to legacy-packages/ (inactive, preserved for reference)
- Rename templates/ → examples/ for clarity
- Rewrite docs/architecture.md to reflect current uwf architecture
- Active packages remain in packages/: cli-uwf, uwf-agent-hermes, uwf-agent-kit, uwf-moderator, uwf-protocol, workflow-util
小橘 🍊(NEKO Team)
200 lines
6.2 KiB
TypeScript
200 lines
6.2 KiB
TypeScript
import { join } from "node:path";
|
|
import { createCasStore, getContentMerklePayload, parseCasThreadNode } from "@uncaged/workflow-cas";
|
|
import { FORK_BRANCH_ROLE, walkStateFramesNewestFirst } from "@uncaged/workflow-execute";
|
|
import { END } from "@uncaged/workflow-runtime";
|
|
import { getGlobalCasDir } from "@uncaged/workflow-util";
|
|
import { Hono } from "hono";
|
|
|
|
import { pathExists } from "../../fs-utils.js";
|
|
import type { HistoricalThreadRow, ResolvedThreadRecord } from "../../thread-scan.js";
|
|
import {
|
|
listHistoricalThreads,
|
|
listRunningThreads,
|
|
resolveThreadListStatus,
|
|
resolveThreadRecord,
|
|
} from "../../thread-scan.js";
|
|
import { cmdKill, cmdPause, cmdResume } from "../thread/control.js";
|
|
import { cmdRun } from "../thread/run.js";
|
|
|
|
async function readStartInfo(
|
|
cas: ReturnType<typeof createCasStore>,
|
|
startHash: string,
|
|
): Promise<{ name: string | null; prompt: string | null }> {
|
|
const raw = await cas.get(startHash);
|
|
if (raw === null) return { name: null, prompt: null };
|
|
const parsed = parseCasThreadNode(raw);
|
|
if (parsed === null || parsed.kind !== "start") return { name: null, prompt: null };
|
|
const name = parsed.node.payload.name;
|
|
const promptHash = parsed.node.refs[0] ?? null;
|
|
let prompt: string | null = null;
|
|
if (promptHash !== null) {
|
|
prompt = await getContentMerklePayload(cas, promptHash);
|
|
}
|
|
return { name, prompt };
|
|
}
|
|
|
|
async function buildThreadDetailRecords(
|
|
storageRoot: string,
|
|
resolved: ResolvedThreadRecord,
|
|
runningMarkerPresent: boolean,
|
|
statusRow: HistoricalThreadRow,
|
|
): Promise<unknown[]> {
|
|
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
|
const frames = await walkStateFramesNewestFirst(cas, resolved.head);
|
|
const chronological = [...frames].reverse();
|
|
|
|
const { name: workflowName, prompt } = await readStartInfo(cas, resolved.start);
|
|
|
|
const status = await resolveThreadListStatus(storageRoot, statusRow, runningMarkerPresent);
|
|
|
|
const records: unknown[] = [
|
|
{
|
|
type: "thread-start",
|
|
workflow: workflowName ?? "unknown",
|
|
prompt: prompt ?? null,
|
|
threadId: resolved.threadId,
|
|
status,
|
|
timestamp: null,
|
|
},
|
|
];
|
|
|
|
for (const fr of chronological) {
|
|
if (fr.payload.role === FORK_BRANCH_ROLE) {
|
|
continue;
|
|
}
|
|
if (fr.payload.role === END) {
|
|
const returnCode = fr.payload.meta.returnCode;
|
|
const summary = fr.payload.meta.summary;
|
|
if (typeof returnCode === "number" && typeof summary === "string") {
|
|
records.push({
|
|
type: "workflow-result",
|
|
returnCode,
|
|
content: summary,
|
|
timestamp: fr.payload.timestamp,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
const payloadText = await getContentMerklePayload(cas, fr.payload.content);
|
|
const content =
|
|
payloadText !== null
|
|
? payloadText
|
|
: `(content not in CAS; contentHash=${fr.payload.content})`;
|
|
records.push({
|
|
type: "role",
|
|
role: fr.payload.role,
|
|
contentHash: fr.payload.content,
|
|
content,
|
|
meta: fr.payload.meta,
|
|
timestamp: fr.payload.timestamp,
|
|
});
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
export function createThreadRoutes(storageRoot: string): Hono {
|
|
const app = new Hono();
|
|
|
|
app.get("/", async (c) => {
|
|
const nameFilter = c.req.query("workflow") ?? null;
|
|
const rows = await listHistoricalThreads(storageRoot, nameFilter);
|
|
const threads = await Promise.all(
|
|
rows.map(async (r) => {
|
|
const runningPath = join(storageRoot, "logs", r.hash, `${r.threadId}.running`);
|
|
const runningMarkerPresent = await pathExists(runningPath);
|
|
const status = await resolveThreadListStatus(storageRoot, r, runningMarkerPresent);
|
|
return {
|
|
threadId: r.threadId,
|
|
workflow: r.workflowName,
|
|
hash: r.hash,
|
|
startedAt: new Date(r.activityTs).toISOString(),
|
|
status,
|
|
};
|
|
}),
|
|
);
|
|
return c.json({ threads });
|
|
});
|
|
|
|
app.get("/running", async (c) => {
|
|
const rows = await listRunningThreads(storageRoot);
|
|
return c.json({ threads: rows });
|
|
});
|
|
|
|
app.get("/:threadId", async (c) => {
|
|
const threadId = c.req.param("threadId");
|
|
const resolved = await resolveThreadRecord(storageRoot, threadId);
|
|
if (resolved === null) {
|
|
return c.json({ error: `thread not found: ${threadId}` }, 404);
|
|
}
|
|
const runningPath = join(storageRoot, "logs", resolved.bundleHash, `${threadId}.running`);
|
|
const runningMarkerPresent = await pathExists(runningPath);
|
|
const statusRow = {
|
|
threadId: resolved.threadId,
|
|
hash: resolved.bundleHash,
|
|
workflowName: null,
|
|
source: resolved.source,
|
|
activityTs: 0,
|
|
head: resolved.head,
|
|
};
|
|
const records = await buildThreadDetailRecords(
|
|
storageRoot,
|
|
resolved,
|
|
runningMarkerPresent,
|
|
statusRow,
|
|
);
|
|
return c.json({ threadId, records });
|
|
});
|
|
|
|
app.post("/", async (c) => {
|
|
let body: Record<string, unknown>;
|
|
try {
|
|
body = (await c.req.json()) as Record<string, unknown>;
|
|
} catch {
|
|
return c.json({ error: "invalid JSON body" }, 400);
|
|
}
|
|
|
|
const name = body.workflow;
|
|
const prompt = body.prompt;
|
|
|
|
if (typeof name !== "string" || typeof prompt !== "string") {
|
|
return c.json({ error: "workflow (string) and prompt (string) are required" }, 400);
|
|
}
|
|
|
|
const result = await cmdRun(storageRoot, name, prompt);
|
|
if (!result.ok) {
|
|
return c.json({ error: result.error }, 400);
|
|
}
|
|
return c.json({ threadId: result.value.threadId }, 201);
|
|
});
|
|
|
|
app.post("/:threadId/kill", async (c) => {
|
|
const threadId = c.req.param("threadId");
|
|
const result = await cmdKill(storageRoot, threadId);
|
|
if (!result.ok) {
|
|
return c.json({ error: result.error }, 400);
|
|
}
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.post("/:threadId/pause", async (c) => {
|
|
const threadId = c.req.param("threadId");
|
|
const result = await cmdPause(storageRoot, threadId);
|
|
if (!result.ok) {
|
|
return c.json({ error: result.error }, 400);
|
|
}
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
app.post("/:threadId/resume", async (c) => {
|
|
const threadId = c.req.param("threadId");
|
|
const result = await cmdResume(storageRoot, threadId);
|
|
if (!result.ok) {
|
|
return c.json({ error: result.error }, 400);
|
|
}
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
return app;
|
|
}
|