@signalridge/pi-workflows 1.5.0 → 1.6.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/CHANGELOG.md +114 -0
- package/README.md +182 -46
- package/package.json +1 -1
- package/skills/workflow-authoring/SKILL.md +22 -11
- package/skills/workflow-authoring/references/capabilities.md +5 -5
- package/skills/workflow-authoring/references/capability-details.md +13 -5
- package/skills/workflow-authoring/references/debugging.md +1 -1
- package/skills/workflow-authoring/references/quality-helpers.md +2 -2
- package/skills/workflow-authoring/references/registry-ownership.md +4 -4
- package/skills/workflow-authoring/references/review.md +4 -3
- package/skills/workflow-authoring/references/runtime.md +2 -2
- package/skills/workflow-authoring/references/specialized-helpers.md +1 -1
- package/skills/workflow-authoring/references/versions.md +1 -1
- package/skills/workflow-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +1 -1
- package/src/builtins.ts +23 -23
- package/src/capabilities.ts +22 -7
- package/src/engine.ts +36 -26
- package/src/index.ts +203 -61
- package/src/journal.ts +0 -14
- package/src/runtime.ts +335 -92
- package/src/strengths.ts +93 -0
- package/src/workflow-settings.ts +95 -1
package/src/index.ts
CHANGED
|
@@ -15,8 +15,20 @@ import {
|
|
|
15
15
|
queryChildSessionContextImmediate,
|
|
16
16
|
} from "./rpc-client.js";
|
|
17
17
|
import { listSavedWorkflows, loadSavedWorkflow, saveWorkflow } from "./saved-workflows.js";
|
|
18
|
+
import {
|
|
19
|
+
defaultStrengthTable,
|
|
20
|
+
isWorkflowStrength,
|
|
21
|
+
WORKFLOW_STRENGTH_LIST,
|
|
22
|
+
WORKFLOW_STRENGTHS,
|
|
23
|
+
type WorkflowStrengthTable,
|
|
24
|
+
} from "./strengths.js";
|
|
18
25
|
import { liveWidgetLines, showWorkflowNavigator } from "./ui.js";
|
|
19
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
loadProjectWorkflowSettings,
|
|
28
|
+
loadWorkflowSettings,
|
|
29
|
+
saveWorkflowSettings,
|
|
30
|
+
type WorkflowSettings,
|
|
31
|
+
} from "./workflow-settings.js";
|
|
20
32
|
|
|
21
33
|
const WorkflowToolSchema = Type.Object(
|
|
22
34
|
{
|
|
@@ -129,7 +141,7 @@ export function buildAdHocWorkflowScript(prompt: string): string {
|
|
|
129
141
|
const originalPrompt = ${JSON.stringify(prompt)};
|
|
130
142
|
const plan = await agent("Design a bounded execution plan for this user task. Return 1-8 independent or dependency-linked tasks. Use unique short ids, concise descriptions, exact worker prompts, and only dependencies that appear in the task list. Prefer independent tasks when possible. User task:\\n\\n" + originalPrompt, {
|
|
131
143
|
label: "planner",
|
|
132
|
-
|
|
144
|
+
strength: "medium",
|
|
133
145
|
schema: {
|
|
134
146
|
type: "object",
|
|
135
147
|
properties: {
|
|
@@ -158,7 +170,7 @@ const graph = await orchestrate(plan.tasks.map((task) => ({
|
|
|
158
170
|
id: task.id,
|
|
159
171
|
description: task.description,
|
|
160
172
|
dependsOn: task.dependsOn || [],
|
|
161
|
-
run: ({ results, statuses }) => agent(task.prompt + "\\n\\nDependency results (null means unavailable):\\n" + JSON.stringify({ results, statuses }), { label: task.id,
|
|
173
|
+
run: ({ results, statuses }) => agent(task.prompt + "\\n\\nDependency results (null means unavailable):\\n" + JSON.stringify({ results, statuses }), { label: task.id, strength: "low" }),
|
|
162
174
|
})), { onError: "continue" });
|
|
163
175
|
const SYNTHESIS_CONTEXT_LIMIT = 78000;
|
|
164
176
|
function renderContextValue(value) {
|
|
@@ -232,7 +244,7 @@ while (synthesisJson.length > SYNTHESIS_CONTEXT_LIMIT && (valuePreviewBudget > 1
|
|
|
232
244
|
synthesisJson = JSON.stringify(synthesisContext);
|
|
233
245
|
}
|
|
234
246
|
if (synthesisJson.length > SYNTHESIS_CONTEXT_LIMIT) throw new Error("Unable to fit the synthesis context within its managed prompt budget");
|
|
235
|
-
return await agent("Synthesize the worker results into a direct answer to the original task. Mention incomplete or failed coverage explicitly.\\n\\nOriginal task, execution plan, and graph ledger:\\n" + synthesisJson, { label: "synthesizer",
|
|
247
|
+
return await agent("Synthesize the worker results into a direct answer to the original task. Mention incomplete or failed coverage explicitly.\\n\\nOriginal task, execution plan, and graph ledger:\\n" + synthesisJson, { label: "synthesizer", strength: "medium" });`;
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
export default function piWorkflows(pi: ExtensionAPI): void {
|
|
@@ -346,37 +358,35 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
346
358
|
const DEFAULT_EXCLUDED_TOOLS = ["workflow", "workflow_control"] as const;
|
|
347
359
|
|
|
348
360
|
/**
|
|
349
|
-
*
|
|
350
|
-
* from.
|
|
361
|
+
* Persist one settings change into this project's own file.
|
|
351
362
|
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
363
|
+
* `workflowSettings` is the merged view — defaults, then global, then project
|
|
364
|
+
* — while `saveWorkflowSettings` replaces the project file with whatever it
|
|
365
|
+
* is handed. Handing it the merged view would copy every global value and
|
|
366
|
+
* every default into the project on the first edit, and the project would
|
|
367
|
+
* stop tracking the global file from then on. So the patch lands on project
|
|
368
|
+
* scope and the merged view is re-derived from what actually landed, which is
|
|
369
|
+
* also what makes it safe to report the result rather than the intent.
|
|
358
370
|
*/
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const saved = loadSavedWorkflow(name, sessionCwd);
|
|
363
|
-
if (saved !== undefined) return { script: saved };
|
|
364
|
-
const builtin = BUILTIN_WORKFLOWS[name]?.script;
|
|
365
|
-
return builtin === undefined ? undefined : { script: builtin, shippedScript: true };
|
|
371
|
+
const persistWorkflowSetting = (patch: WorkflowSettings): void => {
|
|
372
|
+
saveWorkflowSettings({ ...loadProjectWorkflowSettings(sessionCwd), ...patch }, sessionCwd);
|
|
373
|
+
workflowSettings = loadWorkflowSettings(sessionCwd);
|
|
366
374
|
};
|
|
367
375
|
|
|
376
|
+
/** Resolve a nested `workflow(name)` reference; a saved file shadows a built-in. */
|
|
377
|
+
const resolveNestedWorkflow = (name: string): string | undefined =>
|
|
378
|
+
loadSavedWorkflow(name, sessionCwd) ?? BUILTIN_WORKFLOWS[name]?.script;
|
|
379
|
+
|
|
368
380
|
const resolveScript = (
|
|
369
381
|
script: string | undefined,
|
|
370
382
|
name: string | undefined,
|
|
371
383
|
args: unknown,
|
|
372
|
-
): { script: string; source: string; toolset?: string
|
|
384
|
+
): { script: string; source: string; toolset?: string } => {
|
|
373
385
|
if (script !== undefined && name !== undefined) {
|
|
374
386
|
throw new Error("Provide either `script` or `name`, not both.");
|
|
375
387
|
}
|
|
376
388
|
if (script !== undefined) return { script, source: "inline" };
|
|
377
389
|
if (name !== undefined) {
|
|
378
|
-
// A saved workflow is the user's own file even when it shadows a built-in
|
|
379
|
-
// name, so it keeps the strict tier check.
|
|
380
390
|
const saved = loadSavedWorkflow(name, sessionCwd);
|
|
381
391
|
if (saved) return { script: saved, source: `saved:${name}` };
|
|
382
392
|
const builtin = BUILTIN_WORKFLOWS[name];
|
|
@@ -384,12 +394,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
384
394
|
// Validate named invocations before starting a potentially expensive fleet.
|
|
385
395
|
// Slash commands provide the same shape through their descriptor primaryArg.
|
|
386
396
|
validateBuiltinArgs(name, args);
|
|
387
|
-
return {
|
|
388
|
-
script: builtin.script,
|
|
389
|
-
source: `builtin:${name}`,
|
|
390
|
-
toolset: builtin.toolset,
|
|
391
|
-
shippedScript: true,
|
|
392
|
-
};
|
|
397
|
+
return { script: builtin.script, source: `builtin:${name}`, toolset: builtin.toolset };
|
|
393
398
|
}
|
|
394
399
|
throw new Error(`Unknown workflow name "${name}". It is neither a saved workflow nor a built-in.`);
|
|
395
400
|
}
|
|
@@ -401,6 +406,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
401
406
|
active = true;
|
|
402
407
|
const workflowEngine = currentEngine();
|
|
403
408
|
workflowEngine.onRunSettled = notifyRunChanged;
|
|
409
|
+
// `control("resume")` and the provider-limit retry start executions with no
|
|
410
|
+
// caller to pass options for them, and the strength table is deliberately
|
|
411
|
+
// never frozen onto a run. Without this they would resume with no table and
|
|
412
|
+
// re-run every finished call untabled.
|
|
413
|
+
workflowEngine.strengths = () => workflowSettings.strengths;
|
|
404
414
|
// Command/tool registrations persist across session replacement, but the
|
|
405
415
|
// widget belongs to the newly restored engine and must be refreshed too.
|
|
406
416
|
refreshWidget();
|
|
@@ -447,12 +457,12 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
447
457
|
? (() => {
|
|
448
458
|
const prior = workflowEngine.getRun(params.resumeFromRunId);
|
|
449
459
|
if (!prior) throw new Error(`Workflow run not found: ${params.resumeFromRunId}`);
|
|
450
|
-
// Resume
|
|
451
|
-
//
|
|
452
|
-
return { script: prior.script, source: "resume", toolset: prior.toolset
|
|
460
|
+
// Resume replays the stored script; only the shared shape
|
|
461
|
+
// has to be satisfied here.
|
|
462
|
+
return { script: prior.script, source: "resume", toolset: prior.toolset };
|
|
453
463
|
})()
|
|
454
464
|
: resolveScript(params.script, params.name, params.args);
|
|
455
|
-
const { script, source, toolset
|
|
465
|
+
const { script, source, toolset } = resolved;
|
|
456
466
|
const options: ScriptStartOptions = {
|
|
457
467
|
args: params.args,
|
|
458
468
|
background: params.background ?? true,
|
|
@@ -462,7 +472,6 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
462
472
|
tokenBudget: params.tokenBudget,
|
|
463
473
|
agentTimeoutMs: params.agentTimeoutMs,
|
|
464
474
|
toolset,
|
|
465
|
-
...(shippedScript === undefined ? {} : { shippedScript }),
|
|
466
475
|
excludeTools: [...DEFAULT_EXCLUDED_TOOLS],
|
|
467
476
|
signal,
|
|
468
477
|
mainModel: ctx?.model?.id,
|
|
@@ -479,6 +488,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
479
488
|
}
|
|
480
489
|
: undefined,
|
|
481
490
|
loadSavedWorkflow: resolveNestedWorkflow,
|
|
491
|
+
strengths: workflowSettings.strengths,
|
|
482
492
|
};
|
|
483
493
|
if (params.resumeFromRunId) {
|
|
484
494
|
const replacement = params.script !== undefined || params.name !== undefined ? script : undefined;
|
|
@@ -546,7 +556,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
546
556
|
|
|
547
557
|
// /workflows: navigator + run/status/watch/stop/pause/resume/rm/save subcommands
|
|
548
558
|
pi.registerCommand("workflows", {
|
|
549
|
-
description: "Workflow runs:
|
|
559
|
+
description: "Workflow runs: navigator, or run/status/watch/stop/pause/resume/rm/save/strength",
|
|
550
560
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
551
561
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
552
562
|
const [subcommand, ...rest] = tokens;
|
|
@@ -656,23 +666,158 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
656
666
|
}
|
|
657
667
|
return;
|
|
658
668
|
}
|
|
669
|
+
if (subcommand === "strength") {
|
|
670
|
+
// The Agent tier catalogue is owned by pi-subagents and still is (A8:
|
|
671
|
+
// no second tier config file). What this edits is narrower and is
|
|
672
|
+
// genuinely ours: which Agent tier each workflow strength runs on.
|
|
673
|
+
//
|
|
674
|
+
// A subcommand rather than a command of its own, because the reason
|
|
675
|
+
// to surface it at all is the three-part view below — two lines of
|
|
676
|
+
// which come from this package and one from the other — and that is
|
|
677
|
+
// a page to read, not a mode to be in.
|
|
678
|
+
//
|
|
679
|
+
// The catalogue lives on the managed peer, so it can legitimately be
|
|
680
|
+
// unreachable here. Status still has its other two parts, and a set
|
|
681
|
+
// made against a table that already exists is stored unvalidated: the
|
|
682
|
+
// run-start check reports a tier this host does not define and drops
|
|
683
|
+
// that entry, which costs the redirect rather than the run. A set made
|
|
684
|
+
// with no table at all is refused instead — see below, where the seed
|
|
685
|
+
// is read.
|
|
686
|
+
const catalogue = await (async (): Promise<string[] | undefined> => {
|
|
687
|
+
try {
|
|
688
|
+
return Object.keys((await awaitProtocol()).routingPolicy.profiles).sort();
|
|
689
|
+
} catch {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
})();
|
|
693
|
+
const knownTiers = catalogue ? new Set(catalogue) : undefined;
|
|
694
|
+
// What a run would resolve, by the same rule the runtime uses: a
|
|
695
|
+
// configured table if there is one, else the shipped default. Shown
|
|
696
|
+
// rather than the configured table alone, because an unconfigured
|
|
697
|
+
// machine is not an unrouted one and a row reading "(unset)" there
|
|
698
|
+
// would describe something other than what the next run does.
|
|
699
|
+
const effective = (): { table: WorkflowStrengthTable; shipped: boolean } => {
|
|
700
|
+
const configured = workflowSettings.strengths;
|
|
701
|
+
return configured === undefined
|
|
702
|
+
? { table: defaultStrengthTable(knownTiers), shipped: true }
|
|
703
|
+
: { table: configured, shipped: false };
|
|
704
|
+
};
|
|
705
|
+
const describe = (): string => {
|
|
706
|
+
const { table, shipped } = effective();
|
|
707
|
+
const rows = WORKFLOW_STRENGTHS.map((name) => {
|
|
708
|
+
// The default is derived from the catalogue, so without one there
|
|
709
|
+
// is nothing honest to print: the run will still route these, and
|
|
710
|
+
// "(unset)" would claim it will not.
|
|
711
|
+
if (shipped && knownTiers === undefined) return ` ${name} → (default; needs the tier catalogue)`;
|
|
712
|
+
if (table[name] === undefined) return ` ${name} → (unset; the agent's own default)`;
|
|
713
|
+
return ` ${name} → ${table[name]}${shipped ? " (default)" : ""}`;
|
|
714
|
+
});
|
|
715
|
+
// The shipped default is identity, so on a stock install workflow
|
|
716
|
+
// work rides the same profiles every ordinary spawn does — the
|
|
717
|
+
// coupling this indirection exists to let you break, but does not
|
|
718
|
+
// break on its own. Saying so here is the only place the two steps
|
|
719
|
+
// out of it are visible together.
|
|
720
|
+
const decouple = shipped
|
|
721
|
+
? "\nThis is the shipped default: each strength on the tier of the same name, so " +
|
|
722
|
+
"workflow work shares the tiers your ordinary spawns use (Explore among them). " +
|
|
723
|
+
"To price workflow work on its own, define a tier in /agents → Model tiers, then " +
|
|
724
|
+
"point a strength at it here."
|
|
725
|
+
: "";
|
|
726
|
+
return (
|
|
727
|
+
`Workflow strengths and the Agent tier each runs on:\n${rows.join("\n")}\n` +
|
|
728
|
+
`Tiers this host defines: ${catalogue?.join(", ") ?? "(unavailable)"}\n` +
|
|
729
|
+
"Edits apply to this project; for a machine-wide table edit " +
|
|
730
|
+
"<agent dir>/workflows/settings.json. The tiers themselves live in " +
|
|
731
|
+
"/agents → Model tiers; this only says which one a strength uses." +
|
|
732
|
+
decouple
|
|
733
|
+
);
|
|
734
|
+
};
|
|
735
|
+
const [strength, target] = rest;
|
|
736
|
+
if (strength === undefined) {
|
|
737
|
+
ctx.ui.notify(describe(), "info");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (!isWorkflowStrength(strength)) {
|
|
741
|
+
ctx.ui.notify(`"${strength}" is not a workflow strength. Use one of ${WORKFLOW_STRENGTH_LIST}.`, "warning");
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (target === undefined || rest.length !== 2) {
|
|
745
|
+
ctx.ui.notify(`/workflows strength [<${WORKFLOW_STRENGTHS.join("|")}> <tier>|off]`, "warning");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
// `off` reads as "unset" unless the host really defines a tier by
|
|
749
|
+
// that name, in which case the user's own catalogue outranks our
|
|
750
|
+
// keyword — a tier you cannot point at is worse than a clumsier way
|
|
751
|
+
// to clear one.
|
|
752
|
+
const unset = target === "off" && catalogue?.includes("off") !== true;
|
|
753
|
+
if (!unset && catalogue && !catalogue.includes(target)) {
|
|
754
|
+
ctx.ui.notify(
|
|
755
|
+
`"${target}" is not a tier this host defines. Available: ${catalogue.join(", ") || "(none)"}. ` +
|
|
756
|
+
"Define it in /agents → Model tiers first.",
|
|
757
|
+
"warning",
|
|
758
|
+
);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const { table: current, shipped } = effective();
|
|
762
|
+
if (shipped && knownTiers === undefined) {
|
|
763
|
+
// Seeding from a default we cannot compute would write a one-entry
|
|
764
|
+
// table, and this key replaces rather than merges — so the other two
|
|
765
|
+
// strengths would go from routed to unmapped as a side effect of
|
|
766
|
+
// setting this one. Refusing costs a retry; writing costs a run.
|
|
767
|
+
ctx.ui.notify(
|
|
768
|
+
"The tier catalogue is unavailable, so the current table cannot be read. " +
|
|
769
|
+
"Retry once the managed peer is reachable.",
|
|
770
|
+
"warning",
|
|
771
|
+
);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
// Seeded from the table a run would use, not from this project's file.
|
|
775
|
+
// Every level of this key replaces the one below rather than merging
|
|
776
|
+
// into it, so a patch built on project scope would silently drop
|
|
777
|
+
// whatever the level below contributed: adding one entry would discard
|
|
778
|
+
// an inherited global table, and `off` against an entry this project
|
|
779
|
+
// never wrote would write nothing and leave the mapping standing. The
|
|
780
|
+
// seed does freeze the current table into the project, which is what a
|
|
781
|
+
// replace-wholesale key costs — the entries a partial file left out
|
|
782
|
+
// would be unmapped, not inherited.
|
|
783
|
+
const { [strength]: _cleared, ...others } = current;
|
|
784
|
+
try {
|
|
785
|
+
persistWorkflowSetting({ strengths: unset ? others : { ...others, [strength]: target } });
|
|
786
|
+
} catch (error: unknown) {
|
|
787
|
+
ctx.ui.notify(
|
|
788
|
+
`Could not save workflow strength routing: ${error instanceof Error ? error.message : String(error)}`,
|
|
789
|
+
"warning",
|
|
790
|
+
);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
// Show the resolved result rather than echoing the write. The
|
|
794
|
+
// settings layer can drop what it was handed, and `off` against the
|
|
795
|
+
// last remaining entry stores an explicit empty table — "map nothing",
|
|
796
|
+
// which is not the same as the shipped default an absent key would
|
|
797
|
+
// mean — so a message announcing the write would describe something
|
|
798
|
+
// other than what the next run will do. Runs already in flight keep
|
|
799
|
+
// what they started with; the table is read fresh at each start and
|
|
800
|
+
// resume.
|
|
801
|
+
//
|
|
802
|
+
// The one case the table alone cannot explain: the host defines a tier
|
|
803
|
+
// named `off`, so the keyword lost to it above and this mapped rather
|
|
804
|
+
// than cleared. The catalogue outranking our keyword is the right call
|
|
805
|
+
// — a tier you cannot point at is worse than a clumsier way to clear
|
|
806
|
+
// one — but a user who typed the documented keyword and got a mapping
|
|
807
|
+
// has to be told which reading won, and where the other one lives.
|
|
808
|
+
const shadowed =
|
|
809
|
+
target === "off" && !unset
|
|
810
|
+
? '\n\nNote: this host defines a tier named "off", so `off` was read as that tier rather ' +
|
|
811
|
+
"than as the clear keyword. To leave a strength unmapped here, remove its entry from the " +
|
|
812
|
+
"`strengths` object in this project's settings.json."
|
|
813
|
+
: "";
|
|
814
|
+
ctx.ui.notify(`${describe()}${shadowed}`, "info");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
659
817
|
ctx.ui.notify(`Unknown /workflows subcommand: ${subcommand}`, "warning");
|
|
660
818
|
},
|
|
661
819
|
});
|
|
662
820
|
|
|
663
|
-
// /workflows-models: tier configuration is owned by pi-subagents; this
|
|
664
|
-
// command documents the delegation (A8: no second tier config file).
|
|
665
|
-
pi.registerCommand("workflows-models", {
|
|
666
|
-
description: "Workflow tier routing: tiers are configured in pi-subagents /agents → Model tiers",
|
|
667
|
-
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
668
|
-
ctx.ui.notify(
|
|
669
|
-
"Workflow tiers (small/medium/large) route through pi-subagents' workflow.tiers settings. " +
|
|
670
|
-
"Use /agents → Model tiers to configure them.",
|
|
671
|
-
"info",
|
|
672
|
-
);
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
|
|
676
821
|
// /workflows-progress: persistent compact/detailed live-panel settings.
|
|
677
822
|
pi.registerCommand("workflows-progress", {
|
|
678
823
|
description: "Workflow progress panel: compact|detailed|status|max <N>",
|
|
@@ -686,21 +831,22 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
686
831
|
);
|
|
687
832
|
return;
|
|
688
833
|
}
|
|
834
|
+
let patch: WorkflowSettings;
|
|
689
835
|
if (action === "compact" || action === "detailed") {
|
|
690
|
-
|
|
836
|
+
patch = { progressMode: action };
|
|
691
837
|
} else if (action === "max") {
|
|
692
838
|
const max = Number(tokens[1]);
|
|
693
839
|
if (!Number.isInteger(max) || max < 1 || max > 32) {
|
|
694
840
|
ctx.ui.notify("Usage: /workflows-progress max <1-32>", "warning");
|
|
695
841
|
return;
|
|
696
842
|
}
|
|
697
|
-
|
|
843
|
+
patch = { maxAgentsShown: max };
|
|
698
844
|
} else {
|
|
699
845
|
ctx.ui.notify("Usage: /workflows-progress compact|detailed|status|max <N>", "warning");
|
|
700
846
|
return;
|
|
701
847
|
}
|
|
702
848
|
try {
|
|
703
|
-
|
|
849
|
+
persistWorkflowSetting(patch);
|
|
704
850
|
refreshWidget();
|
|
705
851
|
ctx.ui.notify(
|
|
706
852
|
`Workflow progress set to ${workflowSettings.progressMode}, max ${workflowSettings.maxAgentsShown}.`,
|
|
@@ -723,9 +869,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
723
869
|
// an ordinary answer.
|
|
724
870
|
const TRIGGER_WORD_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/u;
|
|
725
871
|
const persistTriggerSettings = (): void => {
|
|
726
|
-
workflowSettings = { ...workflowSettings, keywordTriggerWord: triggerKeyword, keywordTriggerEnabled };
|
|
727
872
|
try {
|
|
728
|
-
|
|
873
|
+
persistWorkflowSetting({ keywordTriggerWord: triggerKeyword, keywordTriggerEnabled });
|
|
729
874
|
} catch {
|
|
730
875
|
/* setting changes remain active in memory */
|
|
731
876
|
}
|
|
@@ -784,7 +929,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
784
929
|
if (event.text.includes(WORKFLOW_ARMED_DIRECTIVE)) return { action: "continue" as const };
|
|
785
930
|
const effortDirective =
|
|
786
931
|
effortLevel === "ultra"
|
|
787
|
-
?
|
|
932
|
+
? 'Effort: ULTRA. Be exhaustive: use broad parallel review, verification, and completeness checks; give the deepest steps strength: "high".'
|
|
788
933
|
: effortLevel === "high"
|
|
789
934
|
? "Effort: HIGH. Be thorough: use several independent reviewers and an adversarial verification pass."
|
|
790
935
|
: "";
|
|
@@ -806,9 +951,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
806
951
|
return;
|
|
807
952
|
}
|
|
808
953
|
effortLevel = level;
|
|
809
|
-
workflowSettings = { ...workflowSettings, effort: effortLevel };
|
|
810
954
|
try {
|
|
811
|
-
|
|
955
|
+
persistWorkflowSetting({ effort: effortLevel });
|
|
812
956
|
} catch {
|
|
813
957
|
/* warning is non-fatal */
|
|
814
958
|
}
|
|
@@ -824,9 +968,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
824
968
|
description: "Ultracode: toggle exhaustive workflow effort",
|
|
825
969
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
826
970
|
effortLevel = args.trim().toLowerCase() === "off" ? "off" : "ultra";
|
|
827
|
-
workflowSettings = { ...workflowSettings, effort: effortLevel };
|
|
828
971
|
try {
|
|
829
|
-
|
|
972
|
+
persistWorkflowSetting({ effort: effortLevel });
|
|
830
973
|
} catch {
|
|
831
974
|
/* warning is non-fatal */
|
|
832
975
|
}
|
|
@@ -873,9 +1016,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
873
1016
|
args: scriptArgs,
|
|
874
1017
|
background: true,
|
|
875
1018
|
toolset: descriptor.toolset,
|
|
876
|
-
shippedScript: true,
|
|
877
1019
|
excludeTools: [...DEFAULT_EXCLUDED_TOOLS],
|
|
878
1020
|
loadSavedWorkflow: resolveNestedWorkflow,
|
|
1021
|
+
strengths: workflowSettings.strengths,
|
|
879
1022
|
};
|
|
880
1023
|
try {
|
|
881
1024
|
const workflowEngine = currentEngine();
|
|
@@ -1019,9 +1162,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1019
1162
|
const workflowEngine = currentEngine();
|
|
1020
1163
|
const result = await workflowEngine.start(script, {
|
|
1021
1164
|
background: true,
|
|
1022
|
-
|
|
1023
|
-
// preference against whatever catalogue this host defines.
|
|
1024
|
-
shippedScript: true,
|
|
1165
|
+
strengths: workflowSettings.strengths,
|
|
1025
1166
|
mainModel: ctx.model?.id,
|
|
1026
1167
|
});
|
|
1027
1168
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
@@ -1190,6 +1331,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1190
1331
|
background: true,
|
|
1191
1332
|
mainModel: commandCtx?.model?.id,
|
|
1192
1333
|
loadSavedWorkflow: resolveNestedWorkflow,
|
|
1334
|
+
strengths: workflowSettings.strengths,
|
|
1193
1335
|
};
|
|
1194
1336
|
const result = await workflowEngine.start(script, options);
|
|
1195
1337
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
package/src/journal.ts
CHANGED
|
@@ -133,12 +133,6 @@ export interface ScriptRun {
|
|
|
133
133
|
frozenArgsPresent: boolean;
|
|
134
134
|
/** Number of the latest edited-script revision. */
|
|
135
135
|
revision?: number;
|
|
136
|
-
/**
|
|
137
|
-
* This run's script ships with the package. Frozen for the same reason
|
|
138
|
-
* `toolset` is: a resume must keep treating a shipped script's tier names as
|
|
139
|
-
* preferences, not as assertions about the user's catalogue.
|
|
140
|
-
*/
|
|
141
|
-
shippedScript?: boolean;
|
|
142
136
|
/** Toolset marker persisted across resume (e.g. "web-research" for deep-research). */
|
|
143
137
|
toolset?: string;
|
|
144
138
|
/** Frozen run params — persisted so resume after restore keeps original budget/scale. */
|
|
@@ -238,7 +232,6 @@ export type JournalEvent =
|
|
|
238
232
|
args?: unknown;
|
|
239
233
|
/** Explicit args presence marker for deterministic resume. */
|
|
240
234
|
frozenArgsPresent: boolean;
|
|
241
|
-
shippedScript?: boolean;
|
|
242
235
|
toolset?: string;
|
|
243
236
|
frozenMaxAgents?: number;
|
|
244
237
|
frozenConcurrency?: number;
|
|
@@ -678,11 +671,6 @@ function parseEvent(raw: unknown, runs: Map<string, ScriptRun>): JournalEvent {
|
|
|
678
671
|
const frozenArgsPresent = parseFrozenArgsPresence(raw);
|
|
679
672
|
const args = raw.args === undefined ? undefined : parsePersistedValue(raw.args, "workflow args");
|
|
680
673
|
const toolset = Object.hasOwn(raw, "toolset") ? boundedString(raw.toolset, "toolset", 64) : undefined;
|
|
681
|
-
const shippedScript = (() => {
|
|
682
|
-
if (!Object.hasOwn(raw, "shippedScript")) return undefined;
|
|
683
|
-
if (typeof raw.shippedScript !== "boolean") throw new Error("shippedScript is invalid");
|
|
684
|
-
return raw.shippedScript;
|
|
685
|
-
})();
|
|
686
674
|
const frozenMaxAgents = (() => {
|
|
687
675
|
if (!Object.hasOwn(raw, "frozenMaxAgents")) return undefined;
|
|
688
676
|
if (typeof raw.frozenMaxAgents !== "number" || !Number.isFinite(raw.frozenMaxAgents) || raw.frozenMaxAgents < 1) {
|
|
@@ -752,7 +740,6 @@ function parseEvent(raw: unknown, runs: Map<string, ScriptRun>): JournalEvent {
|
|
|
752
740
|
meta,
|
|
753
741
|
...(args === undefined ? {} : { args }),
|
|
754
742
|
frozenArgsPresent,
|
|
755
|
-
...(shippedScript === undefined ? {} : { shippedScript }),
|
|
756
743
|
...(toolset ? { toolset } : {}),
|
|
757
744
|
...(frozenMaxAgents !== undefined ? { frozenMaxAgents } : {}),
|
|
758
745
|
...(frozenConcurrency !== undefined ? { frozenConcurrency } : {}),
|
|
@@ -1192,7 +1179,6 @@ function applyJournalEvent(runs: Map<string, ScriptRun>, event: JournalEvent): v
|
|
|
1192
1179
|
...(event.args === undefined ? {} : { args: structuredClone(event.args) }),
|
|
1193
1180
|
frozenArgsPresent: event.frozenArgsPresent,
|
|
1194
1181
|
revision: 0,
|
|
1195
|
-
...(event.shippedScript === undefined ? {} : { shippedScript: event.shippedScript }),
|
|
1196
1182
|
...(event.toolset ? { toolset: event.toolset } : {}),
|
|
1197
1183
|
...(event.frozenMaxAgents !== undefined ? { frozenMaxAgents: event.frozenMaxAgents } : {}),
|
|
1198
1184
|
...(event.frozenConcurrency !== undefined ? { frozenConcurrency: event.frozenConcurrency } : {}),
|