Files
writing-agent/src/server/skill-catalog.ts
T
chenmoranzhiandCursor f37388761b Keep creation as a living DAG and archive playable products separately.
Opening accept no longer locks the graph; session.json stays creation-only while play starts from an instance snapshot. Sidebar products nest run saves, and play reading fills the feed at a denser type size.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 00:52:05 +08:00

239 lines
6.5 KiB
TypeScript

import type { RuntimeSession } from "../types/runtime.js";
import {
deriveDesignStageScope,
deriveRunWorkerScope,
instantiateMeta,
parseWorkerSetYaml,
runWorkerMeta,
} from "../skills/worker-set-parse.js";
import {
canEnterPlay,
hasAcceptedWorkerSet,
inferLifecycleStage,
type LifecycleStage,
} from "../skills/worker-declaration.js";
export type { LifecycleStage };
export { canEnterPlay, hasAcceptedWorkerSet, inferLifecycleStage };
export type SkillCatalogEntry = {
id: string;
stage: "design" | "run";
label: string;
/** 这一步要干嘛(占位说明,详细设计后续补充) */
purpose: string;
status: "pending" | "active" | "done" | "skipped";
/** 同一 skill 被 invoke 的次数(design 阶段 instantiate 可多次) */
runCount?: number;
};
type CatalogTemplate = {
id: string;
stage: "design" | "run";
label: string;
purpose: string;
};
export type BuildSkillCatalogOptions = {
/** `设计.worker集` 或 `.草稿` 的 YAML 正文 */
workerSetYaml?: string;
};
function templatesFor(_skillPackId?: string): CatalogTemplate[] {
return [];
}
type ArtifactRunStats = {
total: number;
accepted: number;
pending: number;
rejected: number;
};
function collectArtifactRuns(session: RuntimeSession): Map<string, ArtifactRunStats> {
const map = new Map<string, ArtifactRunStats>();
for (const art of session.artifacts) {
if (!art.workerId) continue;
const prev = map.get(art.workerId) ?? {
total: 0,
accepted: 0,
pending: 0,
rejected: 0,
};
prev.total += 1;
if (art.status === "accepted") prev.accepted += 1;
else if (art.status === "rejected") prev.rejected += 1;
else prev.pending += 1;
map.set(art.workerId, prev);
}
return map;
}
function statusForWorkerId(
id: string,
session: RuntimeSession,
runs: Map<string, ArtifactRunStats>,
skipped: Set<string>,
): SkillCatalogEntry["status"] {
if (skipped.has(id)) return "skipped";
const stats = runs.get(id);
if (stats?.accepted) return "done";
if (session.currentWorkerId === id) return "active";
if (stats?.pending) return "active";
return "pending";
}
function buildWorldSimulatorCatalog(
session: RuntimeSession,
lifecycle: LifecycleStage,
workerSetYaml?: string,
): SkillCatalogEntry[] {
const workerSet = parseWorkerSetYaml(workerSetYaml);
const runs = collectArtifactRuns(session);
const skipped = new Set(workerSet?.instantiate_hints?.skip ?? []);
if (lifecycle === "play") {
const runIds = deriveRunWorkerScope(workerSet);
const entries: SkillCatalogEntry[] = [
{
id: "agent-burst",
stage: "run",
label: "总管调度",
purpose: "总管 tool loop:读黑板 → 选择下一步 Worker。",
status:
session.phase === "running" && !session.currentWorkerId
? "active"
: "pending",
},
];
for (const id of runIds) {
const meta = runWorkerMeta(id);
const stats = runs.get(id);
entries.push({
id,
stage: "run",
label: meta.label,
purpose: meta.purpose,
status: statusForWorkerId(id, session, runs, new Set()),
runCount: stats?.total || undefined,
});
}
return entries;
}
const designSteps: Array<{ id: string; label: string; purpose: string }> = [
{
id: "design-flow",
label: "创作 · 流程编排",
purpose: "编排/增量修订可变 DAG → 设计.创作流程(可反复编入同能力)。",
},
{
id: "design-step",
label: "创作 · 执行步骤",
purpose: "按已认可流程执行当前一步模块。",
},
];
const entries: SkillCatalogEntry[] = designSteps.map((step) => ({
id: step.id,
stage: "design" as const,
label: step.label,
purpose: step.purpose,
status: statusForWorkerId(step.id, session, runs, skipped),
runCount: runs.get(step.id)?.total || undefined,
}));
const planned = deriveDesignStageScope(workerSet);
for (const id of planned) {
const meta = instantiateMeta(id);
const stats = runs.get(id);
entries.push({
id,
stage: "design",
label: meta.label,
purpose: meta.purpose,
status: statusForWorkerId(id, session, runs, skipped),
runCount: stats?.total || undefined,
});
}
const designIds = new Set(designSteps.map((s) => s.id));
for (const [id, stats] of runs) {
if (designIds.has(id) || id === "design-intake") continue;
if (planned.includes(id) || skipped.has(id)) continue;
const meta = instantiateMeta(id);
entries.push({
id,
stage: "design",
label: meta.label,
purpose: meta.purpose,
status: statusForWorkerId(id, session, runs, skipped),
runCount: stats.total || undefined,
});
}
entries.push({
id: "declare-ready",
stage: "design",
label: "实例就绪",
purpose: "收口或 Worker 集验收后可保存产物;产物可单独开玩。",
status: hasAcceptedWorkerSet(session) ? "done" : "pending",
});
return entries;
}
export function buildSkillCatalog(
session: RuntimeSession,
skillPackId?: string,
lifecycle: LifecycleStage = inferLifecycleStage(session),
options?: BuildSkillCatalogOptions,
): SkillCatalogEntry[] {
if (skillPackId === "world-simulator") {
return buildWorldSimulatorCatalog(session, lifecycle, options?.workerSetYaml);
}
const templates = templatesFor(skillPackId);
const filtered = templates.filter((t) =>
lifecycle === "design" ? t.stage === "design" : t.stage === "run",
);
const runs = collectArtifactRuns(session);
return filtered.map((t) => {
let status: SkillCatalogEntry["status"] = "pending";
if (t.id === "intake") {
if (session.slots.startupCompleted) status = "done";
else if (
session.waitingReason?.kind === "intake" ||
session.waitingReason?.kind === "input"
) {
status = "active";
}
} else if (t.id === "declare-ready") {
if (session.slots.startupCompleted || hasAcceptedWorkerSet(session)) {
status = "done";
}
} else if (t.id === "agent-burst") {
if (session.phase === "running" && !session.currentWorkerId) {
status = "active";
}
} else if (runs.has(t.id)) {
status = statusForWorkerId(t.id, session, runs, new Set());
} else if (session.currentWorkerId === t.id) {
status = "active";
}
const runCount = runs.get(t.id)?.total;
return {
...t,
status,
runCount: runCount || undefined,
};
});
}
export const TOOL_LOOP_BURST_MAX = 12;