pi-legion 2.0.0 → 2.0.2
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 +136 -0
- package/CONTRIBUTING.md +51 -18
- package/README.md +101 -18
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +2 -3
- package/agents/worker.md +2 -3
- package/docs/contracts/esc-abort-v1.md +2 -0
- package/docs/contracts/esc-cascade-v2.md +5 -1
- package/docs/contracts/esc-no-retire-v3.md +57 -0
- package/docs/design.md +37 -18
- package/docs/vexillatio-contract.md +1 -1
- package/index.ts +145 -134
- package/package.json +8 -4
- package/skills/pi-legion/SKILL.md +22 -8
- package/src/agents/agents.ts +60 -5
- package/src/api.ts +2 -0
- package/src/extension/legion-commands.ts +64 -10
- package/src/extension/legion-doctor.ts +128 -41
- package/src/extension/legion-status.ts +172 -0
- package/src/extension/legion-stop.ts +5 -37
- package/src/extension/playbook.ts +4 -4
- package/src/extension/runtime.ts +97 -22
- package/src/extension/scout-guard.ts +12 -3
- package/src/runs/args.ts +69 -13
- package/src/runs/orchestrator.ts +28 -3
- package/src/runs/provider-pin.ts +59 -0
- package/src/runs/provision.ts +7 -4
- package/src/runs/role-tool-gates.ts +103 -0
- package/src/runs/worktree.ts +32 -0
- package/src/shared/session.ts +37 -2
- package/src/shared/types.ts +8 -0
- package/src/tui/status.ts +12 -14
- package/src/tui/viz/canvas.ts +5 -0
- package/src/tui/viz/panels.ts +16 -8
- package/src/tui/viz/railway.ts +34 -11
- package/src/tui/viz/rate.ts +41 -9
- package/src/tui/viz/view.ts +18 -11
package/src/tui/viz/rate.ts
CHANGED
|
@@ -6,17 +6,29 @@
|
|
|
6
6
|
*
|
|
7
7
|
* numerator = Δ cumulative output tokens over the trailing window
|
|
8
8
|
* denominator = Δ cumulative generationMs over the same window
|
|
9
|
-
* (falls back to wall-clock window
|
|
10
|
-
*
|
|
9
|
+
* (falls back to wall-clock window when generationMs is
|
|
10
|
+
* unavailable / zero, or when in-window ΔgenerationMs is
|
|
11
|
+
* below MIN_RATE_GENERATION_MS — same 1s floor as tree t/s)
|
|
11
12
|
*
|
|
12
13
|
* generationMs comes from session JSONL: sum of (assistant_ts − prior
|
|
13
14
|
* user/toolResult_ts). Idle and tool gaps are excluded. Approximate —
|
|
14
15
|
* usage lands when each assistant message completes, not mid-stream.
|
|
16
|
+
*
|
|
17
|
+
* Header / snap "now" (`nowRate` / `totalRate` with live node ids) only sums
|
|
18
|
+
* agents that are still `running`. Settled/retired series keep contributing to
|
|
19
|
+
* chart buckets/avg so the history window stays accurate; once nothing is live,
|
|
20
|
+
* "now" is forced to 0 even if the 60s window still holds Δtokens.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
23
|
import type { VizNode } from "./model.ts";
|
|
18
24
|
|
|
19
25
|
export const RATE_WINDOW_MS = 60_000;
|
|
26
|
+
/**
|
|
27
|
+
* Min in-window ΔgenerationMs before trusting gen as the tok/s denominator.
|
|
28
|
+
* Matches tree-row t/s gate (`generationMs >= 1000` in status.ts). Below this,
|
|
29
|
+
* fall back to wall-clock window length so a 1ms gen flush cannot spike to M.
|
|
30
|
+
*/
|
|
31
|
+
export const MIN_RATE_GENERATION_MS = 1_000;
|
|
20
32
|
const DEFAULT_RETENTION_MS = 6 * 60 * 60 * 1000;
|
|
21
33
|
const MAX_SAMPLES_PER_NODE = 4096;
|
|
22
34
|
|
|
@@ -125,24 +137,39 @@ export class RateSampler {
|
|
|
125
137
|
|
|
126
138
|
/**
|
|
127
139
|
* Output tok/s over the trailing window: ΔtokensOut / (ΔgenerationMs/1000).
|
|
128
|
-
* When
|
|
129
|
-
* wall-clock window length so a ledger-only path still yields a
|
|
140
|
+
* When in-window ΔgenerationMs is 0 or below MIN_RATE_GENERATION_MS, falls
|
|
141
|
+
* back to wall-clock window length so a ledger-only path still yields a
|
|
142
|
+
* number and a sub-second gen flush cannot report M-level TPS.
|
|
130
143
|
*/
|
|
131
144
|
nodeRate(nodeId: string, now: number, windowMs = RATE_WINDOW_MS): number {
|
|
132
145
|
if (windowMs <= 0) return 0;
|
|
133
146
|
const dOut = Math.max(0, this.valueAt(nodeId, now) - this.valueAt(nodeId, now - windowMs));
|
|
134
147
|
if (dOut <= 0) return 0;
|
|
135
148
|
const dGen = Math.max(0, this.genAt(nodeId, now) - this.genAt(nodeId, now - windowMs));
|
|
136
|
-
const seconds = dGen
|
|
149
|
+
const seconds = dGen >= MIN_RATE_GENERATION_MS ? dGen / 1000 : windowMs / 1000;
|
|
137
150
|
return dOut / seconds;
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Sum of node rates. When `onlyNodeIds` is set (including empty), only those
|
|
155
|
+
* series contribute — used for header "now". Omit it for chart buckets/avg
|
|
156
|
+
* so settled agents still paint their trailing-window history.
|
|
157
|
+
*/
|
|
158
|
+
totalRate(now: number, windowMs = RATE_WINDOW_MS, onlyNodeIds?: Iterable<string>): number {
|
|
159
|
+
const filter = onlyNodeIds === undefined ? undefined : new Set(onlyNodeIds);
|
|
141
160
|
let sum = 0;
|
|
142
|
-
for (const id of this.series.keys())
|
|
161
|
+
for (const id of this.series.keys()) {
|
|
162
|
+
if (filter && !filter.has(id)) continue;
|
|
163
|
+
sum += this.nodeRate(id, now, windowMs);
|
|
164
|
+
}
|
|
143
165
|
return sum;
|
|
144
166
|
}
|
|
145
167
|
|
|
168
|
+
/** Header / snap "now" tok/s: live (running) agents only; none live → 0. */
|
|
169
|
+
nowRate(now: number, nodes: readonly VizNode[], windowMs = RATE_WINDOW_MS): number {
|
|
170
|
+
return this.totalRate(now, windowMs, liveRateNodeIds(nodes));
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
nodeIds(): string[] {
|
|
147
174
|
return [...this.series.keys()];
|
|
148
175
|
}
|
|
@@ -150,8 +177,8 @@ export class RateSampler {
|
|
|
150
177
|
/**
|
|
151
178
|
* Bucketed tok/s for [from, to], stacked by role. Each bucket is the
|
|
152
179
|
* trailing-window average ending at the bucket edge (window = max(bucket
|
|
153
|
-
* span, RATE_WINDOW_MS)),
|
|
154
|
-
*
|
|
180
|
+
* span, RATE_WINDOW_MS)), including settled series so history stays painted.
|
|
181
|
+
* Header "now" may be lower (live-only); bursty flushes stay smoothed.
|
|
155
182
|
*/
|
|
156
183
|
buckets(from: number, to: number, count: number, windowMs = RATE_WINDOW_MS): RateSeries {
|
|
157
184
|
const n = Math.max(1, Math.floor(count));
|
|
@@ -191,6 +218,11 @@ export class RateSampler {
|
|
|
191
218
|
}
|
|
192
219
|
}
|
|
193
220
|
|
|
221
|
+
/** Node ids that contribute to header "now" tok/s (running agents; root excluded). */
|
|
222
|
+
export function liveRateNodeIds(nodes: readonly VizNode[]): string[] {
|
|
223
|
+
return nodes.filter((n) => n.id !== "root" && n.status === "running").map((n) => n.id);
|
|
224
|
+
}
|
|
225
|
+
|
|
194
226
|
function interpolate(ts: number[], value: number[], t: number): number {
|
|
195
227
|
if (ts.length === 0) return 0;
|
|
196
228
|
if (t <= ts[0]!) return value[0]!;
|
package/src/tui/viz/view.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** /legion full view: galaxy + tok/s chart + git-log history with linked selection. */
|
|
2
2
|
|
|
3
3
|
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
4
|
-
import { Canvas, ellipsize } from "./canvas.ts";
|
|
4
|
+
import { Canvas, cellWidth, ellipsize } from "./canvas.ts";
|
|
5
5
|
import { computeLayout, type LayoutKind } from "./layout.ts";
|
|
6
6
|
import { formatCount, formatDuration, isAgent, type VizNode } from "./model.ts";
|
|
7
7
|
import { roleColor, type VizTheme } from "./palette.ts";
|
|
@@ -22,6 +22,9 @@ export function initialViewState(): LegionViewState {
|
|
|
22
22
|
|
|
23
23
|
const PANES: Pane[] = ["galaxy", "chart", "log"];
|
|
24
24
|
|
|
25
|
+
/** Min grapheme cells before painting a selection badge. Shared by portrait + landscape so a truncated stub never paints (#128). Was portrait `>4` vs landscape `>0`. */
|
|
26
|
+
export const MIN_SELECTION_BADGE_W = 5;
|
|
27
|
+
|
|
25
28
|
export function renderLegionView(data: ViewData, state: LegionViewState, cols: number, rows: number): { canvas: Canvas; layout: LayoutKind } {
|
|
26
29
|
const W = Math.max(1, Math.floor(cols));
|
|
27
30
|
const H = Math.max(6, Math.floor(rows));
|
|
@@ -44,21 +47,25 @@ export function renderLegionView(data: ViewData, state: LegionViewState, cols: n
|
|
|
44
47
|
`${agents.length} agents`,
|
|
45
48
|
...(failed ? [`${failed} failed`] : []),
|
|
46
49
|
];
|
|
47
|
-
const
|
|
50
|
+
const badgeRaw = selected ? ` ${selected.name} (${selected.role} · ${statusLabel(selected)}) ` : "";
|
|
51
|
+
// Cap the selection badge so a long agent name never overwrites "⚔ /legion" / the run name.
|
|
52
|
+
const badgeCap = Math.max(0, Math.min(cellWidth(badgeRaw), Math.floor(W * 0.45), W - 14));
|
|
53
|
+
const badge = badgeRaw ? ellipsize(badgeRaw, badgeCap) : "";
|
|
54
|
+
const badgeW = cellWidth(badge);
|
|
55
|
+
const showBadge = badgeW >= MIN_SELECTION_BADGE_W;
|
|
48
56
|
if (portrait) {
|
|
49
57
|
cv.text(0, 0, "⚔ /legion ", "accent", { bold: true });
|
|
50
|
-
|
|
58
|
+
const rootRoom = Math.max(0, W - 10 - (showBadge ? badgeW + 1 : 0));
|
|
59
|
+
cv.text(10, 0, ellipsize(root?.name ?? "", rootRoom), "text", { bold: true });
|
|
51
60
|
cv.text(0, 1, ellipsize(parts.join(" · "), W), "dim");
|
|
52
|
-
if (badge) {
|
|
53
|
-
const b = ellipsize(badge, Math.max(0, W - 12 - Array.from(root?.name ?? "").length));
|
|
54
|
-
if (b.length > 4) cv.text(W - Array.from(b).length, 0, b, roleColor(selected!.role), { inverse: true });
|
|
55
|
-
}
|
|
61
|
+
if (showBadge) cv.text(W - badgeW, 0, badge, roleColor(selected!.role), { inverse: true });
|
|
56
62
|
} else {
|
|
57
63
|
let x = cv.text(0, 0, "⚔ /legion ", "accent", { bold: true });
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
cv.text(x, 0, ellipsize(
|
|
61
|
-
|
|
64
|
+
const reservedBadge = showBadge ? badgeW : 0;
|
|
65
|
+
const rootRoom = Math.max(0, W - x - reservedBadge - 1);
|
|
66
|
+
x = cv.text(x, 0, ellipsize(`${root?.name ?? "legion"} `, rootRoom), "text", { bold: true });
|
|
67
|
+
cv.text(x, 0, ellipsize(parts.join(" · "), Math.max(0, W - x - reservedBadge - 1)), "dim");
|
|
68
|
+
if (showBadge) cv.text(W - badgeW, 0, badge, roleColor(selected!.role), { inverse: true });
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
const galaxyInner = frame(cv, layout.galaxy, portrait ? "galaxy" : "galaxy · structure", state.focus === "galaxy");
|