@agwab/pi-workflow 0.2.0 → 0.2.1

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/dist/store.js CHANGED
@@ -8,6 +8,9 @@ const TERMINAL_INDEX_LIMIT = 50;
8
8
  const LEASE_STALE_MS = 30_000;
9
9
  const INDEX_LOCK_WAIT_MS = 5_000;
10
10
  const INDEX_LOCK_RETRY_MS = 50;
11
+ const DEFAULT_INDEX_UPDATE_DEBOUNCE_MS = 500;
12
+ let indexUpdateDebounceMs = DEFAULT_INDEX_UPDATE_DEBOUNCE_MS;
13
+ const pendingIndexUpdates = new Map();
11
14
  const runLeaseContext = new AsyncLocalStorage();
12
15
  const TASK_STATUSES = [
13
16
  "pending",
@@ -256,7 +259,46 @@ export async function writeRunRecord(cwd, run) {
256
259
  const derived = deriveRunStatus(run);
257
260
  Object.assign(run, derived);
258
261
  await writeJsonAtomic(workflowRunPath(cwd, run.runId), run);
259
- await updateIndex(cwd).catch(() => undefined);
262
+ scheduleIndexUpdate(cwd, run.runId, {
263
+ immediate: isTerminalWorkflowStatus(run.status),
264
+ });
265
+ }
266
+ function indexUpdateKey(cwd, runId) {
267
+ return `${cwd}\0${runId}`;
268
+ }
269
+ function scheduleIndexUpdate(cwd, runId, options) {
270
+ const key = indexUpdateKey(cwd, runId);
271
+ const existing = pendingIndexUpdates.get(key);
272
+ if (existing) {
273
+ clearTimeout(existing.timer);
274
+ pendingIndexUpdates.delete(key);
275
+ }
276
+ const runUpdate = () => {
277
+ pendingIndexUpdates.delete(key);
278
+ void updateIndex(cwd, runId).catch(() => undefined);
279
+ };
280
+ if (options.immediate) {
281
+ runUpdate();
282
+ return;
283
+ }
284
+ // Pending debounced index writes are intentionally not flushed on process exit:
285
+ // the next explicit index rebuild/read path self-heals from run.json records.
286
+ const timer = setTimeout(runUpdate, indexUpdateDebounceMs);
287
+ timer.unref?.();
288
+ pendingIndexUpdates.set(key, { cwd, runId, timer });
289
+ }
290
+ export async function flushPendingIndexUpdatesForTests() {
291
+ const pending = [...pendingIndexUpdates.values()];
292
+ pendingIndexUpdates.clear();
293
+ for (const item of pending)
294
+ clearTimeout(item.timer);
295
+ await Promise.all(pending.map((item) => updateIndex(item.cwd, item.runId)));
296
+ }
297
+ export function setIndexUpdateDebounceMsForTests(value) {
298
+ indexUpdateDebounceMs =
299
+ value === undefined
300
+ ? DEFAULT_INDEX_UPDATE_DEBOUNCE_MS
301
+ : Math.max(0, Math.floor(value));
260
302
  }
261
303
  export async function writeCompiledRunArtifact(cwd, runId, compiled) {
262
304
  const runDir = workflowRunDir(cwd, runId);
@@ -830,48 +872,15 @@ function isRunRecordLike(value) {
830
872
  typeof task.status === "string" &&
831
873
  TASK_STATUSES.includes(task.status)));
832
874
  }
