@diegopetrucci/pi-extensions 0.1.78 → 0.1.79
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 +1 -1
- package/extensions/agent-workflow-audit/index.ts +3 -20
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/glimpseui.d.ts +5 -65
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +5 -65
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/web/index.html +0 -27
- package/extensions/code-reviewer/index.ts +0 -34
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +3 -3
- package/extensions/context-cap/package.json +2 -2
- package/extensions/context-inspector/index.ts +2 -46
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1 -27
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +16 -7
- package/extensions/dynamic-context-pruning/index.ts +9 -114
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +16 -9
- package/extensions/fast/index.ts +9 -5
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/index.ts +2 -16
- package/extensions/git-footer/package.json +1 -1
- package/extensions/librarian/index.ts +7 -39
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +3 -17
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/openai-usage.ts +2 -11
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/index.ts +1 -27
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +31 -63
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/review/README.md +2 -0
- package/extensions/review/package.json +1 -1
- package/extensions/triage-comments/index.ts +0 -15
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +1 -1
|
@@ -143,7 +143,6 @@ type MinimalMessage = {
|
|
|
143
143
|
customType?: string;
|
|
144
144
|
display?: boolean;
|
|
145
145
|
summary?: string;
|
|
146
|
-
fromId?: string;
|
|
147
146
|
tokensBefore?: number;
|
|
148
147
|
command?: string;
|
|
149
148
|
output?: string;
|
|
@@ -165,7 +164,6 @@ type MinimalEntry = {
|
|
|
165
164
|
display?: boolean;
|
|
166
165
|
details?: unknown;
|
|
167
166
|
summary?: string;
|
|
168
|
-
fromId?: string;
|
|
169
167
|
tokensBefore?: number;
|
|
170
168
|
firstKeptEntryId?: string;
|
|
171
169
|
};
|
|
@@ -177,7 +175,6 @@ type MinimalProjectionEntry = {
|
|
|
177
175
|
|
|
178
176
|
type MinimalSessionProjection = {
|
|
179
177
|
entries: MinimalProjectionEntry[];
|
|
180
|
-
messages?: MinimalMessage[];
|
|
181
178
|
};
|
|
182
179
|
|
|
183
180
|
type AnalysisResult = {
|
|
@@ -192,7 +189,6 @@ type Segment = {
|
|
|
192
189
|
source: string;
|
|
193
190
|
role: string;
|
|
194
191
|
tokens: number;
|
|
195
|
-
chars: number;
|
|
196
192
|
turn: number;
|
|
197
193
|
sequence: number;
|
|
198
194
|
entryId?: string;
|
|
@@ -211,7 +207,6 @@ type Segment = {
|
|
|
211
207
|
type CategoryStat = {
|
|
212
208
|
id: CategoryId;
|
|
213
209
|
label: string;
|
|
214
|
-
shortLabel: string;
|
|
215
210
|
color: string;
|
|
216
211
|
description: string;
|
|
217
212
|
tokens: number;
|
|
@@ -230,13 +225,9 @@ type AggregateStat = {
|
|
|
230
225
|
|
|
231
226
|
type DatasetStats = {
|
|
232
227
|
tokens: number;
|
|
233
|
-
rawTokens: number;
|
|
234
|
-
visibleRawTokens: number;
|
|
235
228
|
excludedTokens: number;
|
|
236
229
|
providerDeltaTokens: number;
|
|
237
230
|
estimatorOverageTokens: number;
|
|
238
|
-
reconciliationScale: number;
|
|
239
|
-
segmentCount: number;
|
|
240
231
|
messageCount: number;
|
|
241
232
|
categories: CategoryStat[];
|
|
242
233
|
topSegments: Segment[];
|
|
@@ -247,8 +238,6 @@ type DatasetStats = {
|
|
|
247
238
|
|
|
248
239
|
type Dataset = {
|
|
249
240
|
id: DatasetId;
|
|
250
|
-
label: string;
|
|
251
|
-
description: string;
|
|
252
241
|
segments: Segment[];
|
|
253
242
|
stats: DatasetStats;
|
|
254
243
|
};
|
|
@@ -259,7 +248,6 @@ type RoutedModel = {
|
|
|
259
248
|
};
|
|
260
249
|
|
|
261
250
|
type ReportData = {
|
|
262
|
-
generatedAt: string;
|
|
263
251
|
cwd: string;
|
|
264
252
|
session: {
|
|
265
253
|
id?: string;
|
|
@@ -280,7 +268,6 @@ type ReportData = {
|
|
|
280
268
|
percent: number | null;
|
|
281
269
|
};
|
|
282
270
|
options: {
|
|
283
|
-
redacted: boolean;
|
|
284
271
|
defaultDataset: DatasetId;
|
|
285
272
|
};
|
|
286
273
|
categoryMeta: typeof CATEGORY_META;
|
|
@@ -507,7 +494,6 @@ function addTextSegment(
|
|
|
507
494
|
source: base.source,
|
|
508
495
|
role: base.role,
|
|
509
496
|
tokens,
|
|
510
|
-
chars,
|
|
511
497
|
turn: base.turn ?? state.turn,
|
|
512
498
|
entryId: base.entryId,
|
|
513
499
|
timestamp: base.timestamp,
|
|
@@ -543,7 +529,6 @@ function addImageSegment(
|
|
|
543
529
|
source: base.source,
|
|
544
530
|
role: base.role,
|
|
545
531
|
tokens: IMAGE_TOKEN_ESTIMATE,
|
|
546
|
-
chars: IMAGE_TOKEN_ESTIMATE * 4,
|
|
547
532
|
turn: base.turn ?? state.turn,
|
|
548
533
|
entryId: base.entryId,
|
|
549
534
|
timestamp: base.timestamp,
|
|
@@ -820,7 +805,6 @@ function contextEntryToMessage(entry: MinimalEntry): MinimalMessage | undefined
|
|
|
820
805
|
return {
|
|
821
806
|
role: "branchSummary",
|
|
822
807
|
summary: entry.summary,
|
|
823
|
-
fromId: entry.fromId,
|
|
824
808
|
timestamp: entry.timestamp ? new Date(entry.timestamp).getTime() : undefined,
|
|
825
809
|
};
|
|
826
810
|
}
|
|
@@ -916,10 +900,7 @@ function resolveSessionProjection(
|
|
|
916
900
|
(!Array.isArray(projection.messages) || !projection.messages.every(isProjectionMessage))
|
|
917
901
|
)
|
|
918
902
|
return undefined;
|
|
919
|
-
return {
|
|
920
|
-
entries,
|
|
921
|
-
messages: entries.flatMap((entry) => entry.messages),
|
|
922
|
-
};
|
|
903
|
+
return { entries };
|
|
923
904
|
}
|
|
924
905
|
|
|
925
906
|
function resolveCurrentContextEntries(
|
|
@@ -1045,8 +1026,6 @@ function aggregateBy(
|
|
|
1045
1026
|
|
|
1046
1027
|
function finalizeDataset(
|
|
1047
1028
|
id: DatasetId,
|
|
1048
|
-
label: string,
|
|
1049
|
-
description: string,
|
|
1050
1029
|
segments: Segment[],
|
|
1051
1030
|
messageCount: number,
|
|
1052
1031
|
authoritativeTokens: number | null,
|
|
@@ -1091,7 +1070,6 @@ function finalizeDataset(
|
|
|
1091
1070
|
return {
|
|
1092
1071
|
id: categoryId,
|
|
1093
1072
|
label: meta.label,
|
|
1094
|
-
shortLabel: meta.shortLabel,
|
|
1095
1073
|
color: meta.color,
|
|
1096
1074
|
description: meta.description,
|
|
1097
1075
|
tokens: Math.round(rawTokens),
|
|
@@ -1105,18 +1083,12 @@ function finalizeDataset(
|
|
|
1105
1083
|
|
|
1106
1084
|
return {
|
|
1107
1085
|
id,
|
|
1108
|
-
label,
|
|
1109
|
-
description,
|
|
1110
1086
|
segments,
|
|
1111
1087
|
stats: {
|
|
1112
1088
|
tokens: Math.round(displayTotal),
|
|
1113
|
-
rawTokens: Math.round(segments.reduce((sum, segment) => sum + segment.tokens, 0)),
|
|
1114
|
-
visibleRawTokens: Math.round(visibleRawTokens),
|
|
1115
1089
|
excludedTokens: Math.round(excludedTokens),
|
|
1116
1090
|
providerDeltaTokens,
|
|
1117
1091
|
estimatorOverageTokens,
|
|
1118
|
-
reconciliationScale,
|
|
1119
|
-
segmentCount: segments.length,
|
|
1120
1092
|
messageCount,
|
|
1121
1093
|
categories,
|
|
1122
1094
|
topSegments: [...visibleSegments].sort((a, b) => b.tokens - a.tokens).slice(0, 24),
|
|
@@ -1192,20 +1164,11 @@ function buildReportData(
|
|
|
1192
1164
|
const fullSegments = [...overhead, ...fullAnalysis.segments];
|
|
1193
1165
|
const currentDataset = finalizeDataset(
|
|
1194
1166
|
"current",
|
|
1195
|
-
"Current model context",
|
|
1196
|
-
"What pi is expected to send to the model on the next turn: latest compaction summary plus unsummarized messages, system prompt, and active tool schemas.",
|
|
1197
1167
|
currentSegments,
|
|
1198
1168
|
currentAnalysis.messageCount,
|
|
1199
1169
|
contextTokens,
|
|
1200
1170
|
);
|
|
1201
|
-
const fullDataset = finalizeDataset(
|
|
1202
|
-
"full",
|
|
1203
|
-
"Full active branch history",
|
|
1204
|
-
"Every context-bearing entry on the active branch, including pre-compaction history that may no longer be sent verbatim.",
|
|
1205
|
-
fullSegments,
|
|
1206
|
-
fullAnalysis.messageCount,
|
|
1207
|
-
null,
|
|
1208
|
-
);
|
|
1171
|
+
const fullDataset = finalizeDataset("full", fullSegments, fullAnalysis.messageCount, null);
|
|
1209
1172
|
|
|
1210
1173
|
const notes: string[] = [
|
|
1211
1174
|
"Per-component token counts are local estimates based mostly on characters/4. Providers expose aggregate usage, not exact per-message attribution.",
|
|
@@ -1250,7 +1213,6 @@ function buildReportData(
|
|
|
1250
1213
|
}
|
|
1251
1214
|
|
|
1252
1215
|
return {
|
|
1253
|
-
generatedAt: new Date().toISOString(),
|
|
1254
1216
|
cwd: options.redact ? "[redacted cwd]" : ctx.cwd,
|
|
1255
1217
|
session: {
|
|
1256
1218
|
id: options.redact ? "[redacted session]" : safeCall(() => ctx.sessionManager.getSessionId()),
|
|
@@ -1273,7 +1235,6 @@ function buildReportData(
|
|
|
1273
1235
|
percent: contextPercent,
|
|
1274
1236
|
},
|
|
1275
1237
|
options: {
|
|
1276
|
-
redacted: options.redact,
|
|
1277
1238
|
defaultDataset: options.defaultDataset,
|
|
1278
1239
|
},
|
|
1279
1240
|
categoryMeta: CATEGORY_META,
|
|
@@ -1368,16 +1329,12 @@ function buildHtml(data: ReportData): string {
|
|
|
1368
1329
|
color-scheme: dark;
|
|
1369
1330
|
--bg: #070a12;
|
|
1370
1331
|
--panel: rgba(15, 23, 42, 0.72);
|
|
1371
|
-
--panel-strong: rgba(15, 23, 42, 0.94);
|
|
1372
1332
|
--line: rgba(148, 163, 184, 0.18);
|
|
1373
1333
|
--text: #e5edf7;
|
|
1374
1334
|
--muted: #94a3b8;
|
|
1375
1335
|
--dim: #64748b;
|
|
1376
1336
|
--accent: #8b5cf6;
|
|
1377
1337
|
--accent-2: #06b6d4;
|
|
1378
|
-
--good: #22c55e;
|
|
1379
|
-
--warn: #f59e0b;
|
|
1380
|
-
--bad: #ef4444;
|
|
1381
1338
|
--shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
|
|
1382
1339
|
}
|
|
1383
1340
|
* { box-sizing: border-box; }
|
|
@@ -1588,7 +1545,6 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #cbd5e1;
|
|
|
1588
1545
|
for (const [key, value] of Object.entries(attrs)) {
|
|
1589
1546
|
if (key === 'class') node.className = value;
|
|
1590
1547
|
else if (key === 'text') node.textContent = value;
|
|
1591
|
-
else if (key === 'html') node.innerHTML = value;
|
|
1592
1548
|
else if (key.startsWith('on') && typeof value === 'function') node.addEventListener(key.slice(2), value);
|
|
1593
1549
|
else if (value !== undefined && value !== null) node.setAttribute(key, value);
|
|
1594
1550
|
}
|
|
@@ -1944,8 +1944,7 @@ function createContrarianExtension(pi: ExtensionAPI, deps: ContrarianExtensionDe
|
|
|
1944
1944
|
onUpdate?.(partial);
|
|
1945
1945
|
};
|
|
1946
1946
|
|
|
1947
|
-
let
|
|
1948
|
-
for (let index = 0; index < attempts.length; index++) {
|
|
1947
|
+
for (let index = 0; ; index++) {
|
|
1949
1948
|
const previous = index > 0 ? attempts[index - 1] : undefined;
|
|
1950
1949
|
const attempt = previous
|
|
1951
1950
|
? withFallbackReason(attempts[index], previous)
|
|
@@ -1969,7 +1968,6 @@ function createContrarianExtension(pi: ExtensionAPI, deps: ContrarianExtensionDe
|
|
|
1969
1968
|
});
|
|
1970
1969
|
}
|
|
1971
1970
|
|
|
1972
|
-
lastErrorResult = result;
|
|
1973
1971
|
const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
|
|
1974
1972
|
if (!canFallBack) {
|
|
1975
1973
|
return finalizeResult({
|
|
@@ -1978,30 +1976,6 @@ function createContrarianExtension(pi: ExtensionAPI, deps: ContrarianExtensionDe
|
|
|
1978
1976
|
});
|
|
1979
1977
|
}
|
|
1980
1978
|
}
|
|
1981
|
-
|
|
1982
|
-
return finalizeResult({
|
|
1983
|
-
content: [
|
|
1984
|
-
{
|
|
1985
|
-
type: "text",
|
|
1986
|
-
text: lastErrorResult?.error ?? "Contrarian could not select an available model.",
|
|
1987
|
-
},
|
|
1988
|
-
],
|
|
1989
|
-
details: lastErrorResult?.details ?? {
|
|
1990
|
-
modelRef: "",
|
|
1991
|
-
provider: ctx.model?.provider ?? "unknown",
|
|
1992
|
-
modelId: "",
|
|
1993
|
-
modelName: undefined,
|
|
1994
|
-
thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
1995
|
-
autoSelected: true,
|
|
1996
|
-
selectionReason: "No authenticated models were available to run the contrarian.",
|
|
1997
|
-
includeBash: params.includeBash ?? false,
|
|
1998
|
-
usage: createEmptyUsage(),
|
|
1999
|
-
stderr: "",
|
|
2000
|
-
exitCode: 1,
|
|
2001
|
-
durationMs: 0,
|
|
2002
|
-
cwd: params.cwd ?? ctx.cwd,
|
|
2003
|
-
},
|
|
2004
|
-
});
|
|
2005
1979
|
} finally {
|
|
2006
1980
|
activeRuns.delete(toolCallId);
|
|
2007
1981
|
updateContrarianUi(ctx, activeRuns);
|
|
@@ -278,8 +278,10 @@ Notes:
|
|
|
278
278
|
- `gate.breakEvenThreshold`'s default of `22` is calibrated from the
|
|
279
279
|
representative-corpus benchmark at `cachedPriceRatio` r=0.1 (aggressive
|
|
280
280
|
prompt caching, the common case). It is **provider/ratio-
|
|
281
|
-
dependent
|
|
282
|
-
r=0.25, T=54 at r=0.5, T=58
|
|
281
|
+
dependent**. In the **historical, retired turn-end run**, the optimal
|
|
282
|
+
threshold rose as caching got weaker (T=29 at r=0.25, T=54 at r=0.5, T=58
|
|
283
|
+
at r=0.9); these are not current turn-start results. See
|
|
284
|
+
[`docs/v2-design.md` §4](./docs/v2-design.md#4-benchmark-decision-gate).
|
|
283
285
|
|
|
284
286
|
## Prompt-cache trade-off
|
|
285
287
|
|
|
@@ -326,6 +328,8 @@ Options:
|
|
|
326
328
|
- `--limit N`: only process the first N session files found.
|
|
327
329
|
- `--ratio R`: cached-price ratio(s) to model; repeatable and/or
|
|
328
330
|
comma-separated (e.g. `--ratio 0.1,0.2`). Default: `0.1`.
|
|
331
|
+
- `--sweep-max N`: explicit ceiling for the auto-expanding threshold sweep
|
|
332
|
+
(overrides the corpus-derived ceiling). Default: auto-expand from 30 as needed.
|
|
329
333
|
- `--json`: emit a full machine-readable JSON dump instead of aligned text.
|
|
330
334
|
- `--simulate-compression`: additionally SIMULATE v2-style range compression
|
|
331
335
|
(docs/v2-design.md §1/§4 go/no-go evidence) alongside — never mixed into —
|
|
@@ -390,9 +394,11 @@ reports two kinds of numbers:
|
|
|
390
394
|
prune point, and the *actual* net benefit that prune would have produced —
|
|
391
395
|
`realizedNetBenefit = actualRemainingCalls * recurringSaving - penalty`.
|
|
392
396
|
|
|
393
|
-
It also sweeps the break-even threshold `T` from 1 to 30
|
|
394
|
-
|
|
395
|
-
|
|
397
|
+
It also sweeps the break-even threshold `T` from 1 to 30, then keeps doubling
|
|
398
|
+
the range while the best `T` sits at the top edge, up to a corpus-derived
|
|
399
|
+
ceiling (hard cap 500). `--sweep-max N` sets that ceiling explicitly. It
|
|
400
|
+
reports the `T` that maximizes total realized net benefit across all
|
|
401
|
+
gate-eligible candidates, both overall and split by idle/mid-loop agent state.
|
|
396
402
|
|
|
397
403
|
## Comparison
|
|
398
404
|
|
|
@@ -428,9 +434,11 @@ net benefit, at sufficient candidate volume, with enough remaining-calls
|
|
|
428
434
|
runway per session to plausibly amortize a compression summary's own token
|
|
429
435
|
cost — see `docs/v2-design.md` §4 for the exact go/no-go bar.
|
|
430
436
|
|
|
431
|
-
The
|
|
437
|
+
The **historical, retired turn-end run** of the benchmark harness (over
|
|
432
438
|
`~/.the-last-harness/agent/sessions`: 1,390+ session files, 556 gate-eligible
|
|
433
|
-
candidates — see the corpus-choice note above) produced
|
|
439
|
+
candidates — see the corpus-choice note above) produced the following
|
|
440
|
+
figures, preserved as historical context rather than current turn-start
|
|
441
|
+
results (see [`docs/v2-design.md` §4](./docs/v2-design.md#4-benchmark-decision-gate)):
|
|
434
442
|
|
|
435
443
|
- **Candidates and tokens removed by strategy**: `dedupe` 111 candidates /
|
|
436
444
|
93,330 tokens removed; `error-purge` 187 / 28,191; `superseded-file-ops`
|
|
@@ -440,6 +448,7 @@ candidates — see the corpus-choice note above) produced:
|
|
|
440
448
|
this corpus is dominated by long, tool-heavy agent sessions rather than
|
|
441
449
|
short orchestrator sessions.
|
|
442
450
|
|
|
451
|
+
The current turn-start re-derivation has 460 gate-eligible candidates.
|
|
443
452
|
Shipped gate defaults remain `idle=22` / `mid_loop=1` (see
|
|
444
453
|
[Configuration](#configuration)).
|
|
445
454
|
|
|
@@ -140,12 +140,6 @@ export function normalizeSessionProjection(value: unknown): MinimalSessionProjec
|
|
|
140
140
|
return { entries, messages: entries.flatMap((entry) => entry.messages) };
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
export function sessionProjectionToMessages(
|
|
144
|
-
projection: MinimalSessionProjection,
|
|
145
|
-
): MinimalMessage[] {
|
|
146
|
-
return projection.entries.flatMap((entry) => entry.messages);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
143
|
const EXTENSION_ID = "dynamic-context-pruning";
|
|
150
144
|
const CONFIG_FILE_NAME = "dynamic-context-pruning.json";
|
|
151
145
|
const DECISION_ENTRY_TYPE = "dynamic-context-pruning:decision";
|
|
@@ -164,37 +158,10 @@ const DEFAULT_RECENT_TURNS = 4;
|
|
|
164
158
|
/** Cache cost model default: fraction of full (non-cached) price still paid for cached tokens. */
|
|
165
159
|
const DEFAULT_CACHED_PRICE_RATIO = 0.1;
|
|
166
160
|
|
|
167
|
-
/**
|
|
168
|
-
* Default net-benefit gate threshold (max amortization calls). Recalibrated
|
|
169
|
-
* from the representative-corpus benchmark (~/.the-last-harness/
|
|
170
|
-
* agent/sessions, 1,390+ session files, 556 candidates): this is the
|
|
171
|
-
* hindsight-optimal break-even T at cachedPriceRatio r=0.1 (aggressive
|
|
172
|
-
* prompt caching, the common Anthropic case). IMPORTANT caveats:
|
|
173
|
-
* - Provider/ratio-dependent: this optimum shifts higher as caching gets
|
|
174
|
-
* weaker (T=29 at r=0.25, T=54 at r=0.5, T=58 at r=0.9 on the same
|
|
175
|
-
* corpus) — see docs/v2-design.md section 4 and the extension README.
|
|
176
|
-
* - At r=0.1 the realized deterministic savings this threshold buys are
|
|
177
|
-
* economically marginal (~20.6k token-units total across the whole
|
|
178
|
-
* corpus, i.e. pennies); savings only become material at r>=0.25.
|
|
179
|
-
* - Re-derive via `scripts/benchmark.mjs <sessions-dir> --ratio 0.1` if
|
|
180
|
-
* the representative corpus changes meaningfully.
|
|
181
|
-
*/
|
|
161
|
+
/** Default idle net-benefit gate threshold; see README.md and docs/v2-design.md §4. */
|
|
182
162
|
const DEFAULT_BREAK_EVEN_THRESHOLD = 22;
|
|
183
163
|
|
|
184
|
-
/**
|
|
185
|
-
* Default mid_loop break-even threshold. The package's own
|
|
186
|
-
* representative-corpus benchmark (turn-START state definition — the same one
|
|
187
|
-
* the runtime `context` handler uses) favored rejecting mid_loop candidates
|
|
188
|
-
* at cachedPriceRatio r=0.1 (mid_loop-optimal T=1, total realized
|
|
189
|
-
* net benefit ~0.0). This supports a conservative default for the measured
|
|
190
|
-
* corpus, not a universal claim that mid_loop pruning cannot pay off.
|
|
191
|
-
* The reported 2026-09-08 byte-level capture (a mid_loop prune at T=22
|
|
192
|
-
* invalidating 81% of a warm request prefix) illustrates cache invalidation,
|
|
193
|
-
* not an inability to amortize it: enough subsequent calls can recoup a
|
|
194
|
-
* one-time bust. Idle keeps T=22; turn-start classification does not imply
|
|
195
|
-
* the provider cache is cold. Users who want the old behavior can override
|
|
196
|
-
* via gate.breakEvenThresholdByState.mid_loop.
|
|
197
|
-
*/
|
|
164
|
+
/** Default mid_loop threshold; see README.md for the runtime-aligned benchmark rationale. */
|
|
198
165
|
const DEFAULT_MID_LOOP_BREAK_EVEN_THRESHOLD = 1;
|
|
199
166
|
|
|
200
167
|
/** Tool names never pruned by default: orchestration/subagent-style tools and todo-like state. */
|
|
@@ -243,18 +210,7 @@ export interface PruneProtections {
|
|
|
243
210
|
}
|
|
244
211
|
|
|
245
212
|
export interface PruneThresholds {
|
|
246
|
-
/**
|
|
247
|
-
* Minimum raw text characters a fresh AUTOMATIC prune proposal must remove
|
|
248
|
-
* (original text chars minus placeholder chars) to be considered at all
|
|
249
|
-
* Enforced by `runDynamicContextPruningPipeline` BEFORE the
|
|
250
|
-
* net-benefit gate (see `PruneGateConfig`/`gate`, which uses break-even token
|
|
251
|
-
* math and is a separate, later check): text-only candidates below this floor
|
|
252
|
-
* are dropped up front and never reach gate/batch consideration. Image-bearing
|
|
253
|
-
* tool-result candidates bypass this raw-text floor (images receive no
|
|
254
|
-
* synthetic character count) and proceed to the token-based gate. Manual
|
|
255
|
-
* prunes and already-persisted/replayed decisions always bypass this floor,
|
|
256
|
-
* same as the gate. A value of 0 disables the floor (no filtering).
|
|
257
|
-
*/
|
|
213
|
+
/** Minimum automatic-prune text saving; see README.md Configuration for bypass rules. */
|
|
258
214
|
minCharsSaved: number;
|
|
259
215
|
}
|
|
260
216
|
|
|
@@ -493,29 +449,8 @@ export function estimateTokensForContent(
|
|
|
493
449
|
export type TokenEstimator = (text: string) => number;
|
|
494
450
|
|
|
495
451
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
* event handler and (via re-export) by the offline benchmark, so the two can
|
|
499
|
-
* never drift apart.
|
|
500
|
-
*
|
|
501
|
-
* Walk backwards from the end of `messages`, skipping any message that is
|
|
502
|
-
* neither `user`, `assistant`, nor `toolResult` (e.g. system-role bookkeeping
|
|
503
|
-
* entries). The first such message found determines the state:
|
|
504
|
-
* - `user` (a plain user turn message) => "idle": this call is the FIRST LLM
|
|
505
|
-
* call of a turn, i.e. the decision is being made from a turn-start state.
|
|
506
|
-
* - `assistant` or `toolResult` (mid-chain) => "mid_loop": the agent is
|
|
507
|
-
* iterating (already made at least one call this turn, or is reacting to a
|
|
508
|
-
* tool result).
|
|
509
|
-
* - No such message found (empty history, or system-only) => "idle" (safe
|
|
510
|
-
* default: nothing suggests an in-progress loop).
|
|
511
|
-
*
|
|
512
|
-
* The benchmark (scripts/benchmark.mjs) reclassifies its candidate labels to
|
|
513
|
-
* this SAME definition, reusing this function directly, so the
|
|
514
|
-
* defaults below rest on evidence measured with the exact definition the
|
|
515
|
-
* runtime observes. (An earlier turn-end-based benchmark definition -- "idle"
|
|
516
|
-
* meant "this IS the turn's last assistant message", only knowable in
|
|
517
|
-
* hindsight -- has been retired: the runtime cannot know in advance whether a
|
|
518
|
-
* call is a turn's last one, so that definition was never runtime-observable.)
|
|
452
|
+
* Shared runtime/benchmark state classifier; see README.md Configuration and
|
|
453
|
+
* docs/v2-design.md §4 for the turn-start definition and retired turn-end analysis.
|
|
519
454
|
*/
|
|
520
455
|
export function classifyAgentStateFromMessages(messages: MinimalMessage[]): AgentState {
|
|
521
456
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -677,7 +612,7 @@ export function isWithinRecencyWindow(index: number, boundaryIndex: number): boo
|
|
|
677
612
|
* Used by the error-input purge strategy's "older than N turns" rule; a
|
|
678
613
|
* message at the very end of the conversation has 0 turns elapsed.
|
|
679
614
|
*/
|
|
680
|
-
|
|
615
|
+
function computeTurnsElapsedSince(messages: MinimalMessage[], index: number): number {
|
|
681
616
|
let count = 0;
|
|
682
617
|
for (let i = index + 1; i < messages.length; i++) {
|
|
683
618
|
if (messages[i].role === "user") count++;
|
|
@@ -974,7 +909,7 @@ export function resolvePruneTombstoneState(entries: MinimalSessionEntry[]): Prun
|
|
|
974
909
|
return { activeDecisions, activeIdempotencyKeys, restoredKeys, lastDecisionByKey };
|
|
975
910
|
}
|
|
976
911
|
|
|
977
|
-
|
|
912
|
+
function buildPlaceholderText(reason: string, charsRemoved: number): string {
|
|
978
913
|
return `[pruned by ${EXTENSION_ID}: ${reason} (${charsRemoved} chars removed)]`;
|
|
979
914
|
}
|
|
980
915
|
|
|
@@ -1581,7 +1516,7 @@ export function collectCompletedToolCallOccurrences(
|
|
|
1581
1516
|
}
|
|
1582
1517
|
|
|
1583
1518
|
/** Placeholder text for a duplicate tool result whose content was replaced. */
|
|
1584
|
-
|
|
1519
|
+
function buildDedupePlaceholder(toolName: string, newestToolCallId: string): string {
|
|
1585
1520
|
return `[pruned by ${EXTENSION_ID}: duplicate ${toolName} call result; see newest occurrence (call ${newestToolCallId})]`;
|
|
1586
1521
|
}
|
|
1587
1522
|
|
|
@@ -1642,46 +1577,6 @@ export const errorPurgeStrategy: PruneStrategy = {
|
|
|
1642
1577
|
},
|
|
1643
1578
|
};
|
|
1644
1579
|
|
|
1645
|
-
// When the same file path is read and/or written multiple times in a
|
|
1646
|
-
// session, older read/write/edit tool *outputs* for that path can become
|
|
1647
|
-
// stale relative to a later operation. This strategy proposes replacing
|
|
1648
|
-
// those older outputs with a placeholder, per the following CONSERVATIVE,
|
|
1649
|
-
// binding rules (do not loosen without an architect decision):
|
|
1650
|
-
//
|
|
1651
|
-
// 1. A full-file read (no offset/limit args) is superseded by a LATER
|
|
1652
|
-
// full-file read of the same normalized path.
|
|
1653
|
-
// 2. A partial read (offset/limit args present) is NOT superseded by a
|
|
1654
|
-
// later partial read UNLESS the later read's range fully covers the
|
|
1655
|
-
// earlier one's range. If range coverage can't be reliably determined
|
|
1656
|
-
// from the args (e.g. earlier read has no limit — reads to EOF — but
|
|
1657
|
-
// the later read is bounded), skip: do not propose supersession.
|
|
1658
|
-
// Full-file reads are represented as the range [1, EOF); comparing
|
|
1659
|
-
// that representation via the same coverage check naturally satisfies
|
|
1660
|
-
// rule 1 as a special case of rule 2 (see `readRangeCovers`).
|
|
1661
|
-
// 3. ANY read of a path (full or partial) is superseded once a LATER
|
|
1662
|
-
// *successful* (non-error) write/edit to that same path occurs — the
|
|
1663
|
-
// old read output is now known-stale regardless of read-vs-read range
|
|
1664
|
-
// comparisons. The placeholder explicitly notes the file has since
|
|
1665
|
-
// changed. An ERRORED later write/edit does NOT count (the file may be
|
|
1666
|
-
// unchanged; conservative).
|
|
1667
|
-
// 4. Older write/edit outputs for a path are superseded by a LATER
|
|
1668
|
-
// *successful* write/edit to that same path (again, a later errored
|
|
1669
|
-
// write/edit never supersedes). Write/edit tool outputs are usually
|
|
1670
|
-
// small, so proposals are only ever emitted when there is a non-empty
|
|
1671
|
-
// result to prune; the shared net-benefit gate additionally decides
|
|
1672
|
-
// whether the estimated savings are worth the cache-bust cost.
|
|
1673
|
-
//
|
|
1674
|
-
// The NEWEST operation for a given path (by message order) is never
|
|
1675
|
-
// superseded/pruned, regardless of kind — there is nothing later to point
|
|
1676
|
-
// the placeholder at.
|
|
1677
|
-
//
|
|
1678
|
-
// Path extraction is defensive: pi's built-in `read`/`write`/`edit` tools
|
|
1679
|
-
// all accept a `path` argument (see packages/coding-agent/src/core/tools/
|
|
1680
|
-
// {read,write,edit}.ts); some tool-call renderers also fall back to a
|
|
1681
|
-
// legacy `file_path` name, and `filePath` is accepted as a further
|
|
1682
|
-
// defensive fallback. `bash` is explicitly OUT of scope: this strategy does
|
|
1683
|
-
// not attempt to parse shell commands for file paths.
|
|
1684
|
-
|
|
1685
1580
|
const SUPERSEDED_FILE_OPS_STRATEGY_ID = "superseded-file-ops";
|
|
1686
1581
|
|
|
1687
1582
|
type FileOpKind = "read" | "write" | "edit";
|
|
@@ -2234,7 +2129,7 @@ export function sessionEntriesToMessages(
|
|
|
2234
2129
|
entries: MinimalSessionEntry[],
|
|
2235
2130
|
projection?: MinimalSessionProjection,
|
|
2236
2131
|
): MinimalMessage[] {
|
|
2237
|
-
if (projection) return
|
|
2132
|
+
if (projection) return projection.entries.flatMap((entry) => entry.messages);
|
|
2238
2133
|
const messages: MinimalMessage[] = [];
|
|
2239
2134
|
for (const entry of entries) {
|
|
2240
2135
|
if (entry.type === "message" && entry.message) messages.push(entry.message);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A pi extension that provides `/fast` and `/ultrafast` toggles for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models. It selects the provider-specific Fast-mode request shape from the currently selected model.
|
|
4
4
|
|
|
5
|
-
For eligible direct OpenAI API models it injects:
|
|
5
|
+
For eligible direct OpenAI API-key models it injects:
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
@@ -10,6 +10,14 @@ For eligible direct OpenAI API models it injects:
|
|
|
10
10
|
}
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
For eligible direct OpenAI ChatGPT OAuth models it injects:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"service_tier": "priority"
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
13
21
|
For eligible OpenAI Codex OAuth models it injects:
|
|
14
22
|
|
|
15
23
|
```json
|
|
@@ -39,16 +47,15 @@ Direct OpenAI API Fast mode requires all of the following:
|
|
|
39
47
|
- Provider `openai`.
|
|
40
48
|
- API `openai-responses` or `openai-completions`.
|
|
41
49
|
- Model `gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-sol`, or `gpt-6-luna`.
|
|
42
|
-
- API-key auth
|
|
50
|
+
- API-key auth sets `service_tier: "fast"`; Pi's `openai` ChatGPT OAuth sets `priority`.
|
|
43
51
|
- No existing `service_tier` field in the request payload.
|
|
44
52
|
|
|
45
|
-
The direct OpenAI route stays on OpenAI's direct API contract
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
and request payloads, not live or paid provider calls.
|
|
53
|
+
The direct OpenAI route stays on OpenAI's direct API contract but selects the tier by auth
|
|
54
|
+
type. OAuth uses `priority` because its endpoint rejected `fast`; acceptance or application
|
|
55
|
+
of `priority` on Pi's OAuth endpoint has not been live-verified. This direct route is never
|
|
56
|
+
forwarded to legacy `openai-codex` or its subscription-usage endpoint. The request shape is
|
|
57
|
+
support evidence, not certification; tests use mocked eligibility and payloads only, with
|
|
58
|
+
no live or paid provider calls.
|
|
52
59
|
|
|
53
60
|
### Ultrafast support (initial, fail-closed)
|
|
54
61
|
|
package/extensions/fast/index.ts
CHANGED
|
@@ -155,7 +155,7 @@ function describeMode(state: SessionState): string {
|
|
|
155
155
|
return state.config.enabled ? "on (config default)" : "off (config default)";
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function getEligibility(ctx: ExtensionContext): Eligibility {
|
|
158
|
+
function getEligibility(ctx: ExtensionContext, includeServiceTier = true): Eligibility {
|
|
159
159
|
const model = ctx.model;
|
|
160
160
|
if (!model) {
|
|
161
161
|
return { eligible: false, modelKey: "no-model", reason: "no model is selected" };
|
|
@@ -237,7 +237,11 @@ function getEligibility(ctx: ExtensionContext): Eligibility {
|
|
|
237
237
|
eligible: true,
|
|
238
238
|
modelKey: key,
|
|
239
239
|
provider: "openai",
|
|
240
|
-
serviceTier:
|
|
240
|
+
serviceTier: includeServiceTier
|
|
241
|
+
? ctx.modelRegistry.isUsingOAuth(model)
|
|
242
|
+
? OPENAI_FAST_SERVICE_TIER
|
|
243
|
+
: OPENAI_API_FAST_SERVICE_TIER
|
|
244
|
+
: undefined,
|
|
241
245
|
};
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -328,7 +332,7 @@ function updateStatus(ctx: ExtensionContext, state: SessionState): void {
|
|
|
328
332
|
return;
|
|
329
333
|
}
|
|
330
334
|
|
|
331
|
-
const eligibility = getEligibility(ctx);
|
|
335
|
+
const eligibility = getEligibility(ctx, false);
|
|
332
336
|
ctx.ui.setStatus(EXTENSION_ID, isFastEnabled(state) && eligibility.eligible ? "fast" : undefined);
|
|
333
337
|
}
|
|
334
338
|
|
|
@@ -346,7 +350,7 @@ function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
|
|
|
346
350
|
}
|
|
347
351
|
|
|
348
352
|
const enabled = isFastEnabled(state);
|
|
349
|
-
const eligibility = getEligibility(ctx);
|
|
353
|
+
const eligibility = getEligibility(ctx, enabled);
|
|
350
354
|
const active = enabled && eligibility.eligible;
|
|
351
355
|
|
|
352
356
|
if (active) {
|
|
@@ -405,7 +409,7 @@ async function injectAnthropicFastHeader(
|
|
|
405
409
|
hasActiveTools: boolean,
|
|
406
410
|
): Promise<void> {
|
|
407
411
|
if (state.ultrafast || !isFastEnabled(state)) return;
|
|
408
|
-
const eligibility = getEligibility(ctx);
|
|
412
|
+
const eligibility = getEligibility(ctx, false);
|
|
409
413
|
if (!eligibility.eligible || eligibility.provider !== "anthropic") return;
|
|
410
414
|
|
|
411
415
|
const matchingKeys = Object.keys(headers).filter(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-fast",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "A pi extension that enables Fast mode for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models based on the currently selected model.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|