@nicknisi/pi-workflows 0.1.0 → 0.2.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/dist/index.js +43 -24
- package/package.json +2 -2
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,7 +53,7 @@ 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, externalSignal) {
|
|
63
57
|
return async (opts) => {
|
|
64
58
|
const spawnOpts = {
|
|
65
59
|
prompt: opts.prompt,
|
|
@@ -79,7 +73,7 @@ function makeSpawnFn(runtime, cwd, externalSignal) {
|
|
|
79
73
|
spawnOpts.tools = opts.tools;
|
|
80
74
|
else
|
|
81
75
|
spawnOpts.tools = ['read', 'grep', 'find', 'ls'];
|
|
82
|
-
const res = await spawnCancellable(runtime, spawnOpts, externalSignal);
|
|
76
|
+
const res = await spawnCancellable(cancellables, runtime, spawnOpts, externalSignal);
|
|
83
77
|
// Surface the worktree `.patch` path (recorded on the run record after
|
|
84
78
|
// settle) so workflow scripts can return it for the `/patches` apply flow.
|
|
85
79
|
if (res.ok) {
|
|
@@ -199,6 +193,7 @@ function formatRunResult(result, label) {
|
|
|
199
193
|
}
|
|
200
194
|
// ── Extension ─────────────────────────────────────────────────────────────
|
|
201
195
|
export default function workflows(pi) {
|
|
196
|
+
const cancellables = new Map();
|
|
202
197
|
const runtime = createSubagentRuntime({ namespace: NAMESPACE, artifactsDir: ARTIFACTS_ROOT });
|
|
203
198
|
sweepRunArtifactsOnce(ARTIFACTS_ROOT);
|
|
204
199
|
pi.registerTool({
|
|
@@ -296,7 +291,7 @@ export default function workflows(pi) {
|
|
|
296
291
|
if (!runId) {
|
|
297
292
|
return { content: [{ type: 'text', text: 'stop requires runId.' }], details: {} };
|
|
298
293
|
}
|
|
299
|
-
const controller = resolveCancellable(runtime, runId);
|
|
294
|
+
const controller = resolveCancellable(cancellables, runtime, runId);
|
|
300
295
|
if (!controller) {
|
|
301
296
|
const record = findRun(runtime, runId);
|
|
302
297
|
const msg = record && (record.status === 'running' || record.status === 'queued')
|
|
@@ -355,17 +350,40 @@ export default function workflows(pi) {
|
|
|
355
350
|
details: {},
|
|
356
351
|
};
|
|
357
352
|
}
|
|
358
|
-
|
|
353
|
+
// One controller for BOTH abort sources: the tool's signal AND the
|
|
354
|
+
// timeout. spawnCancellable wires it into every child spawn, so firing
|
|
355
|
+
// it actually cancels in-flight work (previously the timeout controller
|
|
356
|
+
// was connected to nothing — dead code).
|
|
359
357
|
const controller = new AbortController();
|
|
360
|
-
|
|
358
|
+
let timedOut = false;
|
|
359
|
+
const onToolAbort = () => controller.abort();
|
|
360
|
+
if (signal) {
|
|
361
|
+
if (signal.aborted)
|
|
362
|
+
controller.abort();
|
|
363
|
+
else
|
|
364
|
+
signal.addEventListener('abort', onToolAbort, { once: true });
|
|
365
|
+
}
|
|
366
|
+
const timer = setTimeout(() => {
|
|
367
|
+
timedOut = true;
|
|
368
|
+
controller.abort();
|
|
369
|
+
}, timeoutMs);
|
|
370
|
+
const spawnFn = makeSpawnFn(cancellables, runtime, ctx.cwd, controller.signal);
|
|
371
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
372
|
+
controller.signal.addEventListener('abort', () => reject(new Error(timedOut
|
|
373
|
+
? `Timed out after ${timeoutMs}ms (in-flight subagents were aborted)`
|
|
374
|
+
: 'Aborted by the host session')), { once: true });
|
|
375
|
+
});
|
|
361
376
|
try {
|
|
362
|
-
const result = await
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
377
|
+
const result = await Promise.race([
|
|
378
|
+
runScript({
|
|
379
|
+
script: src,
|
|
380
|
+
args: params.args,
|
|
381
|
+
spawn: spawnFn,
|
|
382
|
+
cwd: ctx.cwd,
|
|
383
|
+
onLog: () => { },
|
|
384
|
+
}),
|
|
385
|
+
timeoutPromise,
|
|
386
|
+
]);
|
|
369
387
|
const text = formatRunResult(result, label);
|
|
370
388
|
return {
|
|
371
389
|
content: [{ type: 'text', text }],
|
|
@@ -387,6 +405,7 @@ export default function workflows(pi) {
|
|
|
387
405
|
}
|
|
388
406
|
finally {
|
|
389
407
|
clearTimeout(timer);
|
|
408
|
+
signal?.removeEventListener('abort', onToolAbort);
|
|
390
409
|
}
|
|
391
410
|
},
|
|
392
411
|
});
|
|
@@ -403,12 +422,12 @@ export default function workflows(pi) {
|
|
|
403
422
|
return subs.map((s) => ({ value: s + ' ', label: s }));
|
|
404
423
|
},
|
|
405
424
|
handler: async (args, ctx) => {
|
|
406
|
-
await cmdWf(args, ctx, runtime);
|
|
425
|
+
await cmdWf(args, ctx, runtime, cancellables);
|
|
407
426
|
},
|
|
408
427
|
});
|
|
409
428
|
}
|
|
410
429
|
// ── /wf handler (shared, typed against the runtime) ───────────────────────
|
|
411
|
-
async function cmdWf(args, ctx, runtime) {
|
|
430
|
+
async function cmdWf(args, ctx, runtime, cancellables) {
|
|
412
431
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
413
432
|
const sub = parts[0];
|
|
414
433
|
if (!sub || sub === 'list') {
|
|
@@ -453,7 +472,7 @@ async function cmdWf(args, ctx, runtime) {
|
|
|
453
472
|
}
|
|
454
473
|
ctx.ui.setStatus('workflows', `running ${name}…`);
|
|
455
474
|
try {
|
|
456
|
-
const spawnFn = makeSpawnFn(runtime, ctx.cwd, ctx.signal ?? undefined);
|
|
475
|
+
const spawnFn = makeSpawnFn(cancellables, runtime, ctx.cwd, ctx.signal ?? undefined);
|
|
457
476
|
const result = await runScript({ script: src, args: parsedArgs, spawn: spawnFn, cwd: ctx.cwd });
|
|
458
477
|
ctx.ui.notify(formatRunResult(result, name), 'info');
|
|
459
478
|
}
|
|
@@ -485,7 +504,7 @@ async function cmdWf(args, ctx, runtime) {
|
|
|
485
504
|
ctx.ui.notify('Usage: /wf stop <runId>', 'warning');
|
|
486
505
|
return;
|
|
487
506
|
}
|
|
488
|
-
const controller = resolveCancellable(runtime, runId);
|
|
507
|
+
const controller = resolveCancellable(cancellables, runtime, runId);
|
|
489
508
|
if (!controller) {
|
|
490
509
|
const record = findRun(runtime, runId);
|
|
491
510
|
ctx.ui.notify(record && (record.status === 'running' || record.status === 'queued')
|
|
@@ -520,7 +539,7 @@ function findRun(runtime, runId) {
|
|
|
520
539
|
return undefined;
|
|
521
540
|
}
|
|
522
541
|
/** Resolve a cancellable controller by full id or unique prefix. */
|
|
523
|
-
function resolveCancellable(_runtime, runId) {
|
|
542
|
+
function resolveCancellable(cancellables, _runtime, runId) {
|
|
524
543
|
if (cancellables.has(runId))
|
|
525
544
|
return cancellables.get(runId);
|
|
526
545
|
if (runId.length >= 8) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nicknisi/pi-workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.4.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@earendil-works/pi-coding-agent": "*"
|