833
- export async function updateIndex(cwd) {
875
+ export async function updateIndex(cwd, changedRunId) {
834
876
  const lockFile = join(workflowsRoot(cwd), "index.lock");
835
877
  const ownerId = `${process.pid}-${randomBytes(3).toString("hex")}`;
836
878
  await ensureDir(workflowsRoot(cwd));
837
879
  await acquireLockWithWait(lockFile, ownerId);
838
880
  try {
839
- const runs = (await listRunRecords(cwd)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
840
- const active = runs.filter((run) => !isTerminalWorkflowStatus(run.status));
841
- const terminal = runs
842
- .filter((run) => isTerminalWorkflowStatus(run.status))
843
- .slice(0, TERMINAL_INDEX_LIMIT);
844
- const selected = [...active, ...terminal].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
845
- const index = {
846
- schemaVersion: 1,
847
- updatedAt: nowIso(),
848
- runs: selected.map((run) => ({
849
- runId: run.runId,
850
- name: run.name,
851
- type: run.type,
852
- artifactGraph: run.artifactGraph,
853
- status: run.status,
854
- taskSummary: run.taskSummary,
855
- createdAt: run.createdAt,
856
- updatedAt: run.updatedAt,
857
- parentRunId: run.parentRunId,
858
- rootRunId: run.rootRunId,
859
- round: run.round,
860
- fanout: run.fanout,
861
- runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
862
- tasks: run.tasks.map((task) => ({
863
- taskId: task.taskId,
864
- displayName: task.displayName,
865
- agent: task.agent,
866
- kind: task.kind,
867
- stageId: task.stageId,
868
- backendHandle: task.backendHandle,
869
- status: task.status,
870
- statusDetail: task.statusDetail,
871
- lastMessage: task.lastMessage,
872
- })),
873
- })),
874
- };
881
+ const index = changedRunId
882
+ ? await updateIndexIncremental(cwd, changedRunId)
883
+ : await rebuildIndex(cwd);
875
884
  await writeJsonAtomic(workflowIndexPath(cwd), index);
876
885
  return index;
877
886
  }
@@ -879,6 +888,93 @@ export async function updateIndex(cwd) {
879
888
  await releaseLock(lockFile, ownerId);
880
889
  }
881
890
  }
891
+ async function updateIndexIncremental(cwd, changedRunId) {
892
+ const existing = await readIndexForIncremental(cwd);
893
+ if (!existing)
894
+ return rebuildIndex(cwd);
895
+ let changedRun;
896
+ try {
897
+ changedRun = await readRunRecord(cwd, changedRunId);
898
+ }
899
+ catch {
900
+ return rebuildIndex(cwd);
901
+ }
902
+ const changedEntry = buildIndexEntry(cwd, changedRun);
903
+ const entries = existing.runs
904
+ .filter((entry) => entry.runId !== changedRun.runId)
905
+ .concat(changedEntry);
906
+ return {
907
+ schemaVersion: 1,
908
+ updatedAt: nowIso(),
909
+ runs: selectIndexEntries(entries),
910
+ };
911
+ }
912
+ async function readIndexForIncremental(cwd) {
913
+ let index;
914
+ try {
915
+ index = await readIndex(cwd);
916
+ }
917
+ catch {
918
+ return undefined;
919
+ }
920
+ if (!isIndexRecordLike(index))
921
+ return undefined;
922
+ return index;
923
+ }
924
+ async function rebuildIndex(cwd) {
925
+ const runs = await listRunRecords(cwd);
926
+ return {
927
+ schemaVersion: 1,
928
+ updatedAt: nowIso(),
929
+ runs: selectIndexEntries(runs.map((run) => buildIndexEntry(cwd, run))),
930
+ };
931
+ }
932
+ function selectIndexEntries(entries) {
933
+ const sorted = [...entries].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
934
+ const active = sorted.filter((entry) => !isTerminalWorkflowStatus(entry.status));
935
+ const terminal = sorted
936
+ .filter((entry) => isTerminalWorkflowStatus(entry.status))
937
+ .slice(0, TERMINAL_INDEX_LIMIT);
938
+ return [...active, ...terminal].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
939
+ }
940
+ function buildIndexEntry(cwd, run) {
941
+ return {
942
+ runId: run.runId,
943
+ name: run.name,
944
+ type: run.type,
945
+ artifactGraph: run.artifactGraph,
946
+ status: run.status,
947
+ taskSummary: run.taskSummary,
948
+ createdAt: run.createdAt,
949
+ updatedAt: run.updatedAt,
950
+ parentRunId: run.parentRunId,
951
+ rootRunId: run.rootRunId,
952
+ round: run.round,
953
+ fanout: run.fanout,
954
+ runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
955
+ tasks: run.tasks.map((task) => ({
956
+ taskId: task.taskId,
957
+ displayName: task.displayName,
958
+ agent: task.agent,
959
+ kind: task.kind,
960
+ stageId: task.stageId,
961
+ backendHandle: task.backendHandle,
962
+ status: task.status,
963
+ statusDetail: task.statusDetail,
964
+ lastMessage: task.lastMessage,
965
+ })),
966
+ };
967
+ }
968
+ function isIndexRecordLike(value) {
969
+ return (value?.schemaVersion === 1 &&
970
+ Array.isArray(value.runs) &&
971
+ value.runs.every((entry) => entry &&
972
+ typeof entry === "object" &&
973
+ typeof entry.runId === "string" &&
974
+ typeof entry.updatedAt === "string" &&
975
+ typeof entry.status === "string" &&
976
+ Array.isArray(entry.tasks)));
977
+ }
882
978
  export function deriveRunStatus(run) {
883
979
  const next = { ...run, tasks: run.tasks };
884
980
  next.taskSummary = summarizeTasks(next.tasks);
@@ -1,6 +1,10 @@
1
1
  import type { CompiledTask, WorkflowRunRecord, WorkflowTaskRunRecord } from "./types.js";
2
2
  import type { BackendLaunchResult } from "./backend.js";
3
3
  export declare function setSubagentApiForTests(api: unknown | undefined): void;
4
+ export declare function setSubagentLaunchControlsForTests(options?: {
5
+ releaseDelayMs?: number;
6
+ retryJitterMs?: number | (() => number);
7
+ }): void;
4
8
  export declare function cleanupSubagentRun(_cwd: string, run: WorkflowRunRecord): Promise<void>;
5
9
  export declare function launchSubagentTask(cwd: string, run: WorkflowRunRecord, task: WorkflowTaskRunRecord, compiledTask: CompiledTask): Promise<BackendLaunchResult>;
6
10
  export declare function refreshRunFromSubagentArtifacts(cwd: string, run: WorkflowRunRecord): Promise<WorkflowRunRecord>;
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { copyFile, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
3
3
  import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep, } from "node:path";
4
+ import { availableParallelism } from "node:os";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { fromProjectPath, isTerminalTaskStatus, nowIso, toProjectPath, writeRunRecord, } from "./store.js";
6
7
  import { applyTaskResultArtifact, isTaskTimedOut, markTaskTimedOut, } from "./result.js";
@@ -15,6 +16,10 @@ const FETCH_CONTENT_CACHE_ENV = "PI_WORKFLOW_FETCH_CONTENT_CACHE";
15
16
  const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
16
17
  const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
17
18
  const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
19
+ const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
20
+ const DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
21
+ const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
22
+ const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
18
23
  const MODULE_PATH = fileURLToPath(import.meta.url);
19
24
  const MODULE_DIR = dirname(MODULE_PATH);
20
25
  const BUNDLED_PI_WEB_ACCESS_EXTENSION = bundledNodeModulePath("pi-web-access", "index.ts");
@@ -47,6 +52,81 @@ async function loadSubagentApi() {
47
52
  cachedSubagentApi ??= import(subagentApiSpecifier).then((mod) => mod);
48
53
  return cachedSubagentApi;
49
54
  }
55
+ let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
56
+ let transientRetryJitterForTests;
57
+ const launchWaitQueue = [];
58
+ let activeLaunchSlots = 0;
59
+ function resolveMaxConcurrentLaunches() {
60
+ const override = Number.parseInt(process.env[MAX_CONCURRENT_LAUNCHES_ENV] ?? "", 10);
61
+ if (Number.isFinite(override))
62
+ return Math.max(1, Math.floor(override));
63
+ return Math.max(2, Math.floor(availableParallelism() / 2));
64
+ }
65
+ function isLaunchGateSaturated() {
66
+ return activeLaunchSlots >= resolveMaxConcurrentLaunches();
67
+ }
68
+ async function acquireLaunchSlot() {
69
+ if (!isLaunchGateSaturated()) {
70
+ activeLaunchSlots += 1;
71
+ return releaseLaunchSlot;
72
+ }
73
+ await new Promise((resolveWait) => launchWaitQueue.push(resolveWait));
74
+ return releaseLaunchSlot;
75
+ }
76
+ function releaseLaunchSlot() {
77
+ const next = launchWaitQueue.shift();
78
+ if (next) {
79
+ // Transfer the occupied slot directly to the queued launcher.
80
+ next();
81
+ return;
82
+ }
83
+ activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
84
+ }
85
+ function releaseLaunchSlotAfterDelay(delayMs, release) {
86
+ if (delayMs <= 0) {
87
+ release();
88
+ return;
89
+ }
90
+ const timer = setTimeout(release, delayMs);
91
+ timer.unref?.();
92
+ }
93
+ async function runWithLaunchSlot(action) {
94
+ const release = await acquireLaunchSlot();
95
+ let holdAfterReturn = false;
96
+ try {
97
+ const result = await action();
98
+ holdAfterReturn = true;
99
+ return result;
100
+ }
101
+ finally {
102
+ releaseLaunchSlotAfterDelay(holdAfterReturn ? launchSlotReleaseDelayMs : 0, release);
103
+ }
104
+ }
105
+ function transientRetryJitterMs() {
106
+ if (transientRetryJitterForTests)
107
+ return transientRetryJitterForTests();
108
+ return (MIN_TRANSIENT_RETRY_JITTER_MS +
109
+ Math.floor(Math.random() *
110
+ (MAX_TRANSIENT_RETRY_JITTER_MS - MIN_TRANSIENT_RETRY_JITTER_MS + 1)));
111
+ }
112
+ function sleep(ms) {
113
+ return new Promise((resolve) => setTimeout(resolve, ms));
114
+ }
115
+ export function setSubagentLaunchControlsForTests(options) {
116
+ launchSlotReleaseDelayMs =
117
+ options?.releaseDelayMs === undefined
118
+ ? DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS
119
+ : Math.max(0, Math.floor(options.releaseDelayMs));
120
+ transientRetryJitterForTests =
121
+ options?.retryJitterMs === undefined
122
+ ? undefined
123
+ : typeof options.retryJitterMs === "function"
124
+ ? options.retryJitterMs
125
+ : () => Math.max(0, Math.floor(options.retryJitterMs));
126
+ activeLaunchSlots = 0;
127
+ while (launchWaitQueue.length > 0)
128
+ launchWaitQueue.shift()?.();
129
+ }
50
130
  export async function cleanupSubagentRun(_cwd, run) {
51
131
  for (const task of run.tasks) {
52
132
  if (isTerminalTaskStatus(task.status))
@@ -77,6 +157,14 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
77
157
  message: "fast:on is not supported for pi-workflow execution.",
78
158
  };
79
159
  }
160
+ if ((task.launchRetry?.attempts ?? 0) > 0) {
161
+ const jitterMs = transientRetryJitterMs();
162
+ task.statusDetail = "retry_model_failure";
163
+ task.lastMessage = `waiting ${jitterMs}ms before retrying transient-model launch`;
164
+ await writeRunRecord(cwd, run);
165
+ if (jitterMs > 0)
166
+ await sleep(jitterMs);
167
+ }
80
168
  const systemPromptFile = fromProjectPath(cwd, task.files.systemPrompt);
81
169
  const taskPromptFile = fromProjectPath(cwd, task.files.taskPrompt);
82
170
  const outputFile = fromProjectPath(cwd, task.files.output);
@@ -126,7 +214,11 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
126
214
  subagentOptions.extensions = extensions;
127
215
  if (captureToolCallsEnabled())
128
216
  subagentOptions.captureToolCalls = true;
129
- launched = await api.runSubagent(subagentOptions);
217
+ if (isLaunchGateSaturated()) {
218
+ task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
219
+ await writeRunRecord(cwd, run).catch(() => undefined);
220
+ }
221
+ launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions));
130
222
  }
