@yycholla/pi-dynamic-workflows 3.3.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/LICENSE +22 -0
- package/README.md +325 -0
- package/assets/readme/hero.png +0 -0
- package/assets/readme/hero.svg +70 -0
- package/assets/readme/package-cover.png +0 -0
- package/assets/readme/package-cover.svg +66 -0
- package/assets/readme/workflow.png +0 -0
- package/assets/readme/workflow.svg +77 -0
- package/dist/accept-workflow-guidance.d.ts +14 -0
- package/dist/accept-workflow-guidance.js +53 -0
- package/dist/adversarial-review.d.ts +30 -0
- package/dist/adversarial-review.js +107 -0
- package/dist/agent-history.d.ts +20 -0
- package/dist/agent-history.js +122 -0
- package/dist/agent-registry.d.ts +83 -0
- package/dist/agent-registry.js +190 -0
- package/dist/agent.d.ts +364 -0
- package/dist/agent.js +714 -0
- package/dist/builtin-commands.d.ts +19 -0
- package/dist/builtin-commands.js +251 -0
- package/dist/builtin-workflows.d.ts +45 -0
- package/dist/builtin-workflows.js +121 -0
- package/dist/code-review.d.ts +26 -0
- package/dist/code-review.js +181 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +44 -0
- package/dist/deep-research.d.ts +30 -0
- package/dist/deep-research.js +124 -0
- package/dist/display.d.ts +134 -0
- package/dist/display.js +248 -0
- package/dist/effort-command.d.ts +28 -0
- package/dist/effort-command.js +68 -0
- package/dist/enums.d.ts +69 -0
- package/dist/enums.js +78 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.js +140 -0
- package/dist/extension-reload.d.ts +37 -0
- package/dist/extension-reload.js +78 -0
- package/dist/fs-persistence.d.ts +63 -0
- package/dist/fs-persistence.js +102 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +35 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +66 -0
- package/dist/model-routing.d.ts +30 -0
- package/dist/model-routing.js +50 -0
- package/dist/model-spec.d.ts +29 -0
- package/dist/model-spec.js +252 -0
- package/dist/model-tier-config.d.ts +133 -0
- package/dist/model-tier-config.js +249 -0
- package/dist/run-persistence.d.ts +180 -0
- package/dist/run-persistence.js +294 -0
- package/dist/saved-commands.d.ts +28 -0
- package/dist/saved-commands.js +100 -0
- package/dist/shared-store.d.ts +98 -0
- package/dist/shared-store.js +212 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/task-panel.d.ts +61 -0
- package/dist/task-panel.js +422 -0
- package/dist/usage-limit-scheduler.d.ts +145 -0
- package/dist/usage-limit-scheduler.js +368 -0
- package/dist/web-tools.d.ts +20 -0
- package/dist/web-tools.js +120 -0
- package/dist/workflow-authoring-coverage.d.ts +70 -0
- package/dist/workflow-authoring-coverage.js +421 -0
- package/dist/workflow-authoring-reference.d.ts +20 -0
- package/dist/workflow-authoring-reference.js +156 -0
- package/dist/workflow-capability-contract.d.ts +131 -0
- package/dist/workflow-capability-contract.js +604 -0
- package/dist/workflow-commands.d.ts +18 -0
- package/dist/workflow-commands.js +260 -0
- package/dist/workflow-comprehension.d.ts +133 -0
- package/dist/workflow-comprehension.js +1321 -0
- package/dist/workflow-context-measurement.d.ts +72 -0
- package/dist/workflow-context-measurement.js +213 -0
- package/dist/workflow-control-tool.d.ts +30 -0
- package/dist/workflow-control-tool.js +176 -0
- package/dist/workflow-delivery-choice.d.ts +20 -0
- package/dist/workflow-delivery-choice.js +48 -0
- package/dist/workflow-editor.d.ts +93 -0
- package/dist/workflow-editor.js +363 -0
- package/dist/workflow-manager.d.ts +492 -0
- package/dist/workflow-manager.js +1124 -0
- package/dist/workflow-paths.d.ts +22 -0
- package/dist/workflow-paths.js +46 -0
- package/dist/workflow-release-gate.d.ts +39 -0
- package/dist/workflow-release-gate.js +309 -0
- package/dist/workflow-saved.d.ts +38 -0
- package/dist/workflow-saved.js +126 -0
- package/dist/workflow-settings.d.ts +70 -0
- package/dist/workflow-settings.js +131 -0
- package/dist/workflow-tool.d.ts +71 -0
- package/dist/workflow-tool.js +367 -0
- package/dist/workflow-ui.d.ts +182 -0
- package/dist/workflow-ui.js +1587 -0
- package/dist/workflow.d.ts +333 -0
- package/dist/workflow.js +1151 -0
- package/dist/workflows-models-command.d.ts +31 -0
- package/dist/workflows-models-command.js +156 -0
- package/dist/worktree.d.ts +25 -0
- package/dist/worktree.js +61 -0
- package/extensions/workflow.ts +151 -0
- package/package.json +104 -0
- package/skills/workflow-authoring/SKILL.md +30 -0
- package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
- package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
- package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
- package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
- package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
- package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
- package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
- package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
- package/skills/workflow-authoring/examples/structured-output.js +37 -0
- package/skills/workflow-authoring/examples/tournament.js +81 -0
- package/skills/workflow-authoring/examples/validated-gate.js +63 -0
- package/skills/workflow-authoring/references/capabilities.md +41 -0
- package/skills/workflow-authoring/references/capability-details.md +357 -0
- package/skills/workflow-authoring/references/common-helpers.md +4 -0
- package/skills/workflow-authoring/references/debugging.md +27 -0
- package/skills/workflow-authoring/references/focused-recipes.md +11 -0
- package/skills/workflow-authoring/references/helpers.md +9 -0
- package/skills/workflow-authoring/references/lifecycle.md +33 -0
- package/skills/workflow-authoring/references/pattern-selection.md +15 -0
- package/skills/workflow-authoring/references/quality-helpers.md +8 -0
- package/skills/workflow-authoring/references/registry-ownership.md +15 -0
- package/skills/workflow-authoring/references/retry-helper.md +5 -0
- package/skills/workflow-authoring/references/review.md +43 -0
- package/skills/workflow-authoring/references/runtime.md +27 -0
- package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
- package/skills/workflow-authoring/references/versions.md +13 -0
- package/skills/workflow-patterns/SKILL.md +51 -0
- package/src/accept-workflow-guidance.ts +71 -0
- package/src/adversarial-review.ts +120 -0
- package/src/agent-history.ts +157 -0
- package/src/agent-registry.ts +221 -0
- package/src/agent.ts +929 -0
- package/src/builtin-commands.ts +286 -0
- package/src/builtin-workflows.ts +155 -0
- package/src/code-review.ts +183 -0
- package/src/config.ts +55 -0
- package/src/deep-research.ts +135 -0
- package/src/display.ts +367 -0
- package/src/effort-command.ts +87 -0
- package/src/enums.ts +77 -0
- package/src/errors.ts +199 -0
- package/src/extension-reload.ts +100 -0
- package/src/fs-persistence.ts +124 -0
- package/src/index.ts +176 -0
- package/src/logger.ts +88 -0
- package/src/model-routing.ts +73 -0
- package/src/model-spec.ts +309 -0
- package/src/model-tier-config.ts +296 -0
- package/src/run-persistence.ts +484 -0
- package/src/saved-commands.ts +115 -0
- package/src/shared-store.ts +228 -0
- package/src/structured-output.ts +47 -0
- package/src/task-panel.ts +490 -0
- package/src/usage-limit-scheduler.ts +432 -0
- package/src/web-tools.ts +124 -0
- package/src/workflow-authoring-coverage.ts +486 -0
- package/src/workflow-authoring-reference.ts +186 -0
- package/src/workflow-capability-contract.ts +806 -0
- package/src/workflow-commands.ts +287 -0
- package/src/workflow-comprehension.ts +1673 -0
- package/src/workflow-context-measurement.ts +262 -0
- package/src/workflow-control-tool.ts +238 -0
- package/src/workflow-delivery-choice.ts +69 -0
- package/src/workflow-editor.ts +444 -0
- package/src/workflow-manager.ts +1405 -0
- package/src/workflow-paths.ts +63 -0
- package/src/workflow-release-gate.ts +529 -0
- package/src/workflow-saved.ts +180 -0
- package/src/workflow-settings.ts +194 -0
- package/src/workflow-tool.ts +464 -0
- package/src/workflow-ui.ts +1789 -0
- package/src/workflow.ts +1615 -0
- package/src/workflows-models-command.ts +211 -0
- package/src/worktree.ts +76 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model tier configuration for workflow subagent model routing.
|
|
3
|
+
*
|
|
4
|
+
* A tier is a named slot (small/medium/big) holding exactly ONE model spec
|
|
5
|
+
* string (e.g. "openai/gpt-4.1-mini" or "openai-codex/gpt-5.5:xhigh").
|
|
6
|
+
* When an agent() call specifies opts.tier, that single model is resolved with
|
|
7
|
+
* Pi CLI-style parsing and used as the subagent's model/thinking level (unless
|
|
8
|
+
* an explicit opts.model is given, which always wins — see agent.ts).
|
|
9
|
+
*
|
|
10
|
+
* This augments the phase-pattern routing in model-routing.ts: phase routing
|
|
11
|
+
* maps workflow phases → models via the script's meta; tiers give scripts a
|
|
12
|
+
* coarse, user-configurable small/medium/big knob that is independent of any
|
|
13
|
+
* concrete provider/model id.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Model tier configuration. Maps tier names (e.g. "small", "medium", "big")
|
|
17
|
+
* to a single model spec string (e.g. "gpt-4.1-mini", "openai/gpt-4.1-mini",
|
|
18
|
+
* or "openai-codex/gpt-5.5:xhigh").
|
|
19
|
+
*/
|
|
20
|
+
export interface ModelTierConfig {
|
|
21
|
+
tiers: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The minimal projection of a model that tier ranking needs. Deliberately NOT
|
|
25
|
+
* the SDK's full `Model` type: tier logic depends only on these three fields,
|
|
26
|
+
* so it stays decoupled from the SDK (no `@earendil-works/pi-ai` import here)
|
|
27
|
+
* and is trivially unit-testable with plain objects. `agent.ts`'s
|
|
28
|
+
* `listAvailableModels()` produces these from the live registry.
|
|
29
|
+
*/
|
|
30
|
+
export interface RankableModel {
|
|
31
|
+
/** Canonical "provider/id" spec string. */
|
|
32
|
+
spec: string;
|
|
33
|
+
/** Per-token output price, if the registry reports one. Missing or 0 = unknown. */
|
|
34
|
+
costOutput?: number;
|
|
35
|
+
/** Context window size, if the registry reports one. */
|
|
36
|
+
contextWindow?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Path to the model tiers JSON config file (~/.pi/workflows/model-tiers.json). */
|
|
39
|
+
export declare function getModelTierConfigPath(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Substrings that identify small/cheap models (case-insensitive), used only as
|
|
42
|
+
* a fallback capability hint when price signals are absent or tied.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SMALL_MODEL_HINTS: readonly ["mini", "flash", "haiku", "nano", "small"];
|
|
45
|
+
/**
|
|
46
|
+
* Substrings that identify large/capable models (case-insensitive), used only
|
|
47
|
+
* as a fallback capability hint when price signals are absent or tied.
|
|
48
|
+
*/
|
|
49
|
+
export declare const BIG_MODEL_HINTS: readonly ["opus", "pro", "ultra", "large", "plus"];
|
|
50
|
+
/**
|
|
51
|
+
* Fallback capability hint from a model's name: -1 for a small/cheap name, +1
|
|
52
|
+
* for a large/capable name, 0 otherwise. If a name matches both sets, the small
|
|
53
|
+
* hint wins (we never want a "mini"-labelled model to outrank a neutral or
|
|
54
|
+
* clearly-large one). This is only a FALLBACK: `rankByCapability` prefers the
|
|
55
|
+
* registry's price signal, which is robust to new vendor names (e.g. "fable",
|
|
56
|
+
* "mimo") that match no hint and would otherwise all score 0.
|
|
57
|
+
*/
|
|
58
|
+
export declare function hintScore(spec: string): number;
|
|
59
|
+
/**
|
|
60
|
+
* Rank models from least → most capable.
|
|
61
|
+
*
|
|
62
|
+
* PRIMARY signal is output price (higher price ≈ more capable): within a single
|
|
63
|
+
* registry, price tracks the vendor's capability tier far more robustly than
|
|
64
|
+
* model-name substrings, and it works for models whose names match no hint.
|
|
65
|
+
*
|
|
66
|
+
* Models with an UNKNOWN price (missing or 0 — common for self-hosted
|
|
67
|
+
* `models.json` entries) are NOT treated as "cheapest = weakest". Instead they
|
|
68
|
+
* are projected onto the known price range via their substring hint: a
|
|
69
|
+
* big-hint name lands at the top of the range, a small-hint name at the bottom,
|
|
70
|
+
* a neutral name at the middle. When NO model has a known price at all, this
|
|
71
|
+
* degrades to pure hint ordering (the previous behavior).
|
|
72
|
+
*
|
|
73
|
+
* The comparison is a single total order (projected cost → hint → contextWindow
|
|
74
|
+
* → stable registry index), so the sort is transitive and stable.
|
|
75
|
+
*/
|
|
76
|
+
export declare function rankByCapability(models: readonly RankableModel[]): RankableModel[];
|
|
77
|
+
/**
|
|
78
|
+
* Build a default tier config. When the available model registry is known,
|
|
79
|
+
* spread it across tiers so small/medium/big routing is meaningful out of the
|
|
80
|
+
* box. When the registry is empty or unavailable, fall back to the current Pi
|
|
81
|
+
* model so fresh installs still get usable tier values.
|
|
82
|
+
*
|
|
83
|
+
* Models are first ranked least → most capable via `rankByCapability` (price
|
|
84
|
+
* first, name-substring hint as fallback). Tiers are then assigned from this
|
|
85
|
+
* single ranked pool with exclusion — each model is used for at most one tier —
|
|
86
|
+
* so distinct tiers never collapse onto the same model and a weaker model can
|
|
87
|
+
* never outrank a stronger one (no inversion):
|
|
88
|
+
*
|
|
89
|
+
* - big = the most capable model (last in the ranking)
|
|
90
|
+
* - small = the least capable model (first in the ranking)
|
|
91
|
+
* - medium = the middle-ranked model
|
|
92
|
+
*
|
|
93
|
+
* When fewer than 3 distinct models are available, this degrades gracefully by
|
|
94
|
+
* reusing the *strongest* available model for the higher tier(s):
|
|
95
|
+
*
|
|
96
|
+
* - 2 models: small = weaker, medium = big = stronger
|
|
97
|
+
* - 1 / 0 models: small = medium = big = that model (or the current model /
|
|
98
|
+
* "" fallback)
|
|
99
|
+
*
|
|
100
|
+
* `availableModels` is injectable for testing and for callers that already
|
|
101
|
+
* fetched the registry. When omitted, this reads from the live registry.
|
|
102
|
+
*/
|
|
103
|
+
export declare function buildDefaultTierConfig(currentModelSpec?: string, availableModels?: readonly RankableModel[]): ModelTierConfig;
|
|
104
|
+
/**
|
|
105
|
+
* One-time notice shown when an agent requests `opts.tier` but no
|
|
106
|
+
* model-tiers.json is configured — in that state tiers silently fall back to
|
|
107
|
+
* the session model (see `resolveAgentModelSpec` in agent.ts), which is easy to
|
|
108
|
+
* miss. This surfaces the fallback and the mapping the user *would* get by
|
|
109
|
+
* configuring, using the same `buildDefaultTierConfig` ranking so the hint is
|
|
110
|
+
* actionable. Pure/string-only so the caller owns how it's emitted.
|
|
111
|
+
*/
|
|
112
|
+
export declare function formatTierFallbackNotice(mainModel: string | undefined, availableModels: readonly RankableModel[]): string;
|
|
113
|
+
/**
|
|
114
|
+
* Load the model tier config from disk. Returns null if the file does not
|
|
115
|
+
* exist or is unparseable (callers fall back to a default).
|
|
116
|
+
*/
|
|
117
|
+
export declare function loadModelTierConfig(configPath?: string): ModelTierConfig | null;
|
|
118
|
+
/**
|
|
119
|
+
* Save a model tier config to disk. Creates parent directories if needed.
|
|
120
|
+
*
|
|
121
|
+
* Refuses a degenerate `tiers` (e.g. all-empty-string, from buildDefaultTierConfig
|
|
122
|
+
* with an empty model registry) using the same isValidTiersMap check the loader
|
|
123
|
+
* uses — otherwise the write side could produce exactly the shape loadModelTierConfig
|
|
124
|
+
* now rejects on the next read, silently discarding the "saved" config.
|
|
125
|
+
*/
|
|
126
|
+
export declare function saveModelTierConfig(config: ModelTierConfig, configPath?: string): void;
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a tier name to its configured model spec, or undefined if the tier
|
|
129
|
+
* is not configured.
|
|
130
|
+
*/
|
|
131
|
+
export declare function resolveTierModel(tier: string, config: ModelTierConfig): string | undefined;
|
|
132
|
+
/** Return all tier names sorted: small < medium < big, then alphabetically. */
|
|
133
|
+
export declare function sortedTierNames(config: ModelTierConfig): string[];
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model tier configuration for workflow subagent model routing.
|
|
3
|
+
*
|
|
4
|
+
* A tier is a named slot (small/medium/big) holding exactly ONE model spec
|
|
5
|
+
* string (e.g. "openai/gpt-4.1-mini" or "openai-codex/gpt-5.5:xhigh").
|
|
6
|
+
* When an agent() call specifies opts.tier, that single model is resolved with
|
|
7
|
+
* Pi CLI-style parsing and used as the subagent's model/thinking level (unless
|
|
8
|
+
* an explicit opts.model is given, which always wins — see agent.ts).
|
|
9
|
+
*
|
|
10
|
+
* This augments the phase-pattern routing in model-routing.ts: phase routing
|
|
11
|
+
* maps workflow phases → models via the script's meta; tiers give scripts a
|
|
12
|
+
* coarse, user-configurable small/medium/big knob that is independent of any
|
|
13
|
+
* concrete provider/model id.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { listAvailableModels } from "./agent.js";
|
|
19
|
+
import { MODEL_TIERS_FILE } from "./config.js";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Configuration path
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/** Path to the model tiers JSON config file (~/.pi/workflows/model-tiers.json). */
|
|
24
|
+
export function getModelTierConfigPath() {
|
|
25
|
+
return join(homedir(), MODEL_TIERS_FILE);
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Capability signal
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Substrings that identify small/cheap models (case-insensitive), used only as
|
|
32
|
+
* a fallback capability hint when price signals are absent or tied.
|
|
33
|
+
*/
|
|
34
|
+
export const SMALL_MODEL_HINTS = ["mini", "flash", "haiku", "nano", "small"];
|
|
35
|
+
/**
|
|
36
|
+
* Substrings that identify large/capable models (case-insensitive), used only
|
|
37
|
+
* as a fallback capability hint when price signals are absent or tied.
|
|
38
|
+
*/
|
|
39
|
+
export const BIG_MODEL_HINTS = ["opus", "pro", "ultra", "large", "plus"];
|
|
40
|
+
/**
|
|
41
|
+
* Fallback capability hint from a model's name: -1 for a small/cheap name, +1
|
|
42
|
+
* for a large/capable name, 0 otherwise. If a name matches both sets, the small
|
|
43
|
+
* hint wins (we never want a "mini"-labelled model to outrank a neutral or
|
|
44
|
+
* clearly-large one). This is only a FALLBACK: `rankByCapability` prefers the
|
|
45
|
+
* registry's price signal, which is robust to new vendor names (e.g. "fable",
|
|
46
|
+
* "mimo") that match no hint and would otherwise all score 0.
|
|
47
|
+
*/
|
|
48
|
+
export function hintScore(spec) {
|
|
49
|
+
const lower = spec.toLowerCase();
|
|
50
|
+
if (SMALL_MODEL_HINTS.some((hint) => lower.includes(hint)))
|
|
51
|
+
return -1;
|
|
52
|
+
if (BIG_MODEL_HINTS.some((hint) => lower.includes(hint)))
|
|
53
|
+
return 1;
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Rank models from least → most capable.
|
|
58
|
+
*
|
|
59
|
+
* PRIMARY signal is output price (higher price ≈ more capable): within a single
|
|
60
|
+
* registry, price tracks the vendor's capability tier far more robustly than
|
|
61
|
+
* model-name substrings, and it works for models whose names match no hint.
|
|
62
|
+
*
|
|
63
|
+
* Models with an UNKNOWN price (missing or 0 — common for self-hosted
|
|
64
|
+
* `models.json` entries) are NOT treated as "cheapest = weakest". Instead they
|
|
65
|
+
* are projected onto the known price range via their substring hint: a
|
|
66
|
+
* big-hint name lands at the top of the range, a small-hint name at the bottom,
|
|
67
|
+
* a neutral name at the middle. When NO model has a known price at all, this
|
|
68
|
+
* degrades to pure hint ordering (the previous behavior).
|
|
69
|
+
*
|
|
70
|
+
* The comparison is a single total order (projected cost → hint → contextWindow
|
|
71
|
+
* → stable registry index), so the sort is transitive and stable.
|
|
72
|
+
*/
|
|
73
|
+
export function rankByCapability(models) {
|
|
74
|
+
const knownCosts = models
|
|
75
|
+
.map((m) => m.costOutput)
|
|
76
|
+
.filter((c) => typeof c === "number" && c > 0)
|
|
77
|
+
.sort((a, b) => a - b);
|
|
78
|
+
const hasPriceSignal = knownCosts.length > 0;
|
|
79
|
+
const min = knownCosts[0];
|
|
80
|
+
const max = knownCosts[knownCosts.length - 1];
|
|
81
|
+
const median = knownCosts[Math.floor(knownCosts.length / 2)];
|
|
82
|
+
// Project every model onto the price axis. Undefined only when there is no
|
|
83
|
+
// price signal anywhere (all models unpriced) — then the sort falls through
|
|
84
|
+
// to the hint comparison below.
|
|
85
|
+
const costKey = (m) => {
|
|
86
|
+
if (typeof m.costOutput === "number" && m.costOutput > 0)
|
|
87
|
+
return m.costOutput;
|
|
88
|
+
if (!hasPriceSignal)
|
|
89
|
+
return undefined;
|
|
90
|
+
const hint = hintScore(m.spec);
|
|
91
|
+
return hint > 0 ? max : hint < 0 ? min : median;
|
|
92
|
+
};
|
|
93
|
+
return models
|
|
94
|
+
.map((m, index) => ({ m, index, cost: costKey(m), hint: hintScore(m.spec), ctx: m.contextWindow ?? 0 }))
|
|
95
|
+
.sort((a, b) => {
|
|
96
|
+
if (a.cost !== undefined && b.cost !== undefined && a.cost !== b.cost)
|
|
97
|
+
return a.cost - b.cost;
|
|
98
|
+
if (a.hint !== b.hint)
|
|
99
|
+
return a.hint - b.hint;
|
|
100
|
+
if (a.ctx !== b.ctx)
|
|
101
|
+
return a.ctx - b.ctx;
|
|
102
|
+
return a.index - b.index;
|
|
103
|
+
})
|
|
104
|
+
.map((entry) => entry.m);
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Defaults
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
/**
|
|
110
|
+
* Build a default tier config. When the available model registry is known,
|
|
111
|
+
* spread it across tiers so small/medium/big routing is meaningful out of the
|
|
112
|
+
* box. When the registry is empty or unavailable, fall back to the current Pi
|
|
113
|
+
* model so fresh installs still get usable tier values.
|
|
114
|
+
*
|
|
115
|
+
* Models are first ranked least → most capable via `rankByCapability` (price
|
|
116
|
+
* first, name-substring hint as fallback). Tiers are then assigned from this
|
|
117
|
+
* single ranked pool with exclusion — each model is used for at most one tier —
|
|
118
|
+
* so distinct tiers never collapse onto the same model and a weaker model can
|
|
119
|
+
* never outrank a stronger one (no inversion):
|
|
120
|
+
*
|
|
121
|
+
* - big = the most capable model (last in the ranking)
|
|
122
|
+
* - small = the least capable model (first in the ranking)
|
|
123
|
+
* - medium = the middle-ranked model
|
|
124
|
+
*
|
|
125
|
+
* When fewer than 3 distinct models are available, this degrades gracefully by
|
|
126
|
+
* reusing the *strongest* available model for the higher tier(s):
|
|
127
|
+
*
|
|
128
|
+
* - 2 models: small = weaker, medium = big = stronger
|
|
129
|
+
* - 1 / 0 models: small = medium = big = that model (or the current model /
|
|
130
|
+
* "" fallback)
|
|
131
|
+
*
|
|
132
|
+
* `availableModels` is injectable for testing and for callers that already
|
|
133
|
+
* fetched the registry. When omitted, this reads from the live registry.
|
|
134
|
+
*/
|
|
135
|
+
export function buildDefaultTierConfig(currentModelSpec, availableModels) {
|
|
136
|
+
const models = availableModels ?? listAvailableModels();
|
|
137
|
+
const ranked = rankByCapability(models).map((m) => m.spec);
|
|
138
|
+
if (ranked.length >= 3) {
|
|
139
|
+
const small = ranked[0];
|
|
140
|
+
const big = ranked[ranked.length - 1];
|
|
141
|
+
const medium = ranked[Math.floor(ranked.length / 2)];
|
|
142
|
+
return { tiers: { small, medium, big } };
|
|
143
|
+
}
|
|
144
|
+
if (ranked.length === 2) {
|
|
145
|
+
const [weaker, stronger] = ranked;
|
|
146
|
+
return { tiers: { small: weaker, medium: stronger, big: stronger } };
|
|
147
|
+
}
|
|
148
|
+
const fallback = ranked[0] ?? currentModelSpec ?? "";
|
|
149
|
+
return {
|
|
150
|
+
tiers: {
|
|
151
|
+
small: fallback,
|
|
152
|
+
medium: fallback,
|
|
153
|
+
big: fallback,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* One-time notice shown when an agent requests `opts.tier` but no
|
|
159
|
+
* model-tiers.json is configured — in that state tiers silently fall back to
|
|
160
|
+
* the session model (see `resolveAgentModelSpec` in agent.ts), which is easy to
|
|
161
|
+
* miss. This surfaces the fallback and the mapping the user *would* get by
|
|
162
|
+
* configuring, using the same `buildDefaultTierConfig` ranking so the hint is
|
|
163
|
+
* actionable. Pure/string-only so the caller owns how it's emitted.
|
|
164
|
+
*/
|
|
165
|
+
export function formatTierFallbackNotice(mainModel, availableModels) {
|
|
166
|
+
const fallback = mainModel ?? "the session default model";
|
|
167
|
+
const suggested = buildDefaultTierConfig(mainModel, availableModels);
|
|
168
|
+
const mapping = sortedTierNames(suggested)
|
|
169
|
+
.map((tier) => `${tier}=${suggested.tiers[tier] || "?"}`)
|
|
170
|
+
.join(" ");
|
|
171
|
+
return (`[workflow] An agent requested opts.tier but no model-tiers.json is configured, so tiers currently ` +
|
|
172
|
+
`fall back to ${fallback}. Run /workflows-models to configure them` +
|
|
173
|
+
(mapping ? `. Suggested mapping from your available models: ${mapping}` : "."));
|
|
174
|
+
}
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Load / Save
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
/**
|
|
179
|
+
* True iff `value` is a usable tiers map: a plain (non-array) object with at
|
|
180
|
+
* least one entry, every key and value a non-empty string. Anything else
|
|
181
|
+
* (an array, `{}`, or a tier mapped to `""`) is treated as absent rather than
|
|
182
|
+
* a truthy-but-broken config — resolveTierModel would silently resolve such
|
|
183
|
+
* entries to `undefined`/`""` while the caller's "no model-tiers.json
|
|
184
|
+
* configured" warning only fires on an exactly-null config.
|
|
185
|
+
*/
|
|
186
|
+
function isValidTiersMap(value) {
|
|
187
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
188
|
+
return false;
|
|
189
|
+
const entries = Object.entries(value);
|
|
190
|
+
if (entries.length === 0)
|
|
191
|
+
return false;
|
|
192
|
+
return entries.every(([key, val]) => key.trim().length > 0 && typeof val === "string" && val.trim().length > 0);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Load the model tier config from disk. Returns null if the file does not
|
|
196
|
+
* exist or is unparseable (callers fall back to a default).
|
|
197
|
+
*/
|
|
198
|
+
export function loadModelTierConfig(configPath) {
|
|
199
|
+
const path = configPath ?? getModelTierConfigPath();
|
|
200
|
+
if (!existsSync(path))
|
|
201
|
+
return null;
|
|
202
|
+
try {
|
|
203
|
+
const raw = readFileSync(path, "utf-8");
|
|
204
|
+
const parsed = JSON.parse(raw);
|
|
205
|
+
if (!parsed || typeof parsed !== "object")
|
|
206
|
+
return null;
|
|
207
|
+
if (!isValidTiersMap(parsed.tiers))
|
|
208
|
+
return null;
|
|
209
|
+
return parsed;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Save a model tier config to disk. Creates parent directories if needed.
|
|
217
|
+
*
|
|
218
|
+
* Refuses a degenerate `tiers` (e.g. all-empty-string, from buildDefaultTierConfig
|
|
219
|
+
* with an empty model registry) using the same isValidTiersMap check the loader
|
|
220
|
+
* uses — otherwise the write side could produce exactly the shape loadModelTierConfig
|
|
221
|
+
* now rejects on the next read, silently discarding the "saved" config.
|
|
222
|
+
*/
|
|
223
|
+
export function saveModelTierConfig(config, configPath) {
|
|
224
|
+
if (!isValidTiersMap(config?.tiers)) {
|
|
225
|
+
throw new Error("Refusing to save a degenerate model tier config: tiers must be a non-empty map of tier name to a non-empty model spec string.");
|
|
226
|
+
}
|
|
227
|
+
const path = configPath ?? getModelTierConfigPath();
|
|
228
|
+
const dir = dirname(path);
|
|
229
|
+
if (!existsSync(dir)) {
|
|
230
|
+
mkdirSync(dir, { recursive: true });
|
|
231
|
+
}
|
|
232
|
+
writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
|
|
233
|
+
}
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Resolve / helpers
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
/**
|
|
238
|
+
* Resolve a tier name to its configured model spec, or undefined if the tier
|
|
239
|
+
* is not configured.
|
|
240
|
+
*/
|
|
241
|
+
export function resolveTierModel(tier, config) {
|
|
242
|
+
return config.tiers[tier];
|
|
243
|
+
}
|
|
244
|
+
/** Return all tier names sorted: small < medium < big, then alphabetically. */
|
|
245
|
+
export function sortedTierNames(config) {
|
|
246
|
+
const names = Object.keys(config.tiers);
|
|
247
|
+
const rank = { small: 0, medium: 1, big: 2 };
|
|
248
|
+
return names.sort((a, b) => (rank[a] ?? 99) - (rank[b] ?? 99) || a.localeCompare(b));
|
|
249
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow run state persistence for pause/resume support.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentUsage } from "./agent.js";
|
|
5
|
+
import type { AgentHistoryEntry } from "./agent-history.js";
|
|
6
|
+
import type { WorkflowErrorCode } from "./errors.js";
|
|
7
|
+
import { type PersistenceFsLayer } from "./fs-persistence.js";
|
|
8
|
+
export type RunStatus = "pending" | "running" | "paused" | "completed" | "failed" | "aborted";
|
|
9
|
+
export interface PersistedAgentState {
|
|
10
|
+
id: number;
|
|
11
|
+
/** Runtime call identity (`${runId}:${callIndex}`), used to rehydrate journaled results. */
|
|
12
|
+
callId?: string;
|
|
13
|
+
label: string;
|
|
14
|
+
phase?: string;
|
|
15
|
+
prompt: string;
|
|
16
|
+
status: "queued" | "running" | "done" | "error" | "skipped";
|
|
17
|
+
result?: unknown;
|
|
18
|
+
/** Compact result written by releases before full agent results were retained. */
|
|
19
|
+
resultPreview?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
errorCode?: WorkflowErrorCode;
|
|
22
|
+
recoverable?: boolean;
|
|
23
|
+
history?: AgentHistoryEntry[];
|
|
24
|
+
startedAt?: string;
|
|
25
|
+
endedAt?: string;
|
|
26
|
+
/** Tokens used by this agent (a scalar estimate when the provider reports no usage). */
|
|
27
|
+
tokens?: number;
|
|
28
|
+
/** Per-agent token usage breakdown, when the provider reported one. */
|
|
29
|
+
tokenUsage?: AgentUsage;
|
|
30
|
+
/** The model this agent ran on (provider/id), when known. */
|
|
31
|
+
model?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PersistedRunState {
|
|
34
|
+
runId: string;
|
|
35
|
+
workflowName: string;
|
|
36
|
+
script: string;
|
|
37
|
+
args?: unknown;
|
|
38
|
+
/** The pi session this run belongs to. Runs persist on disk across sessions but
|
|
39
|
+
* the navigator shows only the current session's runs (undefined = legacy/global). */
|
|
40
|
+
sessionId?: string;
|
|
41
|
+
status: RunStatus;
|
|
42
|
+
/** Why a paused run is paused (e.g. "usage_limit" when a provider quota was hit). */
|
|
43
|
+
pauseReason?: string;
|
|
44
|
+
/** Provider reset hint for a usage-limit pause, e.g. "Resets in ~3h" (verbatim). */
|
|
45
|
+
resetHint?: string;
|
|
46
|
+
phases: string[];
|
|
47
|
+
currentPhase?: string;
|
|
48
|
+
agents: PersistedAgentState[];
|
|
49
|
+
logs: string[];
|
|
50
|
+
result?: unknown;
|
|
51
|
+
startedAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
completedAt?: string;
|
|
54
|
+
durationMs?: number;
|
|
55
|
+
tokenUsage?: {
|
|
56
|
+
input: number;
|
|
57
|
+
output: number;
|
|
58
|
+
total: number;
|
|
59
|
+
cost?: number;
|
|
60
|
+
cacheRead?: number;
|
|
61
|
+
cacheWrite?: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Cached agent/checkpoint results for resume, keyed by deterministic call
|
|
65
|
+
* index. `runId` namespaces `index` (a nested workflow() call restarts its
|
|
66
|
+
* own callSeq at 0) — absent on journals persisted before that namespacing
|
|
67
|
+
* existed; see JournalEntry.runId in workflow.ts for the resume-time
|
|
68
|
+
* legacy-degradation behavior. `storeDelta` is this call's SharedStore
|
|
69
|
+
* write delta, replayed additively on resume.
|
|
70
|
+
*/
|
|
71
|
+
journal?: Array<{
|
|
72
|
+
index: number;
|
|
73
|
+
runId?: string;
|
|
74
|
+
hash: string;
|
|
75
|
+
result: unknown;
|
|
76
|
+
storeDelta?: Record<string, unknown>;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Opt-out of auto-resume for this run (default true, i.e. eligible unless
|
|
80
|
+
* explicitly set to false via ExecOptions.autoResume). Set once at run start
|
|
81
|
+
* and carried through resumes; see UsageLimitScheduler.
|
|
82
|
+
*/
|
|
83
|
+
autoResume?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The run's resolved hard token budget, fixed at start (per-run value, else
|
|
86
|
+
* the manager default at the time). Resume re-applies THIS value — never the
|
|
87
|
+
* current default — so an explicit no-budget (`null`) or custom cap survives
|
|
88
|
+
* a pause/resume cycle. Absent on legacy runs (resumed unbudgeted).
|
|
89
|
+
*/
|
|
90
|
+
tokenBudget?: number | null;
|
|
91
|
+
/**
|
|
92
|
+
* Named toolset tag (WorkflowManagerOptions.toolsets). ToolDefinitions are
|
|
93
|
+
* functions and can't be serialized, so this tag is how a resumed run (e.g.
|
|
94
|
+
* /deep-research with web tools) re-resolves the tool set it started with.
|
|
95
|
+
*/
|
|
96
|
+
toolset?: string;
|
|
97
|
+
/**
|
|
98
|
+
* The run's resolved cap on total agents, fixed at start (per-run value,
|
|
99
|
+
* else undefined so runWorkflow applies its own MAX_AGENTS_PER_RUN default).
|
|
100
|
+
* Resume re-applies THIS value — never the manager's current default — same
|
|
101
|
+
* rationale as tokenBudget. Absent on legacy runs (resumed with no cap
|
|
102
|
+
* carried forward, i.e. runWorkflow's own default applies).
|
|
103
|
+
*/
|
|
104
|
+
maxAgents?: number;
|
|
105
|
+
/**
|
|
106
|
+
* The run's resolved per-agent timeout, fixed at start (per-run value, else
|
|
107
|
+
* the manager default at the time). Absent on legacy runs — unlike
|
|
108
|
+
* tokenBudget, a legacy run's real timeout was never "no timeout" by
|
|
109
|
+
* omission; it was always the manager's default (pre-A1 resume always fell
|
|
110
|
+
* back to it), so resume applies the manager's CURRENT default for such
|
|
111
|
+
* runs rather than null, preserving both the run's original semantics and
|
|
112
|
+
* pre-fix resume behavior.
|
|
113
|
+
*/
|
|
114
|
+
agentTimeoutMs?: number | null;
|
|
115
|
+
/**
|
|
116
|
+
* The run's resolved concurrency, fixed at start (per-run value, else the
|
|
117
|
+
* manager's concurrency at the time). Same rationale as tokenBudget.
|
|
118
|
+
*/
|
|
119
|
+
concurrency?: number;
|
|
120
|
+
/**
|
|
121
|
+
* The run's resolved agent-retry count, fixed at start (per-run value, else
|
|
122
|
+
* the manager default at the time). Same rationale as tokenBudget.
|
|
123
|
+
*/
|
|
124
|
+
agentRetries?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Auto-resume attempt counter for the current usage_limit pause-cycle, owned
|
|
127
|
+
* and persisted by UsageLimitScheduler (best-effort). Absent/0 means no
|
|
128
|
+
* auto-resume attempt has been recorded yet.
|
|
129
|
+
*/
|
|
130
|
+
autoResumeAttempts?: number;
|
|
131
|
+
}
|
|
132
|
+
export interface RunPersistence {
|
|
133
|
+
/** Save current run state. */
|
|
134
|
+
save(state: PersistedRunState): void;
|
|
135
|
+
/** Load a persisted run by ID. */
|
|
136
|
+
load(runId: string): PersistedRunState | null;
|
|
137
|
+
/** List all persisted runs. */
|
|
138
|
+
list(): PersistedRunState[];
|
|
139
|
+
/** Delete a persisted run. */
|
|
140
|
+
delete(runId: string): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Acquire an exclusive cross-process lease for a run. Returns null when another
|
|
143
|
+
* live process owns the run; stale/corrupt lock files are removed and retried.
|
|
144
|
+
*/
|
|
145
|
+
acquireRunLease(runId: string): RunLease | null;
|
|
146
|
+
/** Release a lease previously returned by acquireRunLease(). */
|
|
147
|
+
releaseRunLease(lease: RunLease): void;
|
|
148
|
+
/** Get runs directory path. */
|
|
149
|
+
getRunsDir(): string;
|
|
150
|
+
}
|
|
151
|
+
export interface RunLease {
|
|
152
|
+
runId: string;
|
|
153
|
+
token: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Filesystem operations used by run persistence.
|
|
157
|
+
* Exposed for testing – pass overrides to inject mock implementations.
|
|
158
|
+
* (Alias of the shared PersistenceFsLayer — see fs-persistence.ts.)
|
|
159
|
+
*/
|
|
160
|
+
export type FsLayer = PersistenceFsLayer;
|
|
161
|
+
/**
|
|
162
|
+
* Retention policy for terminal (completed/failed/aborted) runs kept on
|
|
163
|
+
* disk. Bounded so a long-lived project directory can't accumulate an
|
|
164
|
+
* unbounded number of run files (each polled/listed on every list() call).
|
|
165
|
+
* A run in "running" or "paused" status is NEVER counted against this cap
|
|
166
|
+
* or evicted by it — only genuinely finished runs age out, oldest (by
|
|
167
|
+
* updatedAt) first, once the terminal-run count exceeds the cap. 300 is
|
|
168
|
+
* generous enough to cover weeks of typical usage while keeping list()'s
|
|
169
|
+
* per-call directory scan bounded.
|
|
170
|
+
*/
|
|
171
|
+
export declare const DEFAULT_MAX_TERMINAL_RUNS_ON_DISK = 300;
|
|
172
|
+
export interface RunPersistenceOptions {
|
|
173
|
+
/** Override DEFAULT_MAX_TERMINAL_RUNS_ON_DISK (tests; advanced tuning). */
|
|
174
|
+
maxTerminalRunsOnDisk?: number;
|
|
175
|
+
}
|
|
176
|
+
export declare function createRunPersistence(cwd: string, fsOverride?: Partial<FsLayer>, options?: RunPersistenceOptions): RunPersistence;
|
|
177
|
+
/**
|
|
178
|
+
* Generate a unique run ID.
|
|
179
|
+
*/
|
|
180
|
+
export declare function generateRunId(): string;
|