Files
writing-agent/src/skills/declared-worker.ts

261 lines
7.8 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";
type WorkerTemplateDoc = {
id?: string;
label?: string;
duty?: string;
prompt_excerpt?: string;
suggested_context?: { static?: string[]; dynamic?: string[] };
suggested_outputs?: string[];
};
/**
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
* - design-* / opening-generator 磁盘创作 worker → 始终 user_confirmed
* - Worker 集 acceptance: review → user_confirmedcontinue → no_confirmation
* - 未声明 acceptancedesign 生命周期默认确认play 缺省按 review稳妥
*/
export function resolveAcceptanceModeForWorker(params: {
session: RuntimeSession;
blackboard: Blackboard;
workerId: string;
}): AcceptanceMode {
const workerId = params.workerId.trim();
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
return "user_confirmed";
}
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 } = await import("./creation-flow.js");
const withCtx = await loadWorkerSkillWithContext(
params.skillPackName,
params.workerId,
root,
{
flowRaw,
currentStepName,
acceptedStepNames: parseAcceptedSteps(acceptedRaw),
selectedRecipeRef,
},
);
return {
worker: withCtx.worker,
promptBody: withCtx.promptBody,
source: "disk",
};
} catch {
// fall through to declaration
}
const raw = readWorkerSetYamlForDeclaration(params.blackboard, params.session);
const parsed = raw ? parseWorkerSetYaml(raw.yaml) : null;
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 inputTags = [
...new Set([
...staticTags,
...residentStaticTags,
...dynamicTags,
...residentDynamicTags,
CONTEXT_BRIEF_TAG,
]),
];
const duty =
params.entry.duty?.trim() ||
params.template?.duty?.trim() ||
`执行 ${id}`;
const excerpt = params.template?.prompt_excerpt?.trim() || "";
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 body = [
`# ${params.template?.label ?? id}`,
"",
"## 角色与职责",
"",
duty,
"",
excerpt ? `## 写法要点\n\n${excerpt}` : "",
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,
};
const promptBody = [
"(本 worker 由 设计.worker集 声明驱动,无独立磁盘 SKILL。",
"",
body,
].join("\n");
return { worker, promptBody };
}