@diegopetrucci/pi-extensions 0.1.61 → 0.1.63

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 (72) hide show
  1. package/.pi-fleet-tested-version +1 -1
  2. package/README.md +3 -2
  3. package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
  6. package/extensions/annotate-git-diff/package.json +1 -1
  7. package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
  8. package/extensions/annotate-last-message/package.json +1 -1
  9. package/extensions/brrr/.pi-fleet-tested-version +1 -1
  10. package/extensions/brrr/package.json +1 -1
  11. package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
  12. package/extensions/claude-fast/README.md +2 -0
  13. package/extensions/claude-fast/package.json +1 -1
  14. package/extensions/code-reviewer/.pi-fleet-tested-version +1 -1
  15. package/extensions/code-reviewer/README.md +1 -0
  16. package/extensions/code-reviewer/index.ts +24 -2
  17. package/extensions/code-reviewer/package.json +1 -1
  18. package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
  19. package/extensions/confirm-destructive/package.json +1 -1
  20. package/extensions/context-cap/.pi-fleet-tested-version +1 -1
  21. package/extensions/context-cap/package.json +1 -1
  22. package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
  23. package/extensions/context-inspector/package.json +1 -1
  24. package/extensions/contrarian/.pi-fleet-tested-version +1 -1
  25. package/extensions/contrarian/README.md +2 -0
  26. package/extensions/contrarian/index.ts +34 -8
  27. package/extensions/contrarian/package.json +1 -1
  28. package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
  29. package/extensions/dirty-repo-guard/package.json +1 -1
  30. package/extensions/dynamic-context-pruning/.pi-fleet-tested-version +1 -1
  31. package/extensions/dynamic-context-pruning/README.md +78 -11
  32. package/extensions/dynamic-context-pruning/index.ts +171 -21
  33. package/extensions/dynamic-context-pruning/package.json +1 -1
  34. package/extensions/fast/.pi-fleet-tested-version +1 -0
  35. package/extensions/fast/README.md +119 -0
  36. package/extensions/fast/fast.example.json +4 -0
  37. package/extensions/fast/index.ts +383 -0
  38. package/extensions/fast/package.json +39 -0
  39. package/extensions/git-footer/.pi-fleet-tested-version +1 -1
  40. package/extensions/git-footer/package.json +1 -1
  41. package/extensions/gnosis/.pi-fleet-tested-version +1 -1
  42. package/extensions/gnosis/package.json +1 -1
  43. package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
  44. package/extensions/illustrations-to-explain-things/package.json +1 -1
  45. package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
  46. package/extensions/inline-bash/package.json +1 -1
  47. package/extensions/librarian/.pi-fleet-tested-version +1 -1
  48. package/extensions/librarian/README.md +1 -1
  49. package/extensions/librarian/index.ts +34 -5
  50. package/extensions/librarian/package.json +1 -1
  51. package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
  52. package/extensions/minimal-footer/package.json +1 -1
  53. package/extensions/notify/.pi-fleet-tested-version +1 -1
  54. package/extensions/notify/package.json +1 -1
  55. package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
  56. package/extensions/openai-fast/README.md +2 -0
  57. package/extensions/openai-fast/package.json +1 -1
  58. package/extensions/oracle/.pi-fleet-tested-version +1 -1
  59. package/extensions/oracle/README.md +2 -0
  60. package/extensions/oracle/index.ts +34 -8
  61. package/extensions/oracle/package.json +1 -1
  62. package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
  63. package/extensions/permission-gate/package.json +1 -1
  64. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  65. package/extensions/quiet-tools/package.json +1 -1
  66. package/extensions/review/.pi-fleet-tested-version +1 -1
  67. package/extensions/review/package.json +1 -1
  68. package/extensions/todo/.pi-fleet-tested-version +1 -1
  69. package/extensions/todo/package.json +1 -1
  70. package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
  71. package/extensions/triage-comments/package.json +1 -1
  72. package/package.json +5 -6
