@danypops/pi-jittor 0.2.0 → 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.
Files changed (25) hide show
  1. package/README.md +19 -3
  2. package/docs/USAGE_PRIOR_ART.md +1 -1
  3. package/extension/src/index.ts +379 -118
  4. package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +251 -47
  5. package/extension/src/observability/context-growth.ts +26 -0
  6. package/extension/src/{capabilities → observability}/context-hub.ts +1 -5
  7. package/extension/src/observability/context-report.ts +92 -0
  8. package/extension/src/observability/context-view.ts +264 -0
  9. package/extension/src/{footer.ts → observability/footer.ts} +63 -26
  10. package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +14 -10
  11. package/extension/src/observability/provider-context-snapshot.ts +246 -0
  12. package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +21 -7
  13. package/extension/src/{tui.ts → observability/status.ts} +202 -72
  14. package/extension/src/observability/usage.ts +314 -0
  15. package/extension/src/optimization/model-selection-panel.ts +160 -0
  16. package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +41 -25
  17. package/extension/src/service-client.ts +49 -2
  18. package/extension/src/settings-tui.ts +73 -33
  19. package/extension/src/settings.ts +40 -29
  20. package/package.json +11 -5
  21. package/extension/src/benchmark-tui.ts +0 -113
  22. package/extension/src/context-report.ts +0 -49
  23. package/extension/src/context-view.ts +0 -108
  24. package/extension/src/usage.ts +0 -324
  25. /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
