@signalridge/pi-workflows 1.5.0 → 1.7.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 +227 -0
- package/README.md +207 -48
- package/package.json +2 -2
- package/skills/workflow-authoring/SKILL.md +44 -14
- package/skills/workflow-authoring/examples/defensive-json-parsing.js +1 -1
- package/skills/workflow-authoring/examples/phased-budgets.js +1 -1
- package/skills/workflow-authoring/examples/saved-nested-workflows.js +1 -1
- package/skills/workflow-authoring/examples/structured-output.js +1 -1
- package/skills/workflow-authoring/references/capabilities.md +7 -7
- package/skills/workflow-authoring/references/capability-details.md +24 -10
- package/skills/workflow-authoring/references/debugging.md +1 -1
- package/skills/workflow-authoring/references/lifecycle.md +5 -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 +8 -4
- 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 +6 -4
- package/src/builtins.ts +23 -23
- package/src/capabilities.ts +37 -12
- package/src/engine.ts +70 -26
- package/src/index.ts +243 -70
- package/src/journal.ts +0 -14
- package/src/rpc-client.ts +14 -1
- package/src/runtime.ts +452 -103
- 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
|
{
|
|
@@ -24,11 +36,40 @@ const WorkflowToolSchema = Type.Object(
|
|
|
24
36
|
name: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
|
25
37
|
args: Type.Optional(Type.Any()),
|
|
26
38
|
background: Type.Optional(Type.Boolean()),
|
|
27
|
-
maxAgents: Type.Optional(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
maxAgents: Type.Optional(
|
|
40
|
+
Type.Number({
|
|
41
|
+
minimum: 1,
|
|
42
|
+
description:
|
|
43
|
+
"Cap on real agent dispatches for this run. Default 1000, which no ordinary script reaches. Omit it: a value below what the script dispatches stops the run partway with AGENT_LIMIT_EXCEEDED.",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
concurrency: Type.Optional(
|
|
47
|
+
Type.Number({
|
|
48
|
+
minimum: 1,
|
|
49
|
+
description:
|
|
50
|
+
"Agents in flight at once. Defaults to the host's own subagent pool size, which is also the ceiling. Omit it: a lower number runs the same work more slowly and buys nothing, and a higher one is clamped.",
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
agentRetries: Type.Optional(
|
|
54
|
+
Type.Number({
|
|
55
|
+
minimum: 0,
|
|
56
|
+
maximum: 3,
|
|
57
|
+
description: "Retries for a recoverable agent failure. Default 0; a failed call returns null to the script.",
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
tokenBudget: Type.Optional(
|
|
61
|
+
Type.Number({
|
|
62
|
+
minimum: 1,
|
|
63
|
+
description: "Soft token ceiling for the whole run. Default unlimited; exhausting a set budget aborts the run.",
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
agentTimeoutMs: Type.Optional(
|
|
67
|
+
Type.Number({
|
|
68
|
+
minimum: 1,
|
|
69
|
+
description:
|
|
70
|
+
"Wall clock per agent, there to bound a hang. Default 1800000 (30min), and it counts from when the agent actually starts, not from dispatch. An agent's real budget is the host's turn, tool and token ceiling. Omit it: a short value fails an entire fan-out as timeouts.",
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
32
73
|
resumeFromRunId: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
|
33
74
|
},
|
|
34
75
|
{ additionalProperties: false },
|
|
@@ -127,23 +168,23 @@ function formatStart(result: ScriptStartResult): string {
|
|
|
127
168
|
export function buildAdHocWorkflowScript(prompt: string): string {
|
|
128
169
|
return `export const meta = { name: "ad-hoc", description: "Plan, execute, and synthesize a bounded multi-agent task graph" };
|
|
129
170
|
const originalPrompt = ${JSON.stringify(prompt)};
|
|
130
|
-
const plan = await agent("Design a bounded execution plan for this user task. Return 1-
|
|
171
|
+
const plan = await agent("Design a bounded execution plan for this user task. Return 1-24 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, and prefer narrow tasks over few broad ones: a task asked one question is read further into than a task asked four. User task:\\n\\n" + originalPrompt, {
|
|
131
172
|
label: "planner",
|
|
132
|
-
|
|
173
|
+
strength: "medium",
|
|
133
174
|
schema: {
|
|
134
175
|
type: "object",
|
|
135
176
|
properties: {
|
|
136
177
|
tasks: {
|
|
137
178
|
type: "array",
|
|
138
179
|
minItems: 1,
|
|
139
|
-
maxItems:
|
|
180
|
+
maxItems: 24,
|
|
140
181
|
items: {
|
|
141
182
|
type: "object",
|
|
142
183
|
properties: {
|
|
143
184
|
id: { type: "string", minLength: 1, maxLength: 64 },
|
|
144
185
|
description: { type: "string", minLength: 1, maxLength: 500 },
|
|
145
186
|
prompt: { type: "string", minLength: 1, maxLength: 8000 },
|
|
146
|
-
dependsOn: { type: "array", maxItems:
|
|
187
|
+
dependsOn: { type: "array", maxItems: 24, items: { type: "string", minLength: 1, maxLength: 64 } },
|
|
147
188
|
},
|
|
148
189
|
required: ["id", "description", "prompt"],
|
|
149
190
|
additionalProperties: false,
|
|
@@ -158,7 +199,7 @@ const graph = await orchestrate(plan.tasks.map((task) => ({
|
|
|
158
199
|
id: task.id,
|
|
159
200
|
description: task.description,
|
|
160
201
|
dependsOn: task.dependsOn || [],
|
|
161
|
-
run: ({ results, statuses }) => agent(task.prompt + "\\n\\nDependency results (null means unavailable):\\n" + JSON.stringify({ results, statuses }), { label: task.id,
|
|
202
|
+
run: ({ results, statuses }) => agent(task.prompt + "\\n\\nDependency results (null means unavailable):\\n" + JSON.stringify({ results, statuses }), { label: task.id, strength: "low" }),
|
|
162
203
|
})), { onError: "continue" });
|
|
163
204
|
const SYNTHESIS_CONTEXT_LIMIT = 78000;
|
|
164
205
|
function renderContextValue(value) {
|
|
@@ -232,7 +273,7 @@ while (synthesisJson.length > SYNTHESIS_CONTEXT_LIMIT && (valuePreviewBudget > 1
|
|
|
232
273
|
synthesisJson = JSON.stringify(synthesisContext);
|
|
233
274
|
}
|
|
234
275
|
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",
|
|
276
|
+
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
277
|
}
|
|
237
278
|
|
|
238
279
|
export default function piWorkflows(pi: ExtensionAPI): void {
|
|
@@ -346,37 +387,35 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
346
387
|
const DEFAULT_EXCLUDED_TOOLS = ["workflow", "workflow_control"] as const;
|
|
347
388
|
|
|
348
389
|
/**
|
|
349
|
-
*
|
|
350
|
-
* from.
|
|
390
|
+
* Persist one settings change into this project's own file.
|
|
351
391
|
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
392
|
+
* `workflowSettings` is the merged view — defaults, then global, then project
|
|
393
|
+
* — while `saveWorkflowSettings` replaces the project file with whatever it
|
|
394
|
+
* is handed. Handing it the merged view would copy every global value and
|
|
395
|
+
* every default into the project on the first edit, and the project would
|
|
396
|
+
* stop tracking the global file from then on. So the patch lands on project
|
|
397
|
+
* scope and the merged view is re-derived from what actually landed, which is
|
|
398
|
+
* also what makes it safe to report the result rather than the intent.
|
|
358
399
|
*/
|
|
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 };
|
|
400
|
+
const persistWorkflowSetting = (patch: WorkflowSettings): void => {
|
|
401
|
+
saveWorkflowSettings({ ...loadProjectWorkflowSettings(sessionCwd), ...patch }, sessionCwd);
|
|
402
|
+
workflowSettings = loadWorkflowSettings(sessionCwd);
|
|
366
403
|
};
|
|
367
404
|
|
|
405
|
+
/** Resolve a nested `workflow(name)` reference; a saved file shadows a built-in. */
|
|
406
|
+
const resolveNestedWorkflow = (name: string): string | undefined =>
|
|
407
|
+
loadSavedWorkflow(name, sessionCwd) ?? BUILTIN_WORKFLOWS[name]?.script;
|
|
408
|
+
|
|
368
409
|
const resolveScript = (
|
|
369
410
|
script: string | undefined,
|
|
370
411
|
name: string | undefined,
|
|
371
412
|
args: unknown,
|
|
372
|
-
): { script: string; source: string; toolset?: string
|
|
413
|
+
): { script: string; source: string; toolset?: string } => {
|
|
373
414
|
if (script !== undefined && name !== undefined) {
|
|
374
415
|
throw new Error("Provide either `script` or `name`, not both.");
|
|
375
416
|
}
|
|
376
417
|
if (script !== undefined) return { script, source: "inline" };
|
|
377
418
|
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
419
|
const saved = loadSavedWorkflow(name, sessionCwd);
|
|
381
420
|
if (saved) return { script: saved, source: `saved:${name}` };
|
|
382
421
|
const builtin = BUILTIN_WORKFLOWS[name];
|
|
@@ -384,12 +423,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
384
423
|
// Validate named invocations before starting a potentially expensive fleet.
|
|
385
424
|
// Slash commands provide the same shape through their descriptor primaryArg.
|
|
386
425
|
validateBuiltinArgs(name, args);
|
|
387
|
-
return {
|
|
388
|
-
script: builtin.script,
|
|
389
|
-
source: `builtin:${name}`,
|
|
390
|
-
toolset: builtin.toolset,
|
|
391
|
-
shippedScript: true,
|
|
392
|
-
};
|
|
426
|
+
return { script: builtin.script, source: `builtin:${name}`, toolset: builtin.toolset };
|
|
393
427
|
}
|
|
394
428
|
throw new Error(`Unknown workflow name "${name}". It is neither a saved workflow nor a built-in.`);
|
|
395
429
|
}
|
|
@@ -401,6 +435,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
401
435
|
active = true;
|
|
402
436
|
const workflowEngine = currentEngine();
|
|
403
437
|
workflowEngine.onRunSettled = notifyRunChanged;
|
|
438
|
+
// `control("resume")` and the provider-limit retry start executions with no
|
|
439
|
+
// caller to pass options for them, and the strength table is deliberately
|
|
440
|
+
// never frozen onto a run. Without this they would resume with no table and
|
|
441
|
+
// re-run every finished call untabled.
|
|
442
|
+
workflowEngine.strengths = () => workflowSettings.strengths;
|
|
404
443
|
// Command/tool registrations persist across session replacement, but the
|
|
405
444
|
// widget belongs to the newly restored engine and must be refreshed too.
|
|
406
445
|
refreshWidget();
|
|
@@ -417,9 +456,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
417
456
|
promptGuidelines: [
|
|
418
457
|
"Provide either a full `script` (raw JavaScript with the meta contract first) or a `name` of a saved/built-in workflow.",
|
|
419
458
|
"Use orchestrate([{ id, dependsOn, run }]) for named dependency graphs; use parallel(() => agent(...), ...) for a simple fan-out and pipeline(items, ...stages) for sequential per-item stages.",
|
|
459
|
+
"Size the fan-out to the work: one agent per independent piece of evidence the task actually has. Ten to thirty agents is an ordinary review or audit, and a script that splits a broad task four ways is usually four agents each doing one agent's job. The gain is coverage, not speed — how many run at once is the host's pool, not the script's choice.",
|
|
460
|
+
"Pass script/name/args/background and nothing else unless the user named a specific limit. maxAgents, concurrency, agentRetries, tokenBudget and agentTimeoutMs already default to tuned values, and each one stops or fails the run when it is set too low.",
|
|
420
461
|
"Use background: true for long runs; retrieve results with workflow_control.",
|
|
421
462
|
"Use resumeFromRunId with an edited script to replay the unchanged prefix from cache and re-run the rest live.",
|
|
422
|
-
"
|
|
463
|
+
"Inside the script, never name a model, a thinking level, or a turn limit: `strength` is the only routing a call may express.",
|
|
423
464
|
],
|
|
424
465
|
parameters: WorkflowToolSchema,
|
|
425
466
|
renderCall(args, theme) {
|
|
@@ -447,12 +488,12 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
447
488
|
? (() => {
|
|
448
489
|
const prior = workflowEngine.getRun(params.resumeFromRunId);
|
|
449
490
|
if (!prior) throw new Error(`Workflow run not found: ${params.resumeFromRunId}`);
|
|
450
|
-
// Resume
|
|
451
|
-
//
|
|
452
|
-
return { script: prior.script, source: "resume", toolset: prior.toolset
|
|
491
|
+
// Resume replays the stored script; only the shared shape
|
|
492
|
+
// has to be satisfied here.
|
|
493
|
+
return { script: prior.script, source: "resume", toolset: prior.toolset };
|
|
453
494
|
})()
|
|
454
495
|
: resolveScript(params.script, params.name, params.args);
|
|
455
|
-
const { script, source, toolset
|
|
496
|
+
const { script, source, toolset } = resolved;
|
|
456
497
|
const options: ScriptStartOptions = {
|
|
457
498
|
args: params.args,
|
|
458
499
|
background: params.background ?? true,
|
|
@@ -462,7 +503,6 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
462
503
|
tokenBudget: params.tokenBudget,
|
|
463
504
|
agentTimeoutMs: params.agentTimeoutMs,
|
|
464
505
|
toolset,
|
|
465
|
-
...(shippedScript === undefined ? {} : { shippedScript }),
|
|
466
506
|
excludeTools: [...DEFAULT_EXCLUDED_TOOLS],
|
|
467
507
|
signal,
|
|
468
508
|
mainModel: ctx?.model?.id,
|
|
@@ -479,6 +519,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
479
519
|
}
|
|
480
520
|
: undefined,
|
|
481
521
|
loadSavedWorkflow: resolveNestedWorkflow,
|
|
522
|
+
strengths: workflowSettings.strengths,
|
|
482
523
|
};
|
|
483
524
|
if (params.resumeFromRunId) {
|
|
484
525
|
const replacement = params.script !== undefined || params.name !== undefined ? script : undefined;
|
|
@@ -546,7 +587,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
546
587
|
|
|
547
588
|
// /workflows: navigator + run/status/watch/stop/pause/resume/rm/save subcommands
|
|
548
589
|
pi.registerCommand("workflows", {
|
|
549
|
-
description: "Workflow runs:
|
|
590
|
+
description: "Workflow runs: navigator, or run/status/watch/stop/pause/resume/rm/save/strength",
|
|
550
591
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
551
592
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
552
593
|
const [subcommand, ...rest] = tokens;
|
|
@@ -656,23 +697,158 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
656
697
|
}
|
|
657
698
|
return;
|
|
658
699
|
}
|
|
700
|
+
if (subcommand === "strength") {
|
|
701
|
+
// The Agent tier catalogue is owned by pi-subagents and still is (A8:
|
|
702
|
+
// no second tier config file). What this edits is narrower and is
|
|
703
|
+
// genuinely ours: which Agent tier each workflow strength runs on.
|
|
704
|
+
//
|
|
705
|
+
// A subcommand rather than a command of its own, because the reason
|
|
706
|
+
// to surface it at all is the three-part view below — two lines of
|
|
707
|
+
// which come from this package and one from the other — and that is
|
|
708
|
+
// a page to read, not a mode to be in.
|
|
709
|
+
//
|
|
710
|
+
// The catalogue lives on the managed peer, so it can legitimately be
|
|
711
|
+
// unreachable here. Status still has its other two parts, and a set
|
|
712
|
+
// made against a table that already exists is stored unvalidated: the
|
|
713
|
+
// run-start check reports a tier this host does not define and drops
|
|
714
|
+
// that entry, which costs the redirect rather than the run. A set made
|
|
715
|
+
// with no table at all is refused instead — see below, where the seed
|
|
716
|
+
// is read.
|
|
717
|
+
const catalogue = await (async (): Promise<string[] | undefined> => {
|
|
718
|
+
try {
|
|
719
|
+
return Object.keys((await awaitProtocol()).routingPolicy.profiles).sort();
|
|
720
|
+
} catch {
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
})();
|
|
724
|
+
const knownTiers = catalogue ? new Set(catalogue) : undefined;
|
|
725
|
+
// What a run would resolve, by the same rule the runtime uses: a
|
|
726
|
+
// configured table if there is one, else the shipped default. Shown
|
|
727
|
+
// rather than the configured table alone, because an unconfigured
|
|
728
|
+
// machine is not an unrouted one and a row reading "(unset)" there
|
|
729
|
+
// would describe something other than what the next run does.
|
|
730
|
+
const effective = (): { table: WorkflowStrengthTable; shipped: boolean } => {
|
|
731
|
+
const configured = workflowSettings.strengths;
|
|
732
|
+
return configured === undefined
|
|
733
|
+
? { table: defaultStrengthTable(knownTiers), shipped: true }
|
|
734
|
+
: { table: configured, shipped: false };
|
|
735
|
+
};
|
|
736
|
+
const describe = (): string => {
|
|
737
|
+
const { table, shipped } = effective();
|
|
738
|
+
const rows = WORKFLOW_STRENGTHS.map((name) => {
|
|
739
|
+
// The default is derived from the catalogue, so without one there
|
|
740
|
+
// is nothing honest to print: the run will still route these, and
|
|
741
|
+
// "(unset)" would claim it will not.
|
|
742
|
+
if (shipped && knownTiers === undefined) return ` ${name} → (default; needs the tier catalogue)`;
|
|
743
|
+
if (table[name] === undefined) return ` ${name} → (unset; the agent's own default)`;
|
|
744
|
+
return ` ${name} → ${table[name]}${shipped ? " (default)" : ""}`;
|
|
745
|
+
});
|
|
746
|
+
// The shipped default is identity, so on a stock install workflow
|
|
747
|
+
// work rides the same profiles every ordinary spawn does — the
|
|
748
|
+
// coupling this indirection exists to let you break, but does not
|
|
749
|
+
// break on its own. Saying so here is the only place the two steps
|
|
750
|
+
// out of it are visible together.
|
|
751
|
+
const decouple = shipped
|
|
752
|
+
? "\nThis is the shipped default: each strength on the tier of the same name, so " +
|
|
753
|
+
"workflow work shares the tiers your ordinary spawns use (Explore among them). " +
|
|
754
|
+
"To price workflow work on its own, define a tier in /agents → Model tiers, then " +
|
|
755
|
+
"point a strength at it here."
|
|
756
|
+
: "";
|
|
757
|
+
return (
|
|
758
|
+
`Workflow strengths and the Agent tier each runs on:\n${rows.join("\n")}\n` +
|
|
759
|
+
`Tiers this host defines: ${catalogue?.join(", ") ?? "(unavailable)"}\n` +
|
|
760
|
+
"Edits apply to this project; for a machine-wide table edit " +
|
|
761
|
+
"<agent dir>/workflows/settings.json. The tiers themselves live in " +
|
|
762
|
+
"/agents → Model tiers; this only says which one a strength uses." +
|
|
763
|
+
decouple
|
|
764
|
+
);
|
|
765
|
+
};
|
|
766
|
+
const [strength, target] = rest;
|
|
767
|
+
if (strength === undefined) {
|
|
768
|
+
ctx.ui.notify(describe(), "info");
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (!isWorkflowStrength(strength)) {
|
|
772
|
+
ctx.ui.notify(`"${strength}" is not a workflow strength. Use one of ${WORKFLOW_STRENGTH_LIST}.`, "warning");
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (target === undefined || rest.length !== 2) {
|
|
776
|
+
ctx.ui.notify(`/workflows strength [<${WORKFLOW_STRENGTHS.join("|")}> <tier>|off]`, "warning");
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
// `off` reads as "unset" unless the host really defines a tier by
|
|
780
|
+
// that name, in which case the user's own catalogue outranks our
|
|
781
|
+
// keyword — a tier you cannot point at is worse than a clumsier way
|
|
782
|
+
// to clear one.
|
|
783
|
+
const unset = target === "off" && catalogue?.includes("off") !== true;
|
|
784
|
+
if (!unset && catalogue && !catalogue.includes(target)) {
|
|
785
|
+
ctx.ui.notify(
|
|
786
|
+
`"${target}" is not a tier this host defines. Available: ${catalogue.join(", ") || "(none)"}. ` +
|
|
787
|
+
"Define it in /agents → Model tiers first.",
|
|
788
|
+
"warning",
|
|
789
|
+
);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const { table: current, shipped } = effective();
|
|
793
|
+
if (shipped && knownTiers === undefined) {
|
|
794
|
+
// Seeding from a default we cannot compute would write a one-entry
|
|
795
|
+
// table, and this key replaces rather than merges — so the other two
|
|
796
|
+
// strengths would go from routed to unmapped as a side effect of
|
|
797
|
+
// setting this one. Refusing costs a retry; writing costs a run.
|
|
798
|
+
ctx.ui.notify(
|
|
799
|
+
"The tier catalogue is unavailable, so the current table cannot be read. " +
|
|
800
|
+
"Retry once the managed peer is reachable.",
|
|
801
|
+
"warning",
|
|
802
|
+
);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Seeded from the table a run would use, not from this project's file.
|
|
806
|
+
// Every level of this key replaces the one below rather than merging
|
|
807
|
+
// into it, so a patch built on project scope would silently drop
|
|
808
|
+
// whatever the level below contributed: adding one entry would discard
|
|
809
|
+
// an inherited global table, and `off` against an entry this project
|
|
810
|
+
// never wrote would write nothing and leave the mapping standing. The
|
|
811
|
+
// seed does freeze the current table into the project, which is what a
|
|
812
|
+
// replace-wholesale key costs — the entries a partial file left out
|
|
813
|
+
// would be unmapped, not inherited.
|
|
814
|
+
const { [strength]: _cleared, ...others } = current;
|
|
815
|
+
try {
|
|
816
|
+
persistWorkflowSetting({ strengths: unset ? others : { ...others, [strength]: target } });
|
|
817
|
+
} catch (error: unknown) {
|
|
818
|
+
ctx.ui.notify(
|
|
819
|
+
`Could not save workflow strength routing: ${error instanceof Error ? error.message : String(error)}`,
|
|
820
|
+
"warning",
|
|
821
|
+
);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
// Show the resolved result rather than echoing the write. The
|
|
825
|
+
// settings layer can drop what it was handed, and `off` against the
|
|
826
|
+
// last remaining entry stores an explicit empty table — "map nothing",
|
|
827
|
+
// which is not the same as the shipped default an absent key would
|
|
828
|
+
// mean — so a message announcing the write would describe something
|
|
829
|
+
// other than what the next run will do. Runs already in flight keep
|
|
830
|
+
// what they started with; the table is read fresh at each start and
|
|
831
|
+
// resume.
|
|
832
|
+
//
|
|
833
|
+
// The one case the table alone cannot explain: the host defines a tier
|
|
834
|
+
// named `off`, so the keyword lost to it above and this mapped rather
|
|
835
|
+
// than cleared. The catalogue outranking our keyword is the right call
|
|
836
|
+
// — a tier you cannot point at is worse than a clumsier way to clear
|
|
837
|
+
// one — but a user who typed the documented keyword and got a mapping
|
|
838
|
+
// has to be told which reading won, and where the other one lives.
|
|
839
|
+
const shadowed =
|
|
840
|
+
target === "off" && !unset
|
|
841
|
+
? '\n\nNote: this host defines a tier named "off", so `off` was read as that tier rather ' +
|
|
842
|
+
"than as the clear keyword. To leave a strength unmapped here, remove its entry from the " +
|
|
843
|
+
"`strengths` object in this project's settings.json."
|
|
844
|
+
: "";
|
|
845
|
+
ctx.ui.notify(`${describe()}${shadowed}`, "info");
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
659
848
|
ctx.ui.notify(`Unknown /workflows subcommand: ${subcommand}`, "warning");
|
|
660
849
|
},
|
|
661
850
|
});
|
|
662
851
|
|
|
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
852
|
// /workflows-progress: persistent compact/detailed live-panel settings.
|
|
677
853
|
pi.registerCommand("workflows-progress", {
|
|
678
854
|
description: "Workflow progress panel: compact|detailed|status|max <N>",
|
|
@@ -686,21 +862,22 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
686
862
|
);
|
|
687
863
|
return;
|
|
688
864
|
}
|
|
865
|
+
let patch: WorkflowSettings;
|
|
689
866
|
if (action === "compact" || action === "detailed") {
|
|
690
|
-
|
|
867
|
+
patch = { progressMode: action };
|
|
691
868
|
} else if (action === "max") {
|
|
692
869
|
const max = Number(tokens[1]);
|
|
693
870
|
if (!Number.isInteger(max) || max < 1 || max > 32) {
|
|
694
871
|
ctx.ui.notify("Usage: /workflows-progress max <1-32>", "warning");
|
|
695
872
|
return;
|
|
696
873
|
}
|
|
697
|
-
|
|
874
|
+
patch = { maxAgentsShown: max };
|
|
698
875
|
} else {
|
|
699
876
|
ctx.ui.notify("Usage: /workflows-progress compact|detailed|status|max <N>", "warning");
|
|
700
877
|
return;
|
|
701
878
|
}
|
|
702
879
|
try {
|
|
703
|
-
|
|
880
|
+
persistWorkflowSetting(patch);
|
|
704
881
|
refreshWidget();
|
|
705
882
|
ctx.ui.notify(
|
|
706
883
|
`Workflow progress set to ${workflowSettings.progressMode}, max ${workflowSettings.maxAgentsShown}.`,
|
|
@@ -723,9 +900,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
723
900
|
// an ordinary answer.
|
|
724
901
|
const TRIGGER_WORD_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/u;
|
|
725
902
|
const persistTriggerSettings = (): void => {
|
|
726
|
-
workflowSettings = { ...workflowSettings, keywordTriggerWord: triggerKeyword, keywordTriggerEnabled };
|
|
727
903
|
try {
|
|
728
|
-
|
|
904
|
+
persistWorkflowSetting({ keywordTriggerWord: triggerKeyword, keywordTriggerEnabled });
|
|
729
905
|
} catch {
|
|
730
906
|
/* setting changes remain active in memory */
|
|
731
907
|
}
|
|
@@ -784,7 +960,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
784
960
|
if (event.text.includes(WORKFLOW_ARMED_DIRECTIVE)) return { action: "continue" as const };
|
|
785
961
|
const effortDirective =
|
|
786
962
|
effortLevel === "ultra"
|
|
787
|
-
?
|
|
963
|
+
? 'Effort: ULTRA. Be exhaustive: use broad parallel review, verification, and completeness checks. Width is where the exhaustiveness goes: keep steps at strength: "medium" and reserve "high" for a step whose error is expensive to reverse, if the run has one at all.'
|
|
788
964
|
: effortLevel === "high"
|
|
789
965
|
? "Effort: HIGH. Be thorough: use several independent reviewers and an adversarial verification pass."
|
|
790
966
|
: "";
|
|
@@ -806,9 +982,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
806
982
|
return;
|
|
807
983
|
}
|
|
808
984
|
effortLevel = level;
|
|
809
|
-
workflowSettings = { ...workflowSettings, effort: effortLevel };
|
|
810
985
|
try {
|
|
811
|
-
|
|
986
|
+
persistWorkflowSetting({ effort: effortLevel });
|
|
812
987
|
} catch {
|
|
813
988
|
/* warning is non-fatal */
|
|
814
989
|
}
|
|
@@ -824,9 +999,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
824
999
|
description: "Ultracode: toggle exhaustive workflow effort",
|
|
825
1000
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
826
1001
|
effortLevel = args.trim().toLowerCase() === "off" ? "off" : "ultra";
|
|
827
|
-
workflowSettings = { ...workflowSettings, effort: effortLevel };
|
|
828
1002
|
try {
|
|
829
|
-
|
|
1003
|
+
persistWorkflowSetting({ effort: effortLevel });
|
|
830
1004
|
} catch {
|
|
831
1005
|
/* warning is non-fatal */
|
|
832
1006
|
}
|
|
@@ -873,9 +1047,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
873
1047
|
args: scriptArgs,
|
|
874
1048
|
background: true,
|
|
875
1049
|
toolset: descriptor.toolset,
|
|
876
|
-
shippedScript: true,
|
|
877
1050
|
excludeTools: [...DEFAULT_EXCLUDED_TOOLS],
|
|
878
1051
|
loadSavedWorkflow: resolveNestedWorkflow,
|
|
1052
|
+
strengths: workflowSettings.strengths,
|
|
879
1053
|
};
|
|
880
1054
|
try {
|
|
881
1055
|
const workflowEngine = currentEngine();
|
|
@@ -1019,9 +1193,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1019
1193
|
const workflowEngine = currentEngine();
|
|
1020
1194
|
const result = await workflowEngine.start(script, {
|
|
1021
1195
|
background: true,
|
|
1022
|
-
|
|
1023
|
-
// preference against whatever catalogue this host defines.
|
|
1024
|
-
shippedScript: true,
|
|
1196
|
+
strengths: workflowSettings.strengths,
|
|
1025
1197
|
mainModel: ctx.model?.id,
|
|
1026
1198
|
});
|
|
1027
1199
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
@@ -1190,6 +1362,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1190
1362
|
background: true,
|
|
1191
1363
|
mainModel: commandCtx?.model?.id,
|
|
1192
1364
|
loadSavedWorkflow: resolveNestedWorkflow,
|
|
1365
|
+
strengths: workflowSettings.strengths,
|
|
1193
1366
|
};
|
|
1194
1367
|
const result = await workflowEngine.start(script, options);
|
|
1195
1368
|
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 } : {}),
|
package/src/rpc-client.ts
CHANGED
|
@@ -172,6 +172,16 @@ export interface ManagedProtocolCheck {
|
|
|
172
172
|
routingPolicy: ManagedRoutingPolicy;
|
|
173
173
|
/** Whole-catalogue identity; used only where a per-tier identity cannot be. */
|
|
174
174
|
routingPolicyFingerprint: string;
|
|
175
|
+
/**
|
|
176
|
+
* The host's background-agent pool size, when the peer published one.
|
|
177
|
+
*
|
|
178
|
+
* A workflow agent occupies one of those slots like any other background
|
|
179
|
+
* agent, so this is the width the run can actually achieve; dispatching past
|
|
180
|
+
* it only fills the host's queue. Undefined means the peer predates the
|
|
181
|
+
* field or answered through the readiness broadcast, which carries no
|
|
182
|
+
* requested fields — the caller then falls back to its own default.
|
|
183
|
+
*/
|
|
184
|
+
maxConcurrent?: number;
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
export async function checkManagedSpawnProtocol(
|
|
@@ -213,7 +223,9 @@ export async function checkManagedSpawnProtocol(
|
|
|
213
223
|
});
|
|
214
224
|
const sendPing = () => {
|
|
215
225
|
try {
|
|
216
|
-
|
|
226
|
+
// Additive reply fields are requested by name: the reply envelope is
|
|
227
|
+
// strictly parsed, so a peer may not volunteer one.
|
|
228
|
+
events.emit("subagents:rpc:ping", { requestId: id, include: ["maxConcurrent"] });
|
|
217
229
|
} catch (error: unknown) {
|
|
218
230
|
finish(undefined, error instanceof Error ? error : new Error(String(error)));
|
|
219
231
|
}
|
|
@@ -254,6 +266,7 @@ export async function checkManagedSpawnProtocol(
|
|
|
254
266
|
return {
|
|
255
267
|
routingPolicy: ping.routingPolicy.policy,
|
|
256
268
|
routingPolicyFingerprint: ping.routingPolicy.fingerprint,
|
|
269
|
+
...(ping.maxConcurrent === undefined ? {} : { maxConcurrent: ping.maxConcurrent }),
|
|
257
270
|
};
|
|
258
271
|
}
|
|
259
272
|
|