@danypops/pi-jittor 0.1.1 → 0.2.1
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/extension/src/benchmark-tui.ts +27 -12
- package/extension/src/capabilities/codex-recovery.ts +39 -23
- package/extension/src/capabilities/context-hub.ts +1 -5
- package/extension/src/capabilities/local-run-telemetry.ts +14 -10
- package/extension/src/capabilities/provider-response-telemetry.ts +20 -6
- package/extension/src/context-breakdown.ts +347 -0
- package/extension/src/context-report.ts +39 -25
- package/extension/src/context-view.ts +140 -0
- package/extension/src/footer.ts +56 -26
- package/extension/src/index.ts +261 -97
- package/extension/src/service-client.ts +1 -1
- package/extension/src/settings-tui.ts +35 -20
- package/extension/src/settings.ts +13 -10
- package/extension/src/tui.ts +148 -63
- package/extension/src/usage.ts +124 -42
- package/package.json +4 -4
package/extension/src/index.ts
CHANGED
|
@@ -1,47 +1,58 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ContextSegmentItem } from "@danypops/jittor";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
MAX_DYNAMIC_ROUTES,
|
|
6
|
-
PAPYRUS_CONTEXT_INJECTION_CHANNEL,
|
|
7
|
-
PAPYRUS_TASK_FOCUS_CHANNEL,
|
|
3
|
+
applyTaskFocusEvent,
|
|
4
|
+
CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
|
|
8
5
|
CONTEXT_EVENT_DEDUP_LIMIT,
|
|
6
|
+
CONTEXT_HUB_CONTRIBUTION_CHANNEL,
|
|
9
7
|
CompactionTelemetry,
|
|
10
|
-
papyrusContextMetric,
|
|
11
|
-
validatePapyrusContextInjection,
|
|
12
|
-
applyTaskFocusEvent,
|
|
13
|
-
validateTaskFocusEvent,
|
|
14
|
-
toolLedgerSegment,
|
|
15
|
-
TASK_DOMAINS,
|
|
16
|
-
TASK_TYPES,
|
|
17
|
-
USAGE_PERIODS,
|
|
18
8
|
type ContextAssessment,
|
|
9
|
+
FOOTER_COMPACTION_RENDER_INTERVAL_MS,
|
|
10
|
+
MAX_DYNAMIC_ROUTES,
|
|
19
11
|
type MetricObservation,
|
|
20
12
|
type ModelCandidate,
|
|
21
13
|
type ModelTaskDomain,
|
|
22
14
|
type ModelTaskType,
|
|
15
|
+
PAPYRUS_CONTEXT_INJECTION_CHANNEL,
|
|
16
|
+
PAPYRUS_TASK_FOCUS_CHANNEL,
|
|
23
17
|
type PolicyDecision,
|
|
18
|
+
papyrusContextMetric,
|
|
24
19
|
type Route,
|
|
25
20
|
type RouterStatus,
|
|
26
21
|
type StoredMetricObservation,
|
|
22
|
+
TASK_DOMAINS,
|
|
23
|
+
TASK_TYPES,
|
|
24
|
+
toolLedgerSegment,
|
|
25
|
+
USAGE_PERIODS,
|
|
27
26
|
type UsagePeriod,
|
|
27
|
+
validatePapyrusContextInjection,
|
|
28
|
+
validateTaskFocusEvent,
|
|
28
29
|
} from "@danypops/jittor";
|
|
30
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
29
31
|
import { showBenchmarkPanel } from "./benchmark-tui.ts";
|
|
30
|
-
import {
|
|
32
|
+
import { CodexRecoveryCapability, type CodexRecoveryRuntime, SYSTEM_RECOVERY_RUNTIME } from "./capabilities/codex-recovery.ts";
|
|
33
|
+
import { ContextHubCapability } from "./capabilities/context-hub.ts";
|
|
34
|
+
import { LocalRunTelemetry } from "./capabilities/local-run-telemetry.ts";
|
|
35
|
+
import { ProviderResponseTelemetry } from "./capabilities/provider-response-telemetry.ts";
|
|
36
|
+
import {
|
|
37
|
+
basePromptSegment,
|
|
38
|
+
buildBasePromptItems,
|
|
39
|
+
buildMessageHistoryTree,
|
|
40
|
+
composeContextBreakdown,
|
|
41
|
+
messageHistorySegment,
|
|
42
|
+
type SessionEntryLike,
|
|
43
|
+
type SessionTreeNodeLike,
|
|
44
|
+
} from "./context-breakdown.ts";
|
|
45
|
+
import { showContextView } from "./context-view.ts";
|
|
46
|
+
import { type CompactionProgress, type IntegratedFooterState, installIntegratedFooter } from "./footer.ts";
|
|
31
47
|
import { callJittor } from "./service-client.ts";
|
|
32
|
-
import { persistentEnforcementControl, type CodexRecoveryControl, type EnforcementControl, type UsageBudgetControl } from "./settings.ts";
|
|
33
|
-
import { showSettingsPanel } from "./settings-tui.ts";
|
|
34
|
-
import { buildFooterBudget, formatFooterStatus, providerBudgetMetricQuery, showJittorPanel } from "./tui.ts";
|
|
35
48
|
import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
|
|
49
|
+
import { type CodexRecoveryControl, type EnforcementControl, persistentEnforcementControl, type UsageBudgetControl } from "./settings.ts";
|
|
50
|
+
import { showSettingsPanel } from "./settings-tui.ts";
|
|
51
|
+
import { buildFooterBudget, providerBudgetMetricQuery, showJittorPanel } from "./tui.ts";
|
|
36
52
|
import { showUsagePanel } from "./usage.ts";
|
|
37
|
-
import { CodexRecoveryCapability, SYSTEM_RECOVERY_RUNTIME, type CodexRecoveryRuntime } from "./capabilities/codex-recovery.ts";
|
|
38
|
-
import { ProviderResponseTelemetry } from "./capabilities/provider-response-telemetry.ts";
|
|
39
|
-
import { LocalRunTelemetry } from "./capabilities/local-run-telemetry.ts";
|
|
40
|
-
import { ContextHubCapability } from "./capabilities/context-hub.ts";
|
|
41
|
-
import { buildContextReport } from "./context-report.ts";
|
|
42
53
|
|
|
43
|
-
export { formatFooterStatus } from "./tui.ts";
|
|
44
54
|
export type { CodexRecoveryRuntime } from "./capabilities/codex-recovery.ts";
|
|
55
|
+
export { formatFooterStatus } from "./tui.ts";
|
|
45
56
|
|
|
46
57
|
const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
47
58
|
const RECOVERY_GUIDANCE = "Run /jittor off to disable blocking, or restart the daemon with: systemctl --user restart jittor.service";
|
|
@@ -58,9 +69,9 @@ function usageBudgetControl(enforcement: EnforcementControl): UsageBudgetControl
|
|
|
58
69
|
const candidate = enforcement as EnforcementControl & Partial<UsageBudgetControl>;
|
|
59
70
|
return typeof candidate.getUsageTokenBudget === "function" && typeof candidate.setUsageTokenBudget === "function"
|
|
60
71
|
? {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
getUsageTokenBudget: (period) => candidate.getUsageTokenBudget!(period),
|
|
73
|
+
setUsageTokenBudget: (period, tokens) => candidate.setUsageTokenBudget!(period, tokens),
|
|
74
|
+
}
|
|
64
75
|
: { getUsageTokenBudget: () => undefined, setUsageTokenBudget() {} };
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -69,9 +80,9 @@ function recoveryControl(enforcement: EnforcementControl): CodexRecoveryControl
|
|
|
69
80
|
const set = (candidate as Partial<CodexRecoveryControl>).setCodexRecoveryEnabled;
|
|
70
81
|
return typeof candidate.isCodexRecoveryEnabled === "function" && typeof set === "function"
|
|
71
82
|
? {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
isCodexRecoveryEnabled: () => candidate.isCodexRecoveryEnabled!(),
|
|
84
|
+
setCodexRecoveryEnabled: (enabled) => set.call(candidate, enabled),
|
|
85
|
+
}
|
|
75
86
|
: { isCodexRecoveryEnabled: () => false, setCodexRecoveryEnabled() {} };
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -84,9 +95,9 @@ async function recordMetrics(client: JittorExtensionClient, metrics: MetricObser
|
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
async function refreshFooter(client: JittorExtensionClient, state: IntegratedFooterState, sessionId: string): Promise<void> {
|
|
87
|
-
const status = await client.call("router.status", { session_id: sessionId }) as RouterStatus;
|
|
98
|
+
const status = (await client.call("router.status", { session_id: sessionId })) as RouterStatus;
|
|
88
99
|
const query = providerBudgetMetricQuery(status);
|
|
89
|
-
const metrics = query ? await client.call("metrics.query", query) as StoredMetricObservation[] : [];
|
|
100
|
+
const metrics = query ? ((await client.call("metrics.query", query)) as StoredMetricObservation[]) : [];
|
|
90
101
|
state.providerBudget = buildFooterBudget(status, metrics);
|
|
91
102
|
state.requestRender?.();
|
|
92
103
|
}
|
|
@@ -95,10 +106,14 @@ function delay(milliseconds: number, signal?: AbortSignal): Promise<void> {
|
|
|
95
106
|
if (milliseconds <= 0) return Promise.resolve();
|
|
96
107
|
return new Promise((resolve, reject) => {
|
|
97
108
|
const timer = setTimeout(resolve, milliseconds);
|
|
98
|
-
signal?.addEventListener(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
signal?.addEventListener(
|
|
110
|
+
"abort",
|
|
111
|
+
() => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(new Error("Jittor throttle cancelled"));
|
|
114
|
+
},
|
|
115
|
+
{ once: true },
|
|
116
|
+
);
|
|
102
117
|
});
|
|
103
118
|
}
|
|
104
119
|
|
|
@@ -111,7 +126,7 @@ async function applyRoute(pi: ExtensionAPI, ctx: ExtensionContext, route: Route)
|
|
|
111
126
|
const model = ctx.modelRegistry.find(route.provider, route.model);
|
|
112
127
|
if (!model) return false;
|
|
113
128
|
if (!ctx.model || ctx.model.provider !== route.provider || ctx.model.id !== route.model) {
|
|
114
|
-
if (!await pi.setModel(model)) return false;
|
|
129
|
+
if (!(await pi.setModel(model))) return false;
|
|
115
130
|
}
|
|
116
131
|
if (THINKING_LEVELS.has(route.thinking)) pi.setThinkingLevel(route.thinking as Parameters<ExtensionAPI["setThinkingLevel"]>[0]);
|
|
117
132
|
return true;
|
|
@@ -139,7 +154,12 @@ function modelCost(model: PiRouteModel): number {
|
|
|
139
154
|
export function benchmarkCandidatesFromPi(models: PiRouteModel[], thinking: string): ModelCandidate[] {
|
|
140
155
|
const candidates: ModelCandidate[] = [];
|
|
141
156
|
for (const model of models) {
|
|
142
|
-
if (
|
|
157
|
+
if (
|
|
158
|
+
!model.provider ||
|
|
159
|
+
!model.id ||
|
|
160
|
+
candidates.some((candidate) => candidate.provider === model.provider && candidate.model === model.id)
|
|
161
|
+
)
|
|
162
|
+
continue;
|
|
143
163
|
const level = supportsThinking(model, thinking) ? thinking : "off";
|
|
144
164
|
candidates.push({ provider: model.provider, model: model.id, thinking: level });
|
|
145
165
|
if (candidates.length >= MAX_DYNAMIC_ROUTES) break;
|
|
@@ -150,13 +170,21 @@ export function benchmarkCandidatesFromPi(models: PiRouteModel[], thinking: stri
|
|
|
150
170
|
export function routesFromPi(models: PiRouteModel[], current: PiRouteModel, thinking: string): Route[] {
|
|
151
171
|
const catalog = models
|
|
152
172
|
.filter((model) => model.provider.length > 0 && model.id.length > 0)
|
|
153
|
-
.filter(
|
|
173
|
+
.filter(
|
|
174
|
+
(model, index, rows) => rows.findIndex((candidate) => candidate.provider === model.provider && candidate.id === model.id) === index,
|
|
175
|
+
);
|
|
154
176
|
if (!catalog.some((model) => model.provider === current.provider && model.id === current.id)) catalog.push(current);
|
|
155
|
-
const currentLevel = THINKING_DESCENDING.indexOf(thinking as typeof THINKING_DESCENDING[number]);
|
|
177
|
+
const currentLevel = THINKING_DESCENDING.indexOf(thinking as (typeof THINKING_DESCENDING)[number]);
|
|
156
178
|
const lowerLevels = THINKING_DESCENDING.slice(currentLevel >= 0 ? currentLevel + 1 : 0);
|
|
157
179
|
const routes: Route[] = [];
|
|
158
180
|
const add = (route: Route): void => {
|
|
159
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
routes.length >= MAX_DYNAMIC_ROUTES ||
|
|
183
|
+
routes.some(
|
|
184
|
+
(candidate) => candidate.provider === route.provider && candidate.model === route.model && candidate.thinking === route.thinking,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
return;
|
|
160
188
|
routes.push(route);
|
|
161
189
|
};
|
|
162
190
|
add({ provider: current.provider, model: current.id, thinking });
|
|
@@ -167,7 +195,12 @@ export function routesFromPi(models: PiRouteModel[], current: PiRouteModel, thin
|
|
|
167
195
|
.filter((model) => model.provider !== current.provider || model.id !== current.id)
|
|
168
196
|
.sort((left, right) => {
|
|
169
197
|
const providerPriority = Number(left.provider !== current.provider) - Number(right.provider !== current.provider);
|
|
170
|
-
return
|
|
198
|
+
return (
|
|
199
|
+
providerPriority ||
|
|
200
|
+
modelCost(left) - modelCost(right) ||
|
|
201
|
+
left.provider.localeCompare(right.provider) ||
|
|
202
|
+
left.id.localeCompare(right.id)
|
|
203
|
+
);
|
|
171
204
|
});
|
|
172
205
|
for (const model of alternatives) {
|
|
173
206
|
const level = [thinking, ...lowerLevels].find((candidate) => supportsThinking(model, candidate)) ?? "off";
|
|
@@ -179,7 +212,10 @@ export function routesFromPi(models: PiRouteModel[], current: PiRouteModel, thin
|
|
|
179
212
|
async function syncAvailableRoutes(pi: ExtensionAPI, client: JittorExtensionClient, ctx: ExtensionContext): Promise<void> {
|
|
180
213
|
const session_id = ctx.sessionManager.getSessionId();
|
|
181
214
|
const secret = sessionSecretField(session_id);
|
|
182
|
-
if (!ctx.model) {
|
|
215
|
+
if (!ctx.model) {
|
|
216
|
+
await client.call("router.available_routes", { routes: [], session_id, ...secret });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
183
219
|
const models = ctx.modelRegistry.getAvailable() as PiRouteModel[];
|
|
184
220
|
const routes = routesFromPi(models, ctx.model as PiRouteModel, pi.getThinkingLevel());
|
|
185
221
|
await client.call("router.available_routes", { routes, session_id, ...secret });
|
|
@@ -194,7 +230,13 @@ async function syncCurrentRoute(
|
|
|
194
230
|
): Promise<void> {
|
|
195
231
|
if (!model) return;
|
|
196
232
|
const session_id = ctx.sessionManager.getSessionId();
|
|
197
|
-
await client.call("router.current_route", {
|
|
233
|
+
await client.call("router.current_route", {
|
|
234
|
+
provider: model.provider,
|
|
235
|
+
model: model.id,
|
|
236
|
+
thinking,
|
|
237
|
+
session_id,
|
|
238
|
+
...sessionSecretField(session_id),
|
|
239
|
+
});
|
|
198
240
|
}
|
|
199
241
|
|
|
200
242
|
function halt(ctx: ExtensionContext, reason: string): false {
|
|
@@ -212,12 +254,21 @@ async function applyDecision(
|
|
|
212
254
|
): Promise<boolean> {
|
|
213
255
|
if (decision.action === "halt") return halt(ctx, `Jittor blocked this provider request: ${decision.reason}`);
|
|
214
256
|
if (decision.action === "throttle") await delay(decision.delayMs ?? 0, ctx.signal);
|
|
215
|
-
if (!decision.route || await applyRoute(pi, ctx, decision.route)) return true;
|
|
257
|
+
if (!decision.route || (await applyRoute(pi, ctx, decision.route))) return true;
|
|
216
258
|
if (allowResync) {
|
|
217
259
|
await syncAvailableRoutes(pi, client, ctx);
|
|
218
|
-
return applyDecision(
|
|
260
|
+
return applyDecision(
|
|
261
|
+
pi,
|
|
262
|
+
client,
|
|
263
|
+
ctx,
|
|
264
|
+
(await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision,
|
|
265
|
+
false,
|
|
266
|
+
);
|
|
219
267
|
}
|
|
220
|
-
return halt(
|
|
268
|
+
return halt(
|
|
269
|
+
ctx,
|
|
270
|
+
`Jittor could not apply any authenticated Pi route after ${decision.route.provider}/${decision.route.model} became unavailable`,
|
|
271
|
+
);
|
|
221
272
|
}
|
|
222
273
|
|
|
223
274
|
/**
|
|
@@ -225,22 +276,39 @@ async function applyDecision(
|
|
|
225
276
|
* comes from pi.getThinkingLevel() at message_end time, not from the message itself -- AssistantMessage
|
|
226
277
|
* has no thinking field of its own, and the level can't have changed mid-message.
|
|
227
278
|
*/
|
|
228
|
-
function assistantUsageMetrics(
|
|
279
|
+
function assistantUsageMetrics(
|
|
280
|
+
message: unknown,
|
|
281
|
+
observedAt: number,
|
|
282
|
+
taskId: string | null = null,
|
|
283
|
+
thinking: string | null = null,
|
|
284
|
+
): MetricObservation[] {
|
|
229
285
|
if (typeof message !== "object" || message === null || Array.isArray(message)) return [];
|
|
230
286
|
const value = message as Record<string, unknown>;
|
|
231
|
-
if (value
|
|
232
|
-
const usage = value
|
|
233
|
-
const provider = typeof value
|
|
234
|
-
const model = typeof value
|
|
287
|
+
if (value.role !== "assistant" || typeof value.usage !== "object" || value.usage === null) return [];
|
|
288
|
+
const usage = value.usage as Record<string, unknown>;
|
|
289
|
+
const provider = typeof value.provider === "string" ? value.provider : "unknown";
|
|
290
|
+
const model = typeof value.model === "string" ? value.model : "unknown";
|
|
235
291
|
const scope = `${provider}:${model}`;
|
|
236
|
-
const attributes = {
|
|
292
|
+
const attributes = {
|
|
293
|
+
provider,
|
|
294
|
+
model,
|
|
295
|
+
...(taskId === null ? {} : { taskId }),
|
|
296
|
+
...(thinking === null || thinking.length === 0 ? {} : { thinking }),
|
|
297
|
+
};
|
|
237
298
|
const metrics: MetricObservation[] = [];
|
|
238
|
-
for (const [field, metric] of [
|
|
299
|
+
for (const [field, metric] of [
|
|
300
|
+
["input", "input-tokens"],
|
|
301
|
+
["output", "output-tokens"],
|
|
302
|
+
["cacheRead", "cache-read-tokens"],
|
|
303
|
+
["cacheWrite", "cache-write-tokens"],
|
|
304
|
+
] as const) {
|
|
239
305
|
const amount = usage[field];
|
|
240
|
-
if (typeof amount === "number" && Number.isFinite(amount))
|
|
306
|
+
if (typeof amount === "number" && Number.isFinite(amount))
|
|
307
|
+
metrics.push({ source: "pi", scope, metric, value: amount, unit: "tokens", observedAt, attributes });
|
|
241
308
|
}
|
|
242
|
-
const cost = typeof usage
|
|
243
|
-
if (typeof cost === "number" && Number.isFinite(cost))
|
|
309
|
+
const cost = typeof usage.cost === "object" && usage.cost !== null ? (usage.cost as Record<string, unknown>).total : undefined;
|
|
310
|
+
if (typeof cost === "number" && Number.isFinite(cost))
|
|
311
|
+
metrics.push({ source: "pi", scope, metric: "cost", value: cost, unit: "usd", observedAt, attributes });
|
|
244
312
|
return metrics;
|
|
245
313
|
}
|
|
246
314
|
|
|
@@ -259,6 +327,12 @@ export function registerJittorExtension(
|
|
|
259
327
|
const codexRecoveryCapability = new CodexRecoveryCapability(pi, codexRecovery, recoveryRuntime);
|
|
260
328
|
const contextHub = new ContextHubCapability();
|
|
261
329
|
const stopContextHub = pi.events?.on?.(CONTEXT_HUB_CONTRIBUTION_CHANNEL, (payload) => contextHub.observe(payload));
|
|
330
|
+
// Cached from the most recent before_agent_start observation: Pi's own base system prompt is
|
|
331
|
+
// only ever visible transiently inside that hook's event, so /context reuses this rather than
|
|
332
|
+
// going without it entirely. Measured as of THIS extension's own place in the before_agent_start
|
|
333
|
+
// chain -- see buildBasePromptItems' own doc comment for the load-order caveat this implies.
|
|
334
|
+
let lastObservedBasePromptTokens: number | null = null;
|
|
335
|
+
let lastObservedBasePromptItems: ContextSegmentItem[] = [];
|
|
262
336
|
const contextObservations = new Set<string>();
|
|
263
337
|
const stopPapyrusContext = pi.events?.on?.(PAPYRUS_CONTEXT_INJECTION_CHANNEL, (payload) => {
|
|
264
338
|
try {
|
|
@@ -311,11 +385,14 @@ export function registerJittorExtension(
|
|
|
311
385
|
// Non-blocking: compaction UI starts immediately as cold-start; if a learned estimate resolves
|
|
312
386
|
// before this compaction finishes (and this is still the active compaction, not a later one),
|
|
313
387
|
// upgrade the same progress object in place so the drain bar and status text switch to "learned".
|
|
314
|
-
void client
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
388
|
+
void client
|
|
389
|
+
.call("compaction.estimate", {})
|
|
390
|
+
.then((estimate) => {
|
|
391
|
+
if (footerState.compaction !== compaction || estimate.confidence !== "learned" || estimate.ms === null) return;
|
|
392
|
+
footerState.compaction = { ...compaction, estimatedMs: estimate.ms, confidence: "learned" };
|
|
393
|
+
footerState.requestRender?.();
|
|
394
|
+
})
|
|
395
|
+
.catch(() => undefined);
|
|
319
396
|
compactionTimer = setInterval(() => footerState.requestRender?.(), FOOTER_COMPACTION_RENDER_INTERVAL_MS);
|
|
320
397
|
signal.addEventListener("abort", finishCompactionUi, { once: true });
|
|
321
398
|
if (signal.aborted) finishCompactionUi();
|
|
@@ -329,14 +406,17 @@ export function registerJittorExtension(
|
|
|
329
406
|
enforcement.setEnabled(false);
|
|
330
407
|
ctx.ui.setStatus("jittor", undefined);
|
|
331
408
|
showFooter(ctx);
|
|
332
|
-
ctx.ui.notify(
|
|
409
|
+
ctx.ui.notify(
|
|
410
|
+
"Jittor enforcement is off (monitor-only); the informational footer remains independent and provider requests will not be blocked.",
|
|
411
|
+
"warning",
|
|
412
|
+
);
|
|
333
413
|
};
|
|
334
414
|
const enable = async (ctx: ExtensionContext): Promise<void> => {
|
|
335
415
|
try {
|
|
336
416
|
await syncCurrentRoute(pi, client, ctx);
|
|
337
417
|
await syncAvailableRoutes(pi, client, ctx);
|
|
338
418
|
await client.call("telemetry.poll", {});
|
|
339
|
-
const readinessDecision = await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision;
|
|
419
|
+
const readinessDecision = (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision;
|
|
340
420
|
if (readinessDecision.action === "halt") throw new Error(readinessDecision.reason);
|
|
341
421
|
enforcement.setEnabled(true);
|
|
342
422
|
showFooter(ctx);
|
|
@@ -356,7 +436,7 @@ export function registerJittorExtension(
|
|
|
356
436
|
const action = args.trim().toLowerCase();
|
|
357
437
|
if (action === "" || action === "settings") {
|
|
358
438
|
await showSettingsPanel(ctx, enforcement, codexRecovery, usageBudgets, {
|
|
359
|
-
setEnforcement: async (enabled) => enabled ? enable(ctx) : disable(ctx),
|
|
439
|
+
setEnforcement: async (enabled) => (enabled ? enable(ctx) : disable(ctx)),
|
|
360
440
|
setFooter: async (enabled) => {
|
|
361
441
|
enforcement.setFooterEnabled(enabled);
|
|
362
442
|
showFooter(ctx);
|
|
@@ -392,11 +472,18 @@ export function registerJittorExtension(
|
|
|
392
472
|
return;
|
|
393
473
|
}
|
|
394
474
|
const candidates = benchmarkCandidatesFromPi(ctx.modelRegistry.getAvailable() as PiRouteModel[], pi.getThinkingLevel());
|
|
395
|
-
await showBenchmarkPanel(
|
|
475
|
+
await showBenchmarkPanel(
|
|
476
|
+
ctx,
|
|
477
|
+
client,
|
|
478
|
+
candidates,
|
|
479
|
+
`${ctx.model.provider}/${ctx.model.id}`,
|
|
480
|
+
requestedDomain ?? "general",
|
|
481
|
+
requestedType ?? "general",
|
|
482
|
+
);
|
|
396
483
|
return;
|
|
397
484
|
}
|
|
398
485
|
if (action === "outcome accepted" || action === "outcome rejected") {
|
|
399
|
-
const explicitOutcome = action.endsWith("accepted") ? "accepted" as const : "rejected" as const;
|
|
486
|
+
const explicitOutcome = action.endsWith("accepted") ? ("accepted" as const) : ("rejected" as const);
|
|
400
487
|
const outcomeMetric = localRunTelemetry.explicitOutcomeMetric(explicitOutcome);
|
|
401
488
|
if (!outcomeMetric) {
|
|
402
489
|
ctx.ui.notify("No completed local model run is available for an explicit outcome.", "warning");
|
|
@@ -424,11 +511,20 @@ export function registerJittorExtension(
|
|
|
424
511
|
}
|
|
425
512
|
if (action === "recovery cancel") {
|
|
426
513
|
cancelRecovery(true);
|
|
427
|
-
ctx.ui.notify(
|
|
514
|
+
ctx.ui.notify(
|
|
515
|
+
`Jittor Codex recovery cooldown and attempt window cleared; recovery remains ${codexRecovery.isCodexRecoveryEnabled() ? "on" : "off"}.`,
|
|
516
|
+
"info",
|
|
517
|
+
);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (action === "off" || action === "disable") {
|
|
521
|
+
disable(ctx);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (action === "on" || action === "enable") {
|
|
525
|
+
await enable(ctx);
|
|
428
526
|
return;
|
|
429
527
|
}
|
|
430
|
-
if (action === "off" || action === "disable") { disable(ctx); return; }
|
|
431
|
-
if (action === "on" || action === "enable") { await enable(ctx); return; }
|
|
432
528
|
if (action === "footer off" || action === "footer disable") {
|
|
433
529
|
enforcement.setFooterEnabled(false);
|
|
434
530
|
ctx.ui.setFooter(undefined);
|
|
@@ -443,15 +539,19 @@ export function registerJittorExtension(
|
|
|
443
539
|
return;
|
|
444
540
|
}
|
|
445
541
|
if (action === "context") {
|
|
446
|
-
const summary = await client.call("context.assess", {}) as ContextAssessment;
|
|
447
|
-
const average =
|
|
542
|
+
const summary = (await client.call("context.assess", {})) as ContextAssessment;
|
|
543
|
+
const average =
|
|
544
|
+
summary.injection.averageCharacters === null ? "unknown" : Math.round(summary.injection.averageCharacters).toLocaleString();
|
|
448
545
|
const p95 = summary.injection.p95Characters === null ? "unknown" : Math.round(summary.injection.p95Characters).toLocaleString();
|
|
449
|
-
ctx.ui.notify(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
546
|
+
ctx.ui.notify(
|
|
547
|
+
[
|
|
548
|
+
`Papyrus injection: ${summary.injection.runs} runs · avg ${average} chars · p95 ${p95} chars · unchanged ${summary.injection.unchangedRate === null ? "unknown" : `${(summary.injection.unchangedRate * 100).toFixed(1)}%`}`,
|
|
549
|
+
`Mix: rules ${summary.injection.ruleCharacters.toLocaleString()} chars · tasks ${summary.injection.taskCharacters.toLocaleString()} chars · estimated ${summary.injection.estimatedTokens.toLocaleString()} tokens`,
|
|
550
|
+
`Compactions: ${summary.compaction.completed} completed · ${summary.compaction.aborted} aborted · ${summary.compaction.perRun === null ? "unknown" : summary.compaction.perRun.toFixed(3)} per agent run · ${summary.compaction.perTurn === null ? "unknown" : summary.compaction.perTurn.toFixed(3)} per turn`,
|
|
551
|
+
`Completeness: ${summary.completeness}`,
|
|
552
|
+
].join("\n"),
|
|
553
|
+
"info",
|
|
554
|
+
);
|
|
455
555
|
return;
|
|
456
556
|
}
|
|
457
557
|
// Reached only for the explicit "status" keyword or any other unrecognized text; bare "" is
|
|
@@ -465,11 +565,31 @@ export function registerJittorExtension(
|
|
|
465
565
|
});
|
|
466
566
|
|
|
467
567
|
pi.registerCommand("context", {
|
|
468
|
-
description:
|
|
568
|
+
description:
|
|
569
|
+
"Context Hub: real usage plus every segment's estimated size (base prompt, message history, tool schemas by owning extension, and whatever other extensions contributed), each tagged with how it was attributed",
|
|
469
570
|
handler: async (_args, ctx) => {
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
|
|
571
|
+
const activeToolNames = new Set(pi.getActiveTools());
|
|
572
|
+
const toolSegment = toolLedgerSegment(pi.getAllTools().filter((tool) => activeToolNames.has(tool.name)));
|
|
573
|
+
// Real tree (not just the linear current-branch path): surfaces content sitting in an
|
|
574
|
+
// abandoned /tree branch, which cost real tokens to generate but isn't in context now.
|
|
575
|
+
const tree = ctx.sessionManager.getTree() as SessionTreeNodeLike[];
|
|
576
|
+
// buildContextEntries(), NOT getBranch(): getBranch() returns every raw entry on the current
|
|
577
|
+
// path including everything a real compaction has already summarized away.
|
|
578
|
+
const activeEntryIds = new Set((ctx.sessionManager.buildContextEntries() as SessionEntryLike[]).map((entry) => entry.id));
|
|
579
|
+
const branchEntryIds = new Set((ctx.sessionManager.getBranch() as SessionEntryLike[]).map((entry) => entry.id));
|
|
580
|
+
const messageHistory = buildMessageHistoryTree(tree, activeEntryIds, branchEntryIds);
|
|
581
|
+
const usage = ctx.getContextUsage();
|
|
582
|
+
const ownSegments = [
|
|
583
|
+
basePromptSegment(lastObservedBasePromptTokens, lastObservedBasePromptItems),
|
|
584
|
+
messageHistorySegment(messageHistory),
|
|
585
|
+
toolSegment,
|
|
586
|
+
];
|
|
587
|
+
const breakdown = composeContextBreakdown({
|
|
588
|
+
totalTokens: usage?.tokens ?? null,
|
|
589
|
+
contextWindow: ctx.model?.contextWindow ?? null,
|
|
590
|
+
segments: [...ownSegments, ...contextHub.contributedSegments()],
|
|
591
|
+
});
|
|
592
|
+
await showContextView(ctx, breakdown);
|
|
473
593
|
},
|
|
474
594
|
});
|
|
475
595
|
|
|
@@ -479,14 +599,19 @@ export function registerJittorExtension(
|
|
|
479
599
|
const action = args.trim().toLowerCase();
|
|
480
600
|
if (action === "budget" || action.startsWith("budget ")) {
|
|
481
601
|
const [, periodText, valueText] = action.split(/\s+/);
|
|
482
|
-
const period = USAGE_PERIODS.some((candidate) => candidate.id === periodText) ? periodText as UsagePeriod : undefined;
|
|
602
|
+
const period = USAGE_PERIODS.some((candidate) => candidate.id === periodText) ? (periodText as UsagePeriod) : undefined;
|
|
483
603
|
if (!period) {
|
|
484
|
-
const values = USAGE_PERIODS.map(
|
|
604
|
+
const values = USAGE_PERIODS.map(
|
|
605
|
+
({ id, label }) => `${label}: ${usageBudgets.getUsageTokenBudget(id)?.toLocaleString() ?? "not configured"}`,
|
|
606
|
+
).join(" · ");
|
|
485
607
|
ctx.ui.notify(`Token budgets · ${values}`, "info");
|
|
486
608
|
return;
|
|
487
609
|
}
|
|
488
610
|
if (valueText === undefined) {
|
|
489
|
-
ctx.ui.notify(
|
|
611
|
+
ctx.ui.notify(
|
|
612
|
+
`${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget: ${usageBudgets.getUsageTokenBudget(period)?.toLocaleString() ?? "not configured"}`,
|
|
613
|
+
"info",
|
|
614
|
+
);
|
|
490
615
|
return;
|
|
491
616
|
}
|
|
492
617
|
if (valueText === "off" || valueText === "clear") {
|
|
@@ -500,7 +625,10 @@ export function registerJittorExtension(
|
|
|
500
625
|
return;
|
|
501
626
|
}
|
|
502
627
|
usageBudgets.setUsageTokenBudget(period, tokens);
|
|
503
|
-
ctx.ui.notify(
|
|
628
|
+
ctx.ui.notify(
|
|
629
|
+
`${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget set to ${tokens.toLocaleString()} tokens.`,
|
|
630
|
+
"info",
|
|
631
|
+
);
|
|
504
632
|
return;
|
|
505
633
|
}
|
|
506
634
|
if (action !== "" && action !== "cost" && action !== "tokens") {
|
|
@@ -542,6 +670,15 @@ export function registerJittorExtension(
|
|
|
542
670
|
}
|
|
543
671
|
});
|
|
544
672
|
|
|
673
|
+
pi.on("before_agent_start", async (event) => {
|
|
674
|
+
// No new hook, no new risk: measures event.systemPrompt's length and structural
|
|
675
|
+
// event.systemPromptOptions as-of this handler's own place in the before_agent_start chain --
|
|
676
|
+
// see buildBasePromptItems' own doc comment for the resulting load-order caveat.
|
|
677
|
+
const characters = (event.systemPrompt ?? "").length;
|
|
678
|
+
lastObservedBasePromptTokens = Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN);
|
|
679
|
+
lastObservedBasePromptItems = buildBasePromptItems(event.systemPromptOptions, characters);
|
|
680
|
+
});
|
|
681
|
+
|
|
545
682
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
546
683
|
beginCompactionUi(ctx, event.signal);
|
|
547
684
|
const usage = ctx.getContextUsage();
|
|
@@ -556,13 +693,16 @@ export function registerJittorExtension(
|
|
|
556
693
|
|
|
557
694
|
pi.on("session_compact", async (event) => {
|
|
558
695
|
finishCompactionUi();
|
|
559
|
-
await recordMetrics(client, [compactionTelemetry.complete({ reason: event.reason, willRetry: event.willRetry })]).catch(
|
|
696
|
+
await recordMetrics(client, [compactionTelemetry.complete({ reason: event.reason, willRetry: event.willRetry })]).catch(
|
|
697
|
+
() => undefined,
|
|
698
|
+
);
|
|
560
699
|
});
|
|
561
700
|
|
|
562
701
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
563
702
|
if (footerState.compaction) {
|
|
564
703
|
finishCompactionUi();
|
|
565
|
-
if (compactionTelemetry.hasOpenCompaction())
|
|
704
|
+
if (compactionTelemetry.hasOpenCompaction())
|
|
705
|
+
await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "agent-settled-without-completion")]).catch(() => undefined);
|
|
566
706
|
}
|
|
567
707
|
scheduleCodexRecovery(ctx);
|
|
568
708
|
if (!enforcement.isFooterEnabled()) return;
|
|
@@ -580,7 +720,7 @@ export function registerJittorExtension(
|
|
|
580
720
|
if (event.source !== "extension") cancelRecovery(true);
|
|
581
721
|
if (event.source === "extension" || !enforcement.isEnabled()) return { action: "continue" as const };
|
|
582
722
|
try {
|
|
583
|
-
const next = await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision;
|
|
723
|
+
const next = (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision;
|
|
584
724
|
if (next.action === "halt") {
|
|
585
725
|
ctx.ui.notify(`Jittor blocked input: ${next.reason}. ${RECOVERY_GUIDANCE}.`, "warning");
|
|
586
726
|
return { action: "handled" as const };
|
|
@@ -593,7 +733,9 @@ export function registerJittorExtension(
|
|
|
593
733
|
});
|
|
594
734
|
|
|
595
735
|
pi.on("model_select", async (event, ctx) => {
|
|
596
|
-
await syncCurrentRoute(pi, client, ctx, event.model)
|
|
736
|
+
await syncCurrentRoute(pi, client, ctx, event.model)
|
|
737
|
+
.then(() => syncAvailableRoutes(pi, client, ctx))
|
|
738
|
+
.catch(() => undefined);
|
|
597
739
|
if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
|
|
598
740
|
});
|
|
599
741
|
|
|
@@ -611,7 +753,12 @@ export function registerJittorExtension(
|
|
|
611
753
|
try {
|
|
612
754
|
await syncCurrentRoute(pi, client, ctx);
|
|
613
755
|
await syncAvailableRoutes(pi, client, ctx);
|
|
614
|
-
await applyDecision(
|
|
756
|
+
await applyDecision(
|
|
757
|
+
pi,
|
|
758
|
+
client,
|
|
759
|
+
ctx,
|
|
760
|
+
(await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision,
|
|
761
|
+
);
|
|
615
762
|
await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
|
|
616
763
|
} catch {
|
|
617
764
|
halt(ctx, "Jittor could not verify or apply a safe route");
|
|
@@ -629,7 +776,9 @@ export function registerJittorExtension(
|
|
|
629
776
|
pi.on("after_provider_response", async (event, ctx) => {
|
|
630
777
|
localRunTelemetry.onProviderResponse();
|
|
631
778
|
if (ctx.model?.provider === "openai-codex") codexRecoveryCapability.notifyResponse(event.status, event.headers);
|
|
632
|
-
const notifySchemaDrift = (message: string) => {
|
|
779
|
+
const notifySchemaDrift = (message: string) => {
|
|
780
|
+
if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected ${message}. ${RECOVERY_GUIDANCE}.`, "error");
|
|
781
|
+
};
|
|
633
782
|
await providerResponseTelemetry.handleProviderResponse(client, ctx.model?.provider, event.status, event.headers, notifySchemaDrift);
|
|
634
783
|
if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
|
|
635
784
|
});
|
|
@@ -641,13 +790,27 @@ export function registerJittorExtension(
|
|
|
641
790
|
|
|
642
791
|
pi.on("message_end", async (event, ctx) => {
|
|
643
792
|
if (event.message.role === "assistant") {
|
|
644
|
-
if (event.message.provider === "openai-codex")
|
|
645
|
-
|
|
793
|
+
if (event.message.provider === "openai-codex")
|
|
794
|
+
codexRecoveryCapability.notifyMessageEnd(event.message.stopReason, event.message.errorMessage);
|
|
795
|
+
await providerResponseTelemetry.handleMessageEnd(
|
|
796
|
+
client,
|
|
797
|
+
event.message.provider,
|
|
798
|
+
event.message.stopReason,
|
|
799
|
+
event.message.errorMessage,
|
|
800
|
+
);
|
|
646
801
|
}
|
|
647
802
|
const metrics = assistantUsageMetrics(event.message, Date.now(), focusedTaskId, pi.getThinkingLevel());
|
|
648
803
|
if (metrics.length > 0) {
|
|
649
|
-
const amount = (name: string): number =>
|
|
650
|
-
|
|
804
|
+
const amount = (name: string): number =>
|
|
805
|
+
metrics
|
|
806
|
+
.filter((metric) => metric.metric === name && typeof metric.value === "number")
|
|
807
|
+
.reduce((sum, metric) => sum + (metric.value ?? 0), 0);
|
|
808
|
+
compactionTelemetry.observeProviderUsage({
|
|
809
|
+
input: amount("input-tokens"),
|
|
810
|
+
output: amount("output-tokens"),
|
|
811
|
+
cacheRead: amount("cache-read-tokens"),
|
|
812
|
+
cacheWrite: amount("cache-write-tokens"),
|
|
813
|
+
});
|
|
651
814
|
await recordMetrics(client, metrics).catch(() => undefined);
|
|
652
815
|
}
|
|
653
816
|
if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
|
|
@@ -655,7 +818,8 @@ export function registerJittorExtension(
|
|
|
655
818
|
|
|
656
819
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
657
820
|
finishCompactionUi();
|
|
658
|
-
if (compactionTelemetry.hasOpenCompaction())
|
|
821
|
+
if (compactionTelemetry.hasOpenCompaction())
|
|
822
|
+
await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "session-shutdown")]).catch(() => undefined);
|
|
659
823
|
stopPapyrusContext?.();
|
|
660
824
|
stopPapyrusTaskFocus?.();
|
|
661
825
|
stopContextHub?.();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createRetryingClient, type RetryingClient } from "@danypops/vehicle-client/daemon-client";
|
|
2
1
|
import { connectJittorClient, type JittorClient, type OperationInputs, type OperationName, type OperationOutputs } from "@danypops/jittor";
|
|
2
|
+
import { createRetryingClient, type RetryingClient } from "@danypops/vehicle-client/daemon-client";
|
|
3
3
|
|
|
4
4
|
type JittorConnector = () => Promise<JittorClient>;
|
|
5
5
|
|