@@ -0,0 +1,246 @@
1
+ import {
2
+ CONTEXT_SNAPSHOT_MAX_SEGMENTS,
3
+ type ContextFingerprinter,
4
+ type ContextSegmentSource,
5
+ type ContextSnapshot,
6
+ countTextWithFallback,
7
+ StructuralTextTokenCounter,
8
+ type TextTokenCounter,
9
+ validateContextSnapshot,
10
+ } from "@danypops/jittor";
11
+
12
+ const CANONICAL_MAX_DEPTH = 16;
13
+ const CANONICAL_MAX_NODES = 10_000;
14
+ const CANONICAL_MAX_STRING_CHARACTERS = 65_536;
15
+ const CANONICAL_MAX_COLLECTION_ITEMS = 256;
16
+
17
+ export interface ProviderContextHistoryEntry {
18
+ id: string;
19
+ type: string;
20
+ message?: unknown;
21
+ summary?: string;
22
+ }
23
+
24
+ export interface ProviderContextHistoryNode {
25
+ entry: ProviderContextHistoryEntry;
26
+ children: ProviderContextHistoryNode[];
27
+ }
28
+
29
+ export interface ProviderContextHistory {
30
+ roots: readonly ProviderContextHistoryNode[];
31
+ /** Pi buildContextEntries(): compaction-aware entries that contribute to the request now. */
32
+ activeEntryIds: ReadonlySet<string>;
33
+ /** Pi getBranch(): raw current branch, including entries summarized away by compaction. */
34
+ branchEntryIds: ReadonlySet<string>;
35
+ }
36
+
37
+ export interface ProviderContextSnapshotInput {
38
+ /** Final provider payload. Ephemeral: this function returns only keyed fingerprints and sizes. */
39
+ payload: unknown;
40
+ captureId: string;
41
+ sessionId: string;
42
+ provider: string;
43
+ model: string;
44
+ capturedAt: number;
45
+ fingerprinter: ContextFingerprinter;
46
+ counters?: readonly TextTokenCounter[];
47
+ history?: ProviderContextHistory;
48
+ }
49
+
50
+ interface CanonicalValue {
51
+ text: string;
52
+ truncated: boolean;
53
+ }
54
+
55
+ interface CanonicalState {
56
+ nodes: number;
57
+ truncated: boolean;
58
+ seen: WeakSet<object>;
59
+ }
60
+
61
+ function boundedString(value: string, state: CanonicalState): string {
62
+ if (value.length <= CANONICAL_MAX_STRING_CHARACTERS) return JSON.stringify(value);
63
+ state.truncated = true;
64
+ const half = Math.floor(CANONICAL_MAX_STRING_CHARACTERS / 2);
65
+ return JSON.stringify(`${value.slice(0, half)}…[${value.length} chars]…${value.slice(-half)}`);
66
+ }
67
+
68
+ function canonical(value: unknown, depth: number, state: CanonicalState): string {
69
+ state.nodes += 1;
70
+ if (state.nodes > CANONICAL_MAX_NODES || depth > CANONICAL_MAX_DEPTH) {
71
+ state.truncated = true;
72
+ return '"[bounded]"';
73
+ }
74
+ if (value === null) return "null";
75
+ if (typeof value === "string") return boundedString(value, state);
76
+ if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value) : '"[non-finite]"';
77
+ if (typeof value === "boolean") return value ? "true" : "false";
78
+ if (typeof value === "bigint") return JSON.stringify(`${value.toString()}n`);
79
+ if (typeof value !== "object") return JSON.stringify(`[${typeof value}]`);
80
+ if (state.seen.has(value)) {
81
+ state.truncated = true;
82
+ return '"[cycle]"';
83
+ }
84
+ state.seen.add(value);
85
+ try {
86
+ if (Array.isArray(value)) {
87
+ if (value.length > CANONICAL_MAX_COLLECTION_ITEMS) state.truncated = true;
88
+ const shown = value.slice(0, CANONICAL_MAX_COLLECTION_ITEMS).map((item) => canonical(item, depth + 1, state));
89
+ if (value.length > shown.length) shown.push(JSON.stringify(`[${value.length - shown.length} omitted]`));
90
+ return `[${shown.join(",")}]`;
91
+ }
92
+ const record = value as Record<string, unknown>;
93
+ const keys = Object.keys(record).sort();
94
+ if (keys.length > CANONICAL_MAX_COLLECTION_ITEMS) state.truncated = true;
95
+ const shown = keys
96
+ .slice(0, CANONICAL_MAX_COLLECTION_ITEMS)
97
+ .map((key) => `${JSON.stringify(key)}:${canonical(record[key], depth + 1, state)}`);
98
+ if (keys.length > shown.length) shown.push(`${JSON.stringify("[omitted]")}:${keys.length - shown.length}`);
99
+ return `{${shown.join(",")}}`;
100
+ } finally {
101
+ state.seen.delete(value);
102
+ }
103
+ }
104
+
105
+ function boundedCanonical(value: unknown): CanonicalValue {
106
+ const state: CanonicalState = { nodes: 0, truncated: false, seen: new WeakSet() };
107
+ return { text: canonical(value, 0, state), truncated: state.truncated };
108
+ }
109
+
110
+ function objectRecord(value: unknown): Record<string, unknown> | null {
111
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
112
+ }
113
+
114
+ function blockSource(block: unknown, role: unknown): ContextSegmentSource {
115
+ if (role === "tool" || role === "function") return "tool-result";
116
+ const record = objectRecord(block);
117
+ const type = typeof record?.type === "string" ? record.type.toLowerCase() : "";
118
+ if (type.includes("thinking") || type.includes("reasoning")) return "thinking";
119
+ if (type.includes("tool_result") || type.includes("tool-result") || type.includes("function_call_output")) return "tool-result";
120
+ if (type.includes("tool_use") || type.includes("tool-use") || type.includes("function_call")) return "tool-call";
121
+ if (role === "system" || role === "developer") return "base-prompt";
122
+ return "conversation-message";
123
+ }
124
+
125
+ function imageLike(value: unknown): boolean {
126
+ const record = objectRecord(value);
127
+ if (!record || typeof record.type !== "string") return false;
128
+ return record.type.toLowerCase().includes("image");
129
+ }
130
+
131
+ /**
132
+ * Extracts an ordered, provider-neutral structural view from common chat/responses payload shapes.
133
+ * Unknown shapes remain one conversation segment; the payload is never returned or retained.
134
+ */
135
+ export function captureProviderContextSnapshot(input: ProviderContextSnapshotInput): ContextSnapshot {
136
+ const segments: ContextSnapshot["segments"] = [];
137
+ let truncated = false;
138
+ const fallback = new StructuralTextTokenCounter();
139
+ const push = (
140
+ value: unknown,
141
+ source: ContextSegmentSource,
142
+ logicalPath: string,
143
+ state: ContextSnapshot["segments"][number]["state"] = "active",
144
+ ): void => {
145
+ if (segments.length >= CONTEXT_SNAPSHOT_MAX_SEGMENTS) {
146
+ truncated = true;
147
+ return;
148
+ }
149
+ const encoded = boundedCanonical(value);
150
+ truncated ||= encoded.truncated;
151
+ const measurement = imageLike(value)
152
+ ? null
153
+ : countTextWithFallback(
154
+ { text: encoded.text, scope: "context-item", provider: input.provider, model: input.model },
155
+ input.counters ?? [],
156
+ fallback,
157
+ );
158
+ segments.push({
159
+ id: input.fingerprinter.fingerprint(`segment:${source}:${logicalPath}`),
160
+ fingerprint: input.fingerprinter.fingerprint(`value:${encoded.text}`),
161
+ source,
162
+ tokens: measurement?.tokens ?? 0,
163
+ state,
164
+ requestPosition: state === "active" ? segments.filter((segment) => segment.requestPosition !== null).length : null,
165
+ });
166
+ };
167
+
168
+ const payload = objectRecord(input.payload);
169
+ if (!payload) {
170
+ push(input.payload, "conversation-message", "payload");
171
+ } else {
172
+ for (const key of ["instructions", "system", "system_instruction"] as const) {
173
+ if (payload[key] !== undefined) push(payload[key], "base-prompt", key);
174
+ }
175
+ const tools = Array.isArray(payload.tools) ? payload.tools : Array.isArray(payload.functions) ? payload.functions : [];
176
+ for (let index = 0; index < tools.length; index++) push(tools[index], "tool-definitions", `tools/${index}`);
177
+
178
+ const conversation = Array.isArray(payload.messages)
179
+ ? payload.messages
180
+ : Array.isArray(payload.input)
181
+ ? payload.input
182
+ : Array.isArray(payload.contents)
183
+ ? payload.contents
184
+ : payload.input === undefined
185
+ ? []
186
+ : [payload.input];
187
+ for (let messageIndex = 0; messageIndex < conversation.length; messageIndex++) {
188
+ const message = conversation[messageIndex];
189
+ const record = objectRecord(message);
190
+ const role = record?.role;
191
+ const content = record?.content ?? record?.parts;
192
+ if (Array.isArray(content)) {
193
+ for (let blockIndex = 0; blockIndex < content.length; blockIndex++) {
194
+ push(content[blockIndex], blockSource(content[blockIndex], role), `messages/${messageIndex}/content/${blockIndex}`);
195
+ }
196
+ } else if (content !== undefined) {
197
+ push(content, blockSource(message, role), `messages/${messageIndex}/content`);
198
+ } else {
199
+ push(message, blockSource(message, role), `messages/${messageIndex}`);
200
+ }
201
+ }
202
+ }
203
+
204
+ if (input.history) {
205
+ const visited = new Set<string>();
206
+ const stack = [...input.history.roots].reverse();
207
+ while (stack.length > 0) {
208
+ if (segments.length >= CONTEXT_SNAPSHOT_MAX_SEGMENTS) {
209
+ truncated = true;
210
+ break;
211
+ }
212
+ const node = stack.pop()!;
213
+ if (visited.has(node.entry.id)) {
214
+ truncated = true;
215
+ continue;
216
+ }
217
+ visited.add(node.entry.id);
218
+ stack.push(...[...node.children].reverse());
219
+ if (input.history.activeEntryIds.has(node.entry.id)) continue;
220
+ const state = input.history.branchEntryIds.has(node.entry.id) ? "compacted" : "inactive";
221
+ const message = objectRecord(node.entry.message);
222
+ const role = message?.role;
223
+ const content = message?.content ?? message?.parts;
224
+ if (Array.isArray(content)) {
225
+ for (let index = 0; index < content.length; index++)
226
+ push(content[index], blockSource(content[index], role), `history/${node.entry.id}/${index}`, state);
227
+ } else if (content !== undefined) {
228
+ push(content, blockSource(node.entry.message, role), `history/${node.entry.id}`, state);
229
+ } else if (node.entry.summary !== undefined) {
230
+ push(node.entry.summary, "conversation-message", `history/${node.entry.id}/summary`, state);
231
+ }
232
+ }
233
+ if (stack.length > 0) truncated = true;
234
+ }
235
+
236
+ return validateContextSnapshot({
237
+ version: 1,
238
+ snapshotId: input.fingerprinter.fingerprint(`snapshot:${input.sessionId}:${input.captureId}`),
239
+ sessionId: input.fingerprinter.fingerprint(`session:${input.sessionId}`),
240
+ provider: input.provider,
241
+ model: input.model,
242
+ capturedAt: input.capturedAt,
243
+ truncated,
244
+ segments,
245
+ });
246
+ }
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  classifyGoogleVertexFailure,
3
+ type GoogleVertexFailureMetadata,
3
4
  googleVertexFailureMetrics,
