@tt-a1i/openpi 0.4.0 → 0.6.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 +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +82 -6
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +138 -130
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -303
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
|
+
|
|
4
|
+
export const CACHE_DIAGNOSTICS_CHANNEL = "model-info:cache-diagnostics";
|
|
5
|
+
|
|
6
|
+
export const CACHE_WARM_MINIMUM_TOKENS = 2_048;
|
|
7
|
+
|
|
8
|
+
export type CacheSemantics =
|
|
9
|
+
| "explicit-prefix"
|
|
10
|
+
| "implicit-best-effort"
|
|
11
|
+
| "unknown";
|
|
12
|
+
|
|
13
|
+
export type CacheObservationKind =
|
|
14
|
+
| "first-turn"
|
|
15
|
+
| "cold"
|
|
16
|
+
| "warm"
|
|
17
|
+
| "partial-hit"
|
|
18
|
+
| "miss-after-warm-prefix"
|
|
19
|
+
| "unknown";
|
|
20
|
+
|
|
21
|
+
export type CacheCorrelation =
|
|
22
|
+
| "model-change"
|
|
23
|
+
| "thinking-change"
|
|
24
|
+
| "tool-surface-change"
|
|
25
|
+
| "system-prompt-change"
|
|
26
|
+
| "compaction"
|
|
27
|
+
| "branch-change";
|
|
28
|
+
|
|
29
|
+
export interface CacheTurnIdentity {
|
|
30
|
+
provider: string;
|
|
31
|
+
modelId: string;
|
|
32
|
+
thinking: string;
|
|
33
|
+
toolSurfaceFingerprint: string;
|
|
34
|
+
systemPromptFingerprint: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CacheTurnObservation {
|
|
38
|
+
turnIndex: number;
|
|
39
|
+
provider: string;
|
|
40
|
+
semantics: CacheSemantics;
|
|
41
|
+
kind: CacheObservationKind;
|
|
42
|
+
usage: {
|
|
43
|
+
input: number;
|
|
44
|
+
cacheRead: number;
|
|
45
|
+
cacheWrite: number;
|
|
46
|
+
promptTokens: number;
|
|
47
|
+
};
|
|
48
|
+
previousCacheRead: number | null;
|
|
49
|
+
reprocessedTokens: number | null;
|
|
50
|
+
correlations: CacheCorrelation[];
|
|
51
|
+
evidence: "observation";
|
|
52
|
+
verifiedCause: null;
|
|
53
|
+
explanation: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type TurnSample = {
|
|
57
|
+
identity: CacheTurnIdentity;
|
|
58
|
+
usage: CacheTurnObservation["usage"];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const IMPLICIT_CACHE_PROVIDERS = new Set([
|
|
62
|
+
"azure-openai-responses",
|
|
63
|
+
"google",
|
|
64
|
+
"google-antigravity",
|
|
65
|
+
"google-gemini-cli",
|
|
66
|
+
"openai",
|
|
67
|
+
"openai-codex",
|
|
68
|
+
"openai-responses",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
export function cacheSemanticsForProvider(provider: string): CacheSemantics {
|
|
72
|
+
const normalized = provider.trim().toLowerCase();
|
|
73
|
+
if (normalized === "anthropic") return "explicit-prefix";
|
|
74
|
+
if (IMPLICIT_CACHE_PROVIDERS.has(normalized)) return "implicit-best-effort";
|
|
75
|
+
return "unknown";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function fingerprintCacheSurface(value: unknown) {
|
|
79
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function finiteUsage(value: number) {
|
|
83
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function promptUsage(usage: Usage): CacheTurnObservation["usage"] {
|
|
87
|
+
const input = finiteUsage(usage.input);
|
|
88
|
+
const cacheRead = finiteUsage(usage.cacheRead);
|
|
89
|
+
const cacheWrite = finiteUsage(usage.cacheWrite);
|
|
90
|
+
return {
|
|
91
|
+
input,
|
|
92
|
+
cacheRead,
|
|
93
|
+
cacheWrite,
|
|
94
|
+
promptTokens: input + cacheRead + cacheWrite,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function identityCorrelations(
|
|
99
|
+
previous: CacheTurnIdentity,
|
|
100
|
+
current: CacheTurnIdentity,
|
|
101
|
+
) {
|
|
102
|
+
const correlations: CacheCorrelation[] = [];
|
|
103
|
+
if (
|
|
104
|
+
previous.provider !== current.provider ||
|
|
105
|
+
previous.modelId !== current.modelId
|
|
106
|
+
) {
|
|
107
|
+
correlations.push("model-change");
|
|
108
|
+
}
|
|
109
|
+
if (previous.thinking !== current.thinking) {
|
|
110
|
+
correlations.push("thinking-change");
|
|
111
|
+
}
|
|
112
|
+
if (previous.toolSurfaceFingerprint !== current.toolSurfaceFingerprint) {
|
|
113
|
+
correlations.push("tool-surface-change");
|
|
114
|
+
}
|
|
115
|
+
if (previous.systemPromptFingerprint !== current.systemPromptFingerprint) {
|
|
116
|
+
correlations.push("system-prompt-change");
|
|
117
|
+
}
|
|
118
|
+
return correlations;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function classify(
|
|
122
|
+
semantics: CacheSemantics,
|
|
123
|
+
current: CacheTurnObservation["usage"],
|
|
124
|
+
previous: TurnSample | undefined,
|
|
125
|
+
): Pick<CacheTurnObservation, "kind" | "reprocessedTokens" | "explanation"> {
|
|
126
|
+
if (!previous) {
|
|
127
|
+
return {
|
|
128
|
+
kind: "first-turn",
|
|
129
|
+
reprocessedTokens: null,
|
|
130
|
+
explanation:
|
|
131
|
+
"No prior turn exists, so cache continuity cannot be inferred.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const previousWarm = previous.usage.cacheRead >= CACHE_WARM_MINIMUM_TOKENS;
|
|
136
|
+
if (current.cacheRead > 0) {
|
|
137
|
+
return {
|
|
138
|
+
kind:
|
|
139
|
+
current.cacheRead < previous.usage.cacheRead ? "partial-hit" : "warm",
|
|
140
|
+
reprocessedTokens: null,
|
|
141
|
+
explanation:
|
|
142
|
+
current.cacheRead < previous.usage.cacheRead
|
|
143
|
+
? "The provider reported a smaller cache read than on the prior turn."
|
|
144
|
+
: "The provider reported cached prompt tokens on this turn.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (!previousWarm) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "cold",
|
|
150
|
+
reprocessedTokens: null,
|
|
151
|
+
explanation:
|
|
152
|
+
"The prior turn had no sufficiently warm prefix, so this cold turn is not an invalidation signal.",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (semantics !== "explicit-prefix") {
|
|
156
|
+
return {
|
|
157
|
+
kind: "unknown",
|
|
158
|
+
reprocessedTokens: null,
|
|
159
|
+
explanation:
|
|
160
|
+
semantics === "implicit-best-effort"
|
|
161
|
+
? "A warm-to-cold transition was observed, but this provider exposes only best-effort cache usage."
|
|
162
|
+
: "A warm-to-cold transition was observed, but the provider cache contract is unknown.",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
kind: "miss-after-warm-prefix",
|
|
167
|
+
reprocessedTokens: current.input,
|
|
168
|
+
explanation:
|
|
169
|
+
"An explicit-prefix provider reported a warm prior turn and zero cache read now; local boundaries are correlations, not verified causes.",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function createCacheDiagnosticsTracker() {
|
|
174
|
+
let previous: TurnSample | undefined;
|
|
175
|
+
let pendingCorrelations = new Set<CacheCorrelation>();
|
|
176
|
+
|
|
177
|
+
const reset = () => {
|
|
178
|
+
previous = undefined;
|
|
179
|
+
pendingCorrelations = new Set();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const mark = (correlation: CacheCorrelation) => {
|
|
183
|
+
pendingCorrelations.add(correlation);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const observe = (options: {
|
|
187
|
+
turnIndex: number;
|
|
188
|
+
identity: CacheTurnIdentity;
|
|
189
|
+
usage: Usage;
|
|
190
|
+
}) => {
|
|
191
|
+
const usage = promptUsage(options.usage);
|
|
192
|
+
const semantics = cacheSemanticsForProvider(options.identity.provider);
|
|
193
|
+
const classification = classify(semantics, usage, previous);
|
|
194
|
+
const correlations = previous
|
|
195
|
+
? identityCorrelations(previous.identity, options.identity)
|
|
196
|
+
: [];
|
|
197
|
+
for (const pending of pendingCorrelations) correlations.push(pending);
|
|
198
|
+
const uniqueCorrelations = [...new Set(correlations)];
|
|
199
|
+
|
|
200
|
+
const observation: CacheTurnObservation = {
|
|
201
|
+
turnIndex: options.turnIndex,
|
|
202
|
+
provider: options.identity.provider,
|
|
203
|
+
semantics,
|
|
204
|
+
kind: classification.kind,
|
|
205
|
+
usage,
|
|
206
|
+
previousCacheRead: previous?.usage.cacheRead ?? null,
|
|
207
|
+
reprocessedTokens: classification.reprocessedTokens,
|
|
208
|
+
correlations: uniqueCorrelations,
|
|
209
|
+
evidence: "observation",
|
|
210
|
+
verifiedCause: null,
|
|
211
|
+
explanation: classification.explanation,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
previous = { identity: options.identity, usage };
|
|
215
|
+
pendingCorrelations.clear();
|
|
216
|
+
return observation;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return { mark, observe, reset };
|
|
220
|
+
}
|
|
@@ -7,37 +7,17 @@ import {
|
|
|
7
7
|
MODEL_INFO_CHANNEL,
|
|
8
8
|
REFRESH_CHANNEL,
|
|
9
9
|
} from "../shared/dashboard-state.ts";
|
|
10
|
+
import { createSessionMetricsTracker } from "./session-metrics.ts";
|
|
11
|
+
import {
|
|
12
|
+
CACHE_DIAGNOSTICS_CHANNEL,
|
|
13
|
+
createCacheDiagnosticsTracker,
|
|
14
|
+
fingerprintCacheSurface,
|
|
15
|
+
type CacheTurnIdentity,
|
|
16
|
+
} from "./cache-diagnostics.ts";
|
|
10
17
|
|
|
11
18
|
const CHARS_PER_ESTIMATED_TOKEN = 4;
|
|
12
19
|
const LIVE_UPDATE_INTERVAL_MS = 200;
|
|
13
20
|
|
|
14
|
-
function getSessionMetrics(ctx: ExtensionContext) {
|
|
15
|
-
let cost = 0;
|
|
16
|
-
let cacheRead = 0;
|
|
17
|
-
let promptTokens = 0;
|
|
18
|
-
|
|
19
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
20
|
-
let usage;
|
|
21
|
-
if (entry.type === "message") {
|
|
22
|
-
const message = entry.message;
|
|
23
|
-
if (message.role === "assistant" || message.role === "toolResult") {
|
|
24
|
-
usage = message.usage;
|
|
25
|
-
}
|
|
26
|
-
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
27
|
-
usage = entry.usage;
|
|
28
|
-
}
|
|
29
|
-
if (!usage) continue;
|
|
30
|
-
cost += usage.cost.total;
|
|
31
|
-
cacheRead += usage.cacheRead;
|
|
32
|
-
promptTokens += usage.input + usage.cacheRead + usage.cacheWrite;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
cost,
|
|
37
|
-
cachePercent: promptTokens > 0 ? (cacheRead / promptTokens) * 100 : null,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
21
|
function estimateContentTokens(characters: number) {
|
|
42
22
|
return Math.ceil(characters / CHARS_PER_ESTIMATED_TOKEN);
|
|
43
23
|
}
|
|
@@ -54,6 +34,9 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
54
34
|
let runContentStreamMs = 0;
|
|
55
35
|
let lastLiveUpdate = 0;
|
|
56
36
|
let currentContext: ExtensionContext | undefined;
|
|
37
|
+
const sessionMetrics = createSessionMetricsTracker();
|
|
38
|
+
const cacheDiagnostics = createCacheDiagnosticsTracker();
|
|
39
|
+
let cacheIdentity: CacheTurnIdentity | undefined;
|
|
57
40
|
|
|
58
41
|
const publish = () => pi.events.emit(MODEL_INFO_CHANNEL, { ...state });
|
|
59
42
|
|
|
@@ -61,7 +44,6 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
61
44
|
currentContext = ctx;
|
|
62
45
|
const model = ctx.model;
|
|
63
46
|
const usage = ctx.getContextUsage();
|
|
64
|
-
const metrics = getSessionMetrics(ctx);
|
|
65
47
|
|
|
66
48
|
state = {
|
|
67
49
|
...state,
|
|
@@ -72,12 +54,14 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
72
54
|
contextTokens: usage?.tokens ?? null,
|
|
73
55
|
contextWindow: usage?.contextWindow ?? model?.contextWindow ?? 0,
|
|
74
56
|
contextPercent: usage?.percent ?? null,
|
|
75
|
-
cachePercent: metrics.cachePercent,
|
|
76
|
-
cost: metrics.cost,
|
|
77
57
|
};
|
|
78
58
|
publish();
|
|
79
59
|
}
|
|
80
60
|
|
|
61
|
+
function syncSessionMetrics(ctx: ExtensionContext) {
|
|
62
|
+
state = { ...state, ...sessionMetrics.sync(ctx.sessionManager) };
|
|
63
|
+
}
|
|
64
|
+
|
|
81
65
|
function resetMessageTracking() {
|
|
82
66
|
contentStreamStart = null;
|
|
83
67
|
lastContentDeltaAt = null;
|
|
@@ -97,10 +81,15 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
97
81
|
runContentTokens = 0;
|
|
98
82
|
runContentStreamMs = 0;
|
|
99
83
|
state = { ...state, tokensPerSecond: null, generating: false };
|
|
84
|
+
sessionMetrics.reset();
|
|
85
|
+
cacheDiagnostics.reset();
|
|
86
|
+
cacheIdentity = undefined;
|
|
87
|
+
syncSessionMetrics(ctx);
|
|
100
88
|
refresh(ctx);
|
|
101
89
|
});
|
|
102
90
|
|
|
103
91
|
pi.on("model_select", (event, ctx) => {
|
|
92
|
+
cacheDiagnostics.mark("model-change");
|
|
104
93
|
state = {
|
|
105
94
|
...state,
|
|
106
95
|
provider: event.model.provider,
|
|
@@ -113,6 +102,7 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
113
102
|
});
|
|
114
103
|
|
|
115
104
|
pi.on("thinking_level_select", (event) => {
|
|
105
|
+
cacheDiagnostics.mark("thinking-change");
|
|
116
106
|
state = { ...state, thinking: event.level };
|
|
117
107
|
publish();
|
|
118
108
|
});
|
|
@@ -125,6 +115,17 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
125
115
|
refresh(ctx);
|
|
126
116
|
});
|
|
127
117
|
|
|
118
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
119
|
+
const selectedTools = event.systemPromptOptions.selectedTools ?? [];
|
|
120
|
+
cacheIdentity = {
|
|
121
|
+
provider: ctx.model?.provider ?? "",
|
|
122
|
+
modelId: ctx.model?.id ?? "no-model",
|
|
123
|
+
thinking: ctx.model?.reasoning ? pi.getThinkingLevel() : "off",
|
|
124
|
+
toolSurfaceFingerprint: fingerprintCacheSurface(selectedTools),
|
|
125
|
+
systemPromptFingerprint: fingerprintCacheSurface(event.systemPrompt),
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
128
129
|
pi.on("message_start", (event) => {
|
|
129
130
|
if (event.message.role === "assistant") resetMessageTracking();
|
|
130
131
|
});
|
|
@@ -213,14 +214,42 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
213
214
|
refresh(ctx);
|
|
214
215
|
});
|
|
215
216
|
|
|
216
|
-
pi.on("turn_end", (
|
|
217
|
+
pi.on("turn_end", (event, ctx) => {
|
|
218
|
+
syncSessionMetrics(ctx);
|
|
219
|
+
refresh(ctx);
|
|
220
|
+
// Failed/cancelled responses may carry placeholder zero usage. They do
|
|
221
|
+
// not establish a cache observation or replace the last valid baseline.
|
|
222
|
+
if (
|
|
223
|
+
event.message?.role === "assistant" &&
|
|
224
|
+
event.message.stopReason !== "error" &&
|
|
225
|
+
event.message.stopReason !== "aborted" &&
|
|
226
|
+
cacheIdentity
|
|
227
|
+
) {
|
|
228
|
+
pi.events.emit(
|
|
229
|
+
CACHE_DIAGNOSTICS_CHANNEL,
|
|
230
|
+
cacheDiagnostics.observe({
|
|
231
|
+
turnIndex: event.turnIndex,
|
|
232
|
+
identity: cacheIdentity,
|
|
233
|
+
usage: event.message.usage,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
217
238
|
|
|
218
239
|
// Compaction and branch moves rewrite history, so the cached percentage is
|
|
219
240
|
// stale the moment they land. Pi reports unknown occupancy until the next
|
|
220
241
|
// assistant reply, which is the honest state to show.
|
|
221
|
-
pi.on("session_compact", (_event, ctx) =>
|
|
242
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
243
|
+
cacheDiagnostics.mark("compaction");
|
|
244
|
+
syncSessionMetrics(ctx);
|
|
245
|
+
refresh(ctx);
|
|
246
|
+
});
|
|
222
247
|
|
|
223
|
-
pi.on("session_tree", (_event, ctx) =>
|
|
248
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
249
|
+
cacheDiagnostics.mark("branch-change");
|
|
250
|
+
syncSessionMetrics(ctx);
|
|
251
|
+
refresh(ctx);
|
|
252
|
+
});
|
|
224
253
|
|
|
225
254
|
pi.on("agent_settled", (_event, ctx) => {
|
|
226
255
|
state = { ...state, generating: false };
|
|
@@ -230,5 +259,8 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
230
259
|
pi.on("session_shutdown", () => {
|
|
231
260
|
stopRefreshListener();
|
|
232
261
|
currentContext = undefined;
|
|
262
|
+
sessionMetrics.reset();
|
|
263
|
+
cacheDiagnostics.reset();
|
|
264
|
+
cacheIdentity = undefined;
|
|
233
265
|
});
|
|
234
266
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
|
+
|
|
4
|
+
type SessionMetricsManager = {
|
|
5
|
+
getSessionId(): string;
|
|
6
|
+
getLeafId(): string | null;
|
|
7
|
+
getEntry(id: string): SessionEntry | undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type PrefixMetrics = Readonly<{
|
|
11
|
+
cost: number;
|
|
12
|
+
cacheRead: number;
|
|
13
|
+
promptTokens: number;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
const EMPTY_PREFIX: PrefixMetrics = Object.freeze({
|
|
17
|
+
cost: 0,
|
|
18
|
+
cacheRead: 0,
|
|
19
|
+
promptTokens: 0,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function getUsage(entry: SessionEntry): Usage | undefined {
|
|
23
|
+
if (entry.type === "message") {
|
|
24
|
+
return entry.message.role === "assistant" ||
|
|
25
|
+
entry.message.role === "toolResult"
|
|
26
|
+
? entry.message.usage
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
return entry.type === "compaction" || entry.type === "branch_summary"
|
|
30
|
+
? entry.usage
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function addUsage(prefix: PrefixMetrics, entry: SessionEntry): PrefixMetrics {
|
|
35
|
+
const usage = getUsage(entry);
|
|
36
|
+
if (!usage) return prefix;
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
cost: prefix.cost + usage.cost.total,
|
|
39
|
+
cacheRead: prefix.cacheRead + usage.cacheRead,
|
|
40
|
+
promptTokens:
|
|
41
|
+
prefix.promptTokens + usage.input + usage.cacheRead + usage.cacheWrite,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toMetrics(prefix: PrefixMetrics) {
|
|
46
|
+
return {
|
|
47
|
+
cost: prefix.cost,
|
|
48
|
+
cachePercent:
|
|
49
|
+
prefix.promptTokens > 0
|
|
50
|
+
? (prefix.cacheRead / prefix.promptTokens) * 100
|
|
51
|
+
: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createSessionMetricsTracker() {
|
|
56
|
+
let sessionId: string | undefined;
|
|
57
|
+
let prefixes = new Map<string, PrefixMetrics>();
|
|
58
|
+
|
|
59
|
+
const reset = () => {
|
|
60
|
+
sessionId = undefined;
|
|
61
|
+
prefixes = new Map();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const sync = (manager: SessionMetricsManager) => {
|
|
65
|
+
const nextSessionId = manager.getSessionId();
|
|
66
|
+
if (sessionId !== nextSessionId) reset();
|
|
67
|
+
sessionId = nextSessionId;
|
|
68
|
+
|
|
69
|
+
const leafId = manager.getLeafId();
|
|
70
|
+
if (!leafId) return toMetrics(EMPTY_PREFIX);
|
|
71
|
+
|
|
72
|
+
const cachedLeaf = prefixes.get(leafId);
|
|
73
|
+
if (cachedLeaf) return toMetrics(cachedLeaf);
|
|
74
|
+
|
|
75
|
+
const suffix: SessionEntry[] = [];
|
|
76
|
+
let currentId: string | null = leafId;
|
|
77
|
+
while (currentId && !prefixes.has(currentId)) {
|
|
78
|
+
const entry = manager.getEntry(currentId);
|
|
79
|
+
if (!entry) break;
|
|
80
|
+
suffix.push(entry);
|
|
81
|
+
currentId = entry.parentId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let prefix = currentId
|
|
85
|
+
? (prefixes.get(currentId) ?? EMPTY_PREFIX)
|
|
86
|
+
: EMPTY_PREFIX;
|
|
87
|
+
for (const entry of suffix.reverse()) {
|
|
88
|
+
prefix = addUsage(prefix, entry);
|
|
89
|
+
prefixes.set(entry.id, prefix);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return toMetrics(prefixes.get(leafId) ?? EMPTY_PREFIX);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return { sync, reset };
|
|
96
|
+
}
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
*
|
|
12
12
|
* THREAT MODEL. The adversary is a model that would start changing things
|
|
13
13
|
* before the user approved a plan — including one steered by injected text it
|
|
14
|
-
* read while investigating.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* read while investigating. Repository-configured diff drivers are also in
|
|
15
|
+
* scope for commands that render patches: raw `git diff`, `git show`,
|
|
16
|
+
* `git whatchanged`, and diff-generating `git log` forms are refused here. Plan
|
|
17
|
+
* Mode exposes the structured git-read tools instead; they force
|
|
18
|
+
* `--no-ext-diff --no-textconv --no-color` in argv. This is not a claim that
|
|
19
|
+
* every hostile Git configuration is inert: the remaining Git investigation
|
|
20
|
+
* commands still run under Pi's existing project trust boundary.
|
|
19
21
|
*
|
|
20
22
|
* EVERY LIST BELOW IS AN ALLOWLIST, deliberately. An earlier version scanned
|
|
21
23
|
* for known-dangerous flags instead, and review found four separate escapes in
|
|
@@ -50,9 +52,7 @@ const UNQUOTED_GLOB_CHARACTERS = new Set(["*", "?", "[", "]"]);
|
|
|
50
52
|
*/
|
|
51
53
|
const GIT_SUBCOMMANDS = new Set([
|
|
52
54
|
"log",
|
|
53
|
-
"diff",
|
|
54
55
|
"status",
|
|
55
|
-
"show",
|
|
56
56
|
"blame",
|
|
57
57
|
"shortlog",
|
|
58
58
|
"describe",
|
|
@@ -63,10 +63,11 @@ const GIT_SUBCOMMANDS = new Set([
|
|
|
63
63
|
"cat-file",
|
|
64
64
|
"merge-base",
|
|
65
65
|
"name-rev",
|
|
66
|
-
"whatchanged",
|
|
67
66
|
"grep",
|
|
68
67
|
]);
|
|
69
68
|
|
|
69
|
+
const GIT_DIFF_SUBCOMMANDS = new Set(["diff", "show", "whatchanged"]);
|
|
70
|
+
|
|
70
71
|
/**
|
|
71
72
|
* Flags accepted after a git subcommand. The admission rule for this list is
|
|
72
73
|
* narrow: a flag qualifies only if it shapes OUTPUT or SELECTS commits, and
|
|
@@ -159,6 +160,27 @@ const GIT_FLAGS = new Set([
|
|
|
159
160
|
"--count",
|
|
160
161
|
]);
|
|
161
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Options that make `git log` enter the diff machinery. Git enables textconv
|
|
165
|
+
* by default for porcelain log/diff output, so these must use the structured
|
|
166
|
+
* git tools even though plain history browsing remains safe and useful.
|
|
167
|
+
*/
|
|
168
|
+
const GIT_LOG_DIFF_FLAGS = new Set([
|
|
169
|
+
"-p",
|
|
170
|
+
"--patch",
|
|
171
|
+
"--stat",
|
|
172
|
+
"--shortstat",
|
|
173
|
+
"--numstat",
|
|
174
|
+
"--summary",
|
|
175
|
+
"--raw",
|
|
176
|
+
"--name-only",
|
|
177
|
+
"--name-status",
|
|
178
|
+
"--word-diff",
|
|
179
|
+
"-U",
|
|
180
|
+
"--unified",
|
|
181
|
+
"-L",
|
|
182
|
+
]);
|
|
183
|
+
|
|
162
184
|
/** gh subcommands paired with the verbs under each that only read. */
|
|
163
185
|
const GH_SUBCOMMANDS = new Map<string, ReadonlySet<string>>([
|
|
164
186
|
["pr", new Set(["view", "list", "diff", "checks", "status"])],
|
|
@@ -377,6 +399,16 @@ function flagName(word: string) {
|
|
|
377
399
|
return eq === -1 ? word : word.slice(0, eq);
|
|
378
400
|
}
|
|
379
401
|
|
|
402
|
+
function firstMatchedFlag(
|
|
403
|
+
words: readonly string[],
|
|
404
|
+
flags: ReadonlySet<string>,
|
|
405
|
+
) {
|
|
406
|
+
for (const word of words) {
|
|
407
|
+
if (word === "--") break;
|
|
408
|
+
if (flags.has(flagName(word))) return word;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
380
412
|
/**
|
|
381
413
|
* Check the argument tail. Words after `--` are pathspecs by definition and
|
|
382
414
|
* need no check; before it, a word either is an allowlisted flag or is not a
|
|
@@ -439,11 +471,24 @@ export function planBashDecision(command: unknown): BashPlanDecision {
|
|
|
439
471
|
*/
|
|
440
472
|
if (program === "git") {
|
|
441
473
|
const [subcommand, ...args] = rest;
|
|
474
|
+
if (subcommand && GIT_DIFF_SUBCOMMANDS.has(subcommand)) {
|
|
475
|
+
return refuse(
|
|
476
|
+
`plan mode does not run raw "git ${subcommand}" because repository diff drivers may execute external programs — use git_diff or git_show instead (and git_log to find commits)`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
442
479
|
if (!subcommand || !GIT_SUBCOMMANDS.has(subcommand)) {
|
|
443
480
|
return refuse(
|
|
444
|
-
`plan mode allows only a read-only git subcommand immediately after "git" (log,
|
|
481
|
+
`plan mode allows only a read-only git subcommand immediately after "git" (log, status, blame, …), not "${subcommand ?? "(none)"}"`,
|
|
445
482
|
);
|
|
446
483
|
}
|
|
484
|
+
if (subcommand === "log") {
|
|
485
|
+
const diffFlag = firstMatchedFlag(args, GIT_LOG_DIFF_FLAGS);
|
|
486
|
+
if (diffFlag) {
|
|
487
|
+
return refuse(
|
|
488
|
+
`plan mode does not run raw "git log ${diffFlag}" because diff-generating log options may execute repository diff drivers — use git_log to find commits, then git_show or git_diff to inspect changes`,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
447
492
|
return scanArguments(args, GIT_FLAGS, "git");
|
|
448
493
|
}
|
|
449
494
|
|