@world-engines/dag-flow 0.1.3-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,704 @@
1
+ /** 浏览器和 Node 共用的作者 Agent DAG 文档。 */
2
+ /** 当前持久化版本。读取器仍接受 v1,并通过 upgradeDagDocument 显式规范化。 */
3
+ export const DAG_FLOW_SCHEMA_VERSION = 2;
4
+ export const DAG_FLOW_LEGACY_SCHEMA_VERSION = 1;
5
+ export const DAG_FLOW_SOURCE_SECTION_KEY = "view.agent_dag";
6
+ export const DAG_FLOW_VIEW_PERMISSION = "view";
7
+ export const SYSTEM_USER_INPUT_NODE_ID = "user_input";
8
+ export const SYSTEM_DIRECTOR_NODE_ID = "director";
9
+ export const SYSTEM_TEXT_OUTPUT_NODE_ID = "text_output";
10
+ /** 作者 Agent 只能插入这三个由系统调度的阶段,不能改写系统主线。 */
11
+ export const DAG_INSERTION_SLOTS = ["before_director", "after_scribe", "after_writer"];
12
+ export const DAG_STAGE_BOUNDARIES = [
13
+ { slot: "before_director", start_id: "stage.before_director.start", handoff_id: "stage.before_director.handoff" },
14
+ { slot: "after_scribe", start_id: "stage.after_scribe.start", handoff_id: "stage.after_scribe.handoff" },
15
+ { slot: "after_writer", start_id: "stage.after_writer.start", handoff_id: "stage.after_writer.handoff" },
16
+ ];
17
+ /** 仅供界面展示的真实系统角色;它们不是可编辑 DAG 节点。 */
18
+ export const SYSTEM_AGENT_ROLE_CATALOG = [
19
+ { id: "director", label: "Director" },
20
+ { id: "screenwriter", label: "Screenwriter" },
21
+ { id: "scribe", label: "Scribe" },
22
+ { id: "writer", label: "Writer" },
23
+ { id: "scene", label: "Scene" },
24
+ { id: "photographer", label: "Photographer" },
25
+ { id: "voice", label: "Voice" },
26
+ ];
27
+ /** 编剧召回是 WASM 检索工具而非冻结 LLM Agent,不能承载可执行提示词覆盖。 */
28
+ export const EDITABLE_SYSTEM_AGENT_ROLES = ["director", "scribe", "writer", "scene", "photographer", "voice"];
29
+ export function defaultSystemAgentPromptBlocks(role) {
30
+ const task = {
31
+ director: "根据玩家输入、历史与场景上下文决定后续剧情,并输出可校验的结构化手稿。",
32
+ screenwriter: "根据手稿和场景上下文检索、整理与剧情相关的数据库证据。",
33
+ scribe: "依据手稿、场景和触发器规则准备状态刷新,并报告可提交或需要打回的原因。",
34
+ writer: "严格遵循手稿润色为面向玩家的正文,不新增手稿以外的剧情、动作或台词。每回合只依据回合开始时冻结的 Scene snapshot 与 authority 工作;若最终手稿产生了新的短期事实、在场对象或位置,提出仅包含 scene.context.*、scene.present.*、scene.location.set 的 scene_patch,并附逐字手稿证据、scene id、revision、冻结投影摘要与操作字节摘要。不得通过 scene_patch 修改 worldTime、Scene note、数据库属性或任意 graph。scene_patch 必须与正文在同一 Writer 存档事务中提交并取得 receipt;stale revision、对象或地点身份无效、authority 不匹配或无变化时接受拒绝,不得伪造 revision 或盲重试,下一回合从重新读取的冻结 snapshot 重新判断。",
35
+ scene: "维护当前 Scene cache,并把 contextItems、presentEntityNodeIds 与 location 作为相互独立的字段处理。每回合以冻结 snapshot、scene id、revision 与 authority 为唯一输入;只根据最终手稿提出 scene.context.*、scene.present.*、scene.location.set 的 scene_patch,临时信息必须为字符串,位置必须保持 GPS 或预绘制 location 的完整身份。worldTime 只消费已封印的时间增量,禁止由 Writer 或 scene_patch 修改。patch 与正文必须在同一 Writer 存档事务中原子提交并绑定 receipt;stale revision、缺失对象、错误地点身份、authority 不匹配或无变化时拒绝,随后重新读取 snapshot,不得沿用旧 patch、伪造 revision 或绕过冻结。",
36
+ photographer: "依据已确认手稿准备演出图像指令,不扩展剧情。",
37
+ voice: "依据手稿中的逐字台词准备语音演出指令,不改写台词。",
38
+ };
39
+ return [
40
+ { id: "system-task", name: "Agent 任务", role: "system", content: task[role] },
41
+ { id: "system-primer", name: "System Primer", role: "system", content: "" },
42
+ { id: "primer", name: "Primer", role: "user", content: "" },
43
+ { id: "anchor", name: "Anchor", role: "assistant", content: "" },
44
+ ];
45
+ }
46
+ const FIXED_EDGES = [
47
+ { id: "system.user_input.director", from: { node_id: SYSTEM_USER_INPUT_NODE_ID, port: "output" }, to: { node_id: SYSTEM_DIRECTOR_NODE_ID, port: "input" } },
48
+ { id: "system.director.text_output", from: { node_id: SYSTEM_DIRECTOR_NODE_ID, port: "output" }, to: { node_id: SYSTEM_TEXT_OUTPUT_NODE_ID, port: "input" } },
49
+ ];
50
+ const SYSTEM_NODES = [
51
+ [SYSTEM_USER_INPUT_NODE_ID, "user_input"],
52
+ [SYSTEM_DIRECTOR_NODE_ID, "director"],
53
+ [SYSTEM_TEXT_OUTPUT_NODE_ID, "text_output"],
54
+ ];
55
+ export function createEmptyDagDocument() {
56
+ return {
57
+ schema_version: DAG_FLOW_SCHEMA_VERSION,
58
+ section_key: DAG_FLOW_SOURCE_SECTION_KEY,
59
+ permission: DAG_FLOW_VIEW_PERMISSION,
60
+ nodes: [
61
+ { id: SYSTEM_USER_INPUT_NODE_ID, kind: "user_input", system: true },
62
+ { id: SYSTEM_DIRECTOR_NODE_ID, kind: "director", system: true },
63
+ { id: SYSTEM_TEXT_OUTPUT_NODE_ID, kind: "text_output", system: true, name: "Text Output" },
64
+ ],
65
+ edges: FIXED_EDGES.map(copyEdge),
66
+ stage_boundaries: DAG_STAGE_BOUNDARIES.map((boundary) => ({ ...boundary })),
67
+ };
68
+ }
69
+ /** 创建可序列化、可执行的三阶段控制流图。默认 start → handoff 可由作者删除后重新接线。 */
70
+ export function createStageDagDocument() {
71
+ const base = createEmptyDagDocument();
72
+ const boundaries = DAG_STAGE_BOUNDARIES.map((boundary) => ({ ...boundary }));
73
+ const nodes = [
74
+ ...base.nodes,
75
+ ...boundaries.flatMap((boundary) => [
76
+ { id: boundary.start_id, kind: "stage_start", slot: boundary.slot, system: true },
77
+ { id: boundary.handoff_id, kind: "stage_handoff", slot: boundary.slot, system: true },
78
+ ]),
79
+ ];
80
+ const stageEdges = boundaries.map((boundary) => ({
81
+ id: `system.stage.${boundary.slot}.direct`,
82
+ from: { node_id: boundary.start_id, port: "output" },
83
+ to: { node_id: boundary.handoff_id, port: "input" },
84
+ }));
85
+ return { ...base, nodes, edges: [...base.edges, ...stageEdges], stage_boundaries: boundaries, stage_flow: { enabled: true }, execution: { terminal_on_text_output: true } };
86
+ }
87
+ /**
88
+ * 将 v1 文档升级为 v2。升级只补齐明确的阶段边界,不猜测节点之间的作者连线;
89
+ * 形状不明确或版本过新时拒绝,避免把旧数据静默解释成另一条执行路径。
90
+ */
91
+ export function upgradeDagDocument(value) {
92
+ if (!isRecord(value))
93
+ throw new TypeError("dag_document_shape");
94
+ if (value.schema_version === DAG_FLOW_SCHEMA_VERSION) {
95
+ const result = validateDagDocument(value, { mode: "draft" });
96
+ if (!result.ok)
97
+ throw new DagDocumentValidationErrorLike(result.issues);
98
+ return { document: value, migrated: false };
99
+ }
100
+ if (value.schema_version !== DAG_FLOW_LEGACY_SCHEMA_VERSION)
101
+ throw new TypeError("dag_schema_version_unsupported");
102
+ const result = validateDagDocument(value, { mode: "draft" });
103
+ if (!result.ok)
104
+ throw new DagDocumentValidationErrorLike(result.issues);
105
+ const legacy = value;
106
+ const authorNodes = legacy.nodes.filter((node) => !isSystemNode(node));
107
+ if (legacy.insertion_slots !== undefined) {
108
+ if (authorNodes.some((node) => DAG_STAGE_BOUNDARIES.some((boundary) => node.id === boundary.start_id || node.id === boundary.handoff_id)))
109
+ throw new TypeError("dag_legacy_stage_id_conflict");
110
+ const staged = createStageDagDocument();
111
+ const edges = [...FIXED_EDGES.map(copyEdge)];
112
+ for (const boundary of DAG_STAGE_BOUNDARIES) {
113
+ const members = legacy.insertion_slots[boundary.slot] ?? [];
114
+ const chain = [boundary.start_id, ...members, boundary.handoff_id];
115
+ for (let index = 0; index < chain.length - 1; index += 1)
116
+ edges.push({
117
+ id: `stage.${boundary.slot}.${index}`,
118
+ from: { node_id: chain[index], port: index === 0 ? "output" : "success" },
119
+ to: { node_id: chain[index + 1], port: "input" },
120
+ });
121
+ }
122
+ return {
123
+ migrated: true,
124
+ document: {
125
+ ...staged,
126
+ nodes: [...staged.nodes.filter((node) => isSystemNode(node)), ...authorNodes],
127
+ edges,
128
+ ...(legacy.system_agent_prompts === undefined ? {} : { system_agent_prompts: legacy.system_agent_prompts }),
129
+ ...(legacy.positions === undefined ? {} : { positions: legacy.positions }),
130
+ },
131
+ };
132
+ }
133
+ if (authorNodes.length > 0)
134
+ throw new TypeError("dag_legacy_graph_ambiguous");
135
+ return {
136
+ migrated: true,
137
+ document: { ...legacy, schema_version: DAG_FLOW_SCHEMA_VERSION, stage_boundaries: DAG_STAGE_BOUNDARIES.map((boundary) => ({ ...boundary })) },
138
+ };
139
+ }
140
+ /** 供 document.ts 使用的轻量错误,避免依赖 executor.ts 形成循环导入。 */
141
+ class DagDocumentValidationErrorLike extends Error {
142
+ issues;
143
+ constructor(issues) { super("dag_document_invalid"); this.issues = issues; }
144
+ }
145
+ /** 对不可信 JSON 文档只返回问题清单,永不因形状错误抛出。 */
146
+ export function validateDagDocument(document, options = {}) {
147
+ const mode = options.mode ?? "draft";
148
+ const issues = [];
149
+ if (!isRecord(document)) {
150
+ issue(issues, "document_shape", "DAG 文档必须是对象");
151
+ return { ok: false, issues };
152
+ }
153
+ if (document.schema_version !== DAG_FLOW_SCHEMA_VERSION && document.schema_version !== DAG_FLOW_LEGACY_SCHEMA_VERSION)
154
+ issue(issues, "schema_version", "只支持 schema_version 1 或 2");
155
+ if (document.section_key !== DAG_FLOW_SOURCE_SECTION_KEY)
156
+ issue(issues, "section_key", "DAG 必须属于 view.agent_dag section");
157
+ if (document.permission !== DAG_FLOW_VIEW_PERMISSION)
158
+ issue(issues, "permission", "DAG 只允许 view 权限");
159
+ const nodes = new Map();
160
+ if (!Array.isArray(document.nodes))
161
+ issue(issues, "nodes_shape", "nodes 必须是数组");
162
+ else
163
+ for (const candidate of document.nodes) {
164
+ if (!isRecord(candidate)) {
165
+ issue(issues, "node_shape", "节点必须是对象");
166
+ continue;
167
+ }
168
+ const id = candidate.id;
169
+ if (!validId(id)) {
170
+ issue(issues, "node_id", "节点 id 必须是非空稳定字符串");
171
+ continue;
172
+ }
173
+ if (nodes.has(id))
174
+ issue(issues, "duplicate_node", "节点 id 必须唯一", id);
175
+ if (!isNodeKind(candidate.kind)) {
176
+ issue(issues, "node_kind", "节点 kind 不受支持", id);
177
+ continue;
178
+ }
179
+ const node = candidate;
180
+ nodes.set(id, node);
181
+ validateNode(node, issues);
182
+ }
183
+ validateFixedNodes(nodes, document.stage_flow, issues);
184
+ const slottedNodes = validateInsertionSlots(document.insertion_slots, nodes, issues);
185
+ validateSystemAgentPrompts(document.system_agent_prompts, issues);
186
+ validateStageBoundaries(document.stage_boundaries, document.schema_version, issues);
187
+ if (document.execution !== undefined && (!isRecord(document.execution) || (document.execution.terminal_on_text_output !== undefined && typeof document.execution.terminal_on_text_output !== "boolean")))
188
+ issue(issues, "execution_shape", "execution.terminal_on_text_output 必须是布尔值");
189
+ const edges = new Map();
190
+ const incoming = new Map();
191
+ const outgoing = new Map();
192
+ if (!Array.isArray(document.edges))
193
+ issue(issues, "edges_shape", "edges 必须是数组");
194
+ else
195
+ for (const candidate of document.edges) {
196
+ if (!isEdge(candidate)) {
197
+ issue(issues, "edge_shape", "边必须包含 id、from 与 to");
198
+ continue;
199
+ }
200
+ const edge = candidate;
201
+ if (edges.has(edge.id))
202
+ issue(issues, "duplicate_edge", "边 id 必须唯一", undefined, edge.id);
203
+ edges.set(edge.id, edge);
204
+ const from = nodes.get(edge.from.node_id);
205
+ const to = nodes.get(edge.to.node_id);
206
+ if (!from)
207
+ issue(issues, "edge_from_missing", "边起点节点不存在", undefined, edge.id);
208
+ if (!to)
209
+ issue(issues, "edge_to_missing", "边终点节点不存在", undefined, edge.id);
210
+ if (!from || !to)
211
+ continue;
212
+ const validFromPort = isOutputPort(from, edge.from.port);
213
+ const validToPort = isInputPort(to, edge.to.port);
214
+ if (!validFromPort)
215
+ issue(issues, "edge_from_port", "边起点端口无效", from.id, edge.id);
216
+ if (!validToPort)
217
+ issue(issues, "edge_to_port", "边终点端口无效", to.id, edge.id);
218
+ if (!validFromPort || !validToPort)
219
+ continue;
220
+ const targetKey = key(edge.to.node_id, edge.to.port);
221
+ const inbound = incoming.get(targetKey) ?? [];
222
+ if (inbound.length > 0)
223
+ issue(issues, "input_fanin", "每个输入端口只能有一条入边,保证路径唯一", to.id, edge.id);
224
+ inbound.push(edge);
225
+ incoming.set(targetKey, inbound);
226
+ const sourceKey = key(edge.from.node_id, edge.from.port);
227
+ const list = outgoing.get(sourceKey) ?? [];
228
+ list.push(edge);
229
+ outgoing.set(sourceKey, list);
230
+ }
231
+ validateFixedEdges(edges, issues);
232
+ if (document.stage_flow !== undefined)
233
+ validateStageFlow(document.stage_flow, nodes, edges, incoming, outgoing, issues);
234
+ if (document.insertion_slots !== undefined)
235
+ validateControlledSlotMode(nodes, edges, slottedNodes, issues);
236
+ if (hasCycle(nodes, outgoing))
237
+ issue(issues, "cycle", "Agent DAG 不允许环;循环应由系统运行时处理");
238
+ if (mode !== "draft") {
239
+ if (document.stage_flow !== undefined)
240
+ validateStageRunnable(nodes, incoming, outgoing, issues);
241
+ else
242
+ validateRunnable(nodes, incoming, outgoing, issues, slottedNodes);
243
+ }
244
+ return { ok: issues.length === 0, issues };
245
+ }
246
+ function validateStageBoundaries(value, version, issues) {
247
+ if (value === undefined) {
248
+ if (version === DAG_FLOW_SCHEMA_VERSION)
249
+ issue(issues, "stage_boundaries_missing", "schema_version 2 必须保存三个阶段的控制流边界");
250
+ return;
251
+ }
252
+ if (!Array.isArray(value) || value.length !== DAG_STAGE_BOUNDARIES.length) {
253
+ issue(issues, "stage_boundaries_shape", "stage_boundaries 必须包含三个阶段边界");
254
+ return;
255
+ }
256
+ for (const expected of DAG_STAGE_BOUNDARIES) {
257
+ const actual = value.find((item) => isRecord(item) && item.slot === expected.slot);
258
+ if (!actual || actual.start_id !== expected.start_id || actual.handoff_id !== expected.handoff_id)
259
+ issue(issues, "stage_boundary_invalid", `阶段 ${expected.slot} 的边界不可修改`);
260
+ }
261
+ }
262
+ /** 判断值能否完整且无损地作为 DAG 内部的 JSON 值传递。 */
263
+ export function isDagJson(value) {
264
+ try {
265
+ return isDagJsonValue(value, new Set());
266
+ }
267
+ catch {
268
+ return false;
269
+ }
270
+ }
271
+ export function dagMixedToString(value) { return typeof value === "string" ? value : JSON.stringify(value); }
272
+ function validateNode(node, issues) {
273
+ if (node.kind === "user_input" || node.kind === "director" || node.kind === "stage_start" || node.kind === "stage_handoff") {
274
+ if (!hasSystem(node))
275
+ issue(issues, "system_node_mutated", `系统节点 ${node.id} 不可修改`, node.id);
276
+ if ((node.kind === "stage_start" || node.kind === "stage_handoff") && !DAG_INSERTION_SLOTS.includes(node.slot))
277
+ issue(issues, "stage_node_slot", "阶段边界必须属于已知阶段", node.id);
278
+ return;
279
+ }
280
+ if (node.kind === "agent") {
281
+ if (Object.hasOwn(node, "system"))
282
+ issue(issues, "system_node_extra", "作者节点不得标记为系统节点", node.id);
283
+ if (!isRecord(node.config)) {
284
+ issue(issues, "agent_config", "Agent 必须提供 config", node.id);
285
+ return;
286
+ }
287
+ if (typeof node.config.system_prompt !== "string" || !node.config.system_prompt.trim())
288
+ issue(issues, "agent_system_prompt", "Agent 必须提供 system_prompt", node.id);
289
+ if (typeof node.config.model_id !== "string" || !node.config.model_id.trim())
290
+ issue(issues, "agent_model_id", "Agent 必须提供 model_id", node.id);
291
+ if (typeof node.config.require_json !== "boolean")
292
+ issue(issues, "agent_require_json", "require_json 必须是布尔值", node.id);
293
+ if (node.config.prompt_injection !== "enabled" && node.config.prompt_injection !== "disabled")
294
+ issue(issues, "agent_prompt_injection", "prompt_injection 只能是 enabled 或 disabled", node.id);
295
+ if (!isRecord(node.config.templates) || !isMessageTemplates(node.config.templates))
296
+ issue(issues, "agent_template", "Agent 模板必须恰好为 system/primer/anchor 三段消息", node.id);
297
+ if (node.config.prompt_blocks !== undefined && !validPromptBlocks(node.config.prompt_blocks))
298
+ issue(issues, "agent_prompt_blocks", "提示词对象必须具有唯一 id、名称、合法角色和文本", node.id);
299
+ return;
300
+ }
301
+ if (node.kind === "script") {
302
+ if (Object.hasOwn(node, "system"))
303
+ issue(issues, "system_node_extra", "作者节点不得标记为系统节点", node.id);
304
+ if (!isRecord(node.config)) {
305
+ issue(issues, "script_config", "脚本节点必须提供 config", node.id);
306
+ return;
307
+ }
308
+ if (typeof node.config.source !== "string" || !node.config.source.trim())
309
+ issue(issues, "script_source", "脚本节点必须提供 source", node.id);
310
+ if (node.config.entrypoint !== undefined && node.config.entrypoint !== "main")
311
+ issue(issues, "script_entrypoint", "脚本 entrypoint 只能是 main", node.id);
312
+ if (node.config.paths !== undefined && !Array.isArray(node.config.paths))
313
+ issue(issues, "script_paths", "脚本 paths 必须是数组", node.id);
314
+ const paths = scriptOutputPorts(node);
315
+ if (new Set(paths).size !== paths.length || paths.some((path) => !validId(path)))
316
+ issue(issues, "script_paths", "脚本 path 必须是唯一的非空字符串", node.id);
317
+ return;
318
+ }
319
+ if (node.id === SYSTEM_TEXT_OUTPUT_NODE_ID) {
320
+ if (!hasSystem(node))
321
+ issue(issues, "system_node_mutated", `系统节点 ${node.id} 不可修改`, node.id);
322
+ }
323
+ else if (Object.hasOwn(node, "system") || typeof node.name !== "string" || !node.name.trim()) {
324
+ issue(issues, "text_output_name", "作者 Text Output 必须提供名称且不可标记为系统节点", node.id);
325
+ }
326
+ }
327
+ function validPromptBlocks(value) {
328
+ const ids = new Set();
329
+ return Array.isArray(value) && value.length > 0 && !value.some((block) => {
330
+ if (!isRecord(block) || typeof block.id !== "string" || !block.id.trim() || ids.has(block.id)
331
+ || typeof block.name !== "string" || !block.name.trim()
332
+ || !["system", "assistant", "user"].includes(block.role)
333
+ || typeof block.content !== "string")
334
+ return true;
335
+ ids.add(block.id);
336
+ return false;
337
+ });
338
+ }
339
+ function validateSystemAgentPrompts(value, issues) {
340
+ if (value === undefined)
341
+ return;
342
+ if (!isRecord(value)) {
343
+ issue(issues, "system_agent_prompts_shape", "system_agent_prompts 必须是系统角色到提示词对象数组的映射");
344
+ return;
345
+ }
346
+ const roles = new Set(SYSTEM_AGENT_ROLE_CATALOG.map((role) => role.id));
347
+ for (const [role, blocks] of Object.entries(value)) {
348
+ if (!roles.has(role)) {
349
+ issue(issues, "system_agent_prompt_role", `未知系统 Agent ${role}`);
350
+ continue;
351
+ }
352
+ if (role === "screenwriter") {
353
+ issue(issues, "system_agent_prompt_unsupported", "编剧召回是检索工具,不支持提示词覆盖");
354
+ continue;
355
+ }
356
+ if (!validPromptBlocks(blocks))
357
+ issue(issues, "system_agent_prompt_blocks", `${role} 提示词对象必须具有唯一 id、名称、合法角色和文本`);
358
+ }
359
+ }
360
+ function validateFixedNodes(nodes, stageFlow, issues) {
361
+ for (const [id, kind] of SYSTEM_NODES) {
362
+ const node = nodes.get(id);
363
+ if (!node)
364
+ issue(issues, "missing_system_node", `缺少固定系统节点 ${id}`, id);
365
+ else if (node.kind !== kind || !hasSystem(node))
366
+ issue(issues, "system_node_mutated", `系统节点 ${id} 不可修改`, id);
367
+ }
368
+ const stageSystemCount = stageFlow === undefined ? 0 : DAG_STAGE_BOUNDARIES.length * 2;
369
+ const systemNodes = [...nodes.values()].filter(hasSystem);
370
+ if (systemNodes.length !== SYSTEM_NODES.length + stageSystemCount)
371
+ issue(issues, "system_node_count", `DAG 必须恰好包含 ${SYSTEM_NODES.length + stageSystemCount} 个固定系统节点`);
372
+ for (const node of nodes.values()) {
373
+ if (node.kind === "user_input" && String(node.id) !== SYSTEM_USER_INPUT_NODE_ID)
374
+ issue(issues, "system_node_extra", "不得新增 user_input 系统节点", String(node.id));
375
+ if (node.kind === "director" && String(node.id) !== SYSTEM_DIRECTOR_NODE_ID)
376
+ issue(issues, "system_node_extra", "不得新增 director 系统节点", String(node.id));
377
+ const stageMatch = stageFlow !== undefined && DAG_STAGE_BOUNDARIES.some((boundary) => (node.id === boundary.start_id && node.kind === "stage_start" && node.slot === boundary.slot)
378
+ || (node.id === boundary.handoff_id && node.kind === "stage_handoff" && node.slot === boundary.slot));
379
+ if (hasSystem(node) && !SYSTEM_NODES.some(([id, kind]) => node.id === id && node.kind === kind) && !stageMatch)
380
+ issue(issues, "system_node_extra", "不得新增或伪造系统节点", node.id);
381
+ }
382
+ if (stageFlow !== undefined)
383
+ for (const boundary of DAG_STAGE_BOUNDARIES) {
384
+ if (!nodes.has(boundary.start_id))
385
+ issue(issues, "stage_start_missing", `缺少阶段 ${boundary.slot} 的控制流启动`, boundary.start_id);
386
+ if (!nodes.has(boundary.handoff_id))
387
+ issue(issues, "stage_handoff_missing", `缺少阶段 ${boundary.slot} 的控制流交接`, boundary.handoff_id);
388
+ }
389
+ }
390
+ function validateFixedEdges(edges, issues) {
391
+ for (const expected of FIXED_EDGES) {
392
+ const edge = edges.get(expected.id);
393
+ if (!edge || !samePort(edge.from, expected.from) || !samePort(edge.to, expected.to))
394
+ issue(issues, "missing_system_edge", `固定系统边 ${expected.id} 不可删除或修改`, undefined, expected.id);
395
+ }
396
+ }
397
+ function validateRunnable(nodes, incoming, outgoing, issues, slottedNodes) {
398
+ const reachable = reachableFromRoots(nodes, outgoing);
399
+ const authorNodes = [...nodes.values()].filter((node) => !isSystemNode(node) && !slottedNodes.has(node.id));
400
+ for (const node of authorNodes) {
401
+ if (!reachable.has(node.id))
402
+ issue(issues, "orphan_node", "运行/发布时作者节点必须从 user_input 或 director 可达", node.id);
403
+ if (!(incoming.get(key(node.id, "input"))?.length))
404
+ issue(issues, "missing_input", "运行/发布时作者节点必须连接唯一输入", node.id);
405
+ for (const port of outputPorts(node))
406
+ if (!(outgoing.get(key(node.id, port))?.length))
407
+ issue(issues, "dangling_output", `运行/发布时 ${port} 输出必须连接到 text_output 或后续节点`, node.id);
408
+ }
409
+ for (const node of nodes.values())
410
+ if (node.kind !== "text_output" && !slottedNodes.has(node.id))
411
+ for (const port of outputPorts(node))
412
+ if (!outgoing.get(key(node.id, port))?.length)
413
+ issue(issues, "dangling_output", "运行/发布时所有正常路径必须终到 text_output", node.id);
414
+ const canReachText = nodesThatReachText(nodes, incoming);
415
+ for (const node of nodes.values())
416
+ if (node.kind !== "text_output" && !slottedNodes.has(node.id) && reachable.has(node.id) && !canReachText.has(node.id))
417
+ issue(issues, "path_not_terminal", "运行/发布时每条正常路径必须终到 text_output", node.id);
418
+ }
419
+ function validateInsertionSlots(value, nodes, issues) {
420
+ const members = new Set();
421
+ if (value === undefined)
422
+ return members;
423
+ if (!isRecord(value)) {
424
+ issue(issues, "insertion_slots_shape", "insertion_slots 必须是三个受控槽的对象");
425
+ return members;
426
+ }
427
+ for (const key of Object.keys(value))
428
+ if (!DAG_INSERTION_SLOTS.includes(key)) {
429
+ issue(issues, "insertion_slot_unknown", `未知插入槽 ${key}`);
430
+ }
431
+ for (const slot of DAG_INSERTION_SLOTS) {
432
+ const list = value[slot];
433
+ if (list === undefined)
434
+ continue;
435
+ if (!Array.isArray(list)) {
436
+ issue(issues, "insertion_slot_members", `${slot} 必须是 Agent id 数组`);
437
+ continue;
438
+ }
439
+ for (const id of list) {
440
+ if (!validId(id)) {
441
+ issue(issues, "insertion_slot_member", `${slot} 包含非法 Agent id`);
442
+ continue;
443
+ }
444
+ if (members.has(id)) {
445
+ issue(issues, "insertion_slot_duplicate", `Agent ${id} 只能属于一个插入槽`, id);
446
+ continue;
447
+ }
448
+ members.add(id);
449
+ const node = nodes.get(id);
450
+ if (!node)
451
+ issue(issues, "insertion_slot_missing", `插入槽引用的 Agent ${id} 不存在`, id);
452
+ else if (node.kind !== "agent" || isSystemNode(node))
453
+ issue(issues, "insertion_slot_kind", "插入槽只允许作者 Agent", id);
454
+ }
455
+ }
456
+ return members;
457
+ }
458
+ function validateControlledSlotMode(nodes, edges, slottedNodes, issues) {
459
+ for (const node of nodes.values())
460
+ if (!isSystemNode(node) && !slottedNodes.has(node.id)) {
461
+ issue(issues, "insertion_slot_unassigned", "受控插槽模式下,每个作者节点都必须是已分配插槽的 Agent", node.id);
462
+ }
463
+ for (const edge of edges.values()) {
464
+ if (!FIXED_EDGES.some((fixed) => fixed.id === edge.id && samePort(fixed.from, edge.from) && samePort(fixed.to, edge.to))) {
465
+ issue(issues, "insertion_slot_edge", "受控插槽模式只允许固定系统主线边;槽内顺序由 insertion_slots 决定", undefined, edge.id);
466
+ }
467
+ }
468
+ }
469
+ function validateStageFlow(value, nodes, edges, incoming, outgoing, issues) {
470
+ if (!isRecord(value) || value.enabled !== true || Object.keys(value).length !== 1) {
471
+ issue(issues, "stage_flow_shape", "stage_flow 必须为 { enabled: true }");
472
+ return;
473
+ }
474
+ for (const edge of edges.values()) {
475
+ const source = nodes.get(edge.from.node_id);
476
+ const target = nodes.get(edge.to.node_id);
477
+ if (!source || !target)
478
+ continue;
479
+ const sourceSlot = stageSlot(source);
480
+ const targetSlot = stageSlot(target);
481
+ const authorEdge = !isCoreSystemNode(source) && !isCoreSystemNode(target);
482
+ if (authorEdge && sourceSlot !== undefined && targetSlot !== undefined && sourceSlot !== targetSlot)
483
+ issue(issues, "stage_cross_edge", "阶段控制流不能跨阶段连线", undefined, edge.id);
484
+ if (source.kind === "stage_handoff")
485
+ issue(issues, "stage_handoff_output", "控制流交接没有输出端口", source.id, edge.id);
486
+ if (target.kind === "stage_start")
487
+ issue(issues, "stage_start_input", "控制流启动没有输入端口", target.id, edge.id);
488
+ }
489
+ for (const boundary of DAG_STAGE_BOUNDARIES) {
490
+ const start = nodes.get(boundary.start_id);
491
+ const handoff = nodes.get(boundary.handoff_id);
492
+ if (!start || !handoff)
493
+ continue;
494
+ const startOutgoing = outgoing.get(key(start.id, "output")) ?? [];
495
+ if (startOutgoing.length > 1)
496
+ issue(issues, "stage_output_fanout", "阶段控制流每个输出只能连接一条线", start.id);
497
+ for (const node of nodes.values()) {
498
+ if (isCoreSystemNode(node) || node.kind === "stage_start" || node.kind === "stage_handoff")
499
+ continue;
500
+ const inStage = reachableFromBoundary(start.id, nodes, outgoing).has(node.id);
501
+ if (inStage) {
502
+ for (const port of outputPorts(node))
503
+ if ((outgoing.get(key(node.id, port))?.length ?? 0) > 1)
504
+ issue(issues, "stage_output_fanout", "阶段控制流每个输出只能连接一条线", node.id);
505
+ }
506
+ }
507
+ if ((incoming.get(key(handoff.id, "input"))?.length ?? 0) > 1)
508
+ issue(issues, "input_fanin", "控制流交接只能接收一条入边", handoff.id);
509
+ const reached = reachableFromBoundary(boundary.start_id, nodes, outgoing);
510
+ for (const id of reached) {
511
+ const node = nodes.get(id);
512
+ if (node?.kind === "stage_handoff" && node.slot !== boundary.slot)
513
+ issue(issues, "stage_cross_edge", "阶段控制流不能交接到另一阶段", node.id);
514
+ if (node?.kind === "stage_start" && node.id !== boundary.start_id)
515
+ issue(issues, "stage_cross_edge", "阶段控制流不能进入另一阶段启动器", node.id);
516
+ }
517
+ }
518
+ const ownership = new Map();
519
+ for (const boundary of DAG_STAGE_BOUNDARIES)
520
+ for (const id of reachableFromBoundary(boundary.start_id, nodes, outgoing)) {
521
+ const node = nodes.get(id);
522
+ if (!node || isSystemNode(node))
523
+ continue;
524
+ const prior = ownership.get(id);
525
+ if (prior !== undefined && prior !== boundary.slot)
526
+ issue(issues, "stage_node_reused", "作者节点只能属于一个阶段控制流", id);
527
+ else
528
+ ownership.set(id, boundary.slot);
529
+ }
530
+ }
531
+ function validateStageRunnable(nodes, incoming, outgoing, issues) {
532
+ for (const boundary of DAG_STAGE_BOUNDARIES) {
533
+ const start = nodes.get(boundary.start_id);
534
+ if (!start)
535
+ continue;
536
+ const reachable = reachableFromBoundary(start.id, nodes, outgoing);
537
+ const terminals = [...reachable].map((id) => nodes.get(id)).filter((node) => node !== undefined && (node.kind === "stage_handoff" || (node.kind === "text_output" && !hasSystem(node))));
538
+ if (terminals.length === 0)
539
+ issue(issues, "stage_not_connected", `阶段 ${boundary.slot} 的控制流未连接到交接或文本出口`, start.id);
540
+ for (const id of reachable) {
541
+ const node = nodes.get(id);
542
+ if (!node || node.kind === "stage_handoff" || (node.kind === "text_output" && !hasSystem(node)))
543
+ continue;
544
+ // Agent 的 error 是失败终态:旧受控插槽在 provider 错误时直接失败,
545
+ // 因而 v1 升级无需伪造一条会把错误当正常数据交接的边。
546
+ for (const port of outputPorts(node))
547
+ if (!(node.kind === "agent" && port === "error") && (outgoing.get(key(node.id, port))?.length ?? 0) === 0)
548
+ issue(issues, "stage_dangling_output", `阶段 ${boundary.slot} 的 ${port} 输出未连接`, node.id);
549
+ }
550
+ }
551
+ for (const boundary of DAG_STAGE_BOUNDARIES) {
552
+ const handoff = nodes.get(boundary.handoff_id);
553
+ const start = nodes.get(boundary.start_id);
554
+ const reachable = start ? reachableFromBoundary(start.id, nodes, outgoing) : new Set();
555
+ const hasTerminalOutput = [...reachable].some((id) => {
556
+ const node = nodes.get(id);
557
+ return node?.kind === "text_output" && !hasSystem(node);
558
+ });
559
+ if (handoff && !hasTerminalOutput && (incoming.get(key(handoff.id, "input"))?.length ?? 0) !== 1)
560
+ issue(issues, "stage_handoff_input", `阶段 ${boundary.slot} 的控制流交接必须恰有一个输入`, handoff.id);
561
+ }
562
+ }
563
+ function reachableFromBoundary(root, nodes, outgoing) {
564
+ const found = new Set([root]);
565
+ const queue = [root];
566
+ while (queue.length) {
567
+ const id = queue.shift();
568
+ const node = nodes.get(id);
569
+ if (!node)
570
+ continue;
571
+ for (const port of outputPorts(node))
572
+ for (const edge of outgoing.get(key(id, port)) ?? [])
573
+ if (!found.has(edge.to.node_id)) {
574
+ found.add(edge.to.node_id);
575
+ queue.push(edge.to.node_id);
576
+ }
577
+ }
578
+ return found;
579
+ }
580
+ function stageSlot(node) {
581
+ if (node.kind === "stage_start" || node.kind === "stage_handoff")
582
+ return node.slot;
583
+ return undefined;
584
+ }
585
+ function isCoreSystemNode(node) { return node.kind === "user_input" || node.kind === "director" || (node.kind === "text_output" && hasSystem(node)); }
586
+ function reachableFromRoots(nodes, outgoing) {
587
+ const found = new Set([SYSTEM_USER_INPUT_NODE_ID, SYSTEM_DIRECTOR_NODE_ID]);
588
+ const queue = [...found];
589
+ while (queue.length) {
590
+ const id = queue.shift();
591
+ const node = nodes.get(id);
592
+ if (!node)
593
+ continue;
594
+ for (const port of outputPorts(node))
595
+ for (const edge of outgoing.get(key(id, port)) ?? [])
596
+ if (!found.has(edge.to.node_id)) {
597
+ found.add(edge.to.node_id);
598
+ queue.push(edge.to.node_id);
599
+ }
600
+ }
601
+ return found;
602
+ }
603
+ function nodesThatReachText(nodes, incoming) {
604
+ const queue = [...nodes.values()].filter((node) => node.kind === "text_output").map((node) => node.id);
605
+ const found = new Set(queue);
606
+ while (queue.length) {
607
+ const id = queue.shift();
608
+ const node = nodes.get(id);
609
+ if (!node)
610
+ continue;
611
+ for (const port of inputPorts(node))
612
+ for (const edge of incoming.get(key(id, port)) ?? [])
613
+ if (!found.has(edge.from.node_id)) {
614
+ found.add(edge.from.node_id);
615
+ queue.push(edge.from.node_id);
616
+ }
617
+ }
618
+ return found;
619
+ }
620
+ function hasCycle(nodes, outgoing) {
621
+ const visiting = new Set();
622
+ const visited = new Set();
623
+ const visit = (id) => {
624
+ if (visiting.has(id))
625
+ return true;
626
+ if (visited.has(id))
627
+ return false;
628
+ visiting.add(id);
629
+ const node = nodes.get(id);
630
+ if (node)
631
+ for (const port of outputPorts(node))
632
+ for (const edge of outgoing.get(key(id, port)) ?? [])
633
+ if (visit(edge.to.node_id))
634
+ return true;
635
+ visiting.delete(id);
636
+ visited.add(id);
637
+ return false;
638
+ };
639
+ return [...nodes.keys()].some(visit);
640
+ }
641
+ function isDagJsonValue(value, ancestors) {
642
+ if (value === null || typeof value === "boolean" || typeof value === "string")
643
+ return true;
644
+ if (typeof value === "number")
645
+ return Number.isFinite(value);
646
+ if (typeof value !== "object" || ancestors.has(value))
647
+ return false;
648
+ if (Array.isArray(value)) {
649
+ ancestors.add(value);
650
+ try {
651
+ const ownNames = Object.getOwnPropertyNames(value);
652
+ if (ownNames.length !== value.length + 1 || !ownNames.includes("length") || Object.getOwnPropertySymbols(value).length > 0)
653
+ return false;
654
+ for (let index = 0; index < value.length; index += 1) {
655
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
656
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor) || !isDagJsonValue(descriptor.value, ancestors))
657
+ return false;
658
+ }
659
+ return true;
660
+ }
661
+ finally {
662
+ ancestors.delete(value);
663
+ }
664
+ }
665
+ const prototype = Object.getPrototypeOf(value);
666
+ if ((prototype !== Object.prototype && prototype !== null) || Object.getOwnPropertySymbols(value).length > 0)
667
+ return false;
668
+ ancestors.add(value);
669
+ try {
670
+ for (const property of Object.getOwnPropertyNames(value)) {
671
+ const descriptor = Object.getOwnPropertyDescriptor(value, property);
672
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor) || !isDagJsonValue(descriptor.value, ancestors))
673
+ return false;
674
+ }
675
+ return true;
676
+ }
677
+ finally {
678
+ ancestors.delete(value);
679
+ }
680
+ }
681
+ function isRecord(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
682
+ function isNodeKind(value) { return value === "agent" || value === "script" || value === "text_output" || value === "user_input" || value === "director" || value === "stage_start" || value === "stage_handoff"; }
683
+ function isEdge(value) { return isRecord(value) && validId(value.id) && isPortRef(value.from) && isPortRef(value.to); }
684
+ function isPortRef(value) { return isRecord(value) && validId(value.node_id) && validId(value.port); }
685
+ function isMessageTemplates(value) {
686
+ const parts = ["system", "primer", "anchor"];
687
+ return Object.keys(value).length === parts.length && parts.every((part) => typeof value[part] === "string");
688
+ }
689
+ function hasSystem(node) { return isRecord(node) && node.system === true; }
690
+ function isSystemNode(node) { return node.kind === "user_input" || node.kind === "director" || node.kind === "stage_start" || node.kind === "stage_handoff" || (node.kind === "text_output" && hasSystem(node)); }
691
+ function inputPorts(node) { return node.kind === "user_input" || node.kind === "stage_start" ? [] : ["input"]; }
692
+ function outputPorts(node) { return node.kind === "agent" ? ["success", "error"] : node.kind === "script" ? scriptOutputPorts(node) : node.kind === "text_output" || node.kind === "stage_handoff" ? [] : ["output"]; }
693
+ function scriptOutputPorts(node) {
694
+ const paths = isRecord(node.config) ? node.config.paths : undefined;
695
+ return Array.isArray(paths) && paths.length ? paths.filter((path) => typeof path === "string") : ["output"];
696
+ }
697
+ function isInputPort(node, port) { return inputPorts(node).includes(port); }
698
+ function isOutputPort(node, port) { return outputPorts(node).includes(port); }
699
+ function validId(value) { return typeof value === "string" && value.length > 0; }
700
+ function samePort(a, b) { return a.node_id === b.node_id && a.port === b.port; }
701
+ function copyEdge(edge) { return { id: edge.id, from: { ...edge.from }, to: { ...edge.to } }; }
702
+ /** ID 与 port 可包含冒号,故使用无歧义 JSON 元组作为索引键。 */
703
+ function key(nodeId, port) { return JSON.stringify([nodeId, port]); }
704
+ function issue(issues, code, message, node_id, edge_id) { issues.push({ code, message, ...(node_id ? { node_id } : {}), ...(edge_id ? { edge_id } : {}) }); }