pi-taskflow 0.0.12 → 0.0.13
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 +156 -23
- package/extensions/cache.ts +263 -0
- package/extensions/index.ts +147 -118
- package/extensions/init.ts +607 -0
- package/extensions/render.ts +39 -0
- package/extensions/runtime.ts +342 -17
- package/extensions/schema.ts +166 -1
- package/extensions/store.ts +16 -2
- package/package.json +4 -3
package/extensions/index.ts
CHANGED
|
@@ -10,13 +10,19 @@
|
|
|
10
10
|
* host conversation context — only the final phase output is returned.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import * as fs from "node:fs";
|
|
14
|
-
import * as path from "node:path";
|
|
15
13
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
16
14
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
17
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
19
16
|
import { Text } from "@earendil-works/pi-tui";
|
|
17
|
+
import {
|
|
18
|
+
RECOMMENDED_DEFAULTS,
|
|
19
|
+
readSettings,
|
|
20
|
+
writeSettings,
|
|
21
|
+
formatRolesReport,
|
|
22
|
+
formatDiffReport,
|
|
23
|
+
formatFlowResult,
|
|
24
|
+
runInteractiveInit,
|
|
25
|
+
} from "./init.ts";
|
|
20
26
|
import { Type } from "typebox";
|
|
21
27
|
import { type AgentScope, discoverAgents, readSubagentSettings } from "./agents.ts";
|
|
22
28
|
import { renderRunResult, summarizeRun } from "./render.ts";
|
|
@@ -33,6 +39,7 @@ import {
|
|
|
33
39
|
saveFlow,
|
|
34
40
|
saveRun,
|
|
35
41
|
} from "./store.ts";
|
|
42
|
+
import { CacheStore } from "./cache.ts";
|
|
36
43
|
|
|
37
44
|
interface TaskflowDetails {
|
|
38
45
|
state?: RunState;
|
|
@@ -53,8 +60,8 @@ const ShorthandStep = Type.Object(
|
|
|
53
60
|
);
|
|
54
61
|
|
|
55
62
|
const TaskflowParams = Type.Object({
|
|
56
|
-
action: StringEnum(["run", "save", "resume", "list", "agents", "init"] as const, {
|
|
57
|
-
description: "What to do: run a flow, save a definition, resume a paused run, list saved flows, list available agents,
|
|
63
|
+
action: StringEnum(["run", "save", "resume", "list", "agents", "init", "cache-clear"] as const, {
|
|
64
|
+
description: "What to do: run a flow, save a definition, resume a paused run, list saved flows, list available agents, init model role configuration, or clear the cross-run memoization cache",
|
|
58
65
|
default: "run",
|
|
59
66
|
}),
|
|
60
67
|
name: Type.Optional(Type.String({ description: "Name of a saved flow (for run/save without inline define)" })),
|
|
@@ -87,6 +94,19 @@ const TaskflowParams = Type.Object({
|
|
|
87
94
|
scope: Type.Optional(
|
|
88
95
|
StringEnum(["user", "project"] as const, { description: "Where to save (action=save)", default: "project" }),
|
|
89
96
|
),
|
|
97
|
+
mode: Type.Optional(
|
|
98
|
+
StringEnum(["show", "apply-defaults", "interactive"] as const, {
|
|
99
|
+
description:
|
|
100
|
+
"Init action mode. 'show' is read-only (default); 'apply-defaults' requires force:true; 'interactive' requires a UI session.",
|
|
101
|
+
default: "show",
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
force: Type.Optional(
|
|
105
|
+
Type.Boolean({
|
|
106
|
+
description:
|
|
107
|
+
"Destructive: overwrites modelRoles in settings.json. Required for mode='apply-defaults'.",
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
90
110
|
});
|
|
91
111
|
|
|
92
112
|
function makeRunState(def: Taskflow, args: Record<string, unknown>, cwd: string): RunState {
|
|
@@ -258,7 +278,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
258
278
|
"For simple non-DAG delegations (like the subagent tool) skip the DSL: pass `task` (+optional `agent`) for one task, `tasks:[{task,agent?}]` to run in parallel, or `chain:[{task,agent?}]` to run sequentially (reference the prior step with {previous.output}).",
|
|
259
279
|
"Use action=save to persist a definition as a reusable /tf:<name> command. action=resume continues a paused run. action=list shows saved flows. Use action=agents to list available agents — do NOT invent agent names; either use an agent from that list or omit the 'agent' field to auto-select the default agent.",
|
|
260
280
|
"DSL: {name, args?, concurrency?, budget?:{maxUSD,maxTokens}, phases:[{id, type, agent, task, dependsOn?, join?:'all'|'any', when?, retry?:{max,backoffMs,factor}, over?(map), as?(map), branches?(parallel), from?(reduce), use?(flow), with?(flow), output?:'json', final?}]}.",
|
|
261
|
-
"Phase types: agent (one subagent), parallel (static branches), map (dynamic fan-out over an array), gate (VERDICT: PASS/BLOCK quality gate), reduce (aggregate from N phases), approval (human-in-the-loop pause), flow (run a saved sub-flow). join:'any' is an OR-join; when is a conditional guard; retry adds backoff; budget caps run cost.",
|
|
281
|
+
"Phase types: agent (one subagent), parallel (static branches), map (dynamic fan-out over an array), gate (VERDICT: PASS/BLOCK quality gate), reduce (aggregate from N phases), approval (human-in-the-loop pause), flow (run a saved sub-flow), loop (re-run a task until 'until' is truthy / converged / maxIterations; body reads {loop.iteration} and {loop.lastOutput}), tournament (spawn N variants of 'task' — or distinct 'branches' — then a judge picks the best / aggregates; mode:'best'|'aggregate'). join:'any' is an OR-join; when is a conditional guard; retry adds backoff; budget caps run cost.",
|
|
262
282
|
"Interpolation: {args.X}, {steps.ID.output}, {steps.ID.json}, {item} (map), {previous.output}.",
|
|
263
283
|
].join(" "),
|
|
264
284
|
parameters: TaskflowParams,
|
|
@@ -273,52 +293,81 @@ export default function (pi: ExtensionAPI) {
|
|
|
273
293
|
const action = params.action ?? "run";
|
|
274
294
|
|
|
275
295
|
// init — configure model roles
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
296
|
+
if (action === "init") {
|
|
297
|
+
let settings: Record<string, unknown>;
|
|
298
|
+
try {
|
|
299
|
+
settings = readSettings();
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return errorResult(
|
|
302
|
+
action,
|
|
303
|
+
`Failed to read settings.json: ${e instanceof Error ? e.message : String(e)}. ` +
|
|
304
|
+
`Fix the file or remove it.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const current = (settings.modelRoles ?? {}) as Record<string, string>;
|
|
308
|
+
const mode = params.mode;
|
|
309
|
+
|
|
310
|
+
// v0.0.13 deprecation bridge: mode omitted → old behavior
|
|
311
|
+
if (mode === undefined) {
|
|
312
|
+
if (Object.keys(current).length === 0) {
|
|
313
|
+
// v0.0.12 compat: auto-write recommended defaults when modelRoles is empty
|
|
314
|
+
console.warn(
|
|
315
|
+
"[taskflow] action=init with no mode is deprecated and will require explicit mode in v0.0.14. " +
|
|
316
|
+
"Use mode='apply-defaults' with force=true.",
|
|
317
|
+
);
|
|
318
|
+
writeSettings({ ...settings, modelRoles: { ...RECOMMENDED_DEFAULTS } });
|
|
319
|
+
const text = formatDiffReport({}, RECOMMENDED_DEFAULTS);
|
|
320
|
+
return { content: [{ type: "text", text }], details: { action } satisfies TaskflowDetails };
|
|
321
|
+
}
|
|
322
|
+
// mode omitted + modelRoles exist → show
|
|
323
|
+
const text = formatRolesReport(current);
|
|
324
|
+
return { content: [{ type: "text", text }], details: { action } satisfies TaskflowDetails };
|
|
325
|
+
}
|
|
300
326
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
327
|
+
// mode === "show" (read-only, never overwrites)
|
|
328
|
+
if (mode === "show") {
|
|
329
|
+
const text = formatRolesReport(current);
|
|
330
|
+
return { content: [{ type: "text", text }], details: { action } satisfies TaskflowDetails };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// mode === "apply-defaults" requires explicit force=true
|
|
334
|
+
if (mode === "apply-defaults") {
|
|
335
|
+
if (!params.force)
|
|
336
|
+
return errorResult(action, "mode=apply-defaults requires force=true to overwrite.");
|
|
337
|
+
const merged: Record<string, string> = { ...RECOMMENDED_DEFAULTS };
|
|
338
|
+
for (const key of Object.keys(current)) {
|
|
339
|
+
if (!(key in merged)) merged[key] = current[key]; // stale-preserved
|
|
340
|
+
}
|
|
341
|
+
writeSettings({ ...settings, modelRoles: merged });
|
|
342
|
+
const text = formatDiffReport(current, merged);
|
|
343
|
+
return { content: [{ type: "text", text }], details: { action } satisfies TaskflowDetails };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// mode === "interactive" — requires a UI session
|
|
347
|
+
if (mode === "interactive") {
|
|
348
|
+
if (!ctx.hasUI)
|
|
349
|
+
return errorResult(action, "mode=interactive requires an interactive session.");
|
|
350
|
+
const enabledModels = (settings.enabledModels as string[] | undefined) ?? [];
|
|
351
|
+
const modelList =
|
|
352
|
+
enabledModels.length > 0
|
|
353
|
+
? enabledModels
|
|
354
|
+
.map((id) => ctx.modelRegistry.find(id.split("/")[0], id.split("/").slice(1).join("/")))
|
|
355
|
+
.filter((m): m is NonNullable<typeof m> => m !== undefined)
|
|
356
|
+
: ctx.modelRegistry.getAvailable();
|
|
357
|
+
const result = await runInteractiveInit({
|
|
358
|
+
hasUI: ctx.hasUI,
|
|
359
|
+
signal: signal ?? new AbortController().signal,
|
|
360
|
+
ui: ctx.ui as ExtensionUIContext,
|
|
361
|
+
modelRegistry: ctx.modelRegistry,
|
|
362
|
+
modelList,
|
|
363
|
+
currentRoles: current,
|
|
364
|
+
});
|
|
365
|
+
const text = formatFlowResult(result);
|
|
366
|
+
return { content: [{ type: "text", text }], details: { action } satisfies TaskflowDetails };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return errorResult(action, `Unknown init mode: ${String(mode)}`);
|
|
370
|
+
}
|
|
322
371
|
|
|
323
372
|
// agents — list available agents the LLM can use in phase definitions
|
|
324
373
|
if (action === "agents") {
|
|
@@ -345,6 +394,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
345
394
|
return { content: [{ type: "text", text }], details: { action } satisfies TaskflowDetails };
|
|
346
395
|
}
|
|
347
396
|
|
|
397
|
+
if (action === "cache-clear") {
|
|
398
|
+
const removed = new CacheStore(ctx.cwd).clear();
|
|
399
|
+
return {
|
|
400
|
+
content: [{ type: "text", text: `Cleared ${removed} cross-run cache entr${removed === 1 ? "y" : "ies"}.` }],
|
|
401
|
+
details: { action } satisfies TaskflowDetails,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
348
405
|
// resume
|
|
349
406
|
if (action === "resume") {
|
|
350
407
|
if (!params.runId)
|
|
@@ -559,85 +616,57 @@ export default function (pi: ExtensionAPI) {
|
|
|
559
616
|
}
|
|
560
617
|
|
|
561
618
|
if (sub === "init") {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
{ role: "arbiter", desc: "Final judgment — tiebreak, plan quality gates (plan-arbiter, final-arbiter)" },
|
|
574
|
-
{ role: "vision", desc: "Multimodal — UI work, design reading, Figma analysis (executor-ui, visual-explorer)" },
|
|
575
|
-
{ role: "reasoner", desc: "Cautious reasoning — security, risk review, sensitive changes (risk-reviewer, security-reviewer)" },
|
|
576
|
-
];
|
|
619
|
+
let settings: Record<string, unknown>;
|
|
620
|
+
try {
|
|
621
|
+
settings = readSettings();
|
|
622
|
+
} catch (e) {
|
|
623
|
+
ctx.ui.notify(
|
|
624
|
+
`Failed to read settings.json: ${e instanceof Error ? e.message : String(e)}`,
|
|
625
|
+
"error",
|
|
626
|
+
);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const currentRoles = (settings.modelRoles ?? {}) as Record<string, string>;
|
|
577
630
|
|
|
578
631
|
if (!ctx.hasUI) {
|
|
579
632
|
if (Object.keys(currentRoles).length > 0) {
|
|
580
633
|
ctx.ui.notify(
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
"info"
|
|
634
|
+
formatRolesReport(currentRoles),
|
|
635
|
+
"info",
|
|
584
636
|
);
|
|
585
637
|
} else {
|
|
586
638
|
ctx.ui.notify(
|
|
587
|
-
|
|
588
|
-
|
|
639
|
+
"No modelRoles configured. Run /tf init in an interactive session to select models.",
|
|
640
|
+
"warning",
|
|
589
641
|
);
|
|
590
642
|
}
|
|
591
643
|
return;
|
|
592
644
|
}
|
|
593
645
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (seen.has(m)) continue;
|
|
610
|
-
seen.add(m);
|
|
611
|
-
options.push(m === current ? `${m} (current)` : m);
|
|
612
|
-
}
|
|
613
|
-
options.push("───────────────");
|
|
614
|
-
options.push("Custom (type your own)");
|
|
615
|
-
|
|
616
|
-
const title = `Model for '${rd.role}' — ${rd.desc}` + (current ? `\nCurrent: ${current}` : "");
|
|
617
|
-
const pick = await ctx.ui.select(title, options, { signal: ctx.signal });
|
|
618
|
-
|
|
619
|
-
if (!pick || pick.startsWith("───")) {
|
|
620
|
-
chosen[rd.role] = current ?? modelList[0] ?? "";
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (pick === "Custom (type your own)") {
|
|
625
|
-
const custom = await ctx.ui.input(`Enter model identifier for '${rd.role}'`, "provider/model-id", { signal: ctx.signal });
|
|
626
|
-
chosen[rd.role] = custom?.trim() || current || "";
|
|
627
|
-
} else {
|
|
628
|
-
chosen[rd.role] = pick.replace(" (current)", "");
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Save
|
|
633
|
-
const newSettings = { ...existing, modelRoles: chosen };
|
|
634
|
-
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
635
|
-
fs.writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2) + "\n", "utf-8");
|
|
636
|
-
|
|
646
|
+
const enabledModels = (settings.enabledModels as string[] | undefined) ?? [];
|
|
647
|
+
const modelList =
|
|
648
|
+
enabledModels.length > 0
|
|
649
|
+
? enabledModels
|
|
650
|
+
.map((id) => ctx.modelRegistry.find(id.split("/")[0], id.split("/").slice(1).join("/")))
|
|
651
|
+
.filter((m): m is NonNullable<typeof m> => m !== undefined)
|
|
652
|
+
: ctx.modelRegistry.getAvailable();
|
|
653
|
+
const result = await runInteractiveInit({
|
|
654
|
+
hasUI: ctx.hasUI,
|
|
655
|
+
signal: ctx.signal ?? new AbortController().signal,
|
|
656
|
+
ui: ctx.ui,
|
|
657
|
+
modelRegistry: ctx.modelRegistry,
|
|
658
|
+
modelList,
|
|
659
|
+
currentRoles,
|
|
660
|
+
});
|
|
637
661
|
ctx.ui.notify(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
662
|
+
result.kind === "saved"
|
|
663
|
+
? `Saved model roles to ${result.savedPath}:\n${Object.entries(result.chosen)
|
|
664
|
+
.map(([k, v]) => ` ${k.padEnd(10)} → ${v}`)
|
|
665
|
+
.join("\n")}`
|
|
666
|
+
: result.kind === "no-change"
|
|
667
|
+
? "No changes made."
|
|
668
|
+
: "Init cancelled.",
|
|
669
|
+
result.kind === "saved" ? "info" : "info",
|
|
641
670
|
);
|
|
642
671
|
return;
|
|
643
672
|
}
|