131
223
  catch (error) {
132
224
  task.status = "pending";
@@ -259,8 +351,23 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
259
351
  : undefined;
260
352
  const toolCalls = await readToolCallsSummary(snapshot, subagentResult, artifactRoot);
261
353
  const outputText = await readFile(outputFile, "utf8").catch(() => "");
354
+ const stderrText = await readFile(stderrFile, "utf8").catch(() => "");
262
355
  const outputBytes = Buffer.byteLength(outputText, "utf8");
263
- const statusInfo = workflowStatusFromSubagent(snapshot, subagentResult, outputBytes);
356
+ let statusInfo = workflowStatusFromSubagent(snapshot, subagentResult, outputBytes);
357
+ const deterministicBootFailure = classifyDeterministicBootFailure({
358
+ statusInfo,
359
+ stderrText,
360
+ outputBytes,
361
+ contextLengthExceeded: Boolean(subagentResult?.metadata?.contextLengthExceeded ??
362
+ snapshot.metadata?.contextLengthExceeded),
363
+ });
364
+ if (deterministicBootFailure) {
365
+ statusInfo = {
366
+ status: "failed",
367
+ failureKind: "deterministic_boot",
368
+ errorMessage: deterministicBootFailure,
369
+ };
370
+ }
264
371
  const completedAt = typeof subagentResult?.completedAt === "string"
265
372
  ? subagentResult.completedAt
266
373
  : (snapshot.completedAt ?? nowIso());
@@ -685,6 +792,23 @@ function failArtifactGraphTask(task, options) {
685
792
  task.lastMessage = options.message;
686
793
  return true;
687
794
  }
795
+ function classifyDeterministicBootFailure(options) {
796
+ if (options.statusInfo.status !== "failed" ||
797
+ options.statusInfo.failureKind !== "model" ||
798
+ options.outputBytes !== 0 ||
799
+ options.contextLengthExceeded) {
800
+ return undefined;
801
+ }
802
+ const text = options.stderrText;
803
+ const deterministicPattern = /(Failed to load extension|Cannot find module|(?:failed to load|invalid|missing) (?:workflow )?config(?:uration)?|config(?:uration)? (?:error|failed|invalid))/i;
804
+ if (!deterministicPattern.test(text))
805
+ return undefined;
806
+ const excerpt = text
807
+ .split(/\r?\n/)
808
+ .map((line) => line.trim())
809
+ .find((line) => deterministicPattern.test(line)) ?? text.trim();
810
+ return `deterministic-boot failure: ${excerpt.slice(0, 500)}`;
811
+ }
688
812
  function shouldRetryTransientModelFailure(statusInfo, workflowResult, outputBytes) {
689
813
  return (statusInfo.status === "failed" &&
690
814
  statusInfo.failureKind === "model" &&
@@ -714,14 +838,14 @@ function retryOrFailTransientSubagentFailure(task, options) {
714
838
  if (!exhausted) {
715
839
  task.status = "pending";
716
840
  task.statusDetail = "retry_model_failure";
717
- task.lastMessage = `${options.message}; retrying transient model failure (${attempt}/${maxAttempts})`;
841
+ task.lastMessage = `${options.message}; retrying transient-model failure (${attempt}/${maxAttempts})`;
718
842
  return true;
719
843
  }
720
844
  task.status = "failed";
721
845
  task.statusDetail = task.launchRetry.reason ?? "model_exhausted";
722
846
  task.exitCode = 1;
723
847
  task.completedAt = nowIso();
724
- task.lastMessage = `${options.message}; transient model failure retries exhausted (${maxAttempts})`;
848
+ task.lastMessage = `${options.message}; transient-model failure retries exhausted (${maxAttempts})`;
725
849
  return true;
726
850
  }
727
851
  function retryOrFailArtifactGraphTask(task, options) {
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { WorkflowRuntimeThinkingResolution } from "./workflow-runtime.js";
1
+ import type { WorkflowModelInfo, WorkflowRuntimeDefaults, WorkflowRuntimeThinkingResolution } from "./workflow-runtime.js";
2
2
  export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh"];
3
3
  export declare const FAST_MODES: readonly ["inherit", "off"];
4
4
  export declare const APPROVAL_MODES: readonly ["non-interactive", "on-request"];
@@ -420,6 +420,8 @@ export interface CompiledDynamicWorkflowTask {
420
420
  helpers: Record<string, CompiledDynamicWorkflowHelper>;
421
421
  workflows: Record<string, CompiledDynamicNestedWorkflow>;
422
422
  decisionLoop?: CompiledDynamicDecisionLoop;
423
+ runtimeOverrides?: WorkflowRuntimeDefaults;
424
+ availableModels?: WorkflowModelInfo[];
423
425
  }
424
426
  export interface CompiledArtifactGraphTask {
425
427
  enabled: true;
@@ -35,6 +35,8 @@ export interface ResolveWorkflowRuntimeOptions {
35
35
  availableModels?: WorkflowModelInfo[];
36
36
  prompt?: WorkflowRuntimePrompt;
37
37
  }
38
+ export type WorkflowRuntimeLayer = WorkflowRuntimeDefaults | undefined;
39
+ export declare function selectWorkflowRuntime(...layers: WorkflowRuntimeLayer[]): WorkflowRuntimeResolutionInput;
38
40
  export declare function toWorkflowModelInfo(model: {
39
41
  provider: string;
40
42
  id: string;
@@ -1,4 +1,34 @@
1
1
  import { THINKING_LEVELS } from "./types.js";
2
+ export function selectWorkflowRuntime(...layers) {
3
+ const modelLayer = layers.find((layer) => modelOf(layer));
4
+ const model = modelOf(modelLayer);
5
+ let thinking;
6
+ for (const layer of layers) {
7
+ if (!layer)
8
+ continue;
9
+ if (layer.thinking) {
10
+ thinking = layer.thinking;
11
+ break;
12
+ }
13
+ const layerModel = modelOf(layer);
14
+ const modelThinking = layerModel
15
+ ? splitKnownThinkingSuffix(layerModel).thinking
16
+ : undefined;
17
+ if (modelThinking) {
18
+ thinking = modelThinking;
19
+ break;
20
+ }
21
+ }
22
+ return {
23
+ ...(model ? { model } : {}),
24
+ ...(thinking ? { thinking } : {}),
25
+ };
26
+ }
27
+ function modelOf(layer) {
28
+ return typeof layer?.model === "string" && layer.model.trim()
29
+ ? layer.model.trim()
30
+ : undefined;
31
+ }
2
32
  export function toWorkflowModelInfo(model) {
3
33
  return {
4
34
  provider: model.provider,
package/docs/usage.md CHANGED
@@ -187,6 +187,17 @@ A run prints a `workflow_*` id. Use that id for follow-up commands:
187
187
 
188
188
  The runtime task is not optional. `/workflow run <workflow>` and `/workflow dynamic` without task text fail before launch.
189
189
 
190
+ ### Opt-in fast mode
191
+
192
+ For lower-latency runs, pass `--thinking low` explicitly:
193
+
194
+ ```text
195
+ /workflow run --thinking low deep-research "Research this repository and summarize the architecture tradeoffs."
196
+ /workflow dynamic --thinking low "Research this repository and summarize the architecture tradeoffs."
197
+ ```
198
+
199
+ This is an opt-in fast mode. Package defaults remain conservative until a separate holdout evaluation provides enough evidence to change them. Current evidence is limited but encouraging for explicit fast runs: the 2026-07-02 `deep-research` combined gate on P1/P2/P3-style prompts resolved non-support tasks to `low`, completed selected valid runs in about 15-17 minutes, passed the strict gate 9/9, and had zero source-ref join failures across those 9 runs. Treat this as a speed option, not proof that every workflow should default to `low`.
200
+
190
201
  ### Run-scoped web-source cache
191
202
 
192
203
  Prefer normalized workflow web tools in new workflows:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agwab/pi-workflow",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Workflow orchestration for Pi subagents.",
5
5
  "private": false,
6
6
  "type": "module",