@getpipher/armory-fleet 0.14.0 → 0.15.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/README.md +29 -8
- package/package.json +1 -1
- package/src/index.ts +27 -1
- package/src/panel/fleet-panel.ts +10 -18
- package/src/tiers/builtin.ts +14 -5
- package/src/tiers/resolve.ts +14 -0
package/README.md
CHANGED
|
@@ -123,22 +123,22 @@ export const meta = {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
phase('Plan')
|
|
126
|
-
const plan = await agent('Plan this feature: ' + args.task, { tier: '
|
|
126
|
+
const plan = await agent('Plan this feature: ' + args.task, { tier: 'economy' })
|
|
127
127
|
|
|
128
128
|
phase('Implement')
|
|
129
|
-
const impl = await agent(`Implement the plan:\n${plan}`, { tier: '
|
|
129
|
+
const impl = await agent(`Implement the plan:\n${plan}`, { tier: 'standard' })
|
|
130
130
|
|
|
131
131
|
phase('Review')
|
|
132
132
|
const angles = ['security', 'performance', 'correctness']
|
|
133
133
|
const reviews = await parallel(
|
|
134
|
-
angles.map((a) => () => agent(`Review the implementation for ${a} issues.`, { tier: '
|
|
134
|
+
angles.map((a) => () => agent(`Review the implementation for ${a} issues.`, { tier: 'economy' })),
|
|
135
135
|
)
|
|
136
136
|
|
|
137
137
|
// gate: revise the synthesis until it passes a validator
|
|
138
138
|
const synthesis = await gate(
|
|
139
139
|
async (_feedback, n) => n === 0
|
|
140
|
-
? agent(`Synthesize ${reviews.length} reviews.`, { tier: '
|
|
141
|
-
: agent('Revise synthesis per feedback.', { tier: '
|
|
140
|
+
? agent(`Synthesize ${reviews.length} reviews.`, { tier: 'economy' })
|
|
141
|
+
: agent('Revise synthesis per feedback.', { tier: 'economy' }),
|
|
142
142
|
(v) => typeof v === 'string' && v.length > 200 ? { ok: true } : { ok: false, feedback: 'more detail' },
|
|
143
143
|
{ attempts: 3 },
|
|
144
144
|
)
|
|
@@ -236,9 +236,21 @@ Composite helpers (`src/workflows/helpers/`) — usable from any workflow:
|
|
|
236
236
|
|
|
237
237
|
| Tier | Models | Cost cap | Context floor |
|
|
238
238
|
|---|---|---|---|
|
|
239
|
-
| `economy` | `
|
|
240
|
-
| `standard` | `
|
|
241
|
-
| `frontier` | `
|
|
239
|
+
| `economy` | `inherit` | — | — |
|
|
240
|
+
| `standard` | `inherit` | — | — |
|
|
241
|
+
| `frontier` | `inherit` | — | 200k ctx |
|
|
242
|
+
|
|
243
|
+
The shipped defaults use the **`inherit` sentinel** — each tier resolves to your **active session model**, so tier routing works on any provider out of the box. To route across models, override a tier by name with a concrete `provider/id` chain in `~/.pi/agent/fleet/tiers.json` (global) or `<project>/.pi/fleet/tiers.json` (project):
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
[
|
|
247
|
+
{ "name": "economy", "models": ["Ollama/minimax-m3:cloud"] },
|
|
248
|
+
{ "name": "standard", "models": ["Ollama/glm-5.2:cloud", "inherit"] },
|
|
249
|
+
{ "name": "frontier", "models": ["anthropic/claude-sonnet-4"], "costCap": 5, "contextFloor": 200000 }
|
|
250
|
+
]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Models are an ordered fallback chain (primary first; a spawn retries the next candidate if model creation is rejected); the `inherit` sentinel (case-insensitive) may appear anywhere in the chain as a provider-agnostic fallback and always resolves to the session model without catalog or floor checks. `contextFloor` skips catalog models below the window size; `costCap` aborts a run whose live cost exceeds the cap (a no-op on flat subscriptions). Tier routing applies to pi-backend agents — `backend: "claude"` agents receive the resolved string via `--model` and the claude CLI expects its own model names, so route those by `model:` instead.
|
|
242
254
|
|
|
243
255
|
Live cost $ and context % are tracked per run and surfaced in the Tiers view. Override per-run with `model`, or let the tier registry route based on the task class.
|
|
244
256
|
|
|
@@ -316,6 +328,15 @@ Every workflow run is **journaled** (`workflows/journal.ts`) and **resumable**.
|
|
|
316
328
|
- **Panel Run-action:** a 3rd `cwd` input step (task → name → cwd), prefilled with the session cwd; Enter accepts, Escape cancels.
|
|
317
329
|
- **Deferred:** bg/scheduled + worktree cwd-isolation (the `cwd` param is honored by foreground dispatches only for now) — tracked in #62.
|
|
318
330
|
|
|
331
|
+
## Provider-agnostic tiers (v0.15.0)
|
|
332
|
+
|
|
333
|
+
Small-backlog batch (issues #57/#63/#64/#65):
|
|
334
|
+
|
|
335
|
+
- **Tier `inherit` sentinel (#64)** — builtin tiers no longer hardcode a provider: `economy`/`standard`/`frontier` now resolve to your **active session model** out of the box (frontier keeps its 200k context floor; the $5 cost cap moved to the override example — a no-op on flat subs). Override by name in `tiers.json` with concrete `provider/id` chains for real multi-model routing; `inherit` can appear mid-chain as a provider-agnostic fallback. See [Cost-aware tiers](#cost-aware-tiers).
|
|
336
|
+
- **Self-correcting model errors (#57)** — dispatching with a model the runtime doesn't have now lists the session's available (authed) models in the error, so the orchestrating model can pick a valid one on the retry instead of guessing.
|
|
337
|
+
- **Panel Escape semantics documented + dead code removed (#63)** — Escape always cancels the active panel flow; defaults are accepted via Enter-on-blank. (Also fixed: ctrl+c could trigger the never-documented "escape accepts default" callbacks.)
|
|
338
|
+
- **README example tier names fixed (#65)** — the `ship-feature` example now uses real tier names (`economy`/`standard`).
|
|
339
|
+
|
|
319
340
|
## Dogfood reliability (v0.14.0)
|
|
320
341
|
|
|
321
342
|
Four fixes from dogfooding the fleet on itself (issues #58–#61):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-fleet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The armory suite's subagent orchestrator for the pi coding agent — a cross-harness, superpowers-native fleet where every agent is armory-native from birth.",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -109,6 +109,26 @@ async function buildDefaultBackendRegistry(modelRuntime: ModelRuntime): Promise<
|
|
|
109
109
|
return reg;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Format the runtime's available models for the #57 self-correcting error message:
|
|
114
|
+
* dedup `provider/id` pairs, cap the list (min 1 — a non-empty input always lists
|
|
115
|
+
* something), actionable hint when empty.
|
|
116
|
+
*/
|
|
117
|
+
export function formatAvailableModels(models: readonly { provider: string; id: string }[], cap = 12): string {
|
|
118
|
+
const effectiveCap = Math.max(1, cap);
|
|
119
|
+
const seen = new Set<string>();
|
|
120
|
+
const list: string[] = [];
|
|
121
|
+
for (const m of models) {
|
|
122
|
+
const ref = `${m.provider}/${m.id}`;
|
|
123
|
+
if (seen.has(ref)) continue;
|
|
124
|
+
seen.add(ref);
|
|
125
|
+
if (list.length < effectiveCap) list.push(ref);
|
|
126
|
+
}
|
|
127
|
+
if (list.length === 0) return "(none — check provider auth / models.json)";
|
|
128
|
+
const omitted = seen.size - list.length;
|
|
129
|
+
return list.join(", ") + (omitted > 0 ? ` … +${omitted} more` : "");
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
export function createChildSessionFactory(modelRuntime: ModelRuntime, memoryPort: MemoryHydratePort, resumeStore: ResumeStore): ChildSessionFactory {
|
|
113
133
|
return {
|
|
114
134
|
async create(opts) {
|
|
@@ -119,7 +139,13 @@ export function createChildSessionFactory(modelRuntime: ModelRuntime, memoryPort
|
|
|
119
139
|
const provider = opts.model.slice(0, slash);
|
|
120
140
|
const id = opts.model.slice(slash + 1);
|
|
121
141
|
model = modelRuntime.getModel(provider, id);
|
|
122
|
-
|
|
142
|
+
// #57: name what IS usable so the orchestrating model can self-correct on retry.
|
|
143
|
+
if (!model) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`agent model '${opts.model}' not found in runtime (provider '${provider}', id '${id}'). ` +
|
|
146
|
+
`Available: ${formatAvailableModels(modelRuntime.getAvailableSnapshot())} — pick one of these for the model param`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
123
149
|
}
|
|
124
150
|
// Fleet CustomResourceLoader: noExtensions + composed systemPromptOverride (rolePrompt + memory + base) + scoped skills.
|
|
125
151
|
const loader = buildChildLoader({ cwd: opts.cwd, agent: opts.agent, memoryPort });
|
package/src/panel/fleet-panel.ts
CHANGED
|
@@ -425,10 +425,8 @@ export class FleetPanel extends Container {
|
|
|
425
425
|
this.linkInput.onSubmit = (todoIdRaw: string) => {
|
|
426
426
|
void this.executeRun(agentName, task.trim(), todoIdRaw.trim() || undefined);
|
|
427
427
|
};
|
|
428
|
-
this.linkInput.onEscape = () => { void this.executeRun(agentName, task.trim(), undefined); };
|
|
429
428
|
this.renderShell();
|
|
430
429
|
};
|
|
431
|
-
this.taskInput.onEscape = () => this.cancelRun();
|
|
432
430
|
this.runMode = true;
|
|
433
431
|
this.renderShell();
|
|
434
432
|
}
|
|
@@ -492,6 +490,12 @@ export class FleetPanel extends Container {
|
|
|
492
490
|
}
|
|
493
491
|
|
|
494
492
|
handleInput(data: string): void {
|
|
493
|
+
// Escape policy (#63): every modal branch below intercepts Escape BEFORE the active Input
|
|
494
|
+
// sees it, so pi-tui's Input.onEscape never fires in this panel — Escape always cancels
|
|
495
|
+
// the active flow. Defaults are accepted via Enter-on-blank ("blank=default" prompts).
|
|
496
|
+
// Caveat: ctrl+c also matches pi-tui's tui.select.cancel but is NOT intercepted here —
|
|
497
|
+
// it forwards to the Input, which ignores control characters (silent no-op). An onEscape
|
|
498
|
+
// callback re-added later would fire on ctrl+c but never on Escape — do not re-add.
|
|
495
499
|
if (this.infoAgent) {
|
|
496
500
|
if (matchesKey(data, "escape")) { this.infoAgent = null; this.renderShell(); }
|
|
497
501
|
return;
|
|
@@ -811,13 +815,13 @@ export class FleetPanel extends Container {
|
|
|
811
815
|
this.pendingCheckpoint = null;
|
|
812
816
|
this.renderShell();
|
|
813
817
|
};
|
|
814
|
-
this.lcReviseInput.onEscape = () => { this.lcRevising = false; this.lcReviseInput = null; this.renderShell(); };
|
|
815
818
|
this.renderShell();
|
|
816
819
|
return;
|
|
817
820
|
}
|
|
818
821
|
}
|
|
819
822
|
if (this.lcRevising && this.lcReviseInput) {
|
|
820
|
-
|
|
823
|
+
// Escape never reaches here — the panel-level intercept above resolves the pending
|
|
824
|
+
// checkpoint as abort + closes the panel first (#63). Only printable input forwards.
|
|
821
825
|
this.lcReviseInput.handleInput(data);
|
|
822
826
|
this.invalidate();
|
|
823
827
|
return;
|
|
@@ -842,13 +846,10 @@ export class FleetPanel extends Container {
|
|
|
842
846
|
const lcName = name.trim() || "default";
|
|
843
847
|
this.executeScheduleAdd(task.trim(), expr.trim(), lcName);
|
|
844
848
|
};
|
|
845
|
-
this.schedNameInput.onEscape = () => { this.executeScheduleAdd(task.trim(), expr.trim(), "default"); };
|
|
846
849
|
this.renderShell();
|
|
847
850
|
};
|
|
848
|
-
this.schedExprInput.onEscape = () => this.cancelScheduleAdd();
|
|
849
851
|
this.renderShell();
|
|
850
852
|
};
|
|
851
|
-
this.schedTaskInput.onEscape = () => this.cancelScheduleAdd();
|
|
852
853
|
this.schedRunMode = true;
|
|
853
854
|
this.renderShell();
|
|
854
855
|
}
|
|
@@ -892,7 +893,6 @@ export class FleetPanel extends Container {
|
|
|
892
893
|
if (!followUp.trim()) { this.cancelResume(); return; }
|
|
893
894
|
void this.executeResume(run, followUp.trim());
|
|
894
895
|
};
|
|
895
|
-
this.resumeInput.onEscape = () => this.cancelResume();
|
|
896
896
|
this.resumeMode = true;
|
|
897
897
|
this.renderShell();
|
|
898
898
|
}
|
|
@@ -916,7 +916,6 @@ export class FleetPanel extends Container {
|
|
|
916
916
|
if (!text.trim()) { this.cancelSteer(); return; }
|
|
917
917
|
void this.executeSteer(run.runId, text.trim());
|
|
918
918
|
};
|
|
919
|
-
this.steerInput.onEscape = () => this.cancelSteer();
|
|
920
919
|
this.steerMode = true;
|
|
921
920
|
this.renderShell();
|
|
922
921
|
}
|
|
@@ -969,7 +968,6 @@ export class FleetPanel extends Container {
|
|
|
969
968
|
}
|
|
970
969
|
this.tiersInput = new Input();
|
|
971
970
|
this.tiersInput.onSubmit = (value: string) => { void this.executeTiersEdit(value, phase); };
|
|
972
|
-
this.tiersInput.onEscape = () => this.cancelTiersEdit();
|
|
973
971
|
this.tiersEditPhase = phase;
|
|
974
972
|
this.renderShell();
|
|
975
973
|
}
|
|
@@ -995,7 +993,6 @@ export class FleetPanel extends Container {
|
|
|
995
993
|
}
|
|
996
994
|
this.cancelWorkflowRun();
|
|
997
995
|
};
|
|
998
|
-
this.wfPromptInput.onEscape = () => this.cancelWorkflowRun();
|
|
999
996
|
this.wfRunMode = true;
|
|
1000
997
|
this.renderShell();
|
|
1001
998
|
}
|
|
@@ -1097,10 +1094,8 @@ export class FleetPanel extends Container {
|
|
|
1097
1094
|
this.linkInput.onSubmit = (todoIdRaw: string) => {
|
|
1098
1095
|
void this.executeFork(run.agent, finalTask, todoIdRaw.trim() || undefined, run.runId);
|
|
1099
1096
|
};
|
|
1100
|
-
this.linkInput.onEscape = () => { void this.executeFork(run.agent, finalTask, undefined, run.runId); };
|
|
1101
1097
|
this.renderShell();
|
|
1102
1098
|
};
|
|
1103
|
-
this.taskInput.onEscape = () => this.cancelRun();
|
|
1104
1099
|
this.runMode = true;
|
|
1105
1100
|
this.renderShell();
|
|
1106
1101
|
}
|
|
@@ -1134,19 +1129,16 @@ export class FleetPanel extends Container {
|
|
|
1134
1129
|
const lcName = name.trim() || "default";
|
|
1135
1130
|
this.lcPhase = "cwd";
|
|
1136
1131
|
this.lcCwdInput = new Input();
|
|
1137
|
-
// SPEC-6-5: 3rd input step — the dispatch cwd.
|
|
1138
|
-
//
|
|
1132
|
+
// SPEC-6-5: 3rd input step — the dispatch cwd. Enter accepts the session cwd (blank)
|
|
1133
|
+
// or a typed path; Escape cancels the run (panel-level intercept — #63).
|
|
1139
1134
|
this.lcCwdInput.onSubmit = (cwd: string) => {
|
|
1140
1135
|
const picked = cwd.trim() || this.deps.parentCwd;
|
|
1141
1136
|
void this.executeLifecycleRun(task.trim(), lcName, picked);
|
|
1142
1137
|
};
|
|
1143
|
-
this.lcCwdInput.onEscape = () => { void this.executeLifecycleRun(task.trim(), lcName, this.deps.parentCwd); };
|
|
1144
1138
|
this.renderShell();
|
|
1145
1139
|
};
|
|
1146
|
-
this.lcNameInput.onEscape = () => { void this.executeLifecycleRun(task.trim(), "default", this.deps.parentCwd); };
|
|
1147
1140
|
this.renderShell();
|
|
1148
1141
|
};
|
|
1149
|
-
this.lcTaskInput.onEscape = () => this.cancelLifecycleRun();
|
|
1150
1142
|
this.lcRunMode = true;
|
|
1151
1143
|
this.renderShell();
|
|
1152
1144
|
}
|
package/src/tiers/builtin.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { Tier } from "./tier-registry.ts";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Shipped default tiers (Q10). Overridable via global/project tiers.json.
|
|
5
|
+
*
|
|
6
|
+
* Provider-agnostic since #64: the `inherit` sentinel resolves to the parent/active
|
|
7
|
+
* session model, so zero-config fleets work on ANY provider. Users who want real
|
|
8
|
+
* multi-model cost routing override these by name with concrete `provider/id`
|
|
9
|
+
* chains (`inherit` may also appear mid-chain as a fallback). `contextFloor`
|
|
10
|
+
* guards concrete candidates; `costCap` is a per-run $ abort (configure where
|
|
11
|
+
* meaningful — it is a no-op on flat subscriptions).
|
|
12
|
+
*/
|
|
4
13
|
export const BUILTIN_TIERS: Tier[] = [
|
|
5
|
-
{ name: "economy", models: ["
|
|
6
|
-
{ name: "standard", models: ["
|
|
7
|
-
{ name: "frontier", models: ["
|
|
8
|
-
];
|
|
14
|
+
{ name: "economy", models: ["inherit"] },
|
|
15
|
+
{ name: "standard", models: ["inherit"] },
|
|
16
|
+
{ name: "frontier", models: ["inherit"], contextFloor: 200000 },
|
|
17
|
+
];
|
package/src/tiers/resolve.ts
CHANGED
|
@@ -34,6 +34,20 @@ export function resolveAgentModel(
|
|
|
34
34
|
if (!tier) return { error: `tier '${agent.tier}' not found; available: ${tiers.list().map((t) => t.name).join(", ")}` };
|
|
35
35
|
const candidates: string[] = [];
|
|
36
36
|
for (const m of tier.models) {
|
|
37
|
+
if (m.trim().toLowerCase() === "inherit") {
|
|
38
|
+
// #64: "inherit" = use the parent/active model — always eligible, no catalog or
|
|
39
|
+
// contextFloor check (the session model is presumed appropriate; the floor guards
|
|
40
|
+
// concrete candidates). Participates in the chain: eligible concrete models listed
|
|
41
|
+
// before it win; after them it acts as the provider-agnostic fallback.
|
|
42
|
+
// Sentinel is case-insensitive/trimmed; concrete model strings stay verbatim.
|
|
43
|
+
// Empty parentModel (dispatch before a session model exists) is NOT pushed — a
|
|
44
|
+
// pure-inherit tier then falls to the descriptive no-eligible-model error below.
|
|
45
|
+
if (parentModel.provider || parentModel.id) {
|
|
46
|
+
const parent = `${parentModel.provider}/${parentModel.id}`;
|
|
47
|
+
if (!candidates.includes(parent)) candidates.push(parent);
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
37
51
|
const { provider, id } = splitModel(m, parentModel.provider);
|
|
38
52
|
const model = modelRegistry.find(provider, id);
|
|
39
53
|
if (!model) continue; // not in catalog → skip
|