@@ -165,10 +165,16 @@ export interface PruneProtections {
165
165
 
166
166
  export interface PruneThresholds {
167
167
  /**
168
- * Minimum characters a prune must remove to be worth persisting. Reserved
169
- * for a future minimum-size filter; NOT used by the net-benefit gate
170
- * (see `PruneGateConfig`/`gate`), which uses break-even token math instead.
171
- * Not enforced by this pipeline yet.
168
+ * Minimum raw text characters a fresh AUTOMATIC prune proposal must remove
169
+ * (original text chars minus placeholder chars) to be considered at all
170
+ * (pe-qdzb). Enforced by `runDynamicContextPruningPipeline` BEFORE the
171
+ * net-benefit gate (see `PruneGateConfig`/`gate`, which uses break-even token
172
+ * math and is a separate, later check): text-only candidates below this floor
173
+ * are dropped up front and never reach gate/batch consideration. Image-bearing
174
+ * tool-result candidates bypass this raw-text floor (images receive no
175
+ * synthetic character count) and proceed to the token-based gate. Manual
176
+ * prunes and already-persisted/replayed decisions always bypass this floor,
177
+ * same as the gate. A value of 0 disables the floor (no filtering).
172
178
  */
173
179
  minCharsSaved: number;
174
180
  }
@@ -194,19 +200,42 @@ export type GateMode = "on" | "off" | "always-apply";
194
200
  * making tool calls) vs idle (waiting on the user). Both states default to
195
201
  * the same threshold today.
196
202
  *
197
- * pe-c5n9 evidence update: the representative-corpus benchmark shows
198
- * mid_loop candidates carry ~all realized net benefit while idle candidates
199
- * carry ~none (literally 0 at r=0.1). That would argue for a stricter idle
200
- * default. However, the only live runtime caller (the `context` event
201
- * handler below) does not yet perform real mid-loop/idle detection — it
202
- * always passes agentState:"idle". Defaulting idle to a much stricter
203
- * threshold today would therefore silently disable most automatic pruning
204
- * for everyone (100% of live evaluations run as "idle"), which is a far
205
- * bigger behavior change than a threshold recalibration. So the per-state
206
- * split is intentionally left at parity for now; a follow-up ticket should
207
- * wire real agent-state detection through `ctx` and then apply the
208
- * idle-vs-mid_loop split (idle stricter, mid_loop at DEFAULT_BREAK_EVEN_THRESHOLD)
209
- * once mid_loop is actually reachable.
203
+ * pe-c5n9 evidence (turn-END definition, since superseded -- see below):
204
+ * the representative-corpus benchmark showed mid_loop candidates carrying
205
+ * ~all realized net benefit while idle candidates carried ~none. That would
206
+ * have argued for a stricter idle default. However, the only live runtime
207
+ * caller (the `context` event handler below) did not yet perform real
208
+ * mid-loop/idle detection at the time, so no default change was made.
209
+ *
210
+ * pe-zy4s update (2026-07-08): real runtime mid_loop/idle detection is now
211
+ * wired through the `context` event handler (`classifyAgentStateFromMessages`),
212
+ * using a turn-START definition (idle = this is the first LLM call of a turn;
213
+ * mid_loop = the agent already made a call or received a tool result this
214
+ * turn) -- see that function's doc comment for the exact runtime-observable
215
+ * semantics. The benchmark's candidate labeling was aligned to this SAME
216
+ * definition (retiring the old turn-END definition above, which was never
217
+ * runtime-observable), and the state-split was re-derived on the
218
+ * representative corpus (~/.the-last-harness/agent/sessions, 460 candidates)
219
+ * under it.
220
+ *
221
+ * The result REVERSES the earlier (turn-END) finding: at r=0.1 the
222
+ * turn-START "idle" population (calls made at the start of a turn) carries
223
+ * essentially ALL the realized net benefit (idle-optimal T=22, total realized
224
+ * net benefit ~20,594 token-units), while "mid_loop" carries essentially NONE
225
+ * (mid_loop-optimal T=1, total realized net benefit ~0.0 -- i.e. no mid_loop
226
+ * candidate is ever worth accepting at r=0.1 under this definition). This is
227
+ * the OPPOSITE of what the pe-c5n9 evidence (measured under the old,
228
+ * non-runtime-observable turn-END definition) suggested.
229
+ *
230
+ * Per the pe-zy4s decision rule (stricter idle default only if idle itself is
231
+ * shown to be ~worthless): idle is NOT worthless here -- it is the ONLY state
232
+ * carrying value at r=0.1 -- so no state-conditioned strictness is justified
233
+ * in either direction. The per-state split is therefore kept at PARITY
234
+ * (`breakEvenThresholdByState: { idle: DEFAULT_BREAK_EVEN_THRESHOLD, mid_loop:
235
+ * DEFAULT_BREAK_EVEN_THRESHOLD }`), both config-overridable. A future ticket
236
+ * could consider a stricter mid_loop default given this evidence, but that is
237
+ * a new, unreviewed direction outside this ticket's authorized decision rule
238
+ * and is left for a follow-up.
210
239
  */
