Files
united-workforce/legacy-packages/cli-workflow/src/commands/connect/routes-thread.ts
T
xiaoju d63d58ccb5 chore: reorganize repo — legacy packages to legacy-packages/, templates to examples/
- 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)
2026-05-19 07:19:40 +00:00

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;
}