pi-workflow-engine 0.13.0 → 1.1.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/.pi/extensions/pi-workflow-engine/index.ts +152 -416
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +17 -14
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +12 -13
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +131 -117
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +3 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/classify.ts +107 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +58 -117
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +41 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +0 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/model-ref.ts +19 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +29 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +7 -9
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +17 -4
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +30 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -129
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +41 -34
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +162 -22
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +48 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/README.md +1 -1
- package/USAGE.md +79 -20
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -24
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { abortReason, isFatalWorkflowError, throwIfAborted } from "./cancellation.ts";
|
|
3
|
+
import { isFiniteNumber } from "./guards.ts";
|
|
3
4
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -59,10 +60,6 @@ export class Semaphore {
|
|
|
59
60
|
cleanup();
|
|
60
61
|
reject(abortReason(signal));
|
|
61
62
|
};
|
|
62
|
-
if (signal?.aborted) {
|
|
63
|
-
reject(abortReason(signal));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
63
|
this.waiters.push(waiter);
|
|
67
64
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
68
65
|
});
|
|
@@ -138,13 +135,6 @@ export async function parallel<T>(
|
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
export function bindParallel(options: ParallelOptions): WorkflowParallel {
|
|
141
|
-
const executionOptions: ParallelOptions = {
|
|
142
|
-
signal: options.signal,
|
|
143
|
-
abortController: options.abortController,
|
|
144
|
-
limit: options.limit,
|
|
145
|
-
drainTimeoutMs: options.drainTimeoutMs,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
138
|
function boundParallel<T>(thunks: Array<() => Promise<T>>): Promise<Array<T | null>>;
|
|
149
139
|
function boundParallel<T>(
|
|
150
140
|
thunks: Array<() => Promise<T>>,
|
|
@@ -155,9 +145,9 @@ export function bindParallel(options: ParallelOptions): WorkflowParallel {
|
|
|
155
145
|
workflowOptions?: ParallelSettledOptions,
|
|
156
146
|
): Promise<Array<T | null> | Array<ParallelSettledResult<T>>> {
|
|
157
147
|
if (workflowOptions?.settled) {
|
|
158
|
-
return await parallel(thunks, { ...
|
|
148
|
+
return await parallel(thunks, { ...options, settled: true });
|
|
159
149
|
}
|
|
160
|
-
return await parallel(thunks,
|
|
150
|
+
return await parallel(thunks, options);
|
|
161
151
|
}
|
|
162
152
|
|
|
163
153
|
return boundParallel;
|
|
@@ -224,7 +214,6 @@ async function runParallel<T, Result>(
|
|
|
224
214
|
};
|
|
225
215
|
|
|
226
216
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
227
|
-
if (options.signal?.aborted) onAbort();
|
|
228
217
|
for (let worker = 0; worker < limit; worker++) {
|
|
229
218
|
void runWorker().then(completeWorker, (error) => {
|
|
230
219
|
recordFatalFailure(error);
|
|
@@ -264,7 +253,7 @@ function readSerializableDetails(
|
|
|
264
253
|
typeof detail === "string" ||
|
|
265
254
|
typeof detail === "boolean" ||
|
|
266
255
|
detail === null ||
|
|
267
|
-
(
|
|
256
|
+
isFiniteNumber(detail)
|
|
268
257
|
) {
|
|
269
258
|
details[key] = detail;
|
|
270
259
|
}
|
|
@@ -285,12 +274,12 @@ function readProperty(value: unknown, property: string): unknown {
|
|
|
285
274
|
}
|
|
286
275
|
|
|
287
276
|
function normalizeLimit(limit: number | undefined, itemCount: number): number {
|
|
288
|
-
if (
|
|
277
|
+
if (!isFiniteNumber(limit)) return Math.max(1, itemCount);
|
|
289
278
|
return Math.max(1, Math.min(itemCount, Math.trunc(limit)));
|
|
290
279
|
}
|
|
291
280
|
|
|
292
281
|
function normalizeDrainTimeout(timeoutMs: number | undefined): number {
|
|
293
|
-
if (
|
|
282
|
+
if (!isFiniteNumber(timeoutMs)) return DEFAULT_FATAL_DRAIN_TIMEOUT_MS;
|
|
294
283
|
return Math.max(0, Math.trunc(timeoutMs));
|
|
295
284
|
}
|
|
296
285
|
|
|
@@ -302,48 +291,7 @@ function normalizeDrainTimeout(timeoutMs: number | undefined): number {
|
|
|
302
291
|
*
|
|
303
292
|
* Each stage receives (previousResult, originalItem, index).
|
|
304
293
|
*/
|
|
305
|
-
export
|
|
306
|
-
items: readonly Item[],
|
|
307
|
-
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
308
|
-
): Promise<Array<A | null>>;
|
|
309
|
-
export async function pipeline<Item, A, B>(
|
|
310
|
-
items: readonly Item[],
|
|
311
|
-
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
312
|
-
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
313
|
-
): Promise<Array<B | null>>;
|
|
314
|
-
export async function pipeline<Item, A, B, C>(
|
|
315
|
-
items: readonly Item[],
|
|
316
|
-
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
317
|
-
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
318
|
-
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
319
|
-
): Promise<Array<C | null>>;
|
|
320
|
-
export async function pipeline<Item, A, B, C, D>(
|
|
321
|
-
items: readonly Item[],
|
|
322
|
-
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
323
|
-
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
324
|
-
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
325
|
-
stage4: (prev: C, item: Item, index: number) => Promise<D>,
|
|
326
|
-
): Promise<Array<D | null>>;
|
|
327
|
-
export async function pipeline<Item, A, B, C, D, E>(
|
|
328
|
-
items: readonly Item[],
|
|
329
|
-
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
330
|
-
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
331
|
-
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
332
|
-
stage4: (prev: C, item: Item, index: number) => Promise<D>,
|
|
333
|
-
stage5: (prev: D, item: Item, index: number) => Promise<E>,
|
|
334
|
-
): Promise<Array<E | null>>;
|
|
335
|
-
export async function pipeline(
|
|
336
|
-
items: readonly unknown[],
|
|
337
|
-
...stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>
|
|
338
|
-
): Promise<Array<unknown | null>>;
|
|
339
|
-
export async function pipeline(
|
|
340
|
-
items: readonly unknown[],
|
|
341
|
-
...stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>
|
|
342
|
-
): Promise<Array<unknown | null>> {
|
|
343
|
-
return pipelineWithOptions(items, stages);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
export function bindPipeline(options: PipelineOptions): Pipeline {
|
|
294
|
+
export function bindPipeline(options: PipelineOptions) {
|
|
347
295
|
async function boundPipeline<Item, A>(
|
|
348
296
|
items: readonly Item[],
|
|
349
297
|
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
@@ -374,33 +322,30 @@ export function bindPipeline(options: PipelineOptions): Pipeline {
|
|
|
374
322
|
stage4: (prev: C, item: Item, index: number) => Promise<D>,
|
|
375
323
|
stage5: (prev: D, item: Item, index: number) => Promise<E>,
|
|
376
324
|
): Promise<Array<E | null>>;
|
|
325
|
+
async function boundPipeline(
|
|
326
|
+
items: readonly unknown[],
|
|
327
|
+
...stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>
|
|
328
|
+
): Promise<Array<unknown | null>>;
|
|
377
329
|
async function boundPipeline(
|
|
378
330
|
items: readonly unknown[],
|
|
379
331
|
...stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>
|
|
380
332
|
): Promise<Array<unknown | null>> {
|
|
381
|
-
return
|
|
333
|
+
return await parallel(
|
|
334
|
+
items.map((item, index) => async () => {
|
|
335
|
+
let acc: unknown = item;
|
|
336
|
+
for (const stage of stages) {
|
|
337
|
+
throwIfAborted(options.signal);
|
|
338
|
+
acc = await stage(acc, item, index);
|
|
339
|
+
}
|
|
340
|
+
return acc;
|
|
341
|
+
}),
|
|
342
|
+
options,
|
|
343
|
+
);
|
|
382
344
|
}
|
|
383
345
|
return boundPipeline;
|
|
384
346
|
}
|
|
385
347
|
|
|
386
|
-
export
|
|
387
|
-
items: readonly unknown[],
|
|
388
|
-
stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>,
|
|
389
|
-
options: PipelineOptions = {},
|
|
390
|
-
): Promise<Array<unknown | null>> {
|
|
391
|
-
throwIfAborted(options.signal);
|
|
392
|
-
return await parallel(
|
|
393
|
-
items.map((item, index) => async () => {
|
|
394
|
-
throwIfAborted(options.signal);
|
|
395
|
-
let acc: unknown = item;
|
|
396
|
-
for (const stage of stages) {
|
|
397
|
-
throwIfAborted(options.signal);
|
|
398
|
-
acc = await stage(acc, item, index);
|
|
399
|
-
}
|
|
400
|
-
return acc;
|
|
401
|
-
}),
|
|
402
|
-
options,
|
|
403
|
-
);
|
|
404
|
-
}
|
|
348
|
+
export type Pipeline = ReturnType<typeof bindPipeline>;
|
|
405
349
|
|
|
406
|
-
|
|
350
|
+
/** `pipeline` without a run's abort signal (tests, benchmarks). */
|
|
351
|
+
export const pipeline: Pipeline = bindPipeline({});
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { LoadedWorkflow, WorkflowSourceIdentity } from "./types.ts";
|
|
6
6
|
import type { PerfSink } from "./perf.ts";
|
|
7
7
|
import { loadWorkflow, parseWorkflowModule } from "./workflow-module.ts";
|
|
8
8
|
import { BUILTIN_SOURCE_ROOT, BUILTIN_WORKFLOW_DEFINITIONS, BUILTIN_WORKFLOW_FILES } from "./workflows.ts";
|
|
9
|
-
import {
|
|
10
|
-
import { FINGERPRINT_EXCLUDED_RELATIVE_PATHS } from "./resume-context.ts";
|
|
9
|
+
import { captureSourceTreeFingerprint, type FingerprintCapture } from "./tree-fingerprint.ts";
|
|
11
10
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
12
11
|
|
|
13
12
|
export interface DiscoverWorkflowsOptions {
|
|
@@ -16,16 +15,13 @@ export interface DiscoverWorkflowsOptions {
|
|
|
16
15
|
readonly userWorkflowDir?: string;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const DISCOVERY_FINGERPRINT_MAX_BYTES = 32 << 20;
|
|
20
|
-
const DISCOVERY_FINGERPRINT_MAX_FILES = 4096;
|
|
21
|
-
|
|
22
18
|
const discoveryCache = new Map<string, Map<string, LoadedWorkflow>>();
|
|
23
19
|
/** Best-effort dynamic load of every `*.ts` workflow in a directory. */
|
|
24
20
|
async function loadDir(
|
|
25
21
|
dir: string,
|
|
26
22
|
sourceIdentity: (path: string) => WorkflowSourceIdentity,
|
|
27
23
|
excludeFiles: ReadonlySet<string> = new Set(),
|
|
28
|
-
|
|
24
|
+
captureProvenance?: () => Promise<FingerprintCapture>,
|
|
29
25
|
): Promise<LoadedWorkflow[]> {
|
|
30
26
|
let entries: string[];
|
|
31
27
|
try {
|
|
@@ -33,12 +29,15 @@ async function loadDir(
|
|
|
33
29
|
} catch {
|
|
34
30
|
return [];
|
|
35
31
|
}
|
|
32
|
+
const candidates = entries.filter(
|
|
33
|
+
(name) => !excludeFiles.has(name) && name.endsWith(".ts") && !name.startsWith("_") && !name.startsWith("."),
|
|
34
|
+
);
|
|
35
|
+
// The shipped workflows/ dir holds only excluded built-ins; skip the tree hash when nothing will be imported.
|
|
36
|
+
if (candidates.length === 0) return [];
|
|
36
37
|
|
|
37
|
-
const before =
|
|
38
|
+
const before = await captureProvenance?.();
|
|
38
39
|
const modules: Array<{ readonly path: string; readonly module: Parameters<typeof loadWorkflow>[0] }> = [];
|
|
39
|
-
for (const name of
|
|
40
|
-
if (excludeFiles.has(name)) continue;
|
|
41
|
-
if (!name.endsWith(".ts") || name.startsWith("_") || name.startsWith(".")) continue;
|
|
40
|
+
for (const name of candidates) {
|
|
42
41
|
try {
|
|
43
42
|
const path = join(dir, name);
|
|
44
43
|
const importUrl = pathToFileURL(path);
|
|
@@ -56,15 +55,6 @@ async function loadDir(
|
|
|
56
55
|
return modules.map(({ path, module }) => loadWorkflow(module, sourceIdentity(path)));
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
async function captureDiscoveryFingerprint(root: string) {
|
|
60
|
-
return await captureTreeFingerprint({
|
|
61
|
-
root,
|
|
62
|
-
excludedRelativePaths: FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
63
|
-
maxBytes: DISCOVERY_FINGERPRINT_MAX_BYTES,
|
|
64
|
-
maxFiles: DISCOVERY_FINGERPRINT_MAX_FILES,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
/**
|
|
69
59
|
* All available workflows by name. Static registry wins on name collisions, so the
|
|
70
60
|
* bundled example is always the verified one even if a same-named file is dropped in.
|
|
@@ -80,14 +70,14 @@ export async function discoverWorkflows(repoDir: string, options: DiscoverWorkfl
|
|
|
80
70
|
|
|
81
71
|
const byName = await timed(options.perf, "discovery.total_ms", async () => {
|
|
82
72
|
const next = new Map<string, LoadedWorkflow>();
|
|
83
|
-
const builtinSource = await
|
|
73
|
+
const builtinSource = await captureSourceTreeFingerprint(BUILTIN_SOURCE_ROOT);
|
|
84
74
|
for (const definition of BUILTIN_WORKFLOW_DEFINITIONS) {
|
|
85
75
|
const source: WorkflowSourceIdentity =
|
|
86
76
|
builtinSource.kind === "verified"
|
|
87
77
|
? {
|
|
88
78
|
kind: "file",
|
|
89
79
|
path: definition.path,
|
|
90
|
-
root:
|
|
80
|
+
root: BUILTIN_SOURCE_ROOT,
|
|
91
81
|
fingerprint: builtinSource.fingerprint,
|
|
92
82
|
}
|
|
93
83
|
: {
|
|
@@ -98,6 +88,9 @@ export async function discoverWorkflows(repoDir: string, options: DiscoverWorkfl
|
|
|
98
88
|
}
|
|
99
89
|
|
|
100
90
|
const repoWorkflowDir = join(repoDir, "workflows");
|
|
91
|
+
// The shipped extension dir is both the built-in root and the drop-in root; hash that tree once.
|
|
92
|
+
const repoProvenance =
|
|
93
|
+
resolve(repoDir) === resolve(BUILTIN_SOURCE_ROOT) ? async () => builtinSource : () => captureSourceTreeFingerprint(repoDir);
|
|
101
94
|
const [repoDynamic, userDynamic] = await Promise.all([
|
|
102
95
|
timed(options.perf, "discovery.repo_dir_ms", () =>
|
|
103
96
|
loadDir(
|
|
@@ -107,7 +100,7 @@ export async function discoverWorkflows(repoDir: string, options: DiscoverWorkfl
|
|
|
107
100
|
reason: "dynamic workflow module graphs are not loaded from an immutable source snapshot",
|
|
108
101
|
}),
|
|
109
102
|
BUILTIN_WORKFLOW_FILES,
|
|
110
|
-
|
|
103
|
+
repoProvenance,
|
|
111
104
|
),
|
|
112
105
|
),
|
|
113
106
|
timed(options.perf, "discovery.user_dir_ms", () =>
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Key, type KeyId } from "@earendil-works/pi-tui";
|
|
5
|
+
import { isRecord } from "./guards.ts";
|
|
5
6
|
|
|
6
7
|
export const DEFAULT_DYNAMAX_INSPECTOR_SHORTCUT = "ctrl+shift+m" satisfies KeyId;
|
|
7
8
|
export const DEFAULT_REVIEW_RESULTS_SHORTCUT = "ctrl+shift+r" satisfies KeyId;
|
|
@@ -112,10 +113,6 @@ function defaultDynamaxShortcuts(): DynamaxShortcuts {
|
|
|
112
113
|
return { inspector: DEFAULT_DYNAMAX_INSPECTOR_SHORTCUT, results: DEFAULT_REVIEW_RESULTS_SHORTCUT };
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
116
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
116
|
function warnUsingDefaults(reason: "Could not read" | "Invalid", configPath: string): void {
|
|
120
117
|
console.warn(`${reason} pi-workflow-engine config at ${configPath}; using default workflow shortcuts.`);
|
|
121
118
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
3
|
-
import type { EditorComponent, KeyId } from "@earendil-works/pi-tui";
|
|
1
|
+
import { CustomEditor, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { EditorComponent } from "@earendil-works/pi-tui";
|
|
4
3
|
import { completeCurrentArgument } from "./command-completions.ts";
|
|
5
|
-
import {
|
|
4
|
+
import type { DynamaxShortcuts } from "./dynamax-shortcuts.ts";
|
|
6
5
|
import { sessionKey } from "./session-identity.ts";
|
|
7
6
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
7
|
+
import { dot, GLYPH } from "./ui/kit.ts";
|
|
8
|
+
import { setWorkflowInspectorShortcut } from "./ui/workflow-widget.ts";
|
|
8
9
|
import {
|
|
9
10
|
decorateDynamaxEditor,
|
|
10
11
|
resolveDynamaxEffect,
|
|
@@ -21,17 +22,21 @@ export interface DynamaxState {
|
|
|
21
22
|
|
|
22
23
|
export interface DynamaxRuntime {
|
|
23
24
|
state: DynamaxState;
|
|
24
|
-
runningWorkflow?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export type DynamaxRuntimeStore = Map<string, DynamaxRuntime>;
|
|
28
28
|
|
|
29
29
|
export interface DynamaxRegistrationOptions {
|
|
30
|
-
openInspector
|
|
30
|
+
openInspector: (ctx: ExtensionContext) => Promise<void> | void;
|
|
31
31
|
effect?: DynamaxEffect;
|
|
32
32
|
animationScheduler?: DynamaxAnimationScheduler;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface DynamaxHandle {
|
|
36
|
+
/** Opt the next agent run in. The `input` hook ignores prompts this extension sends (pi tags them `source: "extension"`). */
|
|
37
|
+
markOneShot(ctx: ExtensionContext): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
export const DYNAMAX_TOKEN_PATTERN = /(^|[^A-Za-z0-9_])dynamax([^A-Za-z0-9_]|$)/i;
|
|
36
41
|
export const DYNAMAX_STATUS_KEY = "dynamax";
|
|
37
42
|
|
|
@@ -63,22 +68,13 @@ Adaptive multi-pass workflows are optional. Use a simple single-pass fan-out whe
|
|
|
63
68
|
- synthesize the first-pass and follow-up results together.
|
|
64
69
|
Do not generate a second pass when the first pass is sufficient, and do not invent iteration, quorum, graph, reduction, or retry primitives for this pattern.`;
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
The user has opted into dynamax multi-agent orchestration. The workflow tool is permitted for this task. You may either run an existing named workflow or author a new inline workflow script when that best serves the user's request.
|
|
70
|
-
|
|
71
|
-
Inline workflow rules:
|
|
72
|
-
- Use the injected Type object for schemas, for example Type.Object({ ok: Type.Boolean() }); do not import typebox.
|
|
73
|
-
- Do not use import statements or dynamic import() in inline workflow scripts.
|
|
74
|
-
- Set profile to "small", "medium", or "big" on each agent() call so routing remains explicit; use model/thinkingLevel only for an intentional override.
|
|
75
|
-
- Subagents receive no skills by default. Add \`skills: ["skill-name"]\` per agent only when that stage should load that skill; grant the smallest useful set.
|
|
76
|
-
- Provide exactly one of workflow.name or workflow.script.
|
|
71
|
+
/** System prompt section pi wraps as `<dynamax>`; adding or dropping it appends a section patch instead of rewriting the prompt. */
|
|
72
|
+
export const DYNAMAX_SECTION = "dynamax";
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
/** Only the opt-in itself: the authoring rules already reach the model as the workflow tool's guidelines. */
|
|
75
|
+
export const DYNAMAX_REMINDER =
|
|
76
|
+
"The user opted into dynamax multi-agent orchestration for this request, so the workflow tool is permitted. " +
|
|
77
|
+
"Run a registered workflow by name, or author an inline workflow script when none fits, following the workflow tool's guidelines.";
|
|
82
78
|
|
|
83
79
|
export function createDynamaxState(): DynamaxState {
|
|
84
80
|
return { sticky: false, oneShotPending: false, turnActive: false };
|
|
@@ -126,43 +122,39 @@ export function describeDynamaxState(state: DynamaxState): string {
|
|
|
126
122
|
return `sticky ${sticky}; one-shot ${oneShot}${turn}`;
|
|
127
123
|
}
|
|
128
124
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
return `dynamax: ${mode} | /workflow:dynamax on|off | ${inspectorHint}`;
|
|
125
|
+
/**
|
|
126
|
+
* Footer status: `◆ dynamax` while sticky, `◆ dynamax · next prompt` while a one-shot waits for its
|
|
127
|
+
* prompt, `◆ dynamax · this turn` while that turn runs. A running workflow names itself in its own
|
|
128
|
+
* status (`◆ repo-scan · Scan 2/3 · 4s`), so this one only states the mode.
|
|
129
|
+
*/
|
|
130
|
+
export function dynamaxStatusText(state: DynamaxState, theme: Theme): string {
|
|
131
|
+
const mode = state.sticky ? undefined : state.oneShotPending ? "next prompt" : state.turnActive ? "this turn" : undefined;
|
|
132
|
+
const label = `${theme.fg("accent", GLYPH.workflow)} ${theme.fg("accent", "dynamax")}`;
|
|
133
|
+
return mode ? `${label}${dot(theme)}${theme.fg("muted", mode)}` : label;
|
|
139
134
|
}
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Add the opt-in section for this run and consume a pending one-shot. pi rebuilds `sections` for every
|
|
138
|
+
* run, so an inactive run simply leaves it out and pi records the removal as a patch.
|
|
139
|
+
*/
|
|
140
|
+
export function applyDynamaxPromptSection(sections: Record<string, string>, state: DynamaxState): boolean {
|
|
141
|
+
if (!state.sticky && !state.oneShotPending) return false;
|
|
143
142
|
state.oneShotPending = false;
|
|
144
143
|
state.turnActive = true;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
export function appendDynamaxContextReminder(messages: AgentMessage[], state: DynamaxState): AgentMessage[] {
|
|
149
|
-
if (!state.sticky) return messages;
|
|
150
|
-
return [...messages, createDynamaxContextMessage()];
|
|
144
|
+
sections[DYNAMAX_SECTION] = DYNAMAX_REMINDER;
|
|
145
|
+
return true;
|
|
151
146
|
}
|
|
152
147
|
|
|
153
|
-
export function registerDynamax(
|
|
154
|
-
pi: ExtensionAPI,
|
|
155
|
-
shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts(),
|
|
156
|
-
options: DynamaxRegistrationOptions = {},
|
|
157
|
-
): void {
|
|
148
|
+
export function registerDynamax(pi: ExtensionAPI, shortcuts: DynamaxShortcuts, options: DynamaxRegistrationOptions): DynamaxHandle {
|
|
158
149
|
const runtimes: DynamaxRuntimeStore = new Map();
|
|
159
150
|
const effect = options.effect ?? resolveDynamaxEffect();
|
|
151
|
+
setWorkflowInspectorShortcut(shortcuts.inspector);
|
|
160
152
|
let editorInstallation: DynamaxEditorInstallation | undefined;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
153
|
+
const markOneShot = (ctx: ExtensionContext): void => {
|
|
154
|
+
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
155
|
+
markDynamaxOneShot(runtime.state);
|
|
156
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
157
|
+
};
|
|
166
158
|
const installEditor = (ctx: ExtensionContext): void => {
|
|
167
159
|
if (ctx.mode !== "tui" || effect === "off") return;
|
|
168
160
|
const previousFactory = ctx.ui.getEditorComponent();
|
|
@@ -229,7 +221,7 @@ export function registerDynamax(
|
|
|
229
221
|
|
|
230
222
|
pi.on("session_start", (_event, ctx) => {
|
|
231
223
|
installEditor(ctx);
|
|
232
|
-
updateDynamaxSurfaces(ctx, getDynamaxRuntime(runtimes, ctx)
|
|
224
|
+
updateDynamaxSurfaces(ctx, getDynamaxRuntime(runtimes, ctx));
|
|
233
225
|
});
|
|
234
226
|
|
|
235
227
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
@@ -238,58 +230,31 @@ export function registerDynamax(
|
|
|
238
230
|
});
|
|
239
231
|
|
|
240
232
|
pi.on("input", (event, ctx) => {
|
|
241
|
-
if (event.source
|
|
242
|
-
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
243
|
-
if (hasDynamaxToken(event.text)) {
|
|
244
|
-
markDynamaxOneShot(runtime.state);
|
|
245
|
-
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
246
|
-
}
|
|
233
|
+
if (event.source !== "extension" && hasDynamaxToken(event.text)) markOneShot(ctx);
|
|
247
234
|
return { action: "continue" };
|
|
248
235
|
});
|
|
249
236
|
|
|
250
237
|
pi.on("before_agent_start", (event, ctx) => {
|
|
251
238
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
updateDynamaxSurfaces(ctx, runtime
|
|
255
|
-
|
|
239
|
+
const options = event.systemPromptOptions;
|
|
240
|
+
if (!applyDynamaxPromptSection(options.sections, runtime.state)) return undefined;
|
|
241
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
242
|
+
// An earlier extension forced the whole prompt, and pi renders no sections then; append to its text instead.
|
|
243
|
+
if (options.forceSystemPrompt === undefined) return undefined;
|
|
244
|
+
return { systemPrompt: `${options.forceSystemPrompt}\n\n<${DYNAMAX_SECTION}>\n${DYNAMAX_REMINDER}\n</${DYNAMAX_SECTION}>` };
|
|
256
245
|
});
|
|
257
246
|
|
|
258
247
|
pi.on("agent_end", (_event, ctx) => {
|
|
259
248
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
260
|
-
runtime.runningWorkflow = undefined;
|
|
261
249
|
if (!runtime.state.sticky) runtime.state.turnActive = false;
|
|
262
|
-
updateDynamaxSurfaces(ctx, runtime
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
pi.on("tool_execution_start", (event, ctx) => {
|
|
266
|
-
if (event.toolName !== "workflow") return undefined;
|
|
267
|
-
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
268
|
-
runtime.runningWorkflow = workflowLabel(event.args);
|
|
269
|
-
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
270
|
-
return undefined;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
pi.on("tool_execution_end", (event, ctx) => {
|
|
274
|
-
if (event.toolName !== "workflow") return undefined;
|
|
275
|
-
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
276
|
-
runtime.runningWorkflow = undefined;
|
|
277
|
-
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
278
|
-
return undefined;
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
pi.on("context", (event, ctx) => {
|
|
282
|
-
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
283
|
-
const messages = appendDynamaxContextReminder(event.messages, runtime.state);
|
|
284
|
-
if (messages === event.messages) return undefined;
|
|
285
|
-
return { messages };
|
|
250
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
286
251
|
});
|
|
287
252
|
|
|
288
253
|
if (shortcuts.inspector) {
|
|
289
254
|
pi.registerShortcut(shortcuts.inspector, {
|
|
290
255
|
description: "Open workflow inspector",
|
|
291
256
|
handler: async (ctx) => {
|
|
292
|
-
await openInspector(ctx);
|
|
257
|
+
await options.openInspector(ctx);
|
|
293
258
|
},
|
|
294
259
|
});
|
|
295
260
|
}
|
|
@@ -306,13 +271,13 @@ export function registerDynamax(
|
|
|
306
271
|
}
|
|
307
272
|
if (action === "on") {
|
|
308
273
|
enableDynamaxSticky(runtime.state);
|
|
309
|
-
updateDynamaxSurfaces(ctx, runtime
|
|
274
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
310
275
|
ctx.ui.notify("Dynamax workflow orchestration is on for this session", "info");
|
|
311
276
|
return;
|
|
312
277
|
}
|
|
313
278
|
if (action === "off") {
|
|
314
279
|
clearDynamax(runtime.state);
|
|
315
|
-
updateDynamaxSurfaces(ctx, runtime
|
|
280
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
316
281
|
ctx.ui.notify("Dynamax workflow orchestration is off", "info");
|
|
317
282
|
return;
|
|
318
283
|
}
|
|
@@ -323,44 +288,20 @@ export function registerDynamax(
|
|
|
323
288
|
ctx.ui.notify("Usage: /workflow:dynamax [on|off|status]", "warning");
|
|
324
289
|
},
|
|
325
290
|
});
|
|
291
|
+
|
|
292
|
+
return { markOneShot };
|
|
326
293
|
}
|
|
327
294
|
|
|
328
|
-
export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">, runtime: DynamaxRuntime
|
|
295
|
+
export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">, runtime: DynamaxRuntime): void {
|
|
329
296
|
if (!ctx.hasUI) return;
|
|
330
|
-
if (!isDynamaxActive(runtime.state)
|
|
297
|
+
if (!isDynamaxActive(runtime.state)) {
|
|
331
298
|
clearDynamaxSurfaces(ctx);
|
|
332
299
|
return;
|
|
333
300
|
}
|
|
334
|
-
|
|
335
|
-
const status = dynamaxWidgetLine(runtime.state, shortcuts.inspector, runtime.runningWorkflow);
|
|
336
|
-
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, status);
|
|
301
|
+
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, dynamaxStatusText(runtime.state, ctx.ui.theme));
|
|
337
302
|
}
|
|
338
303
|
|
|
339
304
|
export function clearDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">): void {
|
|
340
305
|
if (!ctx.hasUI) return;
|
|
341
306
|
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, undefined);
|
|
342
307
|
}
|
|
343
|
-
|
|
344
|
-
function workflowLabel(args: unknown): string {
|
|
345
|
-
if (!isRecord(args)) return "workflow";
|
|
346
|
-
const name = args.name;
|
|
347
|
-
if (typeof name === "string" && name.trim()) return name.trim();
|
|
348
|
-
const script = args.script;
|
|
349
|
-
if (typeof script === "string" && script.trim()) return "inline workflow";
|
|
350
|
-
return "workflow";
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function createDynamaxContextMessage(): AgentMessage {
|
|
354
|
-
return {
|
|
355
|
-
role: "custom",
|
|
356
|
-
customType: DYNAMAX_CONTEXT_CUSTOM_TYPE,
|
|
357
|
-
content: DYNAMAX_REMINDER.trim(),
|
|
358
|
-
display: false,
|
|
359
|
-
details: { dynamax: true, sticky: true },
|
|
360
|
-
timestamp: Date.now(),
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
365
|
-
return typeof value === "object" && value !== null;
|
|
366
|
-
}
|