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>
This commit is contained in:
2026-08-31 01:57:22 +08:00
co-authored by Cursor
parent 872f742d3e
commit bd5d88abfa
65 changed files with 7625 additions and 942 deletions
+7 -17
View File
@@ -2,7 +2,7 @@
* 上下文片段产物(context-fragment.v1)解析与展示辅助。
* 规范:docs/context-fragment-design.md
*/
import { extractJsonObjectText } from "./worker-set-parse.js";
import { tryParseJsonDoc } from "../parse/json-doc.js";
import {
normalizeQuestions,
type QuestionItem,
@@ -75,14 +75,9 @@ function normalizeStability(v: unknown): ContextFragmentStability | undefined {
export function parseContextFragment(raw: unknown): ContextFragment | undefined {
let row: Record<string, unknown> | null = null;
if (typeof raw === "string") {
const text = extractJsonObjectText(raw) ?? raw.trim();
try {
const parsed = JSON.parse(text) as unknown;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
row = parsed as Record<string, unknown>;
}
} catch {
return undefined;
const parsed = tryParseJsonDoc(raw);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
row = parsed as Record<string, unknown>;
}
} else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
row = raw as Record<string, unknown>;
@@ -278,14 +273,9 @@ export function extractFragmentAskSidecar(raw: unknown): {
} {
let row: Record<string, unknown> | null = null;
if (typeof raw === "string") {
const text = extractJsonObjectText(raw) ?? raw.trim();
try {
const parsed = JSON.parse(text) as unknown;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
row = parsed as Record<string, unknown>;
}
} catch {
return { questions: [], assessment: "" };
const parsed = tryParseJsonDoc(raw);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
row = parsed as Record<string, unknown>;
}
} else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
row = raw as Record<string, unknown>;
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -44,7 +44,8 @@ type WorkerTemplateDoc = {
/**
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
* - design-* / opening-generator 磁盘创作 worker → 始终 user_confirmed
* - design-flow → no_confirmation(图上点节点即确认;底栏输入即改编排)
* - design-step / opening-generator 磁盘创作 worker → 始终 user_confirmed
* - 游玩回合:一律 no_confirmation(一句输入连跑,不穿插验收)
* - Worker 集 acceptance: review → user_confirmed;continue → no_confirmation
* - 未声明 acceptance:创作默认确认
@@ -55,6 +56,7 @@ export function resolveAcceptanceModeForWorker(params: {
workerId: string;
}): AcceptanceMode {
const workerId = params.workerId.trim();
if (workerId === "design-flow") return "no_confirmation";
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
return "user_confirmed";
}
@@ -95,7 +97,7 @@ export async function resolveRunnableWorker(params: {
const acceptedRaw =
params.session.slots?.creationAcceptedUnits ??
params.blackboard.getContentByTag("创作.已验收单位");
const { parseAcceptedSteps } = await import("./creation-flow.js");
const { parseAcceptedSteps, isInheritExistingFlag } = await import("./creation-flow.js");
const withCtx = await loadWorkerSkillWithContext(
params.skillPackName,
params.workerId,
@@ -106,6 +108,9 @@ export async function resolveRunnableWorker(params: {
acceptedStepNames: parseAcceptedSteps(acceptedRaw),
selectedRecipeRef,
filledArtifactTags: params.blackboard.listTagIndex().map((item) => item.tag),
inheritExisting: isInheritExistingFlag(
params.blackboard.getContentByTag("创作.继承修改"),
),
},
);
return {
+258
View File
@@ -0,0 +1,258 @@
/**
* 扁平产物契约(wire format):机器字段薄,正文用 sections[] 字符串块。
* 渲染与旧 context-fragment.v1 深树通过 normalize 互转。
*/
export const FLAT_ARTIFACT_SCHEMA_ID = "artifact.flat.v1" as const;
export type FlatArtifactQuestion = {
id: string;
prompt: string;
options: string[];
};
export type FlatArtifactSection = {
id: string;
title: string;
text: string;
};
/** 所有需程序校验的创作产物尽量收成此形 */
export type FlatArtifact = {
schema: typeof FLAT_ARTIFACT_SCHEMA_ID | string;
skill: string;
brief: string;
mount: string[];
stability: "stable" | "semi" | "volatile" | "";
sections: FlatArtifactSection[];
open_questions: string[];
questions: FlatArtifactQuestion[];
summary: string;
};
/** OpenAI/DeepSeek strict 友好的 JSON Schema(全 required + additionalProperties:false) */
export const FLAT_ARTIFACT_JSON_SCHEMA: Record<string, unknown> = {
type: "object",
additionalProperties: false,
required: [
"schema",
"skill",
"brief",
"mount",
"stability",
"sections",
"open_questions",
"questions",
"summary",
],
properties: {
schema: { type: "string" },
skill: { type: "string" },
brief: { type: "string" },
mount: { type: "array", items: { type: "string" } },
stability: {
type: "string",
enum: ["stable", "semi", "volatile", ""],
},
sections: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["id", "title", "text"],
properties: {
id: { type: "string" },
title: { type: "string" },
text: { type: "string" },
},
},
},
open_questions: { type: "array", items: { type: "string" } },
questions: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["id", "prompt", "options"],
properties: {
id: { type: "string" },
prompt: { type: "string" },
options: { type: "array", items: { type: "string" } },
},
},
},
summary: { type: "string" },
},
};
export function isFlatArtifact(raw: unknown): raw is FlatArtifact {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
const row = raw as Record<string, unknown>;
return Array.isArray(row.sections);
}
/** 扁形 → 旧 fragment 外壳,便于现有渲染;正文按 section id 堆对象。
* section.text 若是 JSON 对象/数组字符串,解析回结构,避免专用卡吃到整段 stringify。
*/
export function flatArtifactToFragmentDoc(flat: FlatArtifact): Record<string, unknown> {
const 正文: Record<string, unknown> = {};
for (const s of flat.sections) {
const key = s.id || s.title;
if (!key) continue;
正文[key] = coerceSectionText(s.text);
}
return {
schema: "context-fragment.v1",
技能: flat.skill,
brief: flat.brief,
mount: flat.mount,
...(flat.stability ? { 稳变: flat.stability } : {}),
正文,
开放问题: flat.open_questions,
追问: {
导语: "",
题目: flat.questions.map((q) => ({
问: q.prompt,
建议选项: q.options,
})),
},
...(flat.summary ? { summary: flat.summary } : {}),
};
}
/** section 文本:纯散文保留;JSON 对象/数组还原为结构 */
export function coerceSectionText(text: string): unknown {
const raw = String(text ?? "").trim();
if (!raw) return "";
if (
!(
(raw.startsWith("{") && raw.endsWith("}")) ||
(raw.startsWith("[") && raw.endsWith("]"))
)
) {
return String(text ?? "");
}
try {
return JSON.parse(raw) as unknown;
} catch {
return String(text ?? "");
}
}
/** 旧深树 / 任意对象 → 尽量收成扁形(保存与投递归一) */
export function coerceToFlatArtifact(raw: unknown): FlatArtifact | null {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
const row = raw as Record<string, unknown>;
// DAG / worker 集 / 上下文排序:不是扁形正文稿,不能靠 brief 误收
if (
Array.isArray(row.steps) ||
Array.isArray(row.workers) ||
Array.isArray(row.agents) ||
Array.isArray(row.slots)
) {
return null;
}
if (isFlatArtifact(raw)) {
return {
schema:
typeof row.schema === "string" ? row.schema : FLAT_ARTIFACT_SCHEMA_ID,
skill: String(row.skill ?? row.技能 ?? ""),
brief: String(row.brief ?? ""),
mount: Array.isArray(row.mount)
? row.mount.filter((x): x is string => typeof x === "string")
: [],
stability:
row.stability === "stable" ||
row.stability === "semi" ||
row.stability === "volatile"
? row.stability
: row.稳变 === "stable" ||
row.稳变 === "semi" ||
row.稳变 === "volatile"
? row.稳变
: "",
sections: (row.sections as FlatArtifactSection[]).map((s) => ({
id: String(s?.id ?? ""),
title: String(s?.title ?? ""),
text: String(s?.text ?? ""),
})),
open_questions: Array.isArray(row.open_questions)
? row.open_questions.filter((x): x is string => typeof x === "string")
: Array.isArray(row.开放问题)
? row.开放问题.filter((x): x is string => typeof x === "string")
: [],
questions: Array.isArray(row.questions)
? (row.questions as FlatArtifactQuestion[]).map((q, i) => ({
id: String(q?.id ?? `q${i + 1}`),
prompt: String(q?.prompt ?? ""),
options: Array.isArray(q?.options)
? q.options.filter((x): x is string => typeof x === "string")
: [],
}))
: [],
summary: String(row.summary ?? ""),
};
}
const 正文 = row.正文 ?? row.body;
const sections: FlatArtifactSection[] = [];
if (正文 && typeof 正文 === "object" && !Array.isArray(正文)) {
for (const [key, val] of Object.entries(正文 as Record<string, unknown>)) {
sections.push({
id: key,
title: key,
text:
typeof val === "string"
? val
: JSON.stringify(val, null, 2),
});
}
} else if (typeof 正文 === "string" && 正文.trim()) {
sections.push({ id: "body", title: "正文", text: 正文 });
}
const probe = row.追问;
const questions: FlatArtifactQuestion[] = [];
if (probe && typeof probe === "object" && !Array.isArray(probe)) {
const topics = (probe as { 题目?: unknown }).题目;
if (Array.isArray(topics)) {
topics.forEach((t, i) => {
if (!t || typeof t !== "object") return;
const item = t as Record<string, unknown>;
questions.push({
id: `q${i + 1}`,
prompt: String(item.问 ?? item.prompt ?? ""),
options: Array.isArray(item.建议选项)
? item.建议选项.filter((x): x is string => typeof x === "string")
: Array.isArray(item.options)
? item.options.filter((x): x is string => typeof x === "string")
: [],
});
});
}
}
if (!sections.length) return null;
const stab = row.稳变 ?? row.stability;
return {
schema: FLAT_ARTIFACT_SCHEMA_ID,
skill: String(row.技能 ?? row.skill ?? ""),
brief: String(row.brief ?? ""),
mount: Array.isArray(row.mount)
? row.mount.filter((x): x is string => typeof x === "string")
: [],
stability:
stab === "stable" || stab === "semi" || stab === "volatile" ? stab : "",
sections,
open_questions: Array.isArray(row.开放问题)
? row.开放问题.filter((x): x is string => typeof x === "string")
: Array.isArray(row.open_questions)
? row.open_questions.filter((x): x is string => typeof x === "string")
: [],
questions,
summary: String(row.summary ?? ""),
};
}
+8 -2
View File
@@ -475,6 +475,8 @@ export async function loadWorkerSkillWithContext(
selectedRecipeRef?: string | null;
/** 黑板已有产物 tag,供编排器判断哪些技能已经做过 */
filledArtifactTags?: readonly string[];
/** 用户点已完成节点重进 */
inheritExisting?: boolean;
},
): Promise<{ worker: ParsedWorkerSkill; sharedContext: string | null; promptBody: string }> {
const worker = await loadWorkerSkill(skillIdOrName, workerId, skillsRoot);
@@ -526,6 +528,7 @@ export async function loadWorkerSkillWithContext(
currentStepName: opts?.currentStepName,
acceptedStepNames: opts?.acceptedStepNames,
skillsRoot,
inheritExisting: opts?.inheritExisting,
});
if (binding) {
const inheritTag = binding.inheritTag?.trim() || "";
@@ -536,8 +539,11 @@ export async function loadWorkerSkillWithContext(
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM);用户首答在「用户.worker答复」。勿重复同一开场白,在其答复与提示词基础上继续追问或产出。`
: "";
const paramsBlock = isPriorArtifactModule(binding.module)
? formatPriorArtifactContext(binding.step.params)
: `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
? formatPriorArtifactContext(
binding.step.params,
binding.prototypeSuggestion,
)
: `## 【本步参数】(编排期已钉;直接按此执行,勿再问编排层已定的参数)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
let rosterBlock = "";
const depProjection =
binding.module.id === "context-order" ? "index" : undefined;
+8
View File
@@ -198,3 +198,11 @@ export function closeCreationFlowRaw(raw: string | null | undefined): string | n
if (flow.status === "closed") return stringifyCreationFlow(flow);
return stringifyCreationFlow({ ...flow, status: "closed" });
}
/** 再编排:收口前补节点时把 DAG 改回 open。 */
export function reopenCreationFlowRaw(raw: string | null | undefined): string | null {
const flow = parseCreationFlow(raw);
if (!flow) return raw?.trim() ? raw : null;
if (flow.status !== "closed") return stringifyCreationFlow(flow);
return stringifyCreationFlow({ ...flow, status: "open" });
}
+5
View File
@@ -1,4 +1,5 @@
import { parse as parseYaml } from "yaml";
import { tryParseJsonDoc } from "../parse/json-doc.js";
import {
inferPlaySlotsFromWorkers,
mergeWorkersWithPlaySlots,
@@ -400,6 +401,10 @@ export function parseWorkerSetYaml(raw: string | undefined): ParsedWorkerSet | n
/** 从纯 JSON、```json 围栏或夹杂说明的文本中抽出对象字面量 */
export function extractJsonObjectText(raw: string): string | null {
const parsed = tryParseJsonDoc(raw);
if (parsed != null && typeof parsed === "object") {
return JSON.stringify(parsed);
}
const text = raw.trim();
if (!text) return null;
if (text.startsWith("{")) {