@signalridge/pi-workflows 1.3.1 → 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/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 concurrent agents; pipeline(items, ...stages) for sequential per-item stages.",
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 body = run.error ?? `completed with ${Object.keys(run.callResults).length} agent call(s).`;
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, deterministic script that dispatches the user's instruction
776
- // through a general-purpose agent and returns its answer.
777
- const script = `export const meta = { name: "ad-hoc", description: "Run a user-requested task with a general-purpose agent" };
778
- const answer = await agent(${JSON.stringify(prompt)});
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
  };