@evandu/super-pi 0.1.142 → 0.1.144
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.
- package/agents/super-pi-task/core.ts +55 -27
- package/agents/super-pi-task/index.ts +76 -4
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +7 -7
- package/dist/client/assets/{CodeMirrorEditor-iLUdQy9q.js → CodeMirrorEditor-DfQHX1Hy.js} +2 -2
- package/dist/client/assets/{dist-Bt8G7e7e.js → dist-B8oH6dsc.js} +1 -1
- package/dist/client/assets/{dist-DbLEngKN.js → dist-BCLhQXYY.js} +1 -1
- package/dist/client/assets/{dist-DBAoCOnn.js → dist-BEbEv2KM.js} +1 -1
- package/dist/client/assets/{dist-Dxq1s4j3.js → dist-BGBinEie.js} +1 -1
- package/dist/client/assets/{dist-CWBm3q4a.js → dist-BRjyQs_L.js} +1 -1
- package/dist/client/assets/{dist-JuLMNOUt.js → dist-BS-FBUKW.js} +1 -1
- package/dist/client/assets/{dist-Cfa_e2VZ.js → dist-BsMikSTM.js} +1 -1
- package/dist/client/assets/{dist-0dCNvYEE.js → dist-BuPimjY-.js} +1 -1
- package/dist/client/assets/{dist-rIYPNzbY.js → dist-C5BQsmB7.js} +1 -1
- package/dist/client/assets/{dist-B8FbKhnf.js → dist-CDtFD-PW.js} +1 -1
- package/dist/client/assets/{dist-ic-MopV2.js → dist-CgQ7zaWY.js} +1 -1
- package/dist/client/assets/{dist-F634gv7V.js → dist-Cu3B0JQX.js} +1 -1
- package/dist/client/assets/{dist-Bj2C-QqM.js → dist-Cw_KCDH9.js} +1 -1
- package/dist/client/assets/{dist-BA6EsWzo.js → dist-D7aAMRTb.js} +1 -1
- package/dist/client/assets/{dist-B4JqBNQI.js → dist-DKkMk3XX.js} +1 -1
- package/dist/client/assets/{dist-CoTJns5x.js → dist-DOvzruAQ.js} +1 -1
- package/dist/client/assets/{dist-Nt-BWmBB.js → dist-Dnru8_uX.js} +1 -1
- package/dist/client/assets/{dist-DHyWJ9kA.js → dist-ISoxXlmW.js} +1 -1
- package/dist/client/assets/{dist-DEUl3Yqk.js → dist-KQJCVwxy.js} +1 -1
- package/dist/client/assets/{dist-Dq6y_nK3.js → dist-LRNzDUVb.js} +1 -1
- package/dist/client/assets/{dist-CtCiGxne.js → dist-SzZEAk24.js} +1 -1
- package/dist/client/assets/{dist-Df9zcCXs.js → dist-qAUj_2Ue.js} +1 -1
- package/dist/client/assets/{index-CX5gWDve.js → index-BLWr70ej.js} +22 -22
- package/dist/client/assets/{index-DMhigTiY.css → index-Xaui0uUI.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/application/run-service.d.ts +15 -0
- package/dist/server/application/run-service.js +59 -1
- package/dist/server/application/session-orchestrator-service.d.ts +9 -1
- package/dist/server/application/session-orchestrator-service.js +79 -10
- package/dist/server/http-server.js +18 -11
- package/dist/server/runtime.js +7 -6
- package/dist/server/store.js +7 -1
- package/dist/server/task-tool.d.ts +4 -2
- package/dist/server/task-tool.js +6 -3
- package/dist/server/time-zone.d.ts +20 -0
- package/dist/server/time-zone.js +32 -0
- package/dist/server/types.d.ts +9 -0
- package/dist/server/workflow-engine.d.ts +11 -95
- package/dist/server/workflow-engine.js +122 -134
- package/dist/server/workflow.d.ts +73 -31
- package/dist/server/workflow.js +112 -30
- package/package.json +331 -325
|
@@ -1,37 +1,14 @@
|
|
|
1
1
|
import type { SubagentRosterEntry } from "./task-tool.js";
|
|
2
2
|
import type { SubagentRun } from "./types.js";
|
|
3
3
|
/**
|
|
4
|
-
* The workflow PLAN:
|
|
5
|
-
*
|
|
4
|
+
* The workflow PLAN: a tree of steps, bounded loops and primary-supplied foreach iterations. The server
|
|
5
|
+
* executes the tree; it never asks a sub-agent to choose the control flow. Step outputs are still carried
|
|
6
|
+
* into later prompts, and every rule in this file remains pure so validation and conclusion matching can
|
|
7
|
+
* be tested without spawning a process.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `subagent-runs.ts` records and publishes a run, `subagents.ts` spawns and drains a child. What was
|
|
11
|
-
* missing was that a MODEL, given a request that needs three different specialists in a row, had to
|
|
12
|
-
* remember the order itself and hand-copy each result into the next prompt by hand. That is the
|
|
13
|
-
* orchestration gap - and the reason the seam here is pure is the same reason `task-tool.ts` is: every
|
|
14
|
-
* rule in this file can then be proved without spawning a process, which is the only layer where a
|
|
15
|
-
* plan's shape, its refusals and its truncation budgets are cheap to test exhaustively.
|
|
16
|
-
*
|
|
17
|
-
* WHAT IS BORROWED FROM OPENCODE, and what is not: nothing. That is a deliberate departure from the
|
|
18
|
-
* sibling files, which each cite upstream lines they ported. OpenCode has no multi-sub-agent pipeline
|
|
19
|
-
* primitive to port - it composes `task` calls the same way this project did, by model convention, which
|
|
20
|
-
* is the gap. What this file reuses instead is local: the roster filter from `task-tool.ts` (the same one
|
|
21
|
-
* that decides who may be dispatched), and the `SubagentRunRecorder.dispatch` seam, which always resolves
|
|
22
|
-
* with a settled record and therefore needs no error parsing here.
|
|
23
|
-
*
|
|
24
|
-
* WHAT THIS FILE DELIBERATELY DOES NOT MODEL, because `docs/AGENT_LOOP_DESIGN.md` proposed all of it and
|
|
25
|
-
* each one is a second orchestration authority rather than a capability:
|
|
26
|
-
*
|
|
27
|
-
* - BRANCHES AND EDGES. A declared graph makes the model learn a language to express sequencing it can
|
|
28
|
-
* already express by dispatching twice. It also cannot be expressive enough, and a model that finds it
|
|
29
|
-
* too rigid bypasses it, leaving two ways to orchestrate that behave differently.
|
|
30
|
-
* - LOOP-BACK. `verifier.md` already ends its report with `通过`/`失败`, so the model can read a failed
|
|
31
|
-
* step and decide whether to run another round. That judgement is the thing worth keeping on the side
|
|
32
|
-
* that can read the output; encoding it as an edge would move it to a component that never sees the text.
|
|
33
|
-
* - A SEPARATE ARTIFACT STORE. `WorkflowAttempt` below IS the versioned artefact: one attempt's output is
|
|
34
|
-
* written once and never edited, and the next attempt appends beside it rather than replacing it.
|
|
9
|
+
* The dispatch and persistence seams are the existing ones: `task-tool.ts` supplies the roster,
|
|
10
|
+
* `SubagentRunRecorder.dispatch` settles every child, and `WorkflowAttempt` is the append-only artefact.
|
|
11
|
+
* There is deliberately no graph, expression language, server-side item discovery, or second artefact store.
|
|
35
12
|
*/
|
|
36
13
|
/**
|
|
37
14
|
* The id an orchestrated plan answers to.
|
|
@@ -54,6 +31,9 @@ export declare const WORKFLOW_ID_PATTERN: RegExp;
|
|
|
54
31
|
* one it would hit by accident.
|
|
55
32
|
*/
|
|
56
33
|
export declare const WORKFLOW_MAX_STEPS = 8;
|
|
34
|
+
/** A loop always has a finite bound. The primary may lower it or explicitly raise it to this hard cap. */
|
|
35
|
+
export declare const WORKFLOW_DEFAULT_LOOP_MAX = 8;
|
|
36
|
+
export declare const WORKFLOW_MAX_LOOP_ROUNDS = 100;
|
|
57
37
|
/**
|
|
58
38
|
* What happens to a step that failed.
|
|
59
39
|
*
|
|
@@ -130,12 +110,44 @@ export type WorkflowRun = {
|
|
|
130
110
|
summary: string;
|
|
131
111
|
onFailure: WorkflowFailurePolicy;
|
|
132
112
|
status: WorkflowStatus;
|
|
113
|
+
/** Execution rows. The tree below references these by index rather than duplicating attempts and output. */
|
|
133
114
|
steps: WorkflowStep[];
|
|
134
|
-
/**
|
|
115
|
+
/** Optional only for records written before recursive plans existed. */
|
|
116
|
+
nodes?: WorkflowRunNode[];
|
|
117
|
+
/** Number of settled execution rows; while running, the row currently executing or next. */
|
|
135
118
|
currentIndex: number;
|
|
136
119
|
startedAt: string;
|
|
137
120
|
finishedAt?: string;
|
|
138
121
|
};
|
|
122
|
+
export type WorkflowStepRef = {
|
|
123
|
+
kind: "step";
|
|
124
|
+
stepIndex: number;
|
|
125
|
+
};
|
|
126
|
+
export type WorkflowLoopRound = {
|
|
127
|
+
n: number;
|
|
128
|
+
nodes: WorkflowRunNode[];
|
|
129
|
+
/** The round's last node output. Missing output can never satisfy `until`. */
|
|
130
|
+
output?: string;
|
|
131
|
+
untilMet: boolean;
|
|
132
|
+
};
|
|
133
|
+
export type WorkflowLoopRun = {
|
|
134
|
+
kind: "loop";
|
|
135
|
+
until: string;
|
|
136
|
+
max: number;
|
|
137
|
+
/** The primary's body definition, retained so future rounds survive a reload with the whole plan. */
|
|
138
|
+
do: WorkflowPlanNode[];
|
|
139
|
+
rounds: WorkflowLoopRound[];
|
|
140
|
+
};
|
|
141
|
+
export type WorkflowForeachIteration = {
|
|
142
|
+
item: string;
|
|
143
|
+
nodes: WorkflowRunNode[];
|
|
144
|
+
};
|
|
145
|
+
export type WorkflowForeachRun = {
|
|
146
|
+
kind: "foreach";
|
|
147
|
+
items: string[];
|
|
148
|
+
iterations: WorkflowForeachIteration[];
|
|
149
|
+
};
|
|
150
|
+
export type WorkflowRunNode = WorkflowStepRef | WorkflowLoopRun | WorkflowForeachRun;
|
|
139
151
|
/** One step as the primary agent is told about it: the last attempt, not the whole history. */
|
|
140
152
|
export type WorkflowStepSummary = {
|
|
141
153
|
n: number;
|
|
@@ -165,15 +177,36 @@ export type WorkflowSummary = {
|
|
|
165
177
|
steps: WorkflowStepSummary[];
|
|
166
178
|
finishedAt?: string;
|
|
167
179
|
};
|
|
180
|
+
export type WorkflowStepPlan = {
|
|
181
|
+
kind: "step";
|
|
182
|
+
agentId: string;
|
|
183
|
+
label?: string;
|
|
184
|
+
prompt: string;
|
|
185
|
+
};
|
|
186
|
+
export type WorkflowLoopPlan = {
|
|
187
|
+
kind: "loop";
|
|
188
|
+
until: string;
|
|
189
|
+
max?: number;
|
|
190
|
+
do: WorkflowPlanNode[];
|
|
191
|
+
};
|
|
192
|
+
export type WorkflowForeachPlan = {
|
|
193
|
+
kind: "foreach";
|
|
194
|
+
items: string[];
|
|
195
|
+
do: WorkflowPlanNode[];
|
|
196
|
+
};
|
|
197
|
+
export type WorkflowPlanNode = WorkflowStepPlan | WorkflowLoopPlan | WorkflowForeachPlan;
|
|
168
198
|
/** The plan shape the engine executes, after validation. What `validateWorkflowPlan` returns on success. */
|
|
169
199
|
export type WorkflowPlan = {
|
|
170
200
|
summary: string;
|
|
171
201
|
onFailure: WorkflowFailurePolicy;
|
|
202
|
+
/** Legacy input, and a flat declaration index for recursive input. Runtime executions live on WorkflowRun. */
|
|
172
203
|
steps: {
|
|
173
204
|
agentId: string;
|
|
174
205
|
label?: string;
|
|
175
206
|
prompt: string;
|
|
176
207
|
}[];
|
|
208
|
+
/** Present when the primary used the recursive spelling. Legacy steps are normalized by the engine. */
|
|
209
|
+
nodes?: WorkflowPlanNode[];
|
|
177
210
|
};
|
|
178
211
|
/**
|
|
179
212
|
* Text that had to lose its front, plus what it lost and why.
|
|
@@ -219,6 +252,15 @@ export declare const WORKFLOW_CONTEXT_BUDGET = 32000;
|
|
|
219
252
|
export declare const PREVIOUS_TOKEN = "{{previous}}";
|
|
220
253
|
/** The token that hands a step every prior step's output, with nothing prepended. */
|
|
221
254
|
export declare const HISTORY_TOKEN = "{{history}}";
|
|
255
|
+
export declare const ITEM_TOKEN = "{{item}}";
|
|
256
|
+
export declare const LAST_TOKEN = "{{last}}";
|
|
257
|
+
/** Single-pass replacement: token-looking text inside an item or prior output is data, not another expression. */
|
|
258
|
+
export declare function substituteWorkflowVariables(prompt: string, variables: {
|
|
259
|
+
item?: string;
|
|
260
|
+
last?: string;
|
|
261
|
+
}): string;
|
|
262
|
+
/** Exact whole-line matching against the final line. Empty or missing output is false, never an error. */
|
|
263
|
+
export declare function workflowUntilMet(output: string | undefined, until: string): boolean;
|
|
222
264
|
/** What a composed prompt ended up carrying, so a caller can report it without re-parsing the string. */
|
|
223
265
|
export type ComposedPrompt = {
|
|
224
266
|
prompt: string;
|
package/dist/server/workflow.js
CHANGED
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
/**
|
|
3
|
-
* The workflow PLAN:
|
|
4
|
-
*
|
|
3
|
+
* The workflow PLAN: a tree of steps, bounded loops and primary-supplied foreach iterations. The server
|
|
4
|
+
* executes the tree; it never asks a sub-agent to choose the control flow. Step outputs are still carried
|
|
5
|
+
* into later prompts, and every rule in this file remains pure so validation and conclusion matching can
|
|
6
|
+
* be tested without spawning a process.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `subagent-runs.ts` records and publishes a run, `subagents.ts` spawns and drains a child. What was
|
|
10
|
-
* missing was that a MODEL, given a request that needs three different specialists in a row, had to
|
|
11
|
-
* remember the order itself and hand-copy each result into the next prompt by hand. That is the
|
|
12
|
-
* orchestration gap - and the reason the seam here is pure is the same reason `task-tool.ts` is: every
|
|
13
|
-
* rule in this file can then be proved without spawning a process, which is the only layer where a
|
|
14
|
-
* plan's shape, its refusals and its truncation budgets are cheap to test exhaustively.
|
|
15
|
-
*
|
|
16
|
-
* WHAT IS BORROWED FROM OPENCODE, and what is not: nothing. That is a deliberate departure from the
|
|
17
|
-
* sibling files, which each cite upstream lines they ported. OpenCode has no multi-sub-agent pipeline
|
|
18
|
-
* primitive to port - it composes `task` calls the same way this project did, by model convention, which
|
|
19
|
-
* is the gap. What this file reuses instead is local: the roster filter from `task-tool.ts` (the same one
|
|
20
|
-
* that decides who may be dispatched), and the `SubagentRunRecorder.dispatch` seam, which always resolves
|
|
21
|
-
* with a settled record and therefore needs no error parsing here.
|
|
22
|
-
*
|
|
23
|
-
* WHAT THIS FILE DELIBERATELY DOES NOT MODEL, because `docs/AGENT_LOOP_DESIGN.md` proposed all of it and
|
|
24
|
-
* each one is a second orchestration authority rather than a capability:
|
|
25
|
-
*
|
|
26
|
-
* - BRANCHES AND EDGES. A declared graph makes the model learn a language to express sequencing it can
|
|
27
|
-
* already express by dispatching twice. It also cannot be expressive enough, and a model that finds it
|
|
28
|
-
* too rigid bypasses it, leaving two ways to orchestrate that behave differently.
|
|
29
|
-
* - LOOP-BACK. `verifier.md` already ends its report with `通过`/`失败`, so the model can read a failed
|
|
30
|
-
* step and decide whether to run another round. That judgement is the thing worth keeping on the side
|
|
31
|
-
* that can read the output; encoding it as an edge would move it to a component that never sees the text.
|
|
32
|
-
* - A SEPARATE ARTIFACT STORE. `WorkflowAttempt` below IS the versioned artefact: one attempt's output is
|
|
33
|
-
* written once and never edited, and the next attempt appends beside it rather than replacing it.
|
|
8
|
+
* The dispatch and persistence seams are the existing ones: `task-tool.ts` supplies the roster,
|
|
9
|
+
* `SubagentRunRecorder.dispatch` settles every child, and `WorkflowAttempt` is the append-only artefact.
|
|
10
|
+
* There is deliberately no graph, expression language, server-side item discovery, or second artefact store.
|
|
34
11
|
*/
|
|
35
12
|
/**
|
|
36
13
|
* The id an orchestrated plan answers to.
|
|
@@ -53,6 +30,9 @@ export const WORKFLOW_ID_PATTERN = /^wf_[A-Za-z0-9_-]{1,120}$/;
|
|
|
53
30
|
* one it would hit by accident.
|
|
54
31
|
*/
|
|
55
32
|
export const WORKFLOW_MAX_STEPS = 8;
|
|
33
|
+
/** A loop always has a finite bound. The primary may lower it or explicitly raise it to this hard cap. */
|
|
34
|
+
export const WORKFLOW_DEFAULT_LOOP_MAX = 8;
|
|
35
|
+
export const WORKFLOW_MAX_LOOP_ROUNDS = 100;
|
|
56
36
|
/** Every accepted policy, in the order the refusal message lists them for a model that guesses wrong. */
|
|
57
37
|
export const WORKFLOW_FAILURE_POLICIES = ["stop", "skip", "retry:1", "retry:2", "retry:3"];
|
|
58
38
|
/**
|
|
@@ -135,6 +115,19 @@ export const WORKFLOW_CONTEXT_BUDGET = 32_000;
|
|
|
135
115
|
export const PREVIOUS_TOKEN = "{{previous}}";
|
|
136
116
|
/** The token that hands a step every prior step's output, with nothing prepended. */
|
|
137
117
|
export const HISTORY_TOKEN = "{{history}}";
|
|
118
|
+
export const ITEM_TOKEN = "{{item}}";
|
|
119
|
+
export const LAST_TOKEN = "{{last}}";
|
|
120
|
+
/** Single-pass replacement: token-looking text inside an item or prior output is data, not another expression. */
|
|
121
|
+
export function substituteWorkflowVariables(prompt, variables) {
|
|
122
|
+
return prompt.replace(/\{\{(item|last)\}\}/g, (_token, name) => variables[name] ?? "");
|
|
123
|
+
}
|
|
124
|
+
/** Exact whole-line matching against the final line. Empty or missing output is false, never an error. */
|
|
125
|
+
export function workflowUntilMet(output, until) {
|
|
126
|
+
const text = output?.trim();
|
|
127
|
+
if (!text)
|
|
128
|
+
return false;
|
|
129
|
+
return (text.split(/\r?\n/).at(-1)?.trim() ?? "") === until;
|
|
130
|
+
}
|
|
138
131
|
/**
|
|
139
132
|
* Renders one prior step as a block, or as a sentence when it has no output to hand on.
|
|
140
133
|
*
|
|
@@ -288,6 +281,90 @@ function fillHistoryToken(prompt, ran) {
|
|
|
288
281
|
const replacement = blocks.join("\n\n");
|
|
289
282
|
return { text: replacement ? prompt.split(HISTORY_TOKEN).join(replacement) : prompt.split(HISTORY_TOKEN).join("(这是编排的第一步,前面还没有任何产出。)"), used, truncated };
|
|
290
283
|
}
|
|
284
|
+
/** Validates the complete tree before the engine can allocate even its first pending execution row. */
|
|
285
|
+
function validateRecursivePlan(body, roster) {
|
|
286
|
+
const ids = roster.map((entry) => entry.id);
|
|
287
|
+
const available = ids.length ? ids.join(", ") : "(none)";
|
|
288
|
+
const onFailure = body.onFailure === undefined ? "stop" : parseFailurePolicy(body.onFailure);
|
|
289
|
+
if (!onFailure)
|
|
290
|
+
return { refusal: `workflow.onFailure must be one of ${WORKFLOW_FAILURE_POLICIES.join(", ")}, or "retry" on its own for a single retry. Got ${JSON.stringify(body.onFailure)}. "stop" is used when you leave it out.` };
|
|
291
|
+
if (!Array.isArray(body.nodes))
|
|
292
|
+
return { refusal: `workflow.nodes must be an array of at least one step, loop or foreach node. Got ${body.nodes === undefined ? "nothing" : typeof body.nodes}.` };
|
|
293
|
+
if (!body.nodes.length)
|
|
294
|
+
return { refusal: "workflow.nodes was empty. A workflow needs at least one node; for a single sub-agent, call the task tool instead." };
|
|
295
|
+
const steps = [];
|
|
296
|
+
const visit = (raw, where) => {
|
|
297
|
+
if (typeof raw !== "object" || raw === null)
|
|
298
|
+
return { refusal: `${where} must be a step, loop or foreach object. Got ${raw === null ? "null" : typeof raw}.` };
|
|
299
|
+
const entry = raw;
|
|
300
|
+
if (entry.kind === "step") {
|
|
301
|
+
const agentId = typeof entry.agentId === "string" ? entry.agentId.trim() : "";
|
|
302
|
+
if (!agentId)
|
|
303
|
+
return { refusal: `${where}.agentId is required. Dispatchable sub-agents: ${available}.` };
|
|
304
|
+
if (!ids.includes(agentId))
|
|
305
|
+
return { refusal: `${JSON.stringify(agentId)} is not a dispatchable sub-agent (${where}.agentId). Dispatchable sub-agents: ${available}. Pick one of those, or call the task tool directly for a single delegation.` };
|
|
306
|
+
const prompt = typeof entry.prompt === "string" ? entry.prompt.trim() : "";
|
|
307
|
+
if (!prompt)
|
|
308
|
+
return { refusal: `${where}.prompt is required and must not be empty: the sub-agent starts with a fresh context, so this is everything it will know about the work.` };
|
|
309
|
+
const label = typeof entry.label === "string" ? entry.label.trim() : "";
|
|
310
|
+
steps.push({ agentId, prompt, ...(label ? { label } : {}) });
|
|
311
|
+
return { node: { kind: "step", agentId, prompt, ...(label ? { label } : {}) } };
|
|
312
|
+
}
|
|
313
|
+
if (entry.kind === "loop") {
|
|
314
|
+
const until = typeof entry.until === "string" ? entry.until.trim() : "";
|
|
315
|
+
if (!until)
|
|
316
|
+
return { refusal: `${where}.until is required: write the exact conclusion line the server should match, for example "通过".` };
|
|
317
|
+
const max = entry.max === undefined ? WORKFLOW_DEFAULT_LOOP_MAX : entry.max;
|
|
318
|
+
if (typeof max !== "number" || !Number.isInteger(max) || max < 1 || max > WORKFLOW_MAX_LOOP_ROUNDS) {
|
|
319
|
+
return { refusal: `${where}.max must be an integer from 1 to ${WORKFLOW_MAX_LOOP_ROUNDS}. Leave it out for the default of ${WORKFLOW_DEFAULT_LOOP_MAX}.` };
|
|
320
|
+
}
|
|
321
|
+
if (!Array.isArray(entry.do) || !entry.do.length)
|
|
322
|
+
return { refusal: `${where}.do must be a non-empty array of nodes; a loop body may contain several steps, loops or foreaches.` };
|
|
323
|
+
const children = [];
|
|
324
|
+
for (let index = 0; index < entry.do.length; index += 1) {
|
|
325
|
+
const child = visit(entry.do[index], `${where}.do[${index}]`);
|
|
326
|
+
if (!child.node)
|
|
327
|
+
return { refusal: child.refusal ?? `${where} is invalid.` };
|
|
328
|
+
children.push(child.node);
|
|
329
|
+
}
|
|
330
|
+
return { node: { kind: "loop", until, max, do: children } };
|
|
331
|
+
}
|
|
332
|
+
if (entry.kind === "foreach") {
|
|
333
|
+
if (!Array.isArray(entry.items) || !entry.items.length) {
|
|
334
|
+
return { refusal: `${where}.items must be a non-empty string array supplied by the primary agent. The server never discovers a foreach list; a foreach without items is refused.` };
|
|
335
|
+
}
|
|
336
|
+
const items = [];
|
|
337
|
+
for (let index = 0; index < entry.items.length; index += 1) {
|
|
338
|
+
const item = entry.items[index];
|
|
339
|
+
if (typeof item !== "string" || !item.trim())
|
|
340
|
+
return { refusal: `${where}.items[${index}] must be a non-empty string explicitly supplied by the primary agent.` };
|
|
341
|
+
items.push(item);
|
|
342
|
+
}
|
|
343
|
+
if (!Array.isArray(entry.do) || !entry.do.length)
|
|
344
|
+
return { refusal: `${where}.do must be a non-empty array of nodes to run once for each primary-supplied item.` };
|
|
345
|
+
const children = [];
|
|
346
|
+
for (let index = 0; index < entry.do.length; index += 1) {
|
|
347
|
+
const child = visit(entry.do[index], `${where}.do[${index}]`);
|
|
348
|
+
if (!child.node)
|
|
349
|
+
return { refusal: child.refusal ?? `${where} is invalid.` };
|
|
350
|
+
children.push(child.node);
|
|
351
|
+
}
|
|
352
|
+
return { node: { kind: "foreach", items, do: children } };
|
|
353
|
+
}
|
|
354
|
+
return { refusal: `${where}.kind must be exactly "step", "loop" or "foreach". Got ${JSON.stringify(entry.kind)}.` };
|
|
355
|
+
};
|
|
356
|
+
const nodes = [];
|
|
357
|
+
for (let index = 0; index < body.nodes.length; index += 1) {
|
|
358
|
+
const result = visit(body.nodes[index], `workflow.nodes[${index}]`);
|
|
359
|
+
if (!result.node)
|
|
360
|
+
return { refusal: result.refusal ?? `workflow.nodes[${index}] is invalid.` };
|
|
361
|
+
nodes.push(result.node);
|
|
362
|
+
if (steps.length > WORKFLOW_MAX_STEPS)
|
|
363
|
+
return { refusal: `workflow.nodes declares more than ${WORKFLOW_MAX_STEPS} step nodes and the limit is ${WORKFLOW_MAX_STEPS}. Shorten the plan, or split it into two workflows.` };
|
|
364
|
+
}
|
|
365
|
+
const summary = typeof body.summary === "string" && body.summary.trim() ? body.summary.trim().slice(0, 200) : `${steps.length} 步编排`;
|
|
366
|
+
return { plan: { summary, onFailure, steps, nodes } };
|
|
367
|
+
}
|
|
291
368
|
/**
|
|
292
369
|
* Checks an untrusted plan body against the roster that may actually be dispatched.
|
|
293
370
|
*
|
|
@@ -304,6 +381,11 @@ export function validateWorkflowPlan(raw, roster) {
|
|
|
304
381
|
if (typeof raw !== "object" || raw === null)
|
|
305
382
|
return { refusal: `A workflow must be an object with "summary", "onFailure" and "steps". Got ${raw === null ? "null" : typeof raw}.` };
|
|
306
383
|
const body = raw;
|
|
384
|
+
if (body.nodes !== undefined) {
|
|
385
|
+
if (body.steps !== undefined)
|
|
386
|
+
return { refusal: "workflow accepts either legacy `steps` or recursive `nodes`, not both. Send one plan shape so there is only one execution order." };
|
|
387
|
+
return validateRecursivePlan(body, roster);
|
|
388
|
+
}
|
|
307
389
|
const rawSteps = body.steps;
|
|
308
390
|
if (!Array.isArray(rawSteps))
|
|
309
391
|
return { refusal: `workflow.steps must be an array of at least 1 and at most ${WORKFLOW_MAX_STEPS} steps. Got ${rawSteps === undefined ? "nothing" : typeof rawSteps}.` };
|