pi-workflow-engine 1.3.0 → 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.
@@ -616,7 +616,7 @@ function registerWorkflowTool(
616
616
  parallelSubmissionLimit: Type.Optional(Type.Number({ description: "Optional limit for eagerly submitted parallel thunks" })),
617
617
  maxAgents: Type.Optional(
618
618
  Type.Integer({
619
- description: `Maximum live model calls started across this run; replay hits are excluded; clamped to ${WORKFLOW_MAX_AGENTS_MIN}-${WORKFLOW_MAX_AGENTS_MAX}`,
619
+ description: `Maximum live model calls started across this run; replay hits are excluded; clamped to ${WORKFLOW_MAX_AGENTS_MIN}-${WORKFLOW_MAX_AGENTS_MAX}. Leave unset for a registered workflow unless the user asks for a cap: it sizes its own stages`,
620
620
  }),
621
621
  ),
622
622
  agentTimeoutMs: Type.Optional(
@@ -1,8 +1,6 @@
1
- import { join } from "node:path";
2
1
  import { SessionManager, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
2
  import { WorkflowAbortError, WorkflowPauseError } from "./cancellation.ts";
4
3
  import { isRecord } from "./guards.ts";
5
- import { WORKFLOW_RUNS_DIR } from "./journal.ts";
6
4
  import type { WorkflowBackgroundOrigin } from "./types.ts";
7
5
  import {
8
6
  stopWorkflowRunRecord,
@@ -11,7 +9,7 @@ import {
11
9
  type WorkflowRunState,
12
10
  } from "./workflow-run-record.ts";
13
11
  import { updateWorkflowRunDelivery, type WorkflowRunDelivery } from "./workflow-run-background.ts";
14
- import { ProjectWorkflowRunStore, WORKFLOW_RUN_RECORD_SUFFIX, type WorkflowRunStore } from "./workflow-run-store.ts";
12
+ import { ProjectWorkflowRunStore, type WorkflowRunStore } from "./workflow-run-store.ts";
15
13
  import { truncateText } from "./text.ts";
16
14
  import { formatWorkflowUsageLine } from "./usage.ts";
17
15
  import { workflowResultSummary } from "./workflow-execution.ts";
@@ -375,10 +373,7 @@ function wokenDeliveryBody(record: WorkflowRunRecord, summary: string): string {
375
373
 
376
374
  /** The retained record, script and journal, relative to the workflow cwd like the synchronous result's lines. */
377
375
  function backgroundRunFileLines(record: WorkflowRunRecord): string[] {
378
- return [
379
- `Record: ${join(WORKFLOW_RUNS_DIR, `${record.runId}${WORKFLOW_RUN_RECORD_SUFFIX}`)}`,
380
- ...formatWorkflowRunFileLines(record.runId, record.scriptFile !== undefined),
381
- ];
376
+ return formatWorkflowRunFileLines(record.runId, record.scriptFile !== undefined);
382
377
  }
383
378
 
384
379
  function sessionHasBackgroundDelivery(ctx: ExtensionContext, runId: string): boolean {
@@ -1,8 +1,17 @@
1
1
  import { join } from "node:path";
2
- import { formatAdvisoryLocation, isAdvisoryReport, type AdvisoryFinding, type AdvisoryReport } from "./advisory-schema.ts";
2
+ import { Type, type Static } from "typebox";
3
+ import { Value } from "typebox/value";
4
+ import {
5
+ AdvisoryVerifierVerdictSchema,
6
+ formatAdvisoryLocation,
7
+ isAdvisoryReport,
8
+ type AdvisoryFinding,
9
+ type AdvisoryReport,
10
+ } from "./advisory-schema.ts";
3
11
  import { isRecord } from "./guards.ts";
4
12
  import { WORKFLOW_RUNS_DIR } from "./journal.ts";
5
13
  import { workflowRunScriptFile } from "./workflow-run-record.ts";
14
+ import { WORKFLOW_RUN_RECORD_SUFFIX } from "./workflow-run-store.ts";
6
15
  import { toReviewIssues } from "./review/review-issues.ts";
7
16
  import { formatPerfSummary } from "./perf.ts";
8
17
  import { prettyJson } from "./text.ts";
@@ -35,21 +44,31 @@ export function formatWorkflowRunLine(run: { readonly runId: string; readonly re
35
44
  }
36
45
 
37
46
  /**
38
- * Where the host model can read what the run did, relative to the workflow cwd: the persisted script it
39
- * can edit and rerun with `scriptPath`, and the replay journal of completed agent results.
47
+ * Where the host model can read what the run did, relative to the workflow cwd: the record holding the full
48
+ * result, the persisted script it can edit and rerun with `scriptPath`, and the replay journal of completed
49
+ * agent results.
40
50
  */
41
51
  export function formatWorkflowRunFileLines(runId: string, scriptPersisted: boolean): string[] {
42
52
  return [
53
+ `Record: ${join(WORKFLOW_RUNS_DIR, `${runId}${WORKFLOW_RUN_RECORD_SUFFIX}`)}`,
43
54
  ...(scriptPersisted ? [`Script: ${join(WORKFLOW_RUNS_DIR, workflowRunScriptFile(runId))}`] : []),
44
55
  `Journal: ${join(WORKFLOW_RUNS_DIR, `${runId}.jsonl`)}`,
45
56
  "If the result is empty or unexpected, read the journal before diagnosing.",
46
57
  ];
47
58
  }
48
59
 
60
+ /** A verified candidate as lens reviews retain it beside their report, reduced to the fields the host listing reads. */
61
+ const VerifiedCandidateSchema = Type.Object({
62
+ sourceCandidateIds: Type.Array(Type.String()),
63
+ summary: Type.String(),
64
+ verdict: AdvisoryVerifierVerdictSchema,
65
+ });
66
+ type VerifiedCandidate = Static<typeof VerifiedCandidateSchema>;
67
+
49
68
  /** A workflow result as the host model reads it: advisory reports as findings, other values as bounded JSON. */
50
69
  export function formatResultForContext(result: unknown): string {
51
70
  if (typeof result === "string") return result;
52
- if (isAdvisoryReport(result)) return formatAdvisoryReportForContext(result);
71
+ if (isAdvisoryReport(result)) return formatAdvisoryReportForContext(result, unreportedCandidates(result));
53
72
  const summary = workflowResultSummary(result);
54
73
  if (summary !== undefined && isRecord(result) && Object.keys(result).length === 1) return summary;
55
74
  const json = formatResultJson(result);
@@ -57,7 +76,20 @@ export function formatResultForContext(result: unknown): string {
57
76
  return summary === undefined ? json : `${summary}\n\n${json}`;
58
77
  }
59
78
 
60
- function formatAdvisoryReportForContext(report: AdvisoryReport): string {
79
+ /**
80
+ * Verified candidates that no finding draws on: ones synthesis left out under the reporting bar, and refuted
81
+ * ones. Listing them lets the host resolve the candidate IDs a synthesis summary cites when it explains an
82
+ * exclusion, without reading the run record.
83
+ */
84
+ function unreportedCandidates(report: AdvisoryReport & { readonly verification?: unknown }): VerifiedCandidate[] {
85
+ const { verification } = report;
86
+ if (!Array.isArray(verification)) return [];
87
+ const reported = new Set(report.findings.flatMap((finding) => finding.sourceCandidateIds ?? []));
88
+ return verification.filter((entry): entry is VerifiedCandidate =>
89
+ Value.Check(VerifiedCandidateSchema, entry) && !entry.sourceCandidateIds.some((id) => reported.has(id)));
90
+ }
91
+
92
+ function formatAdvisoryReportForContext(report: AdvisoryReport, unreported: readonly VerifiedCandidate[]): string {
61
93
  const lines = [report.summary];
62
94
  const issues = toReviewIssues(report);
63
95
  if (issues.length > 0) {
@@ -75,6 +107,10 @@ function formatAdvisoryReportForContext(report: AdvisoryReport): string {
75
107
  );
76
108
  }
77
109
  }
110
+ if (unreported.length > 0) {
111
+ lines.push("", "Verified but not reported (verdict, candidate IDs):",
112
+ ...unreported.map((candidate) => `- ${candidate.verdict} ${candidate.sourceCandidateIds.join(", ")}: ${candidate.summary}`));
113
+ }
78
114
  if (report.gaps && report.gaps.length > 0) {
79
115
  lines.push("", "Coverage gaps:", ...report.gaps.map((gap) => `- ${gap}`));
80
116
  }
@@ -65,7 +65,7 @@ export function setWorkflowSize(configPath: string, size: WorkflowSizeGuideline)
65
65
  /** The catalog line for a size, or undefined when the guideline is off. */
66
66
  export function workflowSizeGuidelineLine(size: WorkflowSizeGuideline): string | undefined {
67
67
  if (size === "off") return undefined;
68
- return `Workflow size guideline: ${size} — keep each workflow under ${SIZE_AGENT_LIMITS[size]} agents unless the user asks for more; the user changes this with /workflow:size.`;
68
+ return `Workflow size guideline: ${size} — keep each workflow script you author under ${SIZE_AGENT_LIMITS[size]} agents unless the user asks for more; registered workflows size their own stages, so run them without maxAgents unless the user sets a cap. The user changes this with /workflow:size.`;
69
69
  }
70
70
 
71
71
  type WorkflowSizeCommand =
@@ -76,7 +76,7 @@ type WorkflowSizeCommand =
76
76
  const USAGE = "Usage: /workflow:size [small|medium|large|off] [--user|--project]";
77
77
  const SIZE_COMPLETIONS = WORKFLOW_SIZE_GUIDELINES.map((value) => ({
78
78
  value,
79
- description: value === "off" ? "No size guideline in the prompt" : `Keep each workflow under ${SIZE_AGENT_LIMITS[value]} agents`,
79
+ description: value === "off" ? "No size guideline in the prompt" : `Keep each authored workflow under ${SIZE_AGENT_LIMITS[value]} agents`,
80
80
  }));
81
81
  const SCOPE_COMPLETIONS = [
82
82
  { value: "--user", description: "Write the user-level workflow settings" },
@@ -110,7 +110,7 @@ export function formatWorkflowSize(resolved: ResolvedWorkflowSize): string {
110
110
  const source = resolved.source === "default" ? "default" : `${resolved.source} config ${resolved.configPath}`;
111
111
  const rule = resolved.size === "off"
112
112
  ? "no size guideline is added to the prompt"
113
- : `keep each workflow under ${SIZE_AGENT_LIMITS[resolved.size]} agents unless the user asks for more`;
113
+ : `keep each authored workflow script under ${SIZE_AGENT_LIMITS[resolved.size]} agents unless the user asks for more; registered workflows are not capped by it`;
114
114
  return [
115
115
  `Workflow size guideline: ${resolved.size} (${source}) — ${rule}.`,
116
116
  `The maxAgents run limit (default ${DEFAULT_WORKFLOW_MAX_AGENTS}) stays the hard cap on live agents per run.`,
package/USAGE.md CHANGED
@@ -207,13 +207,13 @@ The catalog ends with one line telling the host agent how large to make each wor
207
207
 
208
208
  ```text
209
209
  /workflow:size # show the current guideline and where it comes from
210
- /workflow:size small # keep each workflow under 5 agents
210
+ /workflow:size small # keep each authored workflow under 5 agents
211
211
  /workflow:size medium # under 10 agents (the default)
212
212
  /workflow:size large --project # under 25 agents, for this project only
213
213
  /workflow:size off # leave the line out
214
214
  ```
215
215
 
216
- The line reads, for example, `Workflow size guideline: medium — keep each workflow under 10 agents unless the user asks for more; the user changes this with /workflow:size.` The setting is stored like the model profiles: `~/.pi/agent/workflow-settings.json` for the user, overridden by `<project>/.pi/workflow-settings.json`; setting a size writes the user file unless `--project` is given. A minimal file is `{ "size": "small" }`. Changing it rebuilds the current session's catalog once.
216
+ The line reads, for example, `Workflow size guideline: medium — keep each workflow script you author under 10 agents unless the user asks for more; registered workflows size their own stages, so run them without maxAgents unless the user sets a cap. The user changes this with /workflow:size.` The setting is stored like the model profiles: `~/.pi/agent/workflow-settings.json` for the user, overridden by `<project>/.pi/workflow-settings.json`; setting a size writes the user file unless `--project` is given. A minimal file is `{ "size": "small" }`. Changing it rebuilds the current session's catalog once.
217
217
 
218
218
  The guideline only shapes what the host agent writes. The `maxAgents` run limit (default `64`, see [Runtime knobs](#runtime-knobs)) stays the separate hard cap: once a run has started that many live agents, further `agent()` calls fail with `WorkflowAgentLimitError`, whatever the guideline says.
219
219
 
@@ -287,7 +287,7 @@ Persistence is intentionally bounded and private by default. Raw workflow args,
287
287
 
288
288
  An inline run (a tool `script` or `scriptPath`) also saves its script to `.pi/.workflow-runs/<run-id>.script.js`, a separate file the record only references, because the record never holds prompts. The script holds nothing new: pi's session transcript already contains it as the tool call's arguments. A pruned record takes its script with it. Whenever the engine creates `.pi/.workflow-runs/`, it adds a `.gitignore` containing `*` so run files stay out of the project's git history; an existing `.gitignore` there is never replaced.
289
289
 
290
- The result handed to the host agent names the run's files after the run ID: `Script:` for an inline run, and `Journal:` with a reminder to read the journal before diagnosing an empty or unexpected result. A failed inline run lists the same lines after its error, a background start names the script too, and a background delivery names the record, script, and journal (see [Background workflows](#background-workflows)). To iterate, edit the script and rerun it with `scriptPath` (see [Author an inline workflow](#author-an-inline-workflow)).
290
+ The result handed to the host agent names the run's files after the run ID: `Record:` (which holds the full result when the text is truncated), `Script:` for an inline run, and `Journal:` with a reminder to read the journal before diagnosing an empty or unexpected result. A failed run lists the same lines after its error, a background start names the script too, and a background delivery names the same files (see [Background workflows](#background-workflows)). A review result also lists the verified candidates that no reported finding draws on, with their verdicts and candidate IDs, so the host can resolve the IDs a synthesis summary cites when it explains what it left out. To iterate, edit the script and rerun it with `scriptPath` (see [Author an inline workflow](#author-an-inline-workflow)).
291
291
 
292
292
  ### Background workflows
293
293
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-workflow-engine",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency Dynamax workflows for pi: live parallel subagents, typed handoffs, synthesized results.",
6
6
  "keywords": [