Files
writing-agent/src/skills/context-segments.ts
moranzhi ec474cb946 完善创作节点循环与等待态交互,并大幅打磨 Web 壳与会话运行时。
- 节点循环:配方开局直坐 DAG 第一步;验收后先问下一步意向,再确认开干并可钉编排参数;「按意见修改」只重跑当前节点。
- 询问分流:能力 opening 走说话面引导,可跳过追问按题干去重;追问与产物同轮挂载,避免拆成两段历史。
- 运行时:补强 revision 重跑、创作流程合并/进度指针、工具循环与 worker 执行;新增运行日志与 web:watch。
- 前端:统一等待态文案与底栏主按钮,完善询问卡/产物验收/呈现壳样式与交互。
- 同步世界模拟器模块提示、编排文档与相关测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 01:28:12 +08:00

261 lines
9.0 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.
/**
* Worker contextSegments 拼装:按 tier 顺序把黑板 tag 拼成 Markdown。
* 见 docs/context-assembly.md
*/
import type { Blackboard } from "../blackboard/blackboard.js";
import type { BlackboardInputMerge } from "../types/blackboard.js";
import { CONTEXT_BRIEF_TAG } from "../runtime/compress-after-worker.js";
import {
CREATION_ACCEPTED_CONTENT_TAG,
CREATION_ACCEPTED_UNITS_TAG,
formatAcceptedContentForPrompt,
formatAcceptedUnitsForPrompt,
} from "./creation-units.js";
import { projectFragmentContent } from "./context-fragment.js";
import {
buildHistoryFallback,
isDialogueHistoryRef,
projectDialogueHistory,
DIALOGUE_HISTORY_TAG,
} from "./dialogue-history.js";
export type ContextSegmentTier = "static" | "dynamic";
export type ContextSegmentDef = {
id: string;
/** 缓存提示;有序拼装时不再按 tier 重排 */
tier: ContextSegmentTier;
tags: string[];
label?: string;
/** latest | concat | tail_lines_N */
policy?: string;
/** 不读黑板,直接注入(如 worker.persona */
inline?: string;
/** context-order 投影级别full | summary | fields | fixed */
projection?: string;
};
export function parseContextSegments(raw: unknown): ContextSegmentDef[] {
if (!Array.isArray(raw)) return [];
const out: ContextSegmentDef[] = [];
for (const row of raw) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const r = row as Record<string, unknown>;
const id = typeof r.id === "string" ? r.id.trim() : "";
const tier = r.tier === "dynamic" ? "dynamic" : "static";
const tags = Array.isArray(r.tags)
? r.tags.filter((t): t is string => typeof t === "string" && t.trim()).map((t) => t.trim())
: [];
const inline = typeof r.inline === "string" ? r.inline.trim() : undefined;
if (!id || (tags.length === 0 && !inline)) continue;
out.push({
id,
tier,
tags,
label: typeof r.label === "string" ? r.label.trim() : undefined,
policy: typeof r.policy === "string" ? r.policy.trim() : undefined,
inline: inline || undefined,
projection: typeof r.projection === "string" ? r.projection.trim() : undefined,
});
}
return out;
}
function applyPolicy(content: string, policy?: string): string {
if (!policy || policy === "latest" || policy === "concat") return content;
const m = /^tail_lines_(\d+)$/.exec(policy);
if (m) {
const n = Number(m[1]);
if (Number.isFinite(n) && n > 0) {
const lines = content.split(/\r?\n/);
return lines.slice(-n).join("\n");
}
}
return content;
}
function readTagContent(
tag: string,
inputs: Record<string, string>,
blackboard: Blackboard,
inputMerge: BlackboardInputMerge,
): string {
if (inputs[tag]?.trim()) return inputs[tag]!.trim();
const items = blackboard.queryByPatterns([tag], inputMerge);
if (items.length === 0) return "";
if (inputMerge === "concat") {
return items.map((i) => i.content).filter(Boolean).join("\n\n");
}
return items[items.length - 1]?.content?.trim() ?? "";
}
function formatSegmentBody(
segment: ContextSegmentDef,
inputs: Record<string, string>,
blackboard: Blackboard,
inputMerge: BlackboardInputMerge,
): string {
if (segment.inline?.trim()) {
return applyProjection(segment.inline.trim(), segment.projection);
}
const parts: string[] = [];
for (const tag of segment.tags) {
let content: string;
if (isDialogueHistoryRef(tag)) {
content = resolveDialogueHistory(inputs, blackboard, inputMerge);
content = projectDialogueHistory(content, segment.projection);
} else {
content = readTagContent(tag, inputs, blackboard, inputMerge);
if (!content) continue;
if (tag === CREATION_ACCEPTED_CONTENT_TAG) {
content = formatAcceptedContentForPrompt(content);
} else if (tag === CREATION_ACCEPTED_UNITS_TAG) {
content = formatAcceptedUnitsForPrompt(content);
}
content = applyPolicy(content, segment.policy);
content = applyProjection(content, segment.projection);
}
if (!content.trim()) continue;
parts.push(content.trim());
}
return parts.join("\n\n");
}
function resolveDialogueHistory(
inputs: Record<string, string>,
blackboard: Blackboard,
inputMerge: BlackboardInputMerge,
): string {
const direct =
readTagContent(DIALOGUE_HISTORY_TAG, inputs, blackboard, inputMerge) ||
readTagContent("对话历史", inputs, blackboard, inputMerge);
return buildHistoryFallback({
dialogueHistory: direct,
eventStream: readTagContent("运行.事件流", inputs, blackboard, "concat"),
latestUser: readTagContent("用户.最新输入", inputs, blackboard, inputMerge),
});
}
function applyProjection(content: string, projection?: string): string {
if (!projection || projection === "full") return content;
return projectFragmentContent(content, projection);
}
/**
* 按 contextSegments 拼装 user 侧上下文。
* 无 segments 时回退为 JSON inputs兼容旧 skill
*/
export function assembleWorkerContext(params: {
inputs: Record<string, string>;
segments?: ContextSegmentDef[] | null;
blackboard: Blackboard;
inputMerge?: BlackboardInputMerge;
workerId: string;
workerName: string;
outputTags: string[];
}): string {
const inputMerge = params.inputMerge ?? "latest";
const segments = params.segments ?? [];
if (segments.length === 0) {
const revisionNote = (
params.inputs["用户.修订说明"] ??
""
).trim();
return JSON.stringify(
{
workerId: params.workerId,
workerName: params.workerName,
outputTags: params.outputTags,
inputs: params.inputs,
instruction: revisionNote
? "修订模式在已有产物inputs 中对应 outputTags上按「用户.修订说明」以及「用户.worker答复」中的追问作答选项与补充修改保留未点名要改的部分禁止无故整份重写。节点方法task/principles/probe/output仍须遵守。"
: "根据 SKILL 说明完成任务。若 inputs 含「上下文.定稿摘要」,以终产物为准。" +
"设计.worker集 / 草稿须为 JSON 对象文本。",
},
null,
2,
);
}
const blocks: string[] = [];
const render = (seg: ContextSegmentDef) => {
const body = formatSegmentBody(seg, params.inputs, params.blackboard, inputMerge);
if (!body) return;
if (seg.label) {
blocks.push(`${seg.label}\n\n${body}`);
} else {
blocks.push(body);
}
};
// 严格按 segments 数组顺序(投影排序表顺序);不再按 static/dynamic 重排
for (const seg of segments) render(seg);
// 定稿摘要:若未在 segments 中声明,仍附在末尾
const brief = params.inputs[CONTEXT_BRIEF_TAG]?.trim();
const briefInSegments = segments.some((s) => s.tags.includes(CONTEXT_BRIEF_TAG));
if (brief && !briefInSegments) {
blocks.push(`## 上下文.定稿摘要\n\n${brief}`);
}
// 修订态:把本步 outputTags 上已有正文钉成「待改底稿」,禁止无故推倒重写
const revisionNote = readTagContent(
"用户.修订说明",
params.inputs,
params.blackboard,
inputMerge,
).trim();
const draftBlocks: string[] = [];
if (revisionNote) {
for (const tag of params.outputTags) {
const alreadyInSegments = segments.some((s) => s.tags.includes(tag));
if (alreadyInSegments) continue;
const draft = readTagContent(
tag,
params.inputs,
params.blackboard,
inputMerge,
).trim();
if (!draft) continue;
draftBlocks.push(`### \`${tag}\`\n\n${draft}`);
}
if (draftBlocks.length) {
blocks.push(
[
"## 【待改底稿】",
"",
"下列为**当前已有产物**。按「用户.修订说明」以及「用户.worker答复」中的追问作答选项与补充在其上修改并写回同名 outputTags。",
"节点方法task / principles / probe / output仍须遵守。保留未要求改动的结构与结论禁止无故整份重写。",
"",
...draftBlocks,
].join("\n"),
);
}
}
const taskLines = [
"## 本步任务",
"",
`- workerId: \`${params.workerId}\``,
`- workerName: ${params.workerName}`,
`- outputTags: ${params.outputTags.map((t) => `\`${t}\``).join("、") || "(无)"}`,
"",
];
if (revisionNote) {
taskLines.push(
"**修订模式**:接着上方【待改底稿】(若有)与依赖产物,落实「用户.修订说明」以及「用户.worker答复」里的追问作答选项与补充意见。",
"不要从零另起一份;不要扩大修改面。标为「只读 / 已定稿」的分区仍不可改。节点方法仍有效。",
);
} else {
taskLines.push(
"按 SKILL 与上方分区完成任务。标为「只读 / 已定稿」的分区不要擅自改写;只改【本单位】范围。",
);
}
taskLines.push("设计.worker集 / 草稿须为 JSON 对象文本(以 `{` 开头)。");
blocks.push(taskLines.join("\n"));
return blocks.join("\n\n---\n\n");
}