211
240
  export type AgentState = "idle" | "mid_loop";
212
241
 
@@ -397,6 +426,41 @@ export function estimateTokensForContent(content: string | (MinimalTextContent |
397
426
 
398
427
  export type TokenEstimator = (text: string) => number;
399
428
 
429
+ /**
430
+ * Classify the agent's current state from the message payload alone (pe-zy4s):
431
+ * this is the runtime-observable definition used both by the live `context`
432
+ * event handler and (via re-export) by the offline benchmark, so the two can
433
+ * never drift apart.
434
+ *
435
+ * Walk backwards from the end of `messages`, skipping any message that is
436
+ * neither `user`, `assistant`, nor `toolResult` (e.g. system-role bookkeeping
437
+ * entries). The first such message found determines the state:
438
+ * - `user` (a plain user turn message) => "idle": this call is the FIRST LLM
439
+ * call of a turn, i.e. the decision is being made from a turn-start state.
440
+ * - `assistant` or `toolResult` (mid-chain) => "mid_loop": the agent is
441
+ * iterating (already made at least one call this turn, or is reacting to a
442
+ * tool result).
443
+ * - No such message found (empty history, or system-only) => "idle" (safe
444
+ * default: nothing suggests an in-progress loop).
445
+ *
446
+ * The benchmark (scripts/benchmark.mjs) reclassifies its candidate labels to
447
+ * this SAME definition (pe-zy4s), reusing this function directly, so the
448
+ * defaults below rest on evidence measured with the exact definition the
449
+ * runtime observes. (An earlier turn-end-based benchmark definition -- "idle"
450
+ * meant "this IS the turn's last assistant message", only knowable in
451
+ * hindsight -- has been retired: the runtime cannot know in advance whether a
452
+ * call is a turn's last one, so that definition was never runtime-observable.)
453
+ */
454
+ export function classifyAgentStateFromMessages(messages: MinimalMessage[]): AgentState {
455
+ for (let i = messages.length - 1; i >= 0; i--) {
456
+ const role = messages[i].role;
457
+ if (role === "user") return "idle";
458
+ if (role === "assistant" || role === "toolResult") return "mid_loop";
459
+ // Any other role (e.g. "system") is skipped: keep walking further back.
460
+ }
461
+ return "idle";
462
+ }
463
+
400
464
  /** Resolve which break-even threshold applies for a given agent state (pe-s2ho state-conditioning hook). */
401
465
  export function resolveBreakEvenThreshold(gateConfig: PruneGateConfig, agentState: AgentState): number {
402
466
  const override = gateConfig.breakEvenThresholdByState[agentState];
@@ -892,6 +956,20 @@ function contentTextLength(content: (MinimalTextContent | MinimalImageContent)[]
892
956
  return content.reduce((sum, block) => (block.type === "text" ? sum + block.text.length : sum), 0);
893
957
  }
894
958
 
959
+ /** Return true when a decision targets a tool result containing one or more images. */
960
+ function isImageBearingToolResultTarget(
961
+ messages: MinimalMessage[],
962
+ decision: PruneDecisionRecord,
963
+ pairIndex?: Map<string, ToolCallPairIndices>,
964
+ ): boolean {
965
+ if (decision.kind !== "tool_result_content" || decision.correlation.type !== "toolCallId") return false;
966
+ const pair = findToolCallPairIndices(messages, decision.correlation.toolCallId, pairIndex);
967
+ if (pair.resultIndex === undefined) return false;
968
+ const target = messages[pair.resultIndex];
969
+ if (target?.role !== "toolResult") return false;
970
+ return (target as MinimalToolResultMessage).content.some((block) => block.type === "image");
971
+ }
972
+
895
973
  export interface ApplyResult {
896
974
  applied: boolean;
897
975
  charsRemoved: number;
@@ -1050,6 +1128,55 @@ export function estimateDecisionSavings(
1050
1128
  return undefined;
1051
1129
  }
1052
1130
 
1131
+ /**
1132
+ * Estimate the character savings of a single decision (pe-qdzb), i.e. the
1133
+ * same before/after delta basis as `estimateDecisionSavings` but measured in
1134
+ * raw characters rather than estimated tokens. Used by the pipeline to
1135
+ * enforce `thresholds.minCharsSaved` against fresh automatic proposals
1136
+ * *before* they reach the net-benefit gate. Returns undefined under the same
1137
+ * conditions as `estimateDecisionSavings` (target absent / would not apply).
1138
+ * Mirrors `estimateDecisionSavings`'s `Math.max(0, ...)` clamp, so a
1139
+ * decision whose placeholder text would be LONGER than the original content
1140
+ * (negative savings) is reported as 0 chars saved, not a negative number.
1141
+ */
1142
+ export function estimateDecisionCharsSaved(
1143
+ messages: MinimalMessage[],
1144
+ decision: PruneDecisionRecord,
1145
+ pairIndex?: Map<string, ToolCallPairIndices>,
1146
+ ): number | undefined {
1147
+ if (decision.correlation.type !== "toolCallId") return undefined;
1148
+ const pair = findToolCallPairIndices(messages, decision.correlation.toolCallId, pairIndex);
1149
+
1150
+ // Same non-mutating preview approach as `estimateDecisionSavings`.
1151
+ const preview = messages.slice();
1152
+ const result = applyPruneDecision(preview, decision, pairIndex);
1153
+ if (!result.applied) return undefined;
1154
+
1155
+ if (decision.kind === "tool_result_content") {
1156
+ if (pair.resultIndex === undefined) return undefined;
1157
+ const before = messages[pair.resultIndex] as MinimalToolResultMessage;
1158
+ const after = preview[pair.resultIndex] as MinimalToolResultMessage;
1159
+ const charsBefore = contentTextLength(before.content);
1160
+ const charsAfter = contentTextLength(after.content);
1161
+ return Math.max(0, charsBefore - charsAfter);
1162
+ }
1163
+
1164
+ if (decision.kind === "tool_call_input") {
1165
+ if (pair.assistantIndex === undefined || pair.toolCallBlockIndex === undefined) return undefined;
1166
+ const beforeBlock = (messages[pair.assistantIndex] as MinimalAssistantMessage).content[
1167
+ pair.toolCallBlockIndex
1168
+ ] as MinimalToolCallContent;
1169
+ const afterBlock = (preview[pair.assistantIndex] as MinimalAssistantMessage).content[
1170
+ pair.toolCallBlockIndex
1171
+ ] as MinimalToolCallContent;
1172
+ const charsBefore = JSON.stringify(beforeBlock.arguments ?? {}).length;
1173
+ const charsAfter = JSON.stringify(afterBlock.arguments ?? {}).length;
1174
+ return Math.max(0, charsBefore - charsAfter);
1175
+ }
1176
+
1177
+ return undefined;
1178
+ }
1179
+
1053
1180
  /**
1054
1181
  * Cache-aware cost model (pe-s2ho ticket NOTES, binding over the looser prose
1055
1182
  * in the ticket body): prompt caches are prefix-based, so pruning at message
@@ -1877,7 +2004,29 @@ export function runDynamicContextPruningPipeline(input: PipelineInput): Pipeline
1877
2004
  // against the pre-mutation `messages` clone, before the apply loop below
1878
2005
  // mutates anything.
1879
2006
  const freshManual = dedupedFresh.filter((decision) => decision.source === "manual");
1880
- const freshAutomatic = dedupedFresh.filter((decision) => decision.source !== "manual");
2007
+ const freshAutomaticUnfiltered = dedupedFresh.filter((decision) => decision.source !== "manual");
2008
+
2009
+ // Minimum-size floor (pe-qdzb): drop fresh automatic proposals whose
2010
+ // character savings (original content chars minus placeholder chars) fall
2011
+ // below `thresholds.minCharsSaved`, BEFORE gate evaluation so dropped
2012
+ // candidates never consume batch/earliest-position gate consideration.
2013
+ // Applies to automatic proposals only; manual (`freshManual` above) and
2014
+ // already-persisted decisions (`input.persistedDecisions`, applied later)
2015
+ // bypass this floor entirely, same as the net-benefit gate. Tool-result
2016
+ // candidates containing images also bypass this raw-text floor because
2017
+ // images have no character count; their token savings still go through the
2018
+ // existing net-benefit gate. A floor of 0 (the config default before
2019
+ // pe-c5n9's DEFAULT_MIN_CHARS_SAVED constant was introduced) is a no-op:
2020
+ // every non-negative delta clears it.
2021
+ const minCharsSaved = input.config.thresholds.minCharsSaved;
2022
+ const freshAutomatic =
2023
+ minCharsSaved <= 0
2024
+ ? freshAutomaticUnfiltered
2025
+ : freshAutomaticUnfiltered.filter((decision) => {
2026
+ if (isImageBearingToolResultTarget(messages, decision, pairIndex)) return true;
2027
+ const charsSaved = estimateDecisionCharsSaved(messages, decision, pairIndex);
2028
+ return charsSaved !== undefined && charsSaved >= minCharsSaved;
2029
+ });
1881
2030
 
1882
2031
  // Collapse fresh AUTOMATIC candidates that target the same (kind,
1883
2032
  // correlation) down to a single decision (pe-j7sb). Two strategies (e.g.
@@ -2360,9 +2509,10 @@ export default function dynamicContextPruningExtension(pi: ExtensionAPI) {
2360
2509
  persistedDecisions,
2361
2510
  knownIdempotencyKeys,
2362
2511
  restoredIdempotencyKeys,
2363
- // Real mid-loop/idle agent-state detection lands in a follow-up ticket;
2364
- // "idle" is the safe default until it's wired through ctx.
2365
- agentState: "idle",
2512
+ // Real mid-loop/idle agent-state detection (pe-zy4s): derived straight from
2513
+ // the message payload via classifyAgentStateFromMessages -- see its doc
2514
+ // comment for the exact runtime-observable definition.
2515
+ agentState: classifyAgentStateFromMessages(event.messages as unknown as MinimalMessage[]),
2366
2516
  cwd: ctx.sessionManager.getCwd(),
2367
2517
  });
2368
2518
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-dynamic-context-pruning",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "A pi extension that dynamically prunes stale context to keep sessions lean.",
6
6
  "keywords": [
@@ -0,0 +1 @@
1
+ 0.83.0
@@ -0,0 +1,119 @@
1
+ # fast
2
+
3
+ A pi extension that provides one `/fast` toggle for supported OpenAI Codex and Anthropic Claude models. It selects the provider-specific Fast-mode request shape from the currently selected model.
4
+
5
+ For eligible OpenAI Codex models it injects:
6
+
7
+ ```json
8
+ {
9
+ "service_tier": "priority"
10
+ }
11
+ ```
12
+
13
+ For eligible Anthropic Claude models it injects:
14
+
15
+ ```json
16
+ {
17
+ "speed": "fast"
18
+ }
19
+ ```
20
+
21
+ It also adds the required Anthropic beta header value:
22
+
23
+ ```text
24
+ anthropic-beta: fast-mode-2026-02-01
25
+ ```
26
+
27
+ ## Eligibility
28
+
29
+ OpenAI Fast mode requires all of the following:
30
+
31
+ - Provider `openai-codex`.
32
+ - API `openai-codex-responses`.
33
+ - Model `gpt-5.4`, `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, or `gpt-5.6-luna`.
34
+ - ChatGPT OAuth/subscription auth, not API-key auth.
35
+ - No existing `service_tier` field in the request payload.
36
+
37
+ Anthropic Fast mode requires all of the following:
38
+
39
+ - Provider `anthropic`.
40
+ - API `anthropic-messages`.
41
+ - Model `claude-opus-4-6`, `claude-opus-4-7`, or `claude-opus-4-8`.
42
+ - No existing `speed` field in the request payload.
43
+
44
+ Anthropic Fast mode supports both API-key and Claude Code OAuth access when the account has access to the research preview.
45
+
46
+ ## Commands
47
+
48
+ ```text
49
+ /fast
50
+ ```
51
+
52
+ Run `/fast` to toggle Fast mode for the current session/runtime. The enabled state follows model changes: switching between eligible OpenAI and Anthropic models automatically changes the request format without resetting the toggle. The footer shows `fast` only while the selected model is eligible and Fast mode is enabled.
53
+
54
+ The extension defaults to off so installing the full collection does not accidentally spend Fast-mode credits.
55
+
56
+ ## Config
57
+
58
+ Optional global config:
59
+
60
+ ```text
61
+ ~/<pi-config-dir>/agent/extensions/fast.json
62
+ ```
63
+
64
+ Optional project config:
65
+
66
+ ```text
67
+ <project>/<pi-config-dir>/fast.json
68
+ ```
69
+
70
+ Here `<pi-config-dir>` is Pi's runtime config directory name (`CONFIG_DIR_NAME`; `.pi` by default). Project config overrides global config after Pi reports that the project is trusted.
71
+
72
+ ```json
73
+ {
74
+ "enabled": false,
75
+ "showStatus": true
76
+ }
77
+ ```
78
+
79
+ - `enabled`: default Fast-mode state when there is no session override.
80
+ - `showStatus`: show a compact `fast` status when Fast mode is active for the selected model.
81
+
82
+ The unified extension intentionally does not read `openai-fast.json` or `claude-fast.json`; those files belong to the provider-specific standalone packages and may contain conflicting defaults.
83
+
84
+ ## Install
85
+
86
+ ### Standalone npm package
87
+
88
+ ```bash
89
+ pi install npm:@diegopetrucci/pi-fast
90
+ ```
91
+
92
+ ### Collection package
93
+
94
+ ```bash
95
+ pi install npm:@diegopetrucci/pi-extensions
96
+ ```
97
+
98
+ ### GitHub package
99
+
100
+ ```bash
101
+ pi install git:github.com/diegopetrucci/pi-extensions
102
+ ```
103
+
104
+ Then reload pi:
105
+
106
+ ```text
107
+ /reload
108
+ ```
109
+
110
+ ## Legacy provider-specific packages
111
+
112
+ The collection package loads this unified extension instead of `openai-fast` and `claude-fast`. Both provider-specific packages remain available as standalone alternatives. Do not load them together with this extension: independent Fast-mode state and duplicate `/fast` registrations can produce conflicting behavior.
113
+
114
+ ## Notes
115
+
116
+ - Anthropic Fast mode has separate rate limits, costs more than standard speed, and does not share prompt-cache prefixes with standard-speed requests.
117
+ - OpenAI Fast mode intentionally does not affect API-key models.
118
+ - Existing `speed` and `service_tier` fields are never overwritten.
119
+ - Cost accounting still depends on the provider reporting the effective tier in its streamed response.
@@ -0,0 +1,4 @@
1
+ {
2
+ "enabled": false,
3
+ "showStatus": true
4
+ }