Files
writing-agent/src/skills/declared-worker.ts
T
chenmoranzhiandCursor bd5d88abfa Add capability-probed structured outputs and a prototype/instance creation graph.
Probe API profiles for json_schema/tools, flatten worker artifacts onto a strict schema, and let users spawn or revise graph nodes instead of a one-shot DAG.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 01:57:22 +08:00

334 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFile } from "node:fs/promises";
import path from "node:path";
import { parse as parseYaml } from "yaml";
import type { Blackboard } from "../blackboard/blackboard.js";
import type { RuntimeSession } from "../types/runtime.js";
import type { AcceptanceMode } from "../types/runtime.js";
import type { ParsedWorkerSkill } from "./types.js";
import {
parseWorkerSetYaml,
type ParsedWorkerSet,
type WorkerSetEntry,
} from "./worker-set-parse.js";
import {
buildInstanceWorkerDeclaration,
inferLifecycleStage,
readWorkerSetYamlForDeclaration,
} from "./worker-declaration.js";
import { loadWorkerSkill, SKILLS_ROOT } from "./loader.js";
import { CONTEXT_BRIEF_TAG } from "../runtime/compress-after-worker.js";
import { isDesignDiskWorker } from "./creation-units.js";
import {
entriesForWorker,
formatResidentPromptSection,
parseResidentContext,
residentTagFor,
} from "./resident-context.js";
import {
CONTEXT_ORDER_TAG,
contextOrderToSegments,
parseContextOrder,
slotOrderForRef,
} from "./context-order.js";
import type { ContextSegmentDef } from "./context-segments.js";
import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js";
type WorkerTemplateDoc = {
id?: string;
label?: string;
duty?: string;
prompt_excerpt?: string;
suggested_context?: { static?: string[]; dynamic?: string[] };
suggested_outputs?: string[];
};
/**
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
* - design-flow → no_confirmation(图上点节点即确认;底栏输入即改编排)
* - design-step / opening-generator 磁盘创作 worker → 始终 user_confirmed
* - 游玩回合:一律 no_confirmation(一句输入连跑,不穿插验收)
* - Worker 集 acceptance: review → user_confirmed;continue → no_confirmation
* - 未声明 acceptance:创作默认确认
*/
export function resolveAcceptanceModeForWorker(params: {
session: RuntimeSession;
blackboard: Blackboard;
workerId: string;
}): AcceptanceMode {
const workerId = params.workerId.trim();
if (workerId === "design-flow") return "no_confirmation";
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
return "user_confirmed";
}
if (inferLifecycleStage(params.session) === "play") {
return "no_confirmation";
}
const decl = buildInstanceWorkerDeclaration(
params.session,
params.blackboard,
inferLifecycleStage(params.session),
);
const entry = decl.parsed?.workers.find((w) => w.ref?.trim() === workerId);
if (entry?.acceptance === "continue") return "no_confirmation";
if (entry?.acceptance === "review") return "user_confirmed";
return "user_confirmed";
}
/** 从声明 + 可选模板构建可执行 worker;有磁盘 SKILL 时优先磁盘(design-intake) */
export async function resolveRunnableWorker(params: {
skillPackName: string;
workerId: string;
session: RuntimeSession;
blackboard: Blackboard;
skillsRoot?: string;
}): Promise<{
worker: ParsedWorkerSkill;
promptBody: string;
source: "disk" | "declaration";
}> {
const root = params.skillsRoot ?? SKILLS_ROOT;
try {
const { loadWorkerSkillWithContext } = await import("./loader.js");
const flowRaw = params.blackboard.getContentByTag("设计.创作流程");
const currentStepName = params.blackboard.getContentByTag("创作.当前步骤");
const selectedRecipeRef = params.blackboard.getContentByTag("创作.选用配方");
const acceptedRaw =
params.session.slots?.creationAcceptedUnits ??
params.blackboard.getContentByTag("创作.已验收单位");
const { parseAcceptedSteps, isInheritExistingFlag } = await import("./creation-flow.js");
const withCtx = await loadWorkerSkillWithContext(
params.skillPackName,
params.workerId,
root,
{
flowRaw,
currentStepName,
acceptedStepNames: parseAcceptedSteps(acceptedRaw),
selectedRecipeRef,
filledArtifactTags: params.blackboard.listTagIndex().map((item) => item.tag),
inheritExisting: isInheritExistingFlag(
params.blackboard.getContentByTag("创作.继承修改"),
),
},
);
return {
worker: withCtx.worker,
promptBody: withCtx.promptBody,
source: "disk",
};
} catch {
// fall through to declaration
}
const raw = readWorkerSetYamlForDeclaration(params.blackboard, params.session);
let parsed = raw ? parseWorkerSetYaml(raw.yaml) : null;
// 用户编排的排序表优先:规格内 context_order → 独立 tag
if (parsed && !parseContextOrder(parsed.context_order)) {
const fromTag = parseContextOrder(
params.blackboard.getContentByTag(CONTEXT_ORDER_TAG),
);
if (fromTag) parsed = { ...parsed, context_order: fromTag };
}
const entry = parsed?.workers.find(
(w) => w.ref?.trim() === params.workerId.trim(),
);
if (!entry?.ref) {
throw new Error(
`未找到 worker「${params.workerId}」的磁盘 SKILL,且 设计.worker集 中无对应声明`,
);
}
const template = await loadWorkerTemplate(
params.skillPackName,
entry.ref,
root,
);
const built = buildDeclaredWorkerSkill({
skillPackName: params.skillPackName,
entry,
template,
workerSet: parsed,
});
return { ...built, source: "declaration" };
}
async function loadWorkerTemplate(
skillPackName: string,
workerId: string,
skillsRoot: string,
): Promise<WorkerTemplateDoc | null> {
const { loadSkill } = await import("./loader.js");
let packRoot: string | undefined;
try {
const skill = await loadSkill(skillPackName, skillsRoot);
packRoot = skill.skillPackRoot;
} catch {
return null;
}
if (!packRoot) return null;
const file = path.join(
skillsRoot,
packRoot,
"worker-templates",
`${workerId}.yaml`,
);
try {
const raw = await readFile(file, "utf8");
const doc = parseYaml(raw) as WorkerTemplateDoc;
return doc && typeof doc === "object" ? doc : null;
} catch {
return null;
}
}
export function buildDeclaredWorkerSkill(params: {
skillPackName: string;
entry: WorkerSetEntry;
template: WorkerTemplateDoc | null;
workerSet: ParsedWorkerSet | null;
}): { worker: ParsedWorkerSkill; promptBody: string } {
const id = params.entry.ref!.trim();
const staticTags =
params.entry.context?.static ??
params.template?.suggested_context?.static ??
["设计.worker集", CONTEXT_BRIEF_TAG];
const dynamicTags =
params.entry.context?.dynamic ??
params.template?.suggested_context?.dynamic ??
["用户.最新输入"];
const outputTags =
params.entry.outputs?.length
? params.entry.outputs
: params.template?.suggested_outputs?.length
? params.template.suggested_outputs
: ["输出.用户展示"];
const resident = parseResidentContext(params.workerSet?.resident_context);
const residentForWorker = entriesForWorker(resident, id);
const residentStaticTags = residentForWorker
.filter((e) => e.position !== "dynamic")
.map(residentTagFor);
const residentDynamicTags = residentForWorker
.filter((e) => e.position === "dynamic")
.map(residentTagFor);
const duty =
params.entry.duty?.trim() ||
params.template?.duty?.trim() ||
`执行 ${id}`;
const excerpt = params.template?.prompt_excerpt?.trim() || "";
const visibleBodyRule =
id === "narrator" || id === "round-present"
? PLAY_VISIBLE_BODY_INSTRUCTION
: "";
const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n");
const presentation = params.entry.presentation
? JSON.stringify(params.entry.presentation, null, 2)
: "";
const narrative = params.workerSet?.narrative_guide?.trim() || "";
const premises = (params.workerSet?.core_premises ?? []).filter(Boolean);
const residentSection = formatResidentPromptSection(resident, id);
const personaText = [duty, excerptWithRule].filter(Boolean).join("\n\n");
const orderDoc = parseContextOrder(params.workerSet?.context_order);
const orderSlot = slotOrderForRef(orderDoc, id);
let contextSegments: ContextSegmentDef[] | undefined;
if (orderSlot && orderSlot.inserts.length) {
contextSegments = contextOrderToSegments({
slot: orderSlot,
personaText,
});
}
const orderTags = (contextSegments ?? []).flatMap((s) => s.tags);
const inputTags = [
...new Set([
...staticTags,
...residentStaticTags,
...dynamicTags,
...residentDynamicTags,
...orderTags,
CONTEXT_BRIEF_TAG,
]),
];
// 无 context_order 时:沿用模板 static→dynamic 两档 segments
if (!contextSegments?.length) {
contextSegments = [
...staticTags.map((tag, i) => ({
id: `static-${i}`,
tier: "static" as const,
tags: [tag],
label: `## ${tag}`,
})),
...residentStaticTags.map((tag, i) => ({
id: `resident-s-${i}`,
tier: "static" as const,
tags: [tag],
label: `## ${tag}`,
})),
...dynamicTags.map((tag, i) => ({
id: `dynamic-${i}`,
tier: "dynamic" as const,
tags: [tag],
label: `## ${tag}`,
})),
...residentDynamicTags.map((tag, i) => ({
id: `resident-d-${i}`,
tier: "dynamic" as const,
tags: [tag],
label: `## ${tag}`,
})),
];
}
const body = [
`# ${params.template?.label ?? id}`,
"",
"## 角色与职责",
"",
duty,
"",
excerptWithRule ? `## 写法要点\n\n${excerptWithRule}` : "",
presentation ? `## presentation(实例)\n\n\`\`\`json\n${presentation}\n\`\`\`` : "",
narrative ? `## 叙事指南\n\n${narrative}` : "",
premises.length
? `## 核心实现前提\n\n${premises.map((p) => `- ${p}`).join("\n")}`
: "",
residentSection,
params.entry.rationale
? `## 为何需要本 worker\n\n${params.entry.rationale}`
: "",
"",
"## 输出",
"",
`写入 outputTags:${outputTags.join("、")}`,
]
.filter(Boolean)
.join("\n");
const worker: ParsedWorkerSkill = {
id,
skill: params.skillPackName,
name: params.template?.label ?? id,
description: duty.slice(0, 200),
version: 1,
inputTags,
outputTags,
inputMerge: "latest",
path: `declaration:${id}`,
body,
contextSegments,
};
const promptBody = [
"(本 worker 由 设计.worker集 声明驱动,无独立磁盘 SKILL。)",
"",
body,
].join("\n");
return { worker, promptBody };
}