4
5
  hasAnthropicRateLimitHeaders,
6
+ type MetricObservation,
5
7
  parseAnthropicRateLimitHeaders,
6
8
  parseCodexRateLimitHeaders,
7
- type GoogleVertexFailureMetadata,
8
- type MetricObservation,
9
9
  } from "@danypops/jittor";
10
- import { headerValue } from "./http-headers.ts";
10
+ import { headerValue } from "../observability/http-headers.ts";
11
11
 
12
12
  export interface ProviderTelemetryClient {
13
13
  call(operation: string, input: unknown): Promise<any>;
@@ -67,22 +67,36 @@ export class ProviderResponseTelemetry {
67
67
  }
68
68
  // Well-evidenced regardless of headers: GCP's own quota system fronts this transport, so the
69
69
  // same failure classification as google-vertex applies below.
70
- this.lastAnthropicVertexResponse = { status, ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}) };
70
+ this.lastAnthropicVertexResponse = {
71
+ status,
72
+ ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}),
73
+ };
71
74
  }
72
75
  if (provider === "google-vertex") {
73
- this.lastGoogleVertexResponse = { status, ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}) };
76
+ this.lastGoogleVertexResponse = {
77
+ status,
78
+ ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}),
79
+ };
74
80
  }
75
81
  if (Object.keys(headers).some((name) => name.toLowerCase().startsWith("x-codex-"))) {
76
82
  try {
77
83
  const updates = parseCodexRateLimitHeaders(new Headers(headers), Date.now());
78
- await recordMetrics(client, updates.flatMap((update) => update.metrics));
84
+ await recordMetrics(
85
+ client,
86
+ updates.flatMap((update) => update.metrics),
87
+ );
79
88
  } catch {
80
89
  notifySchemaDrift("Codex telemetry schema drift");
81
90
  }
82
91
  }
83
92
  }
84
93
 
85
- async handleMessageEnd(client: ProviderTelemetryClient, provider: string | undefined, stopReason: string | undefined, errorMessage: string | undefined): Promise<void> {
94
+ async handleMessageEnd(
95
+ client: ProviderTelemetryClient,
96
+ provider: string | undefined,
97
+ stopReason: string | undefined,
98
+ errorMessage: string | undefined,
99
+ ): Promise<void> {
86
100
  if (provider === "google-vertex") {
87
101
  if (stopReason === "error") {
88
102
  const failure = classifyGoogleVertexFailure(errorMessage, this.lastGoogleVertexResponse);