@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,136 @@
1
+ import { type DagAgentNode, type DagDocument, type DagJson, type DagNode, type DagScriptNode, type DagValidationIssue } from "./document.js";
2
+ export type DagNodeRunStatus = "succeeded" | "failed" | "skipped" | "aborted";
3
+ export interface DagNodeRunResult {
4
+ readonly status: DagNodeRunStatus;
5
+ readonly input?: DagJson;
6
+ readonly outputs?: Readonly<Record<string, DagJson>>;
7
+ readonly error?: string;
8
+ /** 后端已有的真实 dispatch receipt,原样透传;执行器不结算或计费。 */
9
+ readonly receipt?: unknown;
10
+ }
11
+ /** Agent 是字符串边界:内部 mixed JSON 仅在这里序列化。 */
12
+ export interface DagAgentInvocation {
13
+ readonly node: DagAgentNode;
14
+ readonly input: string;
15
+ readonly signal?: AbortSignal;
16
+ }
17
+ export type DagAgentInvocationResult = {
18
+ readonly status: "success";
19
+ readonly body: string;
20
+ readonly receipt?: unknown;
21
+ } | {
22
+ readonly status: "error";
23
+ readonly error: string;
24
+ readonly receipt?: unknown;
25
+ };
26
+ /** 脚本保持 DAG JSON 值,不应在进入 sandbox 前提前字符串化。 */
27
+ export interface DagScriptInvocation {
28
+ readonly node: DagScriptNode;
29
+ readonly input: DagJson;
30
+ readonly signal?: AbortSignal;
31
+ }
32
+ export interface DagScriptResult {
33
+ readonly path: string;
34
+ readonly body: DagJson;
35
+ }
36
+ export interface ExecuteDagDocumentOptions {
37
+ readonly input: string;
38
+ /** 可选的导演上下文;缺省时保持旧的纯文本输入协议。 */
39
+ readonly history?: readonly DagHistoryMessage[];
40
+ readonly historyWindow?: number;
41
+ readonly scene?: unknown;
42
+ readonly currentWorldTimeMs?: number;
43
+ readonly nearestTimeTriggerAtMs?: number;
44
+ /** Director 输出门控;返回错误文本时按 retry 次数重新请求。 */
45
+ readonly directorOutputRetries?: number;
46
+ readonly validateDirectorOutput?: (output: string, context: Readonly<{
47
+ input: string;
48
+ attempt: number;
49
+ }>) => string | void | Promise<string | void>;
50
+ readonly onDirectorRetry?: (event: Readonly<{
51
+ attempt: number;
52
+ reason: string;
53
+ }>) => void | Promise<void>;
54
+ readonly signal?: AbortSignal;
55
+ /** 由系统注入的 Director 正文;不能被作者节点配置替换。 */
56
+ readonly directorOutput?: string | ((input: string, signal?: AbortSignal) => string | Promise<string>);
57
+ /** 真实场记/写手阶段。受控插槽文档要求两个回调均存在,缺失即失败。 */
58
+ readonly runSystemStage?: (invocation: DagSystemStageInvocation) => DagSystemStageResult | Promise<DagSystemStageResult>;
59
+ readonly invokeAgent?: (invocation: DagAgentInvocation) => unknown | Promise<unknown>;
60
+ readonly runScript?: (invocation: DagScriptInvocation) => unknown | Promise<unknown>;
61
+ /** Text Output 是字符串边界:mixed JSON 在发射前才序列化。 */
62
+ readonly emitText?: (event: {
63
+ readonly node: DagNode;
64
+ readonly input: string;
65
+ readonly signal?: AbortSignal;
66
+ }) => void | Promise<void>;
67
+ readonly onNodeResult?: (node: DagNode, result: DagNodeRunResult) => void | Promise<void>;
68
+ }
69
+ export interface DagSystemStageInvocation {
70
+ readonly stage: "scribe" | "writer";
71
+ readonly input: DagJson;
72
+ readonly signal?: AbortSignal;
73
+ }
74
+ export interface DagSystemStageResult {
75
+ readonly body: DagJson;
76
+ readonly receipt?: unknown;
77
+ }
78
+ export interface DagHistoryMessage {
79
+ readonly role: "user" | "assistant";
80
+ readonly content: string;
81
+ readonly createdAtMs?: number;
82
+ }
83
+ /**
84
+ * 手稿通过场记校验后的一次性扇出结果。Writer 与演出组共享同一份不可变手稿,
85
+ * 两类工作在同一调度点启动,任一分支失败不会取消另一分支;调用方决定失败如何呈现。
86
+ */
87
+ export interface ManuscriptFanoutOptions<TWriter, TPerformance> {
88
+ readonly manuscript: string;
89
+ readonly scene?: unknown;
90
+ readonly signal?: AbortSignal;
91
+ readonly runWriter: (input: Readonly<{
92
+ manuscript: string;
93
+ scene?: unknown;
94
+ signal?: AbortSignal;
95
+ }>) => TWriter | Promise<TWriter>;
96
+ readonly runPerformance?: (input: Readonly<{
97
+ manuscript: string;
98
+ scene?: unknown;
99
+ signal?: AbortSignal;
100
+ }>) => TPerformance | Promise<TPerformance>;
101
+ }
102
+ export interface ManuscriptFanoutResult<TWriter, TPerformance> {
103
+ readonly writer: TWriter;
104
+ readonly performance?: TPerformance;
105
+ }
106
+ /**
107
+ * 在手稿确定后并行启动 Writer 和可选摄影/语音演出组。
108
+ * 成功结果会等待两条分支完成;失败立即向调用方传播,另一分支仍可自行完成。
109
+ * provider 自身通过 signal 响应取消,不在这里隐式重试或吞掉错误。
110
+ */
111
+ export declare function runManuscriptFanout<TWriter, TPerformance>(options: ManuscriptFanoutOptions<TWriter, TPerformance>): Promise<ManuscriptFanoutResult<TWriter, TPerformance>>;
112
+ /** 同一 DAG 重试期间复用召回结果,避免再次计费或重复查询。 */
113
+ export declare function cachedDagRecall<T>(cache: Map<string, T>, key: string, producer: () => T | Promise<T>): Promise<T>;
114
+ /** 将场景信息中的时间字段转换为中文绝对时间,避免模型误读 epoch。 */
115
+ export declare function formatSceneTimestamps(value: unknown): unknown;
116
+ export interface DagRunResult {
117
+ readonly status: "completed" | "failed" | "aborted";
118
+ /** 失败时的可展示原因;受控插槽不会把阶段失败伪装为 skipped 成功。 */
119
+ readonly error?: string;
120
+ readonly node_results: Readonly<Record<string, DagNodeRunResult>>;
121
+ readonly emitted_text: readonly string[];
122
+ readonly receipts: readonly unknown[];
123
+ /** 受控系统阶段的真实回执;仅 insertion_slots 文档产生。 */
124
+ readonly stage_results?: Readonly<Partial<Record<"scribe" | "writer", DagSystemStageResult>>>;
125
+ }
126
+ export declare class DagDocumentValidationError extends Error {
127
+ readonly issues: readonly DagValidationIssue[];
128
+ constructor(issues: readonly DagValidationIssue[]);
129
+ }
130
+ /**
131
+ * 每个节点只有一条输入依赖,依赖完成即开始运行。分支间不互相等待,
132
+ * 只有 AbortSignal 会阻止尚未开始的节点并要求运行中的 provider 自行中止。
133
+ */
134
+ export declare function executeDagDocument(document: DagDocument, options: ExecuteDagDocumentOptions): Promise<DagRunResult>;
135
+ export declare function isDagAgentInvocationResult(value: unknown): value is DagAgentInvocationResult;
136
+ export declare function isDagScriptResult(value: unknown): value is DagScriptResult;
@@ -0,0 +1,543 @@
1
+ import { dagMixedToString, isDagJson, DAG_STAGE_BOUNDARIES, upgradeDagDocument, validateDagDocument, } from "./document.js";
2
+ /**
3
+ * 在手稿确定后并行启动 Writer 和可选摄影/语音演出组。
4
+ * 成功结果会等待两条分支完成;失败立即向调用方传播,另一分支仍可自行完成。
5
+ * provider 自身通过 signal 响应取消,不在这里隐式重试或吞掉错误。
6
+ */
7
+ export async function runManuscriptFanout(options) {
8
+ if (typeof options.manuscript !== "string" || options.manuscript.length === 0) {
9
+ throw new TypeError("manuscript_required");
10
+ }
11
+ if (options.signal?.aborted)
12
+ throw new DOMException("The operation was aborted", "AbortError");
13
+ const input = Object.freeze({
14
+ manuscript: options.manuscript,
15
+ ...(options.scene === undefined ? {} : { scene: freezeSceneSnapshot(options.scene) }),
16
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
17
+ });
18
+ const writerPromise = Promise.resolve().then(() => options.runWriter(input));
19
+ const performancePromise = options.runPerformance === undefined
20
+ ? undefined
21
+ : Promise.resolve().then(() => options.runPerformance(input));
22
+ if (performancePromise === undefined)
23
+ return { writer: await writerPromise };
24
+ // 同时订阅两个分支的失败,避免较快的演出失败在等待 Writer 时成为未处理拒绝。
25
+ const [writer, performance] = await Promise.all([writerPromise, performancePromise]);
26
+ return { writer, performance };
27
+ }
28
+ function freezeSceneSnapshot(scene) {
29
+ if (!isDagJson(scene))
30
+ throw new TypeError("manuscript_scene_invalid");
31
+ const snapshot = structuredClone(scene);
32
+ const freeze = (value) => {
33
+ if (value !== null && typeof value === "object") {
34
+ for (const child of Object.values(value))
35
+ freeze(child);
36
+ Object.freeze(value);
37
+ }
38
+ return value;
39
+ };
40
+ return freeze(snapshot);
41
+ }
42
+ /** 同一 DAG 重试期间复用召回结果,避免再次计费或重复查询。 */
43
+ export async function cachedDagRecall(cache, key, producer) {
44
+ if (cache.has(key))
45
+ return cache.get(key);
46
+ const value = await producer();
47
+ cache.set(key, value);
48
+ return value;
49
+ }
50
+ /** 将场景信息中的时间字段转换为中文绝对时间,避免模型误读 epoch。 */
51
+ export function formatSceneTimestamps(value) {
52
+ if (Array.isArray(value))
53
+ return value.map(formatSceneTimestamps);
54
+ if (value === null || typeof value !== "object")
55
+ return value;
56
+ const source = value;
57
+ const result = {};
58
+ for (const [key, child] of Object.entries(source)) {
59
+ const normalized = key.toLowerCase();
60
+ const isTimestamp = normalized.includes("timestamp") || normalized.endsWith("_at_ms") || normalized.endsWith("_ts_ms");
61
+ if (isTimestamp) {
62
+ const numeric = typeof child === "number" ? child : typeof child === "string" && /^-?\\d+(?:\\.\\d+)?$/u.test(child) ? Number(child) : NaN;
63
+ if (Number.isFinite(numeric)) {
64
+ const date = new Date(numeric < 1e12 ? numeric * 1000 : numeric);
65
+ if (!Number.isNaN(date.getTime())) {
66
+ const p = (n) => String(n).padStart(2, "0");
67
+ result[key] = `${date.getUTCFullYear()}年${p(date.getUTCMonth() + 1)}月${p(date.getUTCDate())}日 ${p(date.getUTCHours())}时${p(date.getUTCMinutes())}分${p(date.getUTCSeconds())}秒`;
68
+ continue;
69
+ }
70
+ }
71
+ }
72
+ result[key] = formatSceneTimestamps(child);
73
+ }
74
+ return result;
75
+ }
76
+ function directorContext(input, options) {
77
+ if (options.history === undefined && options.scene === undefined && options.currentWorldTimeMs === undefined && options.nearestTimeTriggerAtMs === undefined)
78
+ return dagMixedToString(input);
79
+ const window = options.historyWindow === undefined ? 4 : Math.max(0, Math.floor(options.historyWindow));
80
+ const evenWindow = window % 2 === 0 ? window : window - 1;
81
+ const history = (options.history ?? []).slice(-evenWindow);
82
+ return JSON.stringify({
83
+ input: dagMixedToString(input),
84
+ history,
85
+ scene: options.scene === undefined ? undefined : formatSceneTimestamps(options.scene),
86
+ currentWorldTimeMs: options.currentWorldTimeMs,
87
+ nearestTimeTriggerAtMs: options.nearestTimeTriggerAtMs,
88
+ });
89
+ }
90
+ export class DagDocumentValidationError extends Error {
91
+ issues;
92
+ constructor(issues) {
93
+ super("dag_document_invalid");
94
+ this.name = "DagDocumentValidationError";
95
+ this.issues = issues;
96
+ }
97
+ }
98
+ /**
99
+ * 每个节点只有一条输入依赖,依赖完成即开始运行。分支间不互相等待,
100
+ * 只有 AbortSignal 会阻止尚未开始的节点并要求运行中的 provider 自行中止。
101
+ */
102
+ export async function executeDagDocument(document, options) {
103
+ // v1 文档只在执行入口做一次显式升级;之后统一按 v2 阶段边界运行。
104
+ const normalized = document.schema_version === 1 ? upgradeDagDocument(document).document : document;
105
+ const validation = validateDagDocument(normalized, { mode: "run" });
106
+ if (!validation.ok)
107
+ throw new DagDocumentValidationError(validation.issues);
108
+ if (normalized.stage_flow?.enabled === true)
109
+ return await executeStageFlow(normalized, options);
110
+ if (normalized.insertion_slots !== undefined)
111
+ return await executeControlledSlots(normalized, options);
112
+ const nodes = new Map(normalized.nodes.map((node) => [node.id, node]));
113
+ const outgoing = indexOutgoing(normalized.edges);
114
+ // 作者可合法使用 __proto__ 作为节点 id;结果表不能继承 Object.prototype。
115
+ const results = Object.create(null);
116
+ const emittedText = [];
117
+ const receipts = [];
118
+ const scheduled = new Set();
119
+ const active = new Set();
120
+ let aborted = false;
121
+ let terminal = false;
122
+ const report = async (node, result) => {
123
+ results[node.id] = result;
124
+ if (result.receipt !== undefined)
125
+ receipts.push(result.receipt);
126
+ await options.onNodeResult?.(node, result);
127
+ };
128
+ const schedule = (node, input) => {
129
+ if (scheduled.has(node.id))
130
+ return;
131
+ scheduled.add(node.id);
132
+ const task = (async () => {
133
+ if (aborted || terminal || options.signal?.aborted) {
134
+ aborted = true;
135
+ await report(node, { status: "skipped", input });
136
+ return;
137
+ }
138
+ const outcome = await executeNode(node, input, options, emittedText);
139
+ await report(node, outcome);
140
+ if (outcome.status === "aborted") {
141
+ aborted = true;
142
+ return;
143
+ }
144
+ // 文本出口是用户可见的 terminal;已启动的 provider 仍由自身完成,
145
+ // 尚未启动的分支跳过,避免一轮回合继续向多个出口发射正文。
146
+ if (outcome.status === "succeeded" && node.kind === "text_output" && !node.system && normalized.execution?.terminal_on_text_output === true)
147
+ terminal = true;
148
+ if (outcome.status === "succeeded" && outcome.outputs && !terminal)
149
+ propagate(node, outcome.outputs);
150
+ })();
151
+ active.add(task);
152
+ void task.then(() => active.delete(task), () => active.delete(task));
153
+ };
154
+ const propagate = (from, outputs) => {
155
+ if (aborted || options.signal?.aborted)
156
+ return;
157
+ for (const [port, value] of Object.entries(outputs))
158
+ for (const edge of outgoing.get(key(from.id, port)) ?? []) {
159
+ const target = nodes.get(edge.to.node_id);
160
+ if (target)
161
+ schedule(target, value);
162
+ }
163
+ };
164
+ const user = nodes.get("user_input");
165
+ if (options.signal?.aborted) {
166
+ aborted = true;
167
+ scheduled.add(user.id);
168
+ await report(user, { status: "skipped", input: options.input });
169
+ }
170
+ else {
171
+ scheduled.add(user.id);
172
+ await report(user, { status: "succeeded", input: options.input, outputs: { output: options.input } });
173
+ propagate(user, { output: options.input });
174
+ }
175
+ while (active.size > 0)
176
+ await Promise.all([...active]);
177
+ if (aborted || options.signal?.aborted)
178
+ aborted = true;
179
+ for (const node of document.nodes)
180
+ if (!results[node.id])
181
+ await report(node, { status: "skipped" });
182
+ return {
183
+ status: aborted ? "aborted" : Object.values(results).some((result) => result.status === "failed") ? "failed" : "completed",
184
+ node_results: results,
185
+ emitted_text: emittedText,
186
+ receipts,
187
+ };
188
+ }
189
+ /** v2 阶段图按 start → 作者节点 → handoff 的真实序列化边执行。 */
190
+ async function executeStageFlow(document, options) {
191
+ const nodes = new Map(document.nodes.map((node) => [node.id, node]));
192
+ const outgoing = indexOutgoing(document.edges);
193
+ const results = Object.create(null);
194
+ const emittedText = [];
195
+ const receipts = [];
196
+ const stageResults = {};
197
+ let aborted = false;
198
+ let terminal = false;
199
+ const report = async (node, result) => {
200
+ results[node.id] = result;
201
+ if (result.receipt !== undefined)
202
+ receipts.push(result.receipt);
203
+ await options.onNodeResult?.(node, result);
204
+ };
205
+ const stop = async (error) => {
206
+ for (const node of document.nodes)
207
+ if (!results[node.id])
208
+ await report(node, { status: "skipped" });
209
+ const status = aborted || options.signal?.aborted ? "aborted" : error === undefined ? "completed" : "failed";
210
+ return status === "failed"
211
+ ? { status, error: error, node_results: results, emitted_text: emittedText, receipts, stage_results: stageResults }
212
+ : { status, node_results: results, emitted_text: emittedText, receipts, stage_results: stageResults };
213
+ };
214
+ const runLinear = async (id, input) => {
215
+ let current = nodes.get(id);
216
+ let body = input;
217
+ const seen = new Set();
218
+ while (current) {
219
+ if (seen.has(current.id))
220
+ return { kind: "failed", error: "stage_cycle" };
221
+ seen.add(current.id);
222
+ if (options.signal?.aborted) {
223
+ aborted = true;
224
+ await report(current, { status: "aborted", input: body });
225
+ return { kind: "failed", error: "aborted" };
226
+ }
227
+ if (current.kind === "stage_handoff") {
228
+ await report(current, { status: "succeeded", input: body });
229
+ return { kind: "handoff", body };
230
+ }
231
+ if (current.kind === "stage_start") {
232
+ await report(current, { status: "succeeded", input: body, outputs: { output: body } });
233
+ const edge = oneOutputEdge(outgoing, current.id, "output");
234
+ if (!edge)
235
+ return { kind: "failed", error: `stage_start_disconnected:${current.id}` };
236
+ current = nodes.get(edge.to.node_id);
237
+ continue;
238
+ }
239
+ const outcome = await executeNode(current, body, options, emittedText);
240
+ await report(current, outcome);
241
+ if (outcome.status === "aborted") {
242
+ aborted = true;
243
+ return { kind: "failed", error: "aborted" };
244
+ }
245
+ if (current.kind === "text_output") {
246
+ if (outcome.status === "succeeded") {
247
+ terminal = true;
248
+ return { kind: "terminal" };
249
+ }
250
+ return { kind: "failed", error: outcome.error ?? "text_output_failed" };
251
+ }
252
+ if (outcome.status !== "succeeded" || !outcome.outputs)
253
+ return { kind: "failed", error: outcome.error ?? "stage_node_failed" };
254
+ const output = Object.entries(outcome.outputs).find(([port]) => oneOutputEdge(outgoing, current.id, port) !== undefined);
255
+ if (!output)
256
+ return { kind: "failed", error: `stage_output_disconnected:${current.id}` };
257
+ const edge = oneOutputEdge(outgoing, current.id, output[0]);
258
+ if (!edge)
259
+ return { kind: "failed", error: `stage_output_disconnected:${current.id}` };
260
+ body = output[1];
261
+ current = nodes.get(edge.to.node_id);
262
+ }
263
+ return { kind: "failed", error: "stage_target_missing" };
264
+ };
265
+ const user = nodes.get("user_input");
266
+ await report(user, options.signal?.aborted ? { status: "skipped", input: options.input } : { status: "succeeded", input: options.input, outputs: { output: options.input } });
267
+ if (options.signal?.aborted) {
268
+ aborted = true;
269
+ return await stop();
270
+ }
271
+ const before = await runLinear(DAG_STAGE_BOUNDARIES[0].start_id, options.input);
272
+ if (before.kind === "terminal")
273
+ return await stop();
274
+ if (before.kind === "failed")
275
+ return await stop(before.error);
276
+ const director = nodes.get("director");
277
+ const directorOutcome = await executeNode(director, before.body, options, emittedText);
278
+ await report(director, directorOutcome);
279
+ const manuscript = directorOutcome.status === "succeeded" ? directorOutcome.outputs?.output : undefined;
280
+ if (manuscript === undefined)
281
+ return await stop(directorOutcome.error ?? "director_failed");
282
+ const scribed = await runRequiredStage("scribe", manuscript, options, stageResults, receipts);
283
+ if (!scribed.ok)
284
+ return await stop(scribed.error);
285
+ const afterScribe = await runLinear(DAG_STAGE_BOUNDARIES[1].start_id, scribed.body);
286
+ if (afterScribe.kind === "terminal")
287
+ return await stop();
288
+ if (afterScribe.kind === "failed")
289
+ return await stop(afterScribe.error);
290
+ const written = await runRequiredStage("writer", afterScribe.body, options, stageResults, receipts);
291
+ if (!written.ok)
292
+ return await stop(written.error);
293
+ const afterWriter = await runLinear(DAG_STAGE_BOUNDARIES[2].start_id, written.body);
294
+ if (afterWriter.kind === "terminal")
295
+ return await stop();
296
+ if (afterWriter.kind === "failed")
297
+ return await stop(afterWriter.error);
298
+ const output = nodes.get("text_output");
299
+ const outputOutcome = await executeNode(output, afterWriter.body, options, emittedText);
300
+ await report(output, outputOutcome);
301
+ return await stop(outputOutcome.status === "succeeded" || terminal ? undefined : outputOutcome.error ?? "text_output_failed");
302
+ }
303
+ function oneOutputEdge(outgoing, nodeId, port) {
304
+ const edges = outgoing.get(key(nodeId, port)) ?? [];
305
+ return edges.length === 1 ? edges[0] : undefined;
306
+ }
307
+ async function runRequiredStage(stage, input, options, stageResults, receipts) {
308
+ if (!options.runSystemStage)
309
+ return { ok: false, error: `system_stage_required:${stage}` };
310
+ try {
311
+ const result = await options.runSystemStage({ stage, input, ...(options.signal === undefined ? {} : { signal: options.signal }) });
312
+ if (!isSystemStageResult(result))
313
+ return { ok: false, error: `system_stage_result_invalid:${stage}` };
314
+ stageResults[stage] = result;
315
+ if (result.receipt !== undefined)
316
+ receipts.push(result.receipt);
317
+ return { ok: true, body: result.body };
318
+ }
319
+ catch (error) {
320
+ return { ok: false, error: errorMessage(error) };
321
+ }
322
+ }
323
+ /**
324
+ * 受控插槽不是普通旁支:三个槽按系统回合顺序串行消费并产出数据。
325
+ * 文档只能登记 Agent id,不能改写 user → director → scribe → writer → text 的主线。
326
+ */
327
+ async function executeControlledSlots(document, options) {
328
+ const nodes = new Map(document.nodes.map((node) => [node.id, node]));
329
+ const results = Object.create(null);
330
+ const emittedText = [];
331
+ const receipts = [];
332
+ const stageResults = {};
333
+ let aborted = false;
334
+ const report = async (node, result) => {
335
+ results[node.id] = result;
336
+ if (result.receipt !== undefined)
337
+ receipts.push(result.receipt);
338
+ await options.onNodeResult?.(node, result);
339
+ };
340
+ const stop = async (error) => {
341
+ for (const node of document.nodes)
342
+ if (!results[node.id])
343
+ await report(node, { status: "skipped" });
344
+ const status = aborted || options.signal?.aborted ? "aborted" : "failed";
345
+ return { status, ...(status === "failed" ? { error } : {}), node_results: results, emitted_text: emittedText, receipts, stage_results: stageResults };
346
+ };
347
+ const runSlot = async (slot, input) => {
348
+ let body = input;
349
+ for (const id of document.insertion_slots?.[slot] ?? []) {
350
+ const node = nodes.get(id);
351
+ if (!node || node.kind !== "agent")
352
+ return { ok: false, error: `insertion_slot_agent_missing:${id}` };
353
+ const outcome = await runAgent(node, body, options);
354
+ await report(node, outcome);
355
+ if (outcome.status === "aborted") {
356
+ aborted = true;
357
+ return { ok: false, error: "aborted" };
358
+ }
359
+ const success = outcome.status === "succeeded" ? outcome.outputs?.success : undefined;
360
+ if (success === undefined)
361
+ return { ok: false, error: outcome.error ?? (outcome.outputs?.error === undefined ? "insertion_slot_agent_failed" : dagMixedToString(outcome.outputs.error)) };
362
+ body = success;
363
+ }
364
+ return { ok: true, body };
365
+ };
366
+ const runStage = async (stage, input) => {
367
+ if (!options.runSystemStage)
368
+ return { ok: false, error: `system_stage_required:${stage}` };
369
+ try {
370
+ const result = await options.runSystemStage({ stage, input, ...(options.signal === undefined ? {} : { signal: options.signal }) });
371
+ if (!isSystemStageResult(result))
372
+ return { ok: false, error: `system_stage_result_invalid:${stage}` };
373
+ if (options.signal?.aborted) {
374
+ aborted = true;
375
+ return { ok: false, error: "aborted" };
376
+ }
377
+ stageResults[stage] = result;
378
+ if (result.receipt !== undefined)
379
+ receipts.push(result.receipt);
380
+ return { ok: true, body: result.body };
381
+ }
382
+ catch (error) {
383
+ if (options.signal?.aborted || isAbortError(error)) {
384
+ aborted = true;
385
+ return { ok: false, error: "aborted" };
386
+ }
387
+ return { ok: false, error: errorMessage(error) };
388
+ }
389
+ };
390
+ const user = nodes.get("user_input");
391
+ await report(user, options.signal?.aborted ? { status: "skipped", input: options.input } : { status: "succeeded", input: options.input, outputs: { output: options.input } });
392
+ if (options.signal?.aborted) {
393
+ aborted = true;
394
+ return await stop("aborted");
395
+ }
396
+ const before = await runSlot("before_director", options.input);
397
+ if (!before.ok)
398
+ return await stop(before.error);
399
+ const director = nodes.get("director");
400
+ const directorOutcome = await executeNode(director, before.body, options, emittedText);
401
+ await report(director, directorOutcome);
402
+ const manuscript = directorOutcome.status === "succeeded" ? directorOutcome.outputs?.output : undefined;
403
+ if (manuscript === undefined)
404
+ return await stop(directorOutcome.error ?? "director_failed");
405
+ const scribed = await runStage("scribe", manuscript);
406
+ if (!scribed.ok)
407
+ return await stop(scribed.error);
408
+ const afterScribe = await runSlot("after_scribe", scribed.body);
409
+ if (!afterScribe.ok)
410
+ return await stop(afterScribe.error);
411
+ const written = await runStage("writer", afterScribe.body);
412
+ if (!written.ok)
413
+ return await stop(written.error);
414
+ const afterWriter = await runSlot("after_writer", written.body);
415
+ if (!afterWriter.ok)
416
+ return await stop(afterWriter.error);
417
+ const output = nodes.get("text_output");
418
+ const outputOutcome = await executeNode(output, afterWriter.body, options, emittedText);
419
+ await report(output, outputOutcome);
420
+ if (outputOutcome.status === "aborted")
421
+ aborted = true;
422
+ for (const node of document.nodes)
423
+ if (!results[node.id])
424
+ await report(node, { status: "skipped" });
425
+ return { status: aborted ? "aborted" : outputOutcome.status === "succeeded" ? "completed" : "failed", node_results: results, emitted_text: emittedText, receipts, stage_results: stageResults };
426
+ }
427
+ async function executeNode(node, input, options, emittedText) {
428
+ try {
429
+ if (options.signal?.aborted)
430
+ return { status: "aborted", input };
431
+ switch (node.kind) {
432
+ case "director": {
433
+ if (options.directorOutput === undefined)
434
+ return { status: "failed", input, error: "director_output_required" };
435
+ const promptInput = directorContext(input, options);
436
+ const maxAttempts = options.directorOutputRetries ?? 1;
437
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1)
438
+ return { status: "failed", input, error: "director_retry_limit_invalid" };
439
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
440
+ const output = typeof options.directorOutput === "string" ? options.directorOutput : await options.directorOutput(promptInput, options.signal);
441
+ if (typeof output !== "string")
442
+ return { status: "failed", input, error: "director_output_invalid" };
443
+ if (options.signal?.aborted)
444
+ return { status: "aborted", input };
445
+ const reason = await options.validateDirectorOutput?.(output, { input: promptInput, attempt });
446
+ if (reason === undefined || attempt >= maxAttempts) {
447
+ if (reason !== undefined)
448
+ return { status: "failed", input, error: reason };
449
+ return { status: "succeeded", input, outputs: { output } };
450
+ }
451
+ await options.onDirectorRetry?.({ attempt, reason });
452
+ }
453
+ return { status: "failed", input, error: "director_output_invalid" };
454
+ }
455
+ case "agent": return await runAgent(node, input, options);
456
+ case "script": return await runScript(node, input, options);
457
+ case "text_output": {
458
+ const text = dagMixedToString(input);
459
+ await options.emitText?.({ node, input: text, ...(options.signal ? { signal: options.signal } : {}) });
460
+ if (options.signal?.aborted)
461
+ return { status: "aborted", input };
462
+ emittedText.push(text);
463
+ return { status: "succeeded", input };
464
+ }
465
+ case "user_input": return { status: "succeeded", input, outputs: { output: input } };
466
+ case "stage_start":
467
+ case "stage_handoff": return { status: "failed", input, error: "stage_boundary_requires_stage_flow" };
468
+ }
469
+ }
470
+ catch (error) {
471
+ if (options.signal?.aborted || isAbortError(error))
472
+ return { status: "aborted", input };
473
+ return { status: "failed", input, error: errorMessage(error) };
474
+ }
475
+ }
476
+ async function runAgent(node, input, options) {
477
+ if (!options.invokeAgent)
478
+ return { status: "failed", input, error: "invoke_agent_required" };
479
+ const candidate = await options.invokeAgent({ node, input: dagMixedToString(input), ...(options.signal ? { signal: options.signal } : {}) });
480
+ if (!isDagAgentInvocationResult(candidate))
481
+ return { status: "failed", input, error: "agent_invocation_result_invalid" };
482
+ if (options.signal?.aborted)
483
+ return { status: "aborted", input };
484
+ if (candidate.status === "error")
485
+ return {
486
+ status: "succeeded", input, outputs: { error: candidate.error }, ...(candidate.receipt === undefined ? {} : { receipt: candidate.receipt }),
487
+ };
488
+ if (node.config.require_json && !isStrictJson(candidate.body))
489
+ return {
490
+ status: "succeeded", input, outputs: { error: "agent_output_invalid_json" }, ...(candidate.receipt === undefined ? {} : { receipt: candidate.receipt }),
491
+ };
492
+ return {
493
+ status: "succeeded", input, outputs: { success: candidate.body }, ...(candidate.receipt === undefined ? {} : { receipt: candidate.receipt }),
494
+ };
495
+ }
496
+ async function runScript(node, input, options) {
497
+ if (!options.runScript)
498
+ return { status: "failed", input, error: "run_script_required" };
499
+ const candidate = await options.runScript({ node, input, ...(options.signal ? { signal: options.signal } : {}) });
500
+ if (!isDagScriptResult(candidate))
501
+ return { status: "failed", input, error: "script_main_result_invalid" };
502
+ if (options.signal?.aborted)
503
+ return { status: "aborted", input };
504
+ const allowedPaths = node.config.paths?.length ? node.config.paths : ["output"];
505
+ if (!allowedPaths.includes(candidate.path))
506
+ return { status: "failed", input, error: "script_main_path_invalid" };
507
+ return { status: "succeeded", input, outputs: { [candidate.path]: candidate.body } };
508
+ }
509
+ export function isDagAgentInvocationResult(value) {
510
+ if (!isRecord(value))
511
+ return false;
512
+ if (value.status === "success")
513
+ return typeof value.body === "string";
514
+ return value.status === "error" && typeof value.error === "string";
515
+ }
516
+ export function isDagScriptResult(value) {
517
+ if (!isRecord(value) || Object.keys(value).length !== 2 || !Object.hasOwn(value, "path") || !Object.hasOwn(value, "body"))
518
+ return false;
519
+ return typeof value.path === "string" && value.path.length > 0 && isDagJson(value.body);
520
+ }
521
+ function isSystemStageResult(value) {
522
+ return isRecord(value) && Object.hasOwn(value, "body") && isDagJson(value.body);
523
+ }
524
+ function indexOutgoing(edges) {
525
+ const result = new Map();
526
+ for (const edge of edges) {
527
+ const list = result.get(key(edge.from.node_id, edge.from.port)) ?? [];
528
+ list.push(edge);
529
+ result.set(key(edge.from.node_id, edge.from.port), list);
530
+ }
531
+ return result;
532
+ }
533
+ /** ID 与 port 可包含冒号,故使用无歧义 JSON 元组作为索引键。 */
534
+ function key(nodeId, port) { return JSON.stringify([nodeId, port]); }
535
+ function isStrictJson(value) { try {
536
+ return isDagJson(JSON.parse(value));
537
+ }
538
+ catch {
539
+ return false;
540
+ } }
541
+ function isRecord(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
542
+ function isAbortError(error) { return error instanceof Error && error.name === "AbortError"; }
543
+ function errorMessage(error) { return error instanceof Error ? error.message : typeof error === "string" ? error : "unknown_error"; }