@signalridge/pi-workflows 1.3.2 → 1.4.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.
- package/CHANGELOG.md +5 -0
- package/README.md +40 -11
- package/package.json +3 -2
- package/skills/workflow-authoring/SKILL.md +166 -18
- package/skills/workflow-authoring/examples/named-task-graph.js +34 -0
- package/skills/workflow-authoring/references/capabilities.md +2 -1
- package/skills/workflow-authoring/references/capability-details.md +22 -4
- package/skills/workflow-authoring/references/focused-recipes.md +1 -0
- package/skills/workflow-authoring/references/lifecycle.md +3 -3
- package/skills/workflow-authoring/references/pattern-selection.md +1 -0
- package/skills/workflow-authoring/references/registry-ownership.md +1 -1
- package/skills/workflow-authoring/references/review.md +7 -5
- package/skills/workflow-authoring/references/runtime.md +4 -3
- package/skills/workflow-patterns/SKILL.md +72 -33
- package/skills/workflow-review/SKILL.md +70 -0
- package/src/capabilities.ts +28 -3
- package/src/engine.ts +384 -124
- package/src/index.ts +126 -9
- package/src/journal.ts +266 -27
- package/src/orchestrator.ts +478 -0
- package/src/runtime.ts +332 -123
package/src/index.ts
CHANGED
|
@@ -118,6 +118,117 @@ function formatStart(result: ScriptStartResult): string {
|
|
|
118
118
|
return lines.join("\n");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** Build the bounded planner → graph → synthesis script used by `/workflows run`. */
|
|
122
|
+
export function buildAdHocWorkflowScript(prompt: string): string {
|
|
123
|
+
return `export const meta = { name: "ad-hoc", description: "Plan, execute, and synthesize a bounded multi-agent task graph" };
|
|
124
|
+
const originalPrompt = ${JSON.stringify(prompt)};
|
|
125
|
+
const plan = await agent("Design a bounded execution plan for this user task. Return 1-8 independent or dependency-linked tasks. Use unique short ids, concise descriptions, exact worker prompts, and only dependencies that appear in the task list. Prefer independent tasks when possible. User task:\\n\\n" + originalPrompt, {
|
|
126
|
+
label: "planner",
|
|
127
|
+
schema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
tasks: {
|
|
131
|
+
type: "array",
|
|
132
|
+
minItems: 1,
|
|
133
|
+
maxItems: 8,
|
|
134
|
+
items: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
id: { type: "string", minLength: 1, maxLength: 64 },
|
|
138
|
+
description: { type: "string", minLength: 1, maxLength: 500 },
|
|
139
|
+
prompt: { type: "string", minLength: 1, maxLength: 8000 },
|
|
140
|
+
dependsOn: { type: "array", maxItems: 8, items: { type: "string", minLength: 1, maxLength: 64 } },
|
|
141
|
+
},
|
|
142
|
+
required: ["id", "description", "prompt"],
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
required: ["tasks"],
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
const graph = await orchestrate(plan.tasks.map((task) => ({
|
|
152
|
+
id: task.id,
|
|
153
|
+
description: task.description,
|
|
154
|
+
dependsOn: task.dependsOn || [],
|
|
155
|
+
run: ({ results, statuses }) => agent(task.prompt + "\\n\\nDependency results (null means unavailable):\\n" + JSON.stringify({ results, statuses }), { label: task.id }),
|
|
156
|
+
})), { onError: "continue" });
|
|
157
|
+
const SYNTHESIS_CONTEXT_LIMIT = 78000;
|
|
158
|
+
function renderContextValue(value) {
|
|
159
|
+
const rendered = JSON.stringify(value);
|
|
160
|
+
return rendered === undefined ? String(value) : rendered;
|
|
161
|
+
}
|
|
162
|
+
function boundContextText(value, maxEncodedChars) {
|
|
163
|
+
const text = String(value);
|
|
164
|
+
const encodedLength = (candidate) => JSON.stringify(candidate).length - 2;
|
|
165
|
+
if (encodedLength(text) <= maxEncodedChars) return { text, truncated: false, marker: null };
|
|
166
|
+
let low = 0;
|
|
167
|
+
let high = text.length;
|
|
168
|
+
while (low < high) {
|
|
169
|
+
const middle = Math.ceil((low + high) / 2);
|
|
170
|
+
if (encodedLength(text.slice(0, middle)) <= maxEncodedChars) low = middle;
|
|
171
|
+
else high = middle - 1;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
text: text.slice(0, low),
|
|
175
|
+
truncated: true,
|
|
176
|
+
marker: "[TRUNCATED " + (text.length - low) + " SOURCE CHARACTERS]",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const planMetadata = {
|
|
180
|
+
taskCount: plan.tasks.length,
|
|
181
|
+
tasks: plan.tasks.map((task) => {
|
|
182
|
+
const description = boundContextText(task.description, 500);
|
|
183
|
+
return {
|
|
184
|
+
id: task.id,
|
|
185
|
+
description: description.text,
|
|
186
|
+
descriptionTruncated: description.truncated,
|
|
187
|
+
descriptionTruncationMarker: description.marker,
|
|
188
|
+
dependsOn: task.dependsOn || [],
|
|
189
|
+
};
|
|
190
|
+
}),
|
|
191
|
+
};
|
|
192
|
+
function makeSynthesisContext(valuePreviewBudget, originalPromptBudget) {
|
|
193
|
+
const boundedOriginalPrompt = boundContextText(originalPrompt, originalPromptBudget);
|
|
194
|
+
const tasks = Object.keys(graph.tasks).map((id) => {
|
|
195
|
+
const task = graph.tasks[id];
|
|
196
|
+
const error = task.error == null ? null : boundContextText(task.error, 1000);
|
|
197
|
+
const value = boundContextText(renderContextValue(task.value), valuePreviewBudget);
|
|
198
|
+
return {
|
|
199
|
+
id,
|
|
200
|
+
status: task.status,
|
|
201
|
+
attempts: task.attempts,
|
|
202
|
+
error: error == null ? null : error.text,
|
|
203
|
+
errorTruncated: error == null ? false : error.truncated,
|
|
204
|
+
errorTruncationMarker: error == null ? null : error.marker,
|
|
205
|
+
valuePreview: value.text,
|
|
206
|
+
valueTruncated: value.truncated,
|
|
207
|
+
valueTruncationMarker: value.marker,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
originalPrompt: boundedOriginalPrompt.text,
|
|
212
|
+
originalPromptTruncated: boundedOriginalPrompt.truncated,
|
|
213
|
+
originalPromptTruncationMarker: boundedOriginalPrompt.marker,
|
|
214
|
+
plan: planMetadata,
|
|
215
|
+
tasks,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
let valuePreviewBudget = 6000;
|
|
219
|
+
let originalPromptBudget = 16000;
|
|
220
|
+
let synthesisContext = makeSynthesisContext(valuePreviewBudget, originalPromptBudget);
|
|
221
|
+
let synthesisJson = JSON.stringify(synthesisContext);
|
|
222
|
+
while (synthesisJson.length > SYNTHESIS_CONTEXT_LIMIT && (valuePreviewBudget > 128 || originalPromptBudget > 256)) {
|
|
223
|
+
valuePreviewBudget = Math.max(128, Math.floor(valuePreviewBudget * 0.75));
|
|
224
|
+
originalPromptBudget = Math.max(256, Math.floor(originalPromptBudget * 0.75));
|
|
225
|
+
synthesisContext = makeSynthesisContext(valuePreviewBudget, originalPromptBudget);
|
|
226
|
+
synthesisJson = JSON.stringify(synthesisContext);
|
|
227
|
+
}
|
|
228
|
+
if (synthesisJson.length > SYNTHESIS_CONTEXT_LIMIT) throw new Error("Unable to fit the synthesis context within its managed prompt budget");
|
|
229
|
+
return await agent("Synthesize the worker results into a direct answer to the original task. Mention incomplete or failed coverage explicitly.\\n\\nOriginal task, execution plan, and graph ledger:\\n" + synthesisJson, { label: "synthesizer" });`;
|
|
230
|
+
}
|
|
231
|
+
|
|
121
232
|
export default function piWorkflows(pi: ExtensionAPI): void {
|
|
122
233
|
let engine: WorkflowEngine | undefined;
|
|
123
234
|
let protocolError: string | undefined;
|
|
@@ -190,11 +301,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
190
301
|
name: "workflow",
|
|
191
302
|
label: "Workflow",
|
|
192
303
|
description:
|
|
193
|
-
"Run a JavaScript workflow script through the pi-subagents managed spawning protocol. The script declares `export const meta = { name, description, phases }` first and uses the runtime globals agent(), parallel(), pipeline(), workflow(), verify(), judgePanel(), loopUntilDry(), completenessCheck(), retry(), gate(), checkpoint(), phase(), log(), args, cwd, process, and budget. Determinism is enforced: Date.now()/Math.random()/new Date() are unavailable.",
|
|
304
|
+
"Run a JavaScript workflow script through the pi-subagents managed spawning protocol. The script declares `export const meta = { name, description, phases }` first and uses the runtime globals agent(), parallel(), pipeline(), orchestrate(), workflow(), verify(), judgePanel(), loopUntilDry(), completenessCheck(), retry(), gate(), checkpoint(), phase(), log(), args, cwd, process, and budget. Determinism is enforced: Date.now()/Math.random()/new Date() are unavailable.",
|
|
194
305
|
promptSnippet: "Run a JavaScript workflow script",
|
|
195
306
|
promptGuidelines: [
|
|
196
307
|
"Provide either a full `script` (raw JavaScript with the meta contract first) or a `name` of a saved/built-in workflow.",
|
|
197
|
-
"Use parallel(() => agent(...), ...) for
|
|
308
|
+
"Use orchestrate([{ id, dependsOn, run }]) for named dependency graphs; use parallel(() => agent(...), ...) for a simple fan-out and pipeline(items, ...stages) for sequential per-item stages.",
|
|
198
309
|
"Use background: true for long runs; retrieve results with workflow_control.",
|
|
199
310
|
"Use resumeFromRunId with an edited script to replay the unchanged prefix from cache and re-run the rest live.",
|
|
200
311
|
"Do not include model, thinking, concurrency, retry, timeout, or turn-limit settings beyond the declared options.",
|
|
@@ -731,7 +842,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
731
842
|
const run = await engine.waitFor(runId).catch(() => undefined);
|
|
732
843
|
if (!run) return;
|
|
733
844
|
const header = `Workflow ${run.runId} ${run.status}`;
|
|
734
|
-
const
|
|
845
|
+
const state = engine.getState(runId);
|
|
846
|
+
const finalResult = state?.result ?? run.finalResult;
|
|
847
|
+
const body =
|
|
848
|
+
run.error ??
|
|
849
|
+
(finalResult === undefined
|
|
850
|
+
? `completed with ${Object.keys(run.callResults).length} agent call(s).`
|
|
851
|
+
: `result:\n${typeof finalResult === "string" ? finalResult : (JSON.stringify(finalResult, null, 2) ?? String(finalResult))}`);
|
|
735
852
|
const text = `${header}: ${body}`;
|
|
736
853
|
if (deliverResult) {
|
|
737
854
|
try {
|
|
@@ -772,17 +889,17 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
772
889
|
|
|
773
890
|
/** Build a script from a natural-language prompt and run it in background. */
|
|
774
891
|
const runWorkflowFromPrompt = async (ctx: ExtensionCommandContext, prompt: string): Promise<string | undefined> => {
|
|
775
|
-
// A bounded
|
|
776
|
-
//
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
return answer;`;
|
|
892
|
+
// A bounded planner → named DAG → synthesis script. Keeping the planner's
|
|
893
|
+
// output inside the workflow makes /workflows run useful for broad tasks,
|
|
894
|
+
// while the graph validates dependencies before dispatching any workers.
|
|
895
|
+
const script = buildAdHocWorkflowScript(prompt);
|
|
780
896
|
try {
|
|
897
|
+
await protocolCheck;
|
|
781
898
|
const result = await currentEngine().start(script, { background: true, mainModel: ctx.model?.id });
|
|
782
899
|
void deliverBackgroundResult(currentEngine(), result.runId);
|
|
783
900
|
return result.runId;
|
|
784
901
|
} catch (error: unknown) {
|
|
785
|
-
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
902
|
+
ctx.ui.notify(protocolError ?? (error instanceof Error ? error.message : String(error)), "error");
|
|
786
903
|
return undefined;
|
|
787
904
|
}
|
|
788
905
|
};
|