@danypops/pi-jittor 0.2.1 → 0.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/README.md +19 -3
- package/docs/USAGE_PRIOR_ART.md +1 -1
- package/extension/src/index.ts +173 -43
- package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +200 -27
- package/extension/src/observability/context-growth.ts +26 -0
- package/extension/src/{context-report.ts → observability/context-report.ts} +35 -3
- package/extension/src/observability/context-view.ts +264 -0
- package/extension/src/{footer.ts → observability/footer.ts} +7 -0
- package/extension/src/observability/provider-context-snapshot.ts +246 -0
- package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +1 -1
- package/extension/src/{tui.ts → observability/status.ts} +55 -10
- package/extension/src/{usage.ts → observability/usage.ts} +74 -166
- package/extension/src/{benchmark-tui.ts → optimization/model-selection-panel.ts} +61 -29
- package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +2 -2
- package/extension/src/service-client.ts +48 -1
- package/extension/src/settings-tui.ts +57 -32
- package/extension/src/settings.ts +27 -19
- package/package.json +9 -3
- package/extension/src/context-view.ts +0 -140
- /package/extension/src/{capabilities → observability}/context-hub.ts +0 -0
- /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
- /package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +0 -0
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { JITTOR_EXTENSION_SETTINGS_FILENAME, JITTOR_STATE_DIRECTORY, USAGE_PERIODS, type UsagePeriod } from "@danypops/jittor";
|
|
5
|
+
import { createAtomicJsonWriter } from "@danypops/vehicle-core";
|
|
6
|
+
import { createNodeAtomicJsonFsAdapter } from "@danypops/vehicle-server/atomic-json";
|
|
4
7
|
|
|
8
|
+
const atomicJson = createAtomicJsonWriter({ fs: createNodeAtomicJsonFsAdapter() });
|
|
9
|
+
|
|
10
|
+
// Every setter persists to disk before resolving, so a caller that awaits it (every real call
|
|
11
|
+
// site in index.ts/settings-tui.ts already runs inside an async handler) observes a completed
|
|
12
|
+
// write -- no fire-and-forget persistence that a subsequent synchronous re-read could race. The
|
|
13
|
+
// return type stays `void | Promise<void>` (not a strict `Promise<void>`) so a test/adapter
|
|
14
|
+
// implementation that has no real persistence to await (e.g. the no-op fallback stubs in
|
|
15
|
+
// index.ts's usageBudgetControl()/recoveryControl()) can stay trivially synchronous.
|
|
5
16
|
export interface EnforcementControl {
|
|
6
17
|
isEnabled(): boolean;
|
|
7
|
-
setEnabled(enabled: boolean): void
|
|
18
|
+
setEnabled(enabled: boolean): void | Promise<void>;
|
|
8
19
|
isFooterEnabled(): boolean;
|
|
9
|
-
setFooterEnabled(enabled: boolean): void
|
|
20
|
+
setFooterEnabled(enabled: boolean): void | Promise<void>;
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
export interface CodexRecoveryControl {
|
|
13
24
|
isCodexRecoveryEnabled(): boolean;
|
|
14
|
-
setCodexRecoveryEnabled(enabled: boolean): void
|
|
25
|
+
setCodexRecoveryEnabled(enabled: boolean): void | Promise<void>;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
export interface UsageBudgetControl {
|
|
18
29
|
getUsageTokenBudget(period: UsagePeriod): number | undefined;
|
|
19
|
-
setUsageTokenBudget(period: UsagePeriod, tokens: number | undefined): void
|
|
30
|
+
setUsageTokenBudget(period: UsagePeriod, tokens: number | undefined): void | Promise<void>;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
export interface PersistentExtensionControl extends EnforcementControl, CodexRecoveryControl, UsageBudgetControl {}
|
|
@@ -64,12 +75,9 @@ function loadSettings(path: string): ExtensionSettings {
|
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
function persistSettings(path: string, settings: ExtensionSettings): void {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
71
|
-
chmodSync(temporary, 0o600);
|
|
72
|
-
renameSync(temporary, path);
|
|
78
|
+
async function persistSettings(path: string, settings: ExtensionSettings): Promise<void> {
|
|
79
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
80
|
+
await atomicJson.write(path, settings, { mode: 0o600, pretty: true, trailingNewline: true });
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
export function persistentEnforcementControl(env: Record<string, string | undefined> = process.env): PersistentExtensionControl {
|
|
@@ -77,29 +85,29 @@ export function persistentEnforcementControl(env: Record<string, string | undefi
|
|
|
77
85
|
const settings = loadSettings(path);
|
|
78
86
|
return {
|
|
79
87
|
isEnabled: () => settings.enforcementEnabled,
|
|
80
|
-
setEnabled(value: boolean): void {
|
|
88
|
+
async setEnabled(value: boolean): Promise<void> {
|
|
81
89
|
settings.enforcementEnabled = value;
|
|
82
|
-
persistSettings(path, settings);
|
|
90
|
+
await persistSettings(path, settings);
|
|
83
91
|
},
|
|
84
92
|
isFooterEnabled: () => settings.footerEnabled,
|
|
85
|
-
setFooterEnabled(value: boolean): void {
|
|
93
|
+
async setFooterEnabled(value: boolean): Promise<void> {
|
|
86
94
|
settings.footerEnabled = value;
|
|
87
|
-
persistSettings(path, settings);
|
|
95
|
+
await persistSettings(path, settings);
|
|
88
96
|
},
|
|
89
97
|
isCodexRecoveryEnabled: () => settings.codexRecoveryEnabled,
|
|
90
|
-
setCodexRecoveryEnabled(value: boolean): void {
|
|
98
|
+
async setCodexRecoveryEnabled(value: boolean): Promise<void> {
|
|
91
99
|
settings.codexRecoveryEnabled = value;
|
|
92
|
-
persistSettings(path, settings);
|
|
100
|
+
await persistSettings(path, settings);
|
|
93
101
|
},
|
|
94
102
|
getUsageTokenBudget(period): number | undefined {
|
|
95
103
|
return settings.usageTokenBudgets[period];
|
|
96
104
|
},
|
|
97
|
-
setUsageTokenBudget(period, tokens): void {
|
|
105
|
+
async setUsageTokenBudget(period, tokens): Promise<void> {
|
|
98
106
|
if (tokens !== undefined && (!Number.isFinite(tokens) || tokens <= 0))
|
|
99
107
|
throw new Error("usage token budget must be a positive finite number");
|
|
100
108
|
if (tokens === undefined) delete settings.usageTokenBudgets[period];
|
|
101
109
|
else settings.usageTokenBudgets[period] = tokens;
|
|
102
|
-
persistSettings(path, settings);
|
|
110
|
+
await persistSettings(path, settings);
|
|
103
111
|
},
|
|
104
112
|
};
|
|
105
113
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-jittor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension for Jittor
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Pi extension for Jittor token and context observability with optimization controls backed by the @danypops/jittor daemon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package", "llm-router", "token-budget"],
|
|
7
7
|
"scripts": {
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@danypops/vehicle-client": "^0.5.1",
|
|
16
|
-
"@danypops/
|
|
16
|
+
"@danypops/vehicle-core": "^0.12.1",
|
|
17
|
+
"@danypops/vehicle-server": "^0.17.0",
|
|
18
|
+
"@danypops/jittor": "workspace:^",
|
|
17
19
|
"malevich-tui-components": "^0.16.1"
|
|
18
20
|
},
|
|
19
21
|
"peerDependencies": {
|
|
@@ -22,6 +24,10 @@
|
|
|
22
24
|
"typebox": "*"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
27
|
+
"@danypops/pi-extension-harness": "^0.6.2",
|
|
28
|
+
"@danypops/pi-process-harness": "^0.1.2",
|
|
29
|
+
"@danypops/pi-tui-harness": "^0.0.2",
|
|
30
|
+
"@earendil-works/pi-ai": "*",
|
|
25
31
|
"bun-types": "latest"
|
|
26
32
|
},
|
|
27
33
|
"repository": {
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { ContextSegment } from "@danypops/jittor";
|
|
2
|
-
import type { ExtensionCommandContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { matchesKey, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import {
|
|
5
|
-
buildContextRows,
|
|
6
|
-
type ContextBarTheme,
|
|
7
|
-
type ContextRow,
|
|
8
|
-
type ContextRowsTheme,
|
|
9
|
-
type ContextSegment as MalevichContextSegment,
|
|
10
|
-
renderContextRowLines,
|
|
11
|
-
renderContextUsageBar,
|
|
12
|
-
} from "malevich-tui-components";
|
|
13
|
-
import type { ContextBreakdown } from "./context-breakdown.ts";
|
|
14
|
-
import { buildContextReport } from "./context-report.ts";
|
|
15
|
-
|
|
16
|
-
const VISIBLE_ROWS = 24;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A dynamically-contributed segment set (any string key from any extension, not a fixed enum)
|
|
20
|
-
* can't use a hardcoded per-key color map the way Papyrus's own ContextViewport did for its
|
|
21
|
-
* fixed seven segments -- this cycles a small categorical palette keyed by a stable hash of the
|
|
22
|
-
* segment key, so the same key always renders the same color within one process without needing
|
|
23
|
-
* every possible contributor's key to be known in advance.
|
|
24
|
-
*/
|
|
25
|
-
const PALETTE: ThemeColor[] = ["accent", "success", "syntaxFunction", "warning", "syntaxKeyword", "syntaxType", "muted"];
|
|
26
|
-
|
|
27
|
-
function paletteColor(key: string): ThemeColor {
|
|
28
|
-
let hash = 0;
|
|
29
|
-
for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
|
|
30
|
-
return PALETTE[hash % PALETTE.length]!;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function formatTokenCount(tokens: number): string {
|
|
34
|
-
return tokens >= 1_000 ? `${(tokens / 1_000).toFixed(1)}k` : String(tokens);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function percentOf(part: number, whole: number): string {
|
|
38
|
-
return whole > 0 ? `${((part / whole) * 100).toFixed(1)}%` : "—";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Folds each segment's confidence tier into its label so it survives Malevich's confidence-unaware row builder. */
|
|
42
|
-
function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
|
|
43
|
-
return {
|
|
44
|
-
key: segment.key,
|
|
45
|
-
label: `${segment.label} [${segment.confidence}]`,
|
|
46
|
-
estimatedTokens: segment.estimatedTokens,
|
|
47
|
-
items: segment.items,
|
|
48
|
-
unknown: segment.unknown,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
class ContextViewport {
|
|
53
|
-
private offsetY = 0;
|
|
54
|
-
private readonly rows: ContextRow[];
|
|
55
|
-
private readonly segments: readonly MalevichContextSegment[];
|
|
56
|
-
|
|
57
|
-
constructor(
|
|
58
|
-
private readonly tui: TUI,
|
|
59
|
-
private readonly theme: Theme,
|
|
60
|
-
private readonly breakdown: ContextBreakdown,
|
|
61
|
-
private readonly close: () => void,
|
|
62
|
-
) {
|
|
63
|
-
// Heaviest-first: Malevich renders segments in the order given, so sorting by weight for a
|
|
64
|
-
// merged multi-producer view is this viewport's own policy, matching context-report.ts.
|
|
65
|
-
this.segments = [...breakdown.segments].sort((left, right) => right.estimatedTokens - left.estimatedTokens).map(withConfidenceLabel);
|
|
66
|
-
this.rows = buildContextRows(this.segments, breakdown.totalTokens ?? undefined);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
invalidate(): void {}
|
|
70
|
-
|
|
71
|
-
render(width: number): string[] {
|
|
72
|
-
const theme = this.theme;
|
|
73
|
-
const contentWidth = Math.max(1, width);
|
|
74
|
-
const border = theme.fg("borderMuted", "─".repeat(contentWidth));
|
|
75
|
-
const lines: string[] = [border, truncateToWidth(theme.fg("accent", theme.bold("Context")), contentWidth, "")];
|
|
76
|
-
|
|
77
|
-
const { totalTokens, effectiveBudget } = this.breakdown;
|
|
78
|
-
if (totalTokens !== null && effectiveBudget !== null) {
|
|
79
|
-
lines.push(
|
|
80
|
-
truncateToWidth(
|
|
81
|
-
`${formatTokenCount(totalTokens)} / ${formatTokenCount(effectiveBudget)} tokens (${percentOf(totalTokens, effectiveBudget)} of usable budget)`,
|
|
82
|
-
contentWidth,
|
|
83
|
-
"",
|
|
84
|
-
),
|
|
85
|
-
);
|
|
86
|
-
} else if (totalTokens !== null) {
|
|
87
|
-
lines.push(truncateToWidth(`${formatTokenCount(totalTokens)} tokens (model context window unknown)`, contentWidth, ""));
|
|
88
|
-
} else {
|
|
89
|
-
lines.push(theme.fg("dim", "No real usage reported yet — sizes below are estimates only"));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const colorFor = (key: string) => (s: string) => theme.fg(paletteColor(key), s);
|
|
93
|
-
const barTheme: ContextBarTheme = { colorFor, empty: (s) => theme.fg("dim", s) };
|
|
94
|
-
lines.push(renderContextUsageBar(barTheme, this.segments, contentWidth, effectiveBudget ?? undefined, totalTokens ?? undefined));
|
|
95
|
-
if (this.breakdown.overshootTokens > 0) {
|
|
96
|
-
lines.push(
|
|
97
|
-
truncateToWidth(
|
|
98
|
-
theme.fg(
|
|
99
|
-
"warning",
|
|
100
|
-
`Estimates exceed real total by ~${this.breakdown.overshootTokens} tok — sizes below are approximate, not exact`,
|
|
101
|
-
),
|
|
102
|
-
contentWidth,
|
|
103
|
-
"",
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
lines.push("");
|
|
108
|
-
|
|
109
|
-
const rowsTheme: ContextRowsTheme = { colorFor, header: (s) => theme.bold(s) };
|
|
110
|
-
const visible = this.rows.slice(this.offsetY, this.offsetY + VISIBLE_ROWS);
|
|
111
|
-
lines.push(...renderContextRowLines(visible, contentWidth, rowsTheme));
|
|
112
|
-
if (this.rows.length === 0) lines.push(theme.fg("dim", " (nothing observed yet)"));
|
|
113
|
-
else lines.push(theme.fg("muted", ` ${Math.min(this.offsetY + VISIBLE_ROWS, this.rows.length)}/${this.rows.length}`));
|
|
114
|
-
|
|
115
|
-
lines.push("");
|
|
116
|
-
lines.push(theme.fg("dim", "↑↓ scroll · esc close"));
|
|
117
|
-
lines.push(border);
|
|
118
|
-
return lines;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
handleInput(data: string): void {
|
|
122
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
123
|
-
this.close();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
|
|
127
|
-
else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.rows.length - VISIBLE_ROWS), this.offsetY + 1);
|
|
128
|
-
else return;
|
|
129
|
-
this.tui.requestRender();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Interactive scrollable Context Hub view in TUI mode; the same plain-text report as /context's non-interactive path otherwise. */
|
|
134
|
-
export async function showContextView(ctx: ExtensionCommandContext, breakdown: ContextBreakdown): Promise<void> {
|
|
135
|
-
if (ctx.mode !== "tui") {
|
|
136
|
-
ctx.ui.notify(buildContextReport(breakdown), "info");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => new ContextViewport(tui, theme, breakdown, done));
|
|
140
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|