@nicknisi/pi-workflows 0.1.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/README.md +1 -1
- package/dist/index.js +44 -24
- package/index.ts +16 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Names are bare file stems (`research`, not `research.js`, never a path — `..`
|
|
|
51
51
|
|
|
52
52
|
## Runs are visible
|
|
53
53
|
|
|
54
|
-
Every `agent()` call spawns through `@nicknisi/pi-shared`'s subagent runtime with `artifactsDir` set to `~/.pi/agent/subagent-runs
|
|
54
|
+
Every `agent()` call spawns through `@nicknisi/pi-shared`'s subagent runtime with `artifactsDir` set to `~/.pi/agent/subagent-runs/`, namespace `workflows`, and the owning Pi session recorded. Active children therefore appear in that session's default `fleet` view; settled and machine-wide records remain available through the fleet's explicit `all` scope. `status <runId>` and `stop <runId>` here read from / cancel via the same runtime's run records — no parallel store. `stop` cancels in-flight spawns through a live `runId → AbortController` registry (mirroring subagents' cascading-cancellation); a run belonging to a different host process is reported as not cancellable from here.
|
|
55
55
|
|
|
56
56
|
## Migration from `@quintinshaw/pi-dynamic-workflows`
|
|
57
57
|
|
package/dist/index.js
CHANGED
|
@@ -36,13 +36,7 @@ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
36
36
|
const MAX_TIMEOUT_MS = 30 * 60 * 1000;
|
|
37
37
|
const MAX_RESULT_CHARS = 16 * 1024;
|
|
38
38
|
const MAX_LOG_CHARS = 2000;
|
|
39
|
-
|
|
40
|
-
// Mirrors @nicknisi/pi-subagents' cascading-cancellation registry. Each
|
|
41
|
-
// agent() call spawns detached and registers its controller here; stop(runId)
|
|
42
|
-
// aborts it. Runs from other hosts show up via readRunArtifacts but are not
|
|
43
|
-
// cancellable here (they belong to a different process).
|
|
44
|
-
const cancellables = new Map();
|
|
45
|
-
function spawnCancellable(runtime, opts, externalSignal) {
|
|
39
|
+
function spawnCancellable(cancellables, runtime, opts, externalSignal) {
|
|
46
40
|
const controller = new AbortController();
|
|
47
41
|
const onExternalAbort = () => controller.abort();
|
|
48
42
|
if (externalSignal) {
|
|
@@ -59,11 +53,12 @@ function spawnCancellable(runtime, opts, externalSignal) {
|
|
|
59
53
|
});
|
|
60
54
|
return Object.assign(done, { runId });
|
|
61
55
|
}
|
|
62
|
-
function makeSpawnFn(runtime, cwd, externalSignal) {
|
|
56
|
+
function makeSpawnFn(cancellables, runtime, cwd, ownerSession, externalSignal) {
|
|
63
57
|
return async (opts) => {
|
|
64
58
|
const spawnOpts = {
|
|
65
59
|
prompt: opts.prompt,
|
|
66
60
|
cwd,
|
|
61
|
+
...(ownerSession !== undefined ? { ownerSession } : {}),
|
|
67
62
|
...(opts.agent !== undefined ? { agent: opts.agent } : {}),
|
|
68
63
|
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
69
64
|
...(opts.systemPrompt !== undefined ? { systemPrompt: opts.systemPrompt } : {}),
|
|
@@ -79,7 +74,7 @@ function makeSpawnFn(runtime, cwd, externalSignal) {
|
|
|
79
74
|
spawnOpts.tools = opts.tools;
|
|
80
75
|
else
|
|
81
76
|
spawnOpts.tools = ['read', 'grep', 'find', 'ls'];
|
|
82
|
-
const res = await spawnCancellable(runtime, spawnOpts, externalSignal);
|
|
77
|
+
const res = await spawnCancellable(cancellables, runtime, spawnOpts, externalSignal);
|
|
83
78
|
// Surface the worktree `.patch` path (recorded on the run record after
|
|
84
79
|
// settle) so workflow scripts can return it for the `/patches` apply flow.
|
|
85
80
|
if (res.ok) {
|
|
@@ -199,6 +194,7 @@ function formatRunResult(result, label) {
|
|
|
199
194
|
}
|
|
200
195
|
// ── Extension ─────────────────────────────────────────────────────────────
|
|
201
196
|
export default function workflows(pi) {
|
|
197
|
+
const cancellables = new Map();
|
|
202
198
|
const runtime = createSubagentRuntime({ namespace: NAMESPACE, artifactsDir: ARTIFACTS_ROOT });
|
|
203
199
|
sweepRunArtifactsOnce(ARTIFACTS_ROOT);
|
|
204
200
|
pi.registerTool({
|
|
@@ -296,7 +292,7 @@ export default function workflows(pi) {
|
|
|
296
292
|
if (!runId) {
|
|
297
293
|
return { content: [{ type: 'text', text: 'stop requires runId.' }], details: {} };
|
|
298
294
|
}
|
|
299
|
-
const controller = resolveCancellable(runtime, runId);
|
|
295
|
+
const controller = resolveCancellable(cancellables, runtime, runId);
|
|
300
296
|
if (!controller) {
|
|
301
297
|
const record = findRun(runtime, runId);
|
|
302
298
|
const msg = record && (record.status === 'running' || record.status === 'queued')
|
|
@@ -355,17 +351,40 @@ export default function workflows(pi) {
|
|
|
355
351
|
details: {},
|
|
356
352
|
};
|
|
357
353
|
}
|
|
358
|
-
|
|
354
|
+
// One controller for BOTH abort sources: the tool's signal AND the
|
|
355
|
+
// timeout. spawnCancellable wires it into every child spawn, so firing
|
|
356
|
+
// it actually cancels in-flight work (previously the timeout controller
|
|
357
|
+
// was connected to nothing — dead code).
|
|
359
358
|
const controller = new AbortController();
|
|
360
|
-
|
|
359
|
+
let timedOut = false;
|
|
360
|
+
const onToolAbort = () => controller.abort();
|
|
361
|
+
if (signal) {
|
|
362
|
+
if (signal.aborted)
|
|
363
|
+
controller.abort();
|
|
364
|
+
else
|
|
365
|
+
signal.addEventListener('abort', onToolAbort, { once: true });
|
|
366
|
+
}
|
|
367
|
+
const timer = setTimeout(() => {
|
|
368
|
+
timedOut = true;
|
|
369
|
+
controller.abort();
|
|
370
|
+
}, timeoutMs);
|
|
371
|
+
const spawnFn = makeSpawnFn(cancellables, runtime, ctx.cwd, ctx.sessionManager.getSessionFile(), controller.signal);
|
|
372
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
373
|
+
controller.signal.addEventListener('abort', () => reject(new Error(timedOut
|
|
374
|
+
? `Timed out after ${timeoutMs}ms (in-flight subagents were aborted)`
|
|
375
|
+
: 'Aborted by the host session')), { once: true });
|
|
376
|
+
});
|
|
361
377
|
try {
|
|
362
|
-
const result = await
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
378
|
+
const result = await Promise.race([
|
|
379
|
+
runScript({
|
|
380
|
+
script: src,
|
|
381
|
+
args: params.args,
|
|
382
|
+
spawn: spawnFn,
|
|
383
|
+
cwd: ctx.cwd,
|
|
384
|
+
onLog: () => { },
|
|
385
|
+
}),
|
|
386
|
+
timeoutPromise,
|
|
387
|
+
]);
|
|
369
388
|
const text = formatRunResult(result, label);
|
|
370
389
|
return {
|
|
371
390
|
content: [{ type: 'text', text }],
|
|
@@ -387,6 +406,7 @@ export default function workflows(pi) {
|
|
|
387
406
|
}
|
|
388
407
|
finally {
|
|
389
408
|
clearTimeout(timer);
|
|
409
|
+
signal?.removeEventListener('abort', onToolAbort);
|
|
390
410
|
}
|
|
391
411
|
},
|
|
392
412
|
});
|
|
@@ -403,12 +423,12 @@ export default function workflows(pi) {
|
|
|
403
423
|
return subs.map((s) => ({ value: s + ' ', label: s }));
|
|
404
424
|
},
|
|
405
425
|
handler: async (args, ctx) => {
|
|
406
|
-
await cmdWf(args, ctx, runtime);
|
|
426
|
+
await cmdWf(args, ctx, runtime, cancellables);
|
|
407
427
|
},
|
|
408
428
|
});
|
|
409
429
|
}
|
|
410
430
|
// ── /wf handler (shared, typed against the runtime) ───────────────────────
|
|
411
|
-
async function cmdWf(args, ctx, runtime) {
|
|
431
|
+
async function cmdWf(args, ctx, runtime, cancellables) {
|
|
412
432
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
413
433
|
const sub = parts[0];
|
|
414
434
|
if (!sub || sub === 'list') {
|
|
@@ -453,7 +473,7 @@ async function cmdWf(args, ctx, runtime) {
|
|
|
453
473
|
}
|
|
454
474
|
ctx.ui.setStatus('workflows', `running ${name}…`);
|
|
455
475
|
try {
|
|
456
|
-
const spawnFn = makeSpawnFn(runtime, ctx.cwd, ctx.signal ?? undefined);
|
|
476
|
+
const spawnFn = makeSpawnFn(cancellables, runtime, ctx.cwd, ctx.sessionManager.getSessionFile(), ctx.signal ?? undefined);
|
|
457
477
|
const result = await runScript({ script: src, args: parsedArgs, spawn: spawnFn, cwd: ctx.cwd });
|
|
458
478
|
ctx.ui.notify(formatRunResult(result, name), 'info');
|
|
459
479
|
}
|
|
@@ -485,7 +505,7 @@ async function cmdWf(args, ctx, runtime) {
|
|
|
485
505
|
ctx.ui.notify('Usage: /wf stop <runId>', 'warning');
|
|
486
506
|
return;
|
|
487
507
|
}
|
|
488
|
-
const controller = resolveCancellable(runtime, runId);
|
|
508
|
+
const controller = resolveCancellable(cancellables, runtime, runId);
|
|
489
509
|
if (!controller) {
|
|
490
510
|
const record = findRun(runtime, runId);
|
|
491
511
|
ctx.ui.notify(record && (record.status === 'running' || record.status === 'queued')
|
|
@@ -520,7 +540,7 @@ function findRun(runtime, runId) {
|
|
|
520
540
|
return undefined;
|
|
521
541
|
}
|
|
522
542
|
/** Resolve a cancellable controller by full id or unique prefix. */
|
|
523
|
-
function resolveCancellable(_runtime, runId) {
|
|
543
|
+
function resolveCancellable(cancellables, _runtime, runId) {
|
|
524
544
|
if (cancellables.has(runId))
|
|
525
545
|
return cancellables.get(runId);
|
|
526
546
|
if (runId.length >= 8) {
|
package/index.ts
CHANGED
|
@@ -88,12 +88,14 @@ function makeSpawnFn(
|
|
|
88
88
|
cancellables: Cancellables,
|
|
89
89
|
runtime: SubagentRuntime,
|
|
90
90
|
cwd: string,
|
|
91
|
+
ownerSession: string | undefined,
|
|
91
92
|
externalSignal: AbortSignal | undefined,
|
|
92
93
|
): EngineSpawnFn {
|
|
93
94
|
return async (opts: EngineSpawnOptions): Promise<EngineSpawnResult> => {
|
|
94
95
|
const spawnOpts: SpawnOptions = {
|
|
95
96
|
prompt: opts.prompt,
|
|
96
97
|
cwd,
|
|
98
|
+
...(ownerSession !== undefined ? { ownerSession } : {}),
|
|
97
99
|
...(opts.agent !== undefined ? { agent: opts.agent } : {}),
|
|
98
100
|
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
99
101
|
...(opts.systemPrompt !== undefined ? { systemPrompt: opts.systemPrompt } : {}),
|
|
@@ -412,7 +414,13 @@ export default function workflows(pi: ExtensionAPI): void {
|
|
|
412
414
|
timedOut = true;
|
|
413
415
|
controller.abort();
|
|
414
416
|
}, timeoutMs);
|
|
415
|
-
const spawnFn = makeSpawnFn(
|
|
417
|
+
const spawnFn = makeSpawnFn(
|
|
418
|
+
cancellables,
|
|
419
|
+
runtime,
|
|
420
|
+
ctx.cwd,
|
|
421
|
+
ctx.sessionManager.getSessionFile(),
|
|
422
|
+
controller.signal,
|
|
423
|
+
);
|
|
416
424
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
417
425
|
controller.signal.addEventListener(
|
|
418
426
|
'abort',
|
|
@@ -531,7 +539,13 @@ async function cmdWf(
|
|
|
531
539
|
}
|
|
532
540
|
ctx.ui.setStatus('workflows', `running ${name}…`);
|
|
533
541
|
try {
|
|
534
|
-
const spawnFn = makeSpawnFn(
|
|
542
|
+
const spawnFn = makeSpawnFn(
|
|
543
|
+
cancellables,
|
|
544
|
+
runtime,
|
|
545
|
+
ctx.cwd,
|
|
546
|
+
ctx.sessionManager.getSessionFile(),
|
|
547
|
+
ctx.signal ?? undefined,
|
|
548
|
+
);
|
|
535
549
|
const result = await runScript({ script: src, args: parsedArgs, spawn: spawnFn, cwd: ctx.cwd });
|
|
536
550
|
ctx.ui.notify(formatRunResult(result, name), 'info');
|
|
537
551
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nicknisi/pi-workflows",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Model-facing front door to the first-party workflow engine — run JS workflow scripts over the subagent runtime, replacing the third-party @quintinshaw/pi-dynamic-workflows extension",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"typebox": "^1.1.0",
|
|
31
|
-
"@nicknisi/pi-shared": "0.
|
|
31
|
+
"@nicknisi/pi-shared": "0.5.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@earendil-works/pi-coding-agent": "*"
|