@sangyaahub/pi-marg 1.0.3 → 1.0.4
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/.omp-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +7 -0
- package/MODEL-SETUP.md +9 -0
- package/README.md +20 -6
- package/adapters/omp/extensions/model-router.ts +188 -16
- package/adapters/pi/extensions/model-router.ts +111 -2
- package/adapters/pi/prompts/auto-models.md +3 -3
- package/agents/auto-intake.md +1 -1
- package/commands/auto-models.md +3 -3
- package/core/model-routing.ts +449 -11
- package/package.json +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/validate.sh +44 -1
- package/skills/auto-mode-router/SKILL.md +1 -1
- package/skills/auto-mode-router/references/intake-and-state.md +3 -0
- package/skills/auto-mode-router/references/models-and-tools.md +9 -0
- package/skills/auto-mode-router/references/session-ledger.md +5 -0
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "PiMarg — a governed development path for Pi Agent and Oh My Pi.",
|
|
10
|
-
"version": "1.0.
|
|
10
|
+
"version": "1.0.4"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
14
14
|
"name": "pi-marg",
|
|
15
15
|
"description": "Intent-led development workflow with live A/B/C/D routing, capability detection, verification, and approval gates.",
|
|
16
16
|
"source": "./",
|
|
17
|
-
"version": "1.0.
|
|
17
|
+
"version": "1.0.4",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Sangyaa",
|
|
20
20
|
"email": "contact@sangyaa.co"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.4 — 2026-09-17
|
|
4
|
+
|
|
5
|
+
- Added two runtime-only recovery choices: a strong **high** backup followed by an economical **low** backup.
|
|
6
|
+
- Automatically switches and resumes after OMP or Pi settles a usage-limit/quota failure, with a visible notification and a no-repeat-side-effects continuation instruction.
|
|
7
|
+
- Handles direct runtime-model failures and detected model-execution tool failures (including external Devin and subagents); old v1 session routes migrate without losing their A/B/C/D selections.
|
|
8
|
+
- Deduplicates repeated failure events, ignores transient throttles, advances to low only when the active high model fails, restores Pi tree-navigation state, and warns without stranding the run if ledger persistence fails after activation.
|
|
9
|
+
|
|
3
10
|
## 1.0.3 — 2026-09-14
|
|
4
11
|
|
|
5
12
|
- Made model routing explicitly model-agnostic: live OMP/Pi authenticated catalogs are the only source of truth, with soft preferred-class ranking that never requires a maintainer's subscriptions.
|
package/MODEL-SETUP.md
CHANGED
|
@@ -11,6 +11,9 @@ LIVE AUTHENTICATED CATALOG
|
|
|
11
11
|
A / C B / D
|
|
12
12
|
│ │
|
|
13
13
|
└─ different ─┘ within each pair
|
|
14
|
+
│
|
|
15
|
+
▼ quota failure
|
|
16
|
+
high backup ──► low backup
|
|
14
17
|
```
|
|
15
18
|
|
|
16
19
|
If the live catalog is empty, PiMarg stops and promotes provider/subscription setup instead of guessing models.
|
|
@@ -56,6 +59,8 @@ The OMP adapter reads `ctx.models.list()`, which supplies authenticated and enab
|
|
|
56
59
|
6. Select B and D when implementation is required; recommended execution choices appear first within their lists when recognizable, but every live model remains selectable.
|
|
57
60
|
7. Resolve any A=C or B=D identity conflict before continuing.
|
|
58
61
|
8. Activate the saved selector immediately before its stage.
|
|
62
|
+
9. Choose a runtime-only **high backup** (strongest recovery fit) and a distinct runtime-only **low backup** (economical recovery fit). External-agent entries cannot be backups because PiMarg must be able to activate them in the current session.
|
|
63
|
+
10. Automatic recovery becomes active only after both backups are saved. On a positively identified terminal usage-limit failure, PiMarg preserves the stage, waits for the runtime to settle, switches high then low, notifies the user, and resumes from the last safe point. Duplicate events and temporary throttles do not consume another backup.
|
|
59
64
|
|
|
60
65
|
### Tool form (`auto_model_route`)
|
|
61
66
|
|
|
@@ -68,6 +73,10 @@ auto_model_route action=catalog workType=<n> provider=<provider-name>
|
|
|
68
73
|
|
|
69
74
|
# 3) Persist and activate an exact selector
|
|
70
75
|
auto_model_route action=select stage=<A|B|C|D> target=<provider>/<model-id> workType=<n>
|
|
76
|
+
|
|
77
|
+
# 4) Persist the automatic recovery pair (does not activate either model yet)
|
|
78
|
+
auto_model_route action=select_fallback fallback=high target=<provider>/<model-id>
|
|
79
|
+
auto_model_route action=select_fallback fallback=low target=<provider>/<model-id>
|
|
71
80
|
```
|
|
72
81
|
|
|
73
82
|
Never present a hand-curated subset that omits a logged-in provider. Every authenticated and enabled runtime model remains selectable at every required stage. Soft preferred-class ranking controls order, not visibility.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://github.com/sangyaahub/pi-marg/releases"><img alt="Version 1.0.
|
|
6
|
+
<a href="https://github.com/sangyaahub/pi-marg/releases"><img alt="Version 1.0.4" src="https://img.shields.io/badge/version-1.0.4-0451F7"></a>
|
|
7
7
|
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-C27B1A"></a>
|
|
8
8
|
<a href="https://pi.dev/packages?name=pi-marg"><img alt="Pi package" src="https://img.shields.io/badge/Pi-package-0338A8"></a>
|
|
9
9
|
<a href="https://omp.sh"><img alt="Oh My Pi plugin" src="https://img.shields.io/badge/OMP-plugin-021E5C"></a>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
# PiMarg
|
|
13
13
|
|
|
14
|
-
**Version 1.0.
|
|
14
|
+
**Version 1.0.4 · MIT licensed · Pi Agent + Oh My Pi · by [Sangyaa](https://sangyaa.co)**
|
|
15
15
|
|
|
16
16
|
PiMarg turns an ordinary prompt into a governed development path. It identifies the work boundary, selects one engineering method, discovers the models and tools the active runtime really provides, separates thinking from execution, records evidence, and stops before protected actions.
|
|
17
17
|
|
|
@@ -55,14 +55,14 @@ The same model-selection and approval logic lives in `core/`. Runtime-specific c
|
|
|
55
55
|
|---|---|---|
|
|
56
56
|
| Ordinary prompts enter the workflow | Pi bootstrap hook | OMP global rule |
|
|
57
57
|
| Shared skills and decision flow | Yes | Yes |
|
|
58
|
-
| Dynamic A/B/C/D model routing | Pi model registry | OMP model registry |
|
|
58
|
+
| Dynamic A/B/C/D model routing + high/low failover | Pi model registry | OMP model registry |
|
|
59
59
|
| Approval guard | Pi event adapter | OMP event adapter |
|
|
60
60
|
| Slash workflow entry points | Pi prompt templates | OMP commands |
|
|
61
61
|
| CodeGraph | CLI/integration when detected | MCP integration when configured |
|
|
62
62
|
| Advisor, checkpoint, security scan, Hindsight | Extension/fallback when detected | Native capability when enabled |
|
|
63
63
|
| Parallel agents | Only when a compatible task extension exists | Native `task`/workflow routes when available |
|
|
64
64
|
|
|
65
|
-
Tested locally against Pi Agent `0.85.1` and OMP `18.1
|
|
65
|
+
Tested locally against Pi Agent `0.85.1` and OMP `18.2.1`. Optional features are always capability-detected, so their absence produces a recorded fallback rather than a false success claim.
|
|
66
66
|
|
|
67
67
|
## Try the demo
|
|
68
68
|
|
|
@@ -141,11 +141,25 @@ pi-marg "add CSV export to this repository"
|
|
|
141
141
|
/auto add CSV export to this repository
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
The native picker asks for work boundary, work type, workflow skill,
|
|
144
|
+
The native picker asks for work boundary, work type, workflow skill, every model required by that work type, then two distinct runtime backups: **high** first and **low** second. Automatic recovery stays disabled until both are saved. It shows only models reported as authenticated and enabled by OMP, with recommended fits first.
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
stage model hits quota
|
|
148
|
+
│
|
|
149
|
+
▼
|
|
150
|
+
OMP/Pi native retry
|
|
151
|
+
│ still failed
|
|
152
|
+
▼
|
|
153
|
+
high backup ──fails──► low backup
|
|
154
|
+
│ │
|
|
155
|
+
└──── notify + resume┘
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
PiMarg switches only after a positively identified terminal usage-limit/quota failure (or a settled model-execution tool failure such as external Devin or a subagent); a temporary 429/rate-limit response does not consume a backup. It records and deduplicates each failure, advances from high to low only when the active high model itself fails, notifies the user, and resumes with an instruction to inspect current state before repeating any side effect. If both backups are exhausted, it stops with an actionable request to run the model chooser again.
|
|
145
159
|
|
|
146
160
|
Useful entry points:
|
|
147
161
|
|
|
148
|
-
- `/auto-models choose` — refresh A/B/C/D choices.
|
|
162
|
+
- `/auto-models choose` — refresh A/B/C/D choices and the high/low recovery pair.
|
|
149
163
|
- `/auto-runtime status` — show detected capabilities and fallbacks.
|
|
150
164
|
- `/auto-review plan|code` — run the independent review contract.
|
|
151
165
|
- `/auto-status` — show the current ledger and evidence.
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
|
+
FALLBACK_LEVELS,
|
|
2
3
|
MODEL_STATE_TYPE,
|
|
3
4
|
STAGES,
|
|
5
|
+
buildFallbackCatalog,
|
|
4
6
|
buildModelCatalog,
|
|
5
7
|
candidatesForProvider,
|
|
6
8
|
emptyCatalogSetupMessage,
|
|
7
9
|
emptyModelRouteState,
|
|
10
|
+
executeAutomaticFailover,
|
|
8
11
|
executeModelRoute,
|
|
12
|
+
failedModelSelector,
|
|
13
|
+
findTerminalUsageLimitFailure,
|
|
14
|
+
formatFailoverRecoveryMessage,
|
|
15
|
+
hasConfiguredFallback,
|
|
9
16
|
matchNumberedOption,
|
|
17
|
+
modelFailureKey,
|
|
18
|
+
modelToolUsageLimitFailure,
|
|
10
19
|
numberedOptionLabel,
|
|
11
20
|
requiredStagesForWorkType,
|
|
12
21
|
restoreModelRouteState,
|
|
13
22
|
uniqueProviders,
|
|
14
23
|
type AutoStage,
|
|
24
|
+
type FallbackLevel,
|
|
15
25
|
type ModelCandidate,
|
|
16
26
|
type ModelLike,
|
|
17
27
|
type ModelRouteParams,
|
|
@@ -45,6 +55,11 @@ const STAGE_PURPOSE: Record<AutoStage, string> = {
|
|
|
45
55
|
D: "implementation second-eye review",
|
|
46
56
|
};
|
|
47
57
|
|
|
58
|
+
const FALLBACK_PURPOSE: Record<FallbackLevel, string> = {
|
|
59
|
+
high: "strong recovery model used first",
|
|
60
|
+
low: "economical recovery model used if high is unavailable or exhausted",
|
|
61
|
+
};
|
|
62
|
+
|
|
48
63
|
function hasExternalDevin(pi: any): boolean {
|
|
49
64
|
return normalizeCapabilityNames(pi.getAllTools?.() ?? [])
|
|
50
65
|
.some((name) => /(?:^|[_-])devin(?:[_-]|$)/i.test(name));
|
|
@@ -83,30 +98,79 @@ async function chooseModel(
|
|
|
83
98
|
return undefined;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
|
|
101
|
+
return chooseCandidateByProvider(ctx, eligible, {
|
|
102
|
+
providerTitle: `Stage ${stage} provider`,
|
|
103
|
+
modelTitle: `Stage ${stage} model`,
|
|
104
|
+
purpose: STAGE_PURPOSE[stage],
|
|
105
|
+
providerDescription: (name) => `All authenticated ${name} models for ${STAGE_PURPOSE[stage]}`,
|
|
106
|
+
modelDescription: (candidate) => optionDescription(stage, candidate),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function chooseFallbackModel(
|
|
111
|
+
ctx: any,
|
|
112
|
+
level: FallbackLevel,
|
|
113
|
+
candidates: ModelCandidate[],
|
|
114
|
+
other?: ModelCandidate,
|
|
115
|
+
): Promise<ModelCandidate | undefined> {
|
|
116
|
+
const eligible = candidates.filter((candidate) => (
|
|
117
|
+
candidate.access === "runtime-model" && (!other || candidate.identity !== other.identity)
|
|
118
|
+
));
|
|
119
|
+
const title = level === "high" ? "High" : "Low";
|
|
120
|
+
if (eligible.length === 0) {
|
|
121
|
+
ctx.ui.notify(
|
|
122
|
+
"Automatic recovery requires two distinct authenticated runtime models. Configure another model, then run /pi-marg again.",
|
|
123
|
+
"error",
|
|
124
|
+
);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return chooseCandidateByProvider(ctx, eligible, {
|
|
129
|
+
providerTitle: `${title} backup provider`,
|
|
130
|
+
modelTitle: `${title} backup model`,
|
|
131
|
+
purpose: FALLBACK_PURPOSE[level],
|
|
132
|
+
providerDescription: (name) => `Authenticated ${name} models · ${FALLBACK_PURPOSE[level]}`,
|
|
133
|
+
modelDescription: (candidate) => (
|
|
134
|
+
`${FALLBACK_PURPOSE[level]}${candidate.billingNote ? ` · ${candidate.billingNote}` : ""}`
|
|
135
|
+
),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function chooseCandidateByProvider(
|
|
140
|
+
ctx: any,
|
|
141
|
+
candidates: ModelCandidate[],
|
|
142
|
+
copy: {
|
|
143
|
+
providerTitle: string;
|
|
144
|
+
modelTitle: string;
|
|
145
|
+
purpose: string;
|
|
146
|
+
providerDescription(provider: string): string;
|
|
147
|
+
modelDescription(candidate: ModelCandidate): string;
|
|
148
|
+
},
|
|
149
|
+
): Promise<ModelCandidate | undefined> {
|
|
150
|
+
const providers = uniqueProviders(candidates);
|
|
87
151
|
const providerOptions = providers.map((name, index) => ({
|
|
88
|
-
label: numberedOptionLabel(index, `${name} (${candidatesForProvider(
|
|
89
|
-
description:
|
|
152
|
+
label: numberedOptionLabel(index, `${name} (${candidatesForProvider(candidates, name).length})`),
|
|
153
|
+
description: copy.providerDescription(name),
|
|
90
154
|
}));
|
|
91
|
-
const providerAnswer = await ctx.ui.select(
|
|
155
|
+
const providerAnswer = await ctx.ui.select(copy.providerTitle, providerOptions, {
|
|
92
156
|
selectionMarker: "radio",
|
|
93
|
-
helpText: `${providers.length} providers · ${
|
|
157
|
+
helpText: `${providers.length} providers · ${candidates.length} models · ${copy.purpose}`,
|
|
94
158
|
});
|
|
95
159
|
const provider = matchNumberedOption(
|
|
96
160
|
providerAnswer,
|
|
97
161
|
providers,
|
|
98
|
-
(name) => `${name} (${candidatesForProvider(
|
|
162
|
+
(name) => `${name} (${candidatesForProvider(candidates, name).length})`,
|
|
99
163
|
);
|
|
100
164
|
if (!provider) return undefined;
|
|
101
165
|
|
|
102
|
-
const providerModels = candidatesForProvider(
|
|
166
|
+
const providerModels = candidatesForProvider(candidates, provider);
|
|
103
167
|
const modelOptions = providerModels.map((candidate, index) => ({
|
|
104
168
|
label: numberedOptionLabel(index, optionLabel(candidate)),
|
|
105
|
-
description:
|
|
169
|
+
description: copy.modelDescription(candidate),
|
|
106
170
|
}));
|
|
107
|
-
const answer = await ctx.ui.select(
|
|
171
|
+
const answer = await ctx.ui.select(copy.modelTitle, modelOptions, {
|
|
108
172
|
selectionMarker: "radio",
|
|
109
|
-
helpText: `${providerModels.length} ${provider} choices · ${
|
|
173
|
+
helpText: `${providerModels.length} ${provider} choices · ${copy.purpose}`,
|
|
110
174
|
});
|
|
111
175
|
return matchNumberedOption(answer, providerModels, optionLabel);
|
|
112
176
|
}
|
|
@@ -114,14 +178,18 @@ async function chooseModel(
|
|
|
114
178
|
function selectedState(
|
|
115
179
|
workType: number,
|
|
116
180
|
selected: Partial<Record<AutoStage, ModelCandidate>>,
|
|
181
|
+
fallbacks: Record<FallbackLevel, ModelCandidate>,
|
|
117
182
|
): ModelRouteState {
|
|
118
183
|
const selectedAt = new Date().toISOString();
|
|
119
184
|
return {
|
|
120
|
-
version:
|
|
185
|
+
version: 2,
|
|
121
186
|
workType,
|
|
122
187
|
selections: Object.fromEntries(
|
|
123
188
|
Object.entries(selected).map(([stage, candidate]) => [stage, { ...candidate, selectedAt }]),
|
|
124
189
|
) as ModelRouteState["selections"],
|
|
190
|
+
fallbacks: Object.fromEntries(
|
|
191
|
+
Object.entries(fallbacks).map(([level, candidate]) => [level, { ...candidate, selectedAt }]),
|
|
192
|
+
) as ModelRouteState["fallbacks"],
|
|
125
193
|
};
|
|
126
194
|
}
|
|
127
195
|
|
|
@@ -131,6 +199,7 @@ function intakeMessage(
|
|
|
131
199
|
workType: string,
|
|
132
200
|
skillMode: string,
|
|
133
201
|
selected: Partial<Record<AutoStage, ModelCandidate>>,
|
|
202
|
+
fallbacks: Record<FallbackLevel, ModelCandidate>,
|
|
134
203
|
): string {
|
|
135
204
|
const models = STAGES
|
|
136
205
|
.filter((stage) => selected[stage])
|
|
@@ -145,15 +214,23 @@ function intakeMessage(
|
|
|
145
214
|
`- Work type: ${workType}`,
|
|
146
215
|
`- Workflow skill: ${skillMode}`,
|
|
147
216
|
models,
|
|
217
|
+
`- High backup: ${fallbacks.high.selector}`,
|
|
218
|
+
`- Low backup: ${fallbacks.low.selector}`,
|
|
148
219
|
"",
|
|
149
|
-
"Load auto-mode-router, record these choices in the session ledger, activate each saved model before its stage, and continue the workflow. Ask interactively only for information that is still missing.",
|
|
220
|
+
"Load auto-mode-router, record these choices in the session ledger, activate each saved model before its stage, and continue the workflow. If a terminal usage-limit failure occurs, PiMarg will switch high then low and resume from the last safe point. Ask interactively only for information that is still missing.",
|
|
150
221
|
].join("\n");
|
|
151
222
|
}
|
|
152
223
|
|
|
153
224
|
export default function modelRouter(pi: any) {
|
|
154
225
|
const z = pi.zod;
|
|
155
226
|
let state = emptyModelRouteState();
|
|
227
|
+
let recoveryToken: symbol | undefined;
|
|
228
|
+
let sessionGeneration = 0;
|
|
229
|
+
let pendingToolFailure: { selector: string; key: string } | undefined;
|
|
156
230
|
const restore = (_event: unknown, ctx: any) => {
|
|
231
|
+
sessionGeneration += 1;
|
|
232
|
+
recoveryToken = undefined;
|
|
233
|
+
pendingToolFailure = undefined;
|
|
157
234
|
state = restoreModelRouteState(ctx.sessionManager.getBranch());
|
|
158
235
|
};
|
|
159
236
|
|
|
@@ -162,6 +239,93 @@ export default function modelRouter(pi: any) {
|
|
|
162
239
|
pi.on("session_branch", restore);
|
|
163
240
|
pi.on("session_tree", restore);
|
|
164
241
|
|
|
242
|
+
const recoverFromUsageLimit = async (failure: { selector: string; key: string }, ctx: any) => {
|
|
243
|
+
if (recoveryToken) return;
|
|
244
|
+
if (!hasConfiguredFallback(state)) return;
|
|
245
|
+
const generation = sessionGeneration;
|
|
246
|
+
const token = Symbol("pi-marg-recovery");
|
|
247
|
+
recoveryToken = token;
|
|
248
|
+
try {
|
|
249
|
+
const execution = await executeAutomaticFailover(state, failure.selector, {
|
|
250
|
+
runtimeName: "OMP",
|
|
251
|
+
models: ctx.models.list() as ModelLike[],
|
|
252
|
+
hasExternalDevin: hasExternalDevin(pi),
|
|
253
|
+
activate: (model) => pi.setModel(model),
|
|
254
|
+
persist: (nextState) => {
|
|
255
|
+
if (sessionGeneration !== generation) throw new Error("session changed during automatic recovery");
|
|
256
|
+
pi.appendEntry(MODEL_STATE_TYPE, nextState);
|
|
257
|
+
},
|
|
258
|
+
}, failure.key);
|
|
259
|
+
if (sessionGeneration !== generation) {
|
|
260
|
+
ctx.ui?.notify?.(
|
|
261
|
+
"PiMarg stopped an in-flight recovery because the session changed. Verify the active model before continuing.",
|
|
262
|
+
"warning",
|
|
263
|
+
);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (execution.ignoredDuplicate) return;
|
|
267
|
+
state = execution.state;
|
|
268
|
+
if (!execution.switched) {
|
|
269
|
+
if (!execution.stateChanged) return;
|
|
270
|
+
ctx.ui?.notify?.(
|
|
271
|
+
"PiMarg could not resume automatically because no configured backup model remains. Run /auto-models choose after restoring provider access.",
|
|
272
|
+
"error",
|
|
273
|
+
);
|
|
274
|
+
if (execution.persistenceError) {
|
|
275
|
+
ctx.ui?.notify?.(
|
|
276
|
+
`PiMarg updated recovery state only in memory because the session ledger could not be saved: ${execution.persistenceError}`,
|
|
277
|
+
"error",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const title = execution.switched.level === "high" ? "High" : "Low";
|
|
284
|
+
ctx.ui?.notify?.(
|
|
285
|
+
execution.reusedCurrent
|
|
286
|
+
? `PiMarg kept the ${title} backup ${execution.switched.selection.selector} active after ${failure.selector} reached its limit and is resuming safely.`
|
|
287
|
+
: `PiMarg switched ${failure.selector} to the ${title} backup ${execution.switched.selection.selector} and is resuming automatically.`,
|
|
288
|
+
"warning",
|
|
289
|
+
);
|
|
290
|
+
if (execution.persistenceError) {
|
|
291
|
+
ctx.ui?.notify?.(
|
|
292
|
+
`The model switched, but PiMarg could not save the recovery state to the session ledger: ${execution.persistenceError}`,
|
|
293
|
+
"error",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
pi.sendUserMessage(
|
|
297
|
+
formatFailoverRecoveryMessage(
|
|
298
|
+
state,
|
|
299
|
+
failure.selector,
|
|
300
|
+
execution.switched.level,
|
|
301
|
+
execution.switched.selection.selector,
|
|
302
|
+
execution.reusedCurrent,
|
|
303
|
+
),
|
|
304
|
+
{ deliverAs: "aside" },
|
|
305
|
+
);
|
|
306
|
+
} finally {
|
|
307
|
+
if (recoveryToken === token) recoveryToken = undefined;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
pi.on("agent_end", async (event: any, ctx: any) => {
|
|
312
|
+
if (event?.willContinue || event?.isTerminal === false) return;
|
|
313
|
+
const failed = findTerminalUsageLimitFailure(event?.messages ?? []);
|
|
314
|
+
const failure = failed
|
|
315
|
+
? {
|
|
316
|
+
selector: failedModelSelector(failed, ctx.models?.current?.() ?? ctx.model),
|
|
317
|
+
key: modelFailureKey(failed),
|
|
318
|
+
}
|
|
319
|
+
: pendingToolFailure;
|
|
320
|
+
pendingToolFailure = undefined;
|
|
321
|
+
if (!failure) return;
|
|
322
|
+
await recoverFromUsageLimit(failure, ctx);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
pi.on("tool_execution_end", (event: any) => {
|
|
326
|
+
pendingToolFailure = modelToolUsageLimitFailure(event) ?? pendingToolFailure;
|
|
327
|
+
});
|
|
328
|
+
|
|
165
329
|
pi.registerCommand("pi-marg", {
|
|
166
330
|
description: "Start PiMarg with interactive intake, or pass a work prompt.",
|
|
167
331
|
async handler(args: string, ctx: any) {
|
|
@@ -203,6 +367,7 @@ export default function modelRouter(pi: any) {
|
|
|
203
367
|
return;
|
|
204
368
|
}
|
|
205
369
|
const catalog = buildModelCatalog(liveModels, hasExternalDevin(pi));
|
|
370
|
+
const fallbackCatalog = buildFallbackCatalog(catalog);
|
|
206
371
|
const selected: Partial<Record<AutoStage, ModelCandidate>> = {};
|
|
207
372
|
for (const stage of requiredStagesForWorkType(workType)) {
|
|
208
373
|
const choice = await chooseModel(ctx, stage, catalog[stage], selected);
|
|
@@ -210,20 +375,27 @@ export default function modelRouter(pi: any) {
|
|
|
210
375
|
selected[stage] = choice;
|
|
211
376
|
}
|
|
212
377
|
|
|
213
|
-
const
|
|
378
|
+
const high = await chooseFallbackModel(ctx, "high", fallbackCatalog.high);
|
|
379
|
+
if (!high) return;
|
|
380
|
+
const low = await chooseFallbackModel(ctx, "low", fallbackCatalog.low, high);
|
|
381
|
+
if (!low) return;
|
|
382
|
+
const fallbacks = { high, low };
|
|
383
|
+
|
|
384
|
+
const nextState = selectedState(workType, selected, fallbacks);
|
|
214
385
|
state = nextState;
|
|
215
386
|
pi.appendEntry(MODEL_STATE_TYPE, nextState);
|
|
216
|
-
pi.sendUserMessage(intakeMessage(request, boundary, workTypeLabel!, skillMode, selected));
|
|
387
|
+
pi.sendUserMessage(intakeMessage(request, boundary, workTypeLabel!, skillMode, selected, fallbacks));
|
|
217
388
|
},
|
|
218
389
|
});
|
|
219
390
|
|
|
220
391
|
pi.registerTool({
|
|
221
392
|
name: "auto_model_route",
|
|
222
393
|
label: "PiMarg Model Router",
|
|
223
|
-
description: "Discover, select, and activate live A/B/C/D
|
|
394
|
+
description: "Discover, select, and activate live A/B/C/D models plus distinct high/low automatic usage-limit backups.",
|
|
224
395
|
parameters: z.object({
|
|
225
|
-
action: z.enum(["catalog", "select", "activate", "status"]),
|
|
396
|
+
action: z.enum(["catalog", "select", "select_fallback", "activate", "status"]),
|
|
226
397
|
stage: z.enum(STAGES).optional(),
|
|
398
|
+
fallback: z.enum(FALLBACK_LEVELS).optional(),
|
|
227
399
|
target: z.string().optional(),
|
|
228
400
|
workType: z.number().optional(),
|
|
229
401
|
provider: z.string().optional(),
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
FALLBACK_LEVELS,
|
|
4
5
|
MODEL_STATE_TYPE,
|
|
5
6
|
emptyModelRouteState,
|
|
7
|
+
executeAutomaticFailover,
|
|
6
8
|
executeModelRoute,
|
|
9
|
+
failedModelSelector,
|
|
10
|
+
findTerminalUsageLimitFailure,
|
|
11
|
+
formatFailoverRecoveryMessage,
|
|
12
|
+
hasConfiguredFallback,
|
|
13
|
+
modelFailureKey,
|
|
14
|
+
modelToolUsageLimitFailure,
|
|
7
15
|
restoreModelRouteState,
|
|
8
16
|
type ModelLike,
|
|
9
17
|
type ModelRouteParams,
|
|
@@ -26,22 +34,123 @@ function availableModels(ctx: any): ModelLike[] {
|
|
|
26
34
|
|
|
27
35
|
export default function modelRouter(pi: any) {
|
|
28
36
|
let state = emptyModelRouteState();
|
|
29
|
-
|
|
37
|
+
let recoveryToken: symbol | undefined;
|
|
38
|
+
let sessionGeneration = 0;
|
|
39
|
+
let pendingModelFailure: { selector: string; key: string } | undefined;
|
|
40
|
+
let pendingToolFailure: { selector: string; key: string } | undefined;
|
|
41
|
+
const restore = (_event: unknown, ctx: any) => {
|
|
42
|
+
sessionGeneration += 1;
|
|
43
|
+
recoveryToken = undefined;
|
|
44
|
+
pendingModelFailure = undefined;
|
|
45
|
+
pendingToolFailure = undefined;
|
|
30
46
|
state = restoreModelRouteState(ctx.sessionManager.getBranch());
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
pi.on("session_start", restore);
|
|
50
|
+
pi.on("session_tree", restore);
|
|
51
|
+
|
|
52
|
+
const recoverFromUsageLimit = async (failure: { selector: string; key: string }, ctx: any) => {
|
|
53
|
+
if (recoveryToken) return;
|
|
54
|
+
if (!hasConfiguredFallback(state)) return;
|
|
55
|
+
const generation = sessionGeneration;
|
|
56
|
+
const token = Symbol("pi-marg-recovery");
|
|
57
|
+
recoveryToken = token;
|
|
58
|
+
try {
|
|
59
|
+
const execution = await executeAutomaticFailover(state, failure.selector, {
|
|
60
|
+
runtimeName: "Pi",
|
|
61
|
+
models: availableModels(ctx),
|
|
62
|
+
hasExternalDevin: hasExternalDevin(pi),
|
|
63
|
+
activate: (model) => pi.setModel(model),
|
|
64
|
+
persist: (nextState) => {
|
|
65
|
+
if (sessionGeneration !== generation) throw new Error("session changed during automatic recovery");
|
|
66
|
+
pi.appendEntry(MODEL_STATE_TYPE, nextState);
|
|
67
|
+
},
|
|
68
|
+
}, failure.key);
|
|
69
|
+
if (sessionGeneration !== generation) {
|
|
70
|
+
ctx.ui?.notify?.(
|
|
71
|
+
"PiMarg stopped an in-flight recovery because the session changed. Verify the active model before continuing.",
|
|
72
|
+
"warning",
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (execution.ignoredDuplicate) return;
|
|
77
|
+
state = execution.state;
|
|
78
|
+
if (!execution.switched) {
|
|
79
|
+
if (!execution.stateChanged) return;
|
|
80
|
+
ctx.ui?.notify?.(
|
|
81
|
+
"PiMarg could not resume automatically because no configured backup model remains. Re-run the model chooser after restoring provider access.",
|
|
82
|
+
"error",
|
|
83
|
+
);
|
|
84
|
+
if (execution.persistenceError) {
|
|
85
|
+
ctx.ui?.notify?.(
|
|
86
|
+
`PiMarg updated recovery state only in memory because the session ledger could not be saved: ${execution.persistenceError}`,
|
|
87
|
+
"error",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const title = execution.switched.level === "high" ? "High" : "Low";
|
|
94
|
+
ctx.ui?.notify?.(
|
|
95
|
+
execution.reusedCurrent
|
|
96
|
+
? `PiMarg kept the ${title} backup ${execution.switched.selection.selector} active after ${failure.selector} reached its limit and is resuming safely.`
|
|
97
|
+
: `PiMarg switched ${failure.selector} to the ${title} backup ${execution.switched.selection.selector} and is resuming automatically.`,
|
|
98
|
+
"warning",
|
|
99
|
+
);
|
|
100
|
+
if (execution.persistenceError) {
|
|
101
|
+
ctx.ui?.notify?.(
|
|
102
|
+
`The model switched, but PiMarg could not save the recovery state to the session ledger: ${execution.persistenceError}`,
|
|
103
|
+
"error",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
pi.sendUserMessage(
|
|
107
|
+
formatFailoverRecoveryMessage(
|
|
108
|
+
state,
|
|
109
|
+
failure.selector,
|
|
110
|
+
execution.switched.level,
|
|
111
|
+
execution.switched.selection.selector,
|
|
112
|
+
execution.reusedCurrent,
|
|
113
|
+
),
|
|
114
|
+
{ deliverAs: "followUp" },
|
|
115
|
+
);
|
|
116
|
+
} finally {
|
|
117
|
+
if (recoveryToken === token) recoveryToken = undefined;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
pi.on("agent_end", (event: any, ctx: any) => {
|
|
122
|
+
const failed = findTerminalUsageLimitFailure(event?.messages ?? []);
|
|
123
|
+
pendingModelFailure = failed
|
|
124
|
+
? { selector: failedModelSelector(failed, ctx.model), key: modelFailureKey(failed) }
|
|
125
|
+
: undefined;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
pi.on("tool_execution_end", (event: any) => {
|
|
129
|
+
pendingToolFailure = modelToolUsageLimitFailure(event) ?? pendingToolFailure;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
pi.on("agent_settled", async (_event: unknown, ctx: any) => {
|
|
133
|
+
const failure = pendingModelFailure ?? pendingToolFailure;
|
|
134
|
+
pendingModelFailure = undefined;
|
|
135
|
+
pendingToolFailure = undefined;
|
|
136
|
+
if (!failure) return;
|
|
137
|
+
await recoverFromUsageLimit(failure, ctx);
|
|
31
138
|
});
|
|
32
139
|
|
|
33
140
|
pi.registerTool({
|
|
34
141
|
name: "auto_model_route",
|
|
35
142
|
label: "PiMarg Model Router",
|
|
36
|
-
description: "Discover, select, and activate live A/B/C/D
|
|
143
|
+
description: "Discover, select, and activate live A/B/C/D models plus distinct high/low automatic usage-limit backups.",
|
|
37
144
|
parameters: Type.Object({
|
|
38
145
|
action: Type.Union([
|
|
39
146
|
Type.Literal("catalog"),
|
|
40
147
|
Type.Literal("select"),
|
|
148
|
+
Type.Literal("select_fallback"),
|
|
41
149
|
Type.Literal("activate"),
|
|
42
150
|
Type.Literal("status"),
|
|
43
151
|
]),
|
|
44
152
|
stage: Type.Optional(Type.Union([Type.Literal("A"), Type.Literal("B"), Type.Literal("C"), Type.Literal("D")])),
|
|
153
|
+
fallback: Type.Optional(Type.Union(FALLBACK_LEVELS.map((level) => Type.Literal(level)))),
|
|
45
154
|
target: Type.Optional(Type.String()),
|
|
46
155
|
workType: Type.Optional(Type.Number()),
|
|
47
156
|
provider: Type.Optional(Type.String()),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: List or change
|
|
2
|
+
description: List or change A/B/C/D models and the high/low automatic recovery pair from Pi's live authenticated catalog.
|
|
3
3
|
argument-hint: [status | choose]
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Load `auto-mode-router` and read its model-routing reference.
|
|
7
7
|
|
|
8
|
-
- With no argument or `status`, call `auto_model_route` with `action: status`, then show the saved A/B/C/D selectors and
|
|
9
|
-
- With `choose`, call `auto_model_route` with `action: catalog` and the active work type. Present
|
|
8
|
+
- With no argument or `status`, call `auto_model_route` with `action: status`, then show the saved A/B/C/D selectors, separation checks, high/low backups, active fallback, and last failover.
|
|
9
|
+
- With `choose`, call `auto_model_route` with `action: catalog` and the active work type. Present required stages and exact available selectors. Save each stage with `action: select`, then save two distinct runtime models with `action: select_fallback`, first `fallback: high`, then `fallback: low`.
|
|
10
10
|
- Do not type or persist a model ID that the live catalog did not return.
|
|
11
11
|
- Treat any model returned by Pi's registry as a normal in-session route. Treat Devin or another external coding-agent session as a capability-gated external route; creating one requires `post-web` approval.
|
|
12
12
|
- C/D are the authoritative independent review selectors. Any detected advisor or watcher is supplemental and must not weaken the A≠C or B≠D checks.
|
package/agents/auto-intake.md
CHANGED
|
@@ -7,4 +7,4 @@ blocking: true
|
|
|
7
7
|
autoloadSkills: [auto-mode-router]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
Complete only the intake and availability checks from `auto-mode-router`. Ask in the required order: work context, work type, skill mode, live A/B/C/D selections, then add-ons. Call `auto_runtime_status` before recommending runtime capabilities. Return the proposed `[AUTO-MODE]` state. Do not plan, implement, or change project files.
|
|
10
|
+
Complete only the intake and availability checks from `auto-mode-router`. Ask in the required order: work context, work type, skill mode, live A/B/C/D selections, high and low runtime backups, then add-ons. Call `auto_runtime_status` before recommending runtime capabilities. Return the proposed `[AUTO-MODE]` state. Do not plan, implement, or change project files.
|
package/commands/auto-models.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: List or change
|
|
2
|
+
description: List or change A/B/C/D models and the high/low automatic recovery pair from OMP's live authenticated catalog.
|
|
3
3
|
argument-hint: [status | choose]
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Load `auto-mode-router` and read its model-routing reference.
|
|
7
7
|
|
|
8
|
-
- With no argument or `status`, call `auto_model_route` with `action: status`, then show the saved A/B/C/D selectors and
|
|
9
|
-
- With `choose`, call `auto_model_route` with `action: catalog` and the active work type. Present
|
|
8
|
+
- With no argument or `status`, call `auto_model_route` with `action: status`, then show the saved A/B/C/D selectors, separation checks, high/low backups, active fallback, and last failover.
|
|
9
|
+
- With `choose`, call `auto_model_route` with `action: catalog` and the active work type. Present required stages and exact available selectors. Save each stage with `action: select`, then save two distinct runtime models with `action: select_fallback`, first `fallback: high`, then `fallback: low`.
|
|
10
10
|
- Do not type or persist a model ID that the live catalog did not return.
|
|
11
11
|
- Distinguish native `devin/swe-*` OMP models from the optional external `devin/mcp-session` route. Only creating the external session requires `post-web` approval.
|
|
12
12
|
- C/D are the authoritative independent review selectors. A configured OMP advisor is supplemental and must not weaken the A≠C or B≠D checks.
|