@mrclrchtr/supi-context 2.6.1 → 2.7.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 CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  # @mrclrchtr/supi-context
10
10
 
11
- Adds a `/supi-context` command to the [pi coding agent](https://github.com/earendil-works/pi) so you can inspect how the current session is spending its context window.
11
+ Adds context-capacity observability to the [pi coding agent](https://github.com/earendil-works/pi).
12
12
 
13
13
  ## Install
14
14
 
@@ -22,56 +22,58 @@ For local development:
22
22
  pi install ./packages/supi-context
23
23
  ```
24
24
 
25
- ![Context usage report](https://raw.githubusercontent.com/mrclrchtr/supi/main/screenshots/supi-context.png)
25
+ ## Two concepts, two surfaces
26
26
 
27
- ## What you get
27
+ - A **Context Pressure Snapshot** is a small point-in-time capacity reading for deciding whether a session has room for another operation.
28
+ - A **Context Usage Report** is a diagnostic account of where the session's context is spent.
28
29
 
29
- After install, pi gets one user command and an optional agent-callable tool:
30
+ ### Human: Context Usage Report
30
31
 
31
- - `/supi-context` render a detailed context-usage report for the current session
32
- - `/supi-context full` — render the same report with the full guideline and tool-definition lists instead of previews
33
- - `supi_context` — agent-callable tool (disabled by default; see Configuration)
32
+ In interactive TUI mode, pi gets these commands:
34
33
 
35
- The command sends a custom `supi-context` message, and this package registers a dedicated renderer so the report shows up as a structured TUI view instead of plain text.
34
+ - `/supi-context` render a report with guideline and tool previews
35
+ - `/supi-context full` — render the same report with complete guideline and tool lists
36
36
 
37
- The `supi_context` tool returns the same analysis as JSON, so the agent can inspect context usage programmatically useful for checking remaining capacity before large operations or after heavy tool results. In the TUI, the tool renders a compact usage summary by default and expands into the same structured report as `/supi-context`.
37
+ The command appends a durable `supi-context` custom entry and uses a dedicated entry renderer. The report remains in the transcript but never enters LLM context.
38
+
39
+ ### Agent: Context Pressure Snapshot
40
+
41
+ `supi_context` is agent-callable when enabled in configuration. It defaults to a one-line, constant-shape JSON **Context Pressure Snapshot**:
42
+
43
+ ```ts
44
+ interface ContextPressureSnapshot {
45
+ modelName: string;
46
+ contextWindow: number | null;
47
+ usedTokens: number;
48
+ usagePercent: number | null;
49
+ compactionEnabled: boolean;
50
+ reserveTokens: number;
51
+ headroomTokens: number | null;
52
+ pressurePercent: number | null;
53
+ compacted: boolean;
54
+ approximationNote: string | null;
55
+ }
56
+ ```
57
+
58
+ Use `supi_context({ mode: "full" })` only when diagnostic attribution is needed. Full mode returns compact JSON for the Context Usage Report. If it exceeds Pi's normal tool-output limits, it returns a small valid-JSON envelope with the path to a temporary file containing the complete report.
59
+
60
+ The tool TUI never shows raw agent JSON: concise results render as a dense snapshot that expands into its metrics; full results expand into the diagnostic report.
38
61
 
39
62
  ## What the report shows
40
63
 
41
- The report is meant to answer questions like:
42
-
43
- - what is taking up space in the current context window?
44
- - how much room is left before compaction pressure gets worse?
45
- - which instruction files, context files, skills, guidelines, or tools are expensive?
46
- - what extra context was injected by other SuPi extensions?
47
-
48
- It includes:
49
-
50
- - model name, context-window size, and total token usage
51
- - approximation or pending-usage notes when exact usage data is not available yet
52
- - a visual usage bar for system prompt, user messages, assistant messages, tool calls, tool results, other, autocompact buffer, and free space
53
- - a category breakdown table for the same usage buckets
54
- - a system-prompt composition breakdown for:
55
- - base prompt content
56
- - instruction files (`AGENTS.md`, `CLAUDE.md`, etc.)
57
- - other context files loaded into the system prompt
58
- - active skills
59
- - guidelines
60
- - tool snippets
61
- - append text
62
- - instruction-file details with token cost, line count, and detected origin (`project` vs `global`)
63
- - legacy injected subdirectory context files from older `supi-claude-md` sessions, when present
64
- - active skill names with per-skill token counts
65
- - guideline bullet previews, plus source attribution for PI defaults, known built-in tools (`read`, `write`, `edit`), and `other`
66
- - active tool definitions with per-tool definition token counts and snippet-token columns when available
67
- - a compaction note when older turns were summarized
68
- - extra provider sections from extensions registered through the shared context-provider registry in `@mrclrchtr/supi-core`
64
+ The Context Usage Report includes:
65
+
66
+ - model name, context-window size, used tokens, usage percentage, pressure percentage, and **Headroom**
67
+ - the effective **Compaction reserve** and factual presence of compaction on the active branch
68
+ - approximation or pending-usage notes when exact usage is not available
69
+ - a visual usage bar and attribution-category breakdown for system prompt, messages, tool calls, tool results, and other context
70
+ - system-prompt composition, instruction/context files, skills, guideline sources, tool definitions, injected files, and registered provider sections
69
71
 
70
72
  ## Configuration
71
73
 
72
- No settings are required for the `/supi-context` command.
74
+ The human `/supi-context` command needs no configuration.
73
75
 
74
- To enable the `supi_context` agent tool, set `agentToolEnabled` to `true` in your supi config:
76
+ To enable the agent-callable `supi_context` tool, set `agentToolEnabled` to `true` in your supi config:
75
77
 
76
78
  ```json
77
79
  {
@@ -87,26 +89,18 @@ The tool is disabled by default and requires a `/reload` or restart after toggli
87
89
 
88
90
  ## Notes
89
91
 
90
- - The command uses the latest cached `systemPromptOptions` captured during `before_agent_start`.
91
- - If those prompt options are missing or incomplete, the package backfills context files and skills by re-parsing the current system prompt.
92
- - Exact totals come from pi's current context-usage data when available. Otherwise the report falls back to rough estimates and/or scales estimated category totals to the latest measured total.
93
- - If no model is selected yet, the report can still render, but the context-window bar cannot show capacity.
92
+ - The extension caches the latest `systemPromptOptions` from `before_agent_start`. If those are missing or incomplete, it backfills context files and skills from the current system prompt.
93
+ - Exact usage comes from Pi's current context-usage data when available. Otherwise the extension estimates usage and preserves an approximation note.
94
+ - The Active Context Limit is the auto-compaction threshold while auto-compaction is enabled, otherwise the raw model context window. Headroom and pressure use that limit.
94
95
 
95
96
  ## Source
96
97
 
97
- - `src/context.ts` — command registration, agent tool registration, cached prompt-option handling, and renderer wiring
98
- - `src/config.ts` — config loading with `agentToolEnabled` toggle
99
- - `src/settings-registration.ts` — `/supi-settings` registration for the agent tool toggle
100
- - `src/analysis.ts` — token accounting, attribution, and report data assembly
101
- - `src/format.ts` report orchestration for the TUI view
102
- - `src/format-helpers.ts` — shared numeric and category helpers for report rendering
103
- - `src/format-summary.ts` — summary, usage bar, category, and composition sections
104
- - `src/format-sections.ts` — instruction file, context file, skill, guideline, tool, compaction, and provider sections
105
- - `src/prompt-inference.ts` — fallback recovery of context files, skills, and guideline sections from the live system prompt
106
- - `src/renderer.ts` — custom renderer for `supi-context` messages
107
- - `src/report-component.ts` — shared width-aware report component for message and tool renderers
108
- - `src/tool/guidance.ts` — tool description, prompt snippet, and guidelines for the agent tool
109
- - `src/tool/render.ts` — TUI call/result renderer for the agent tool
110
- - `src/utils.ts` — token and plural-format helpers
98
+ - `src/context.ts` — surface registration and cached prompt-option handling
99
+ - `src/capacity.ts` — shared capacity analysis and snapshot shape
100
+ - `src/analysis.ts` — diagnostic attribution and report data assembly
101
+ - `src/entry-renderer.ts` — TUI-only custom-entry renderer for the human command
102
+ - `src/format*.ts` and `src/report-component.ts` Context Usage Report rendering
103
+ - `src/snapshot-component.ts` — compact, width-safe snapshot rendering
104
+ - `src/tool/guidance.ts`, `src/tool/output.ts`, and `src/tool/render.ts` agent-tool guidance, safe full JSON output, and tool rendering
111
105
 
112
106
  Tests live under `__tests__/unit/`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-context",
3
- "version": "2.6.1",
4
- "description": "SuPi Context extension — detailed context usage report via /supi-context command",
3
+ "version": "2.7.0",
4
+ "description": "SuPi Context extension — agent pressure snapshots and TUI usage reports",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -32,16 +32,20 @@
32
32
  ],
33
33
  "dependencies": {
34
34
  "typebox": "*",
35
- "@mrclrchtr/supi-core": "2.6.1"
35
+ "@mrclrchtr/supi-core": "2.7.0"
36
36
  },
37
37
  "bundledDependencies": [
38
38
  "@mrclrchtr/supi-core"
39
39
  ],
40
40
  "peerDependencies": {
41
+ "@earendil-works/pi-ai": "*",
41
42
  "@earendil-works/pi-coding-agent": "*",
42
43
  "@earendil-works/pi-tui": "*"
43
44
  },
44
45
  "peerDependenciesMeta": {
46
+ "@earendil-works/pi-ai": {
47
+ "optional": true
48
+ },
45
49
  "@earendil-works/pi-coding-agent": {
46
50
  "optional": true
47
51
  },
@@ -52,8 +56,7 @@
52
56
  "pi": {
53
57
  "extensions": [
54
58
  "./src/extension.ts"
55
- ],
56
- "image": "https://raw.githubusercontent.com/mrclrchtr/supi/main/screenshots/supi-context.png"
59
+ ]
57
60
  },
58
61
  "main": "src/api.ts",
59
62
  "exports": {
package/src/analysis.ts CHANGED
@@ -5,13 +5,18 @@ import {
5
5
  buildSessionContext,
6
6
  type ExtensionAPI,
7
7
  type ExtensionContext,
8
- type estimateTokens,
8
+ estimateTokens,
9
9
  formatSkillsForPrompt,
10
10
  getLatestCompactionEntry,
11
11
  SettingsManager,
12
12
  } from "@earendil-works/pi-coding-agent";
13
13
  import { getRegisteredContextProviders } from "@mrclrchtr/supi-core/context";
14
14
 
15
+ import {
16
+ analyzeContextCapacity,
17
+ type ContextPressureSnapshot,
18
+ createContextPressureSnapshot,
19
+ } from "./capacity.ts";
15
20
  import { deriveOptionsFromSystemPrompt, extractGuidelinesSection } from "./prompt-inference.ts";
16
21
 
17
22
  type AgentMessage = Parameters<typeof estimateTokens>[0];
@@ -69,17 +74,9 @@ export interface ContextProviderSection {
69
74
  data: Record<string, string | number>;
70
75
  }
71
76
 
72
- export interface ContextAnalysis {
73
- modelName: string;
74
- contextWindow: number;
75
- totalTokens: number | null;
77
+ export interface ContextAnalysis extends ContextPressureSnapshot {
76
78
  scaled: boolean;
77
- approximationNote: string | null;
78
- full: boolean;
79
- categories: CategoryTokens & {
80
- autocompactBuffer: number;
81
- freeSpace: number;
82
- };
79
+ categories: CategoryTokens;
83
80
  systemPromptBreakdown: {
84
81
  base: number;
85
82
  instructionFiles: ContextFileInfo[];
@@ -98,7 +95,6 @@ export interface ContextAnalysis {
98
95
  guidelineSources: GuidelineSourceInfo[];
99
96
  toolSnippetDetails: ToolSnippetInfo[];
100
97
  toolDefinitions: { count: number; tokens: number; tools: ToolInfo[] };
101
- compaction: { summarizedTurns: number } | null;
102
98
  providerSections: ContextProviderSection[];
103
99
  }
104
100
 
@@ -106,39 +102,6 @@ export function estimateTextTokens(text: string): number {
106
102
  return Math.ceil(text.length / 4);
107
103
  }
108
104
 
109
- function estimateGenericContent(content: unknown): number {
110
- if (typeof content === "string") {
111
- return estimateTextTokens(content);
112
- }
113
- if (Array.isArray(content)) {
114
- let chars = 0;
115
- for (const block of content as Array<{ type?: string; text?: string }>) {
116
- if (block.type === "text" && block.text) {
117
- chars += block.text.length;
118
- }
119
- }
120
- return Math.ceil(chars / 4);
121
- }
122
- return 0;
123
- }
124
-
125
- function estimateUserMessage(msg: Extract<AgentMessage, { role: "user" }>): number {
126
- const content = msg.content;
127
- if (typeof content === "string") {
128
- return estimateTextTokens(content);
129
- }
130
- if (Array.isArray(content)) {
131
- let chars = 0;
132
- for (const block of content) {
133
- if (block.type === "text" && block.text) {
134
- chars += block.text.length;
135
- }
136
- }
137
- return Math.ceil(chars / 4);
138
- }
139
- return 0;
140
- }
141
-
142
105
  function estimateAssistantMessage(msg: Extract<AgentMessage, { role: "assistant" }>): {
143
106
  text: number;
144
107
  toolCalls: number;
@@ -169,7 +132,7 @@ function estimateMessageByCategory(msg: AgentMessage): {
169
132
  } {
170
133
  if (msg.role === "user") {
171
134
  return {
172
- user: estimateUserMessage(msg),
135
+ user: estimateTokens(msg),
173
136
  assistantText: 0,
174
137
  toolCalls: 0,
175
138
  toolResult: 0,
@@ -185,7 +148,7 @@ function estimateMessageByCategory(msg: AgentMessage): {
185
148
  user: 0,
186
149
  assistantText: 0,
187
150
  toolCalls: 0,
188
- toolResult: estimateGenericContent(msg.content),
151
+ toolResult: estimateTokens(msg),
189
152
  other: 0,
190
153
  };
191
154
  }
@@ -194,10 +157,14 @@ function estimateMessageByCategory(msg: AgentMessage): {
194
157
  assistantText: 0,
195
158
  toolCalls: 0,
196
159
  toolResult: 0,
197
- other: estimateGenericContent((msg as unknown as Record<string, unknown>).content),
160
+ other: estimateTokens(msg),
198
161
  };
199
162
  }
200
163
 
164
+ function estimateMessageTokens(msg: AgentMessage): number {
165
+ return estimateTokens(msg);
166
+ }
167
+
201
168
  function computeMessageCategories(messages: AgentMessage[]): CategoryTokens {
202
169
  const categories: CategoryTokens = {
203
170
  systemPrompt: 0,
@@ -220,6 +187,26 @@ function computeMessageCategories(messages: AgentMessage[]): CategoryTokens {
220
187
  return categories;
221
188
  }
222
189
 
190
+ interface ScalingResult {
191
+ categories: CategoryTokens;
192
+ scaled: boolean;
193
+ approximationNote: string | null;
194
+ usedTokens: number;
195
+ }
196
+
197
+ type CurrentContextUsage = ReturnType<ExtensionContext["getContextUsage"]>;
198
+
199
+ function hasMeasuredTokens(tokens: number | null | undefined): tokens is number {
200
+ return typeof tokens === "number" && tokens > 0;
201
+ }
202
+
203
+ function getApproximationNote(contextUsage: CurrentContextUsage): string | null {
204
+ if (contextUsage === undefined) return "Approximate (no usage data available)";
205
+ return hasMeasuredTokens(contextUsage.tokens)
206
+ ? null
207
+ : "Token count pending — send a message to refresh";
208
+ }
209
+
223
210
  function applyScaling(
224
211
  categories: CategoryTokens,
225
212
  actualTokens: number | null,
@@ -227,20 +214,13 @@ function applyScaling(
227
214
  contextUsage:
228
215
  | { tokens: number | null; contextWindow: number; percent: number | null }
229
216
  | undefined,
230
- ): {
231
- categories: CategoryTokens;
232
- scaled: boolean;
233
- approximationNote: string | null;
234
- totalTokens: number;
235
- } {
217
+ ): ScalingResult {
236
218
  let scaled = false;
237
- let approximationNote: string | null = null;
238
- const hasActualTotal = actualTokens !== null && actualTokens > 0;
239
- const totalTokens = hasActualTotal ? actualTokens : rawTotal;
219
+ const hasActualTotal = hasMeasuredTokens(actualTokens);
220
+ const usedTokens = hasActualTotal ? actualTokens : rawTotal;
221
+ const approximationNote = getApproximationNote(contextUsage);
240
222
 
241
- if (contextUsage === undefined) {
242
- approximationNote = "Approximate (no usage data available)";
243
- } else if (hasActualTotal && rawTotal > 0) {
223
+ if (hasActualTotal && rawTotal > 0) {
244
224
  const scale = actualTokens / rawTotal;
245
225
  categories.systemPrompt = Math.round(categories.systemPrompt * scale);
246
226
  categories.userMessages = Math.round(categories.userMessages * scale);
@@ -249,11 +229,9 @@ function applyScaling(
249
229
  categories.toolResults = Math.round(categories.toolResults * scale);
250
230
  categories.other = Math.round(categories.other * scale);
251
231
  scaled = true;
252
- } else if (actualTokens === null || actualTokens === 0) {
253
- approximationNote = "Token count pending — send a message to refresh";
254
232
  }
255
233
 
256
- return { categories, scaled, approximationNote, totalTokens };
234
+ return { categories, scaled, approximationNote, usedTokens };
257
235
  }
258
236
 
259
237
  /**
@@ -508,18 +486,10 @@ function computeToolDefinitions(pi: ExtensionAPI): {
508
486
  };
509
487
  }
510
488
 
511
- function detectCompaction(branch: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>): {
512
- summarizedTurns: number;
513
- } | null {
514
- const compactionEntry = getLatestCompactionEntry(branch);
515
- if (!compactionEntry) return null;
516
-
517
- const index = branch.findIndex((e) => e.id === compactionEntry.id);
518
- const messagesBefore = branch
519
- .slice(0, Math.max(0, index))
520
- .filter((e) => e.type === "message").length;
521
- const summarizedTurns = Math.floor(messagesBefore / 2);
522
- return { summarizedTurns };
489
+ function hasCompactionOnActiveBranch(
490
+ branch: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>,
491
+ ): boolean {
492
+ return getLatestCompactionEntry(branch) !== null;
523
493
  }
524
494
 
525
495
  export function extractInjectedContextFiles(messages: AgentMessage[]): InjectedFileInfo[] {
@@ -556,22 +526,93 @@ export function extractInjectedContextFiles(messages: AgentMessage[]): InjectedF
556
526
  return Array.from(seen.values()).sort((a, b) => a.turn - b.turn || a.file.localeCompare(b.file));
557
527
  }
558
528
 
559
- export function analyzeContext(
529
+ interface ContextFallback {
530
+ messages: AgentMessage[];
531
+ systemPromptText: string;
532
+ }
533
+
534
+ interface CapacityObservation {
535
+ branch: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>;
536
+ contextUsage: CurrentContextUsage;
537
+ snapshot: ContextPressureSnapshot;
538
+ fallback?: ContextFallback;
539
+ }
540
+
541
+ interface ContextObservation extends ContextFallback {
542
+ scaling: ScalingResult;
543
+ snapshot: ContextPressureSnapshot;
544
+ }
545
+
546
+ function estimateContextTokens(fallback: ContextFallback): number {
547
+ return (
548
+ estimateTextTokens(fallback.systemPromptText) +
549
+ fallback.messages.reduce((total, message) => total + estimateMessageTokens(message), 0)
550
+ );
551
+ }
552
+
553
+ function collectContextFallback(
560
554
  ctx: ExtensionContext,
561
- pi: ExtensionAPI,
562
- cachedOptions: BuildSystemPromptOptions | undefined,
563
- full = false,
564
- ): ContextAnalysis {
555
+ branch: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>,
556
+ ): ContextFallback {
557
+ return {
558
+ messages: buildSessionContext(branch).messages,
559
+ systemPromptText: ctx.getSystemPrompt(),
560
+ };
561
+ }
562
+
563
+ /**
564
+ * Observe the small shared capacity seam. It only walks messages when Pi has
565
+ * no measured usage total and an aggregate estimate is genuinely necessary.
566
+ */
567
+ function observeCapacity(ctx: ExtensionContext): CapacityObservation {
565
568
  const branch = ctx.sessionManager.getBranch();
566
- const apiView = buildSessionContext(branch);
567
569
  const contextUsage = ctx.getContextUsage();
568
- const contextWindow = contextUsage?.contextWindow ?? 0;
569
- const actualTokens = contextUsage?.tokens ?? null;
570
+ let fallback: ContextFallback | undefined;
571
+ let usedTokens: number;
572
+ if (hasMeasuredTokens(contextUsage?.tokens)) {
573
+ usedTokens = contextUsage.tokens;
574
+ } else {
575
+ fallback = collectContextFallback(ctx, branch);
576
+ usedTokens = estimateContextTokens(fallback);
577
+ }
578
+ const settings = SettingsManager.create(ctx.cwd, undefined, {
579
+ projectTrusted: ctx.isProjectTrusted(),
580
+ });
581
+ const capacity = analyzeContextCapacity({
582
+ contextWindow: contextUsage?.contextWindow ?? null,
583
+ usedTokens,
584
+ compactionEnabled: settings.getCompactionEnabled(),
585
+ configuredReserveTokens: settings.getCompactionReserveTokens(),
586
+ compacted: hasCompactionOnActiveBranch(branch),
587
+ approximationNote: getApproximationNote(contextUsage),
588
+ });
570
589
 
571
- const systemPromptText = ctx.getSystemPrompt();
572
- const categories = computeMessageCategories(apiView.messages);
573
- categories.systemPrompt = estimateTextTokens(systemPromptText);
590
+ return {
591
+ branch,
592
+ contextUsage,
593
+ fallback,
594
+ snapshot: createContextPressureSnapshot(
595
+ ctx.model?.name ?? ctx.model?.id ?? "No model selected",
596
+ capacity,
597
+ ),
598
+ };
599
+ }
600
+
601
+ /** Return a constant-shape Context Pressure Snapshot without diagnostic attribution. */
602
+ export function analyzeContextPressure(ctx: ExtensionContext): ContextPressureSnapshot {
603
+ return observeCapacity(ctx).snapshot;
604
+ }
574
605
 
606
+ /** Compose a full Context Usage Report from shared capacity and diagnostic attribution. */
607
+ export function analyzeContext(
608
+ ctx: ExtensionContext,
609
+ pi: ExtensionAPI,
610
+ cachedOptions: BuildSystemPromptOptions | undefined,
611
+ ): ContextAnalysis {
612
+ const capacity = observeCapacity(ctx);
613
+ const fallback = capacity.fallback ?? collectContextFallback(ctx, capacity.branch);
614
+ const categories = computeMessageCategories(fallback.messages);
615
+ categories.systemPrompt = estimateTextTokens(fallback.systemPromptText);
575
616
  const rawTotal =
576
617
  categories.systemPrompt +
577
618
  categories.userMessages +
@@ -579,45 +620,33 @@ export function analyzeContext(
579
620
  categories.toolCalls +
580
621
  categories.toolResults +
581
622
  categories.other;
582
-
583
- const scaling = applyScaling(categories, actualTokens, rawTotal, contextUsage);
584
-
585
- const autocompactBuffer =
586
- contextWindow > 0 ? SettingsManager.create(ctx.cwd).getCompactionReserveTokens() : 0;
587
- const used =
588
- scaling.categories.systemPrompt +
589
- scaling.categories.userMessages +
590
- scaling.categories.assistantMessages +
591
- scaling.categories.toolCalls +
592
- scaling.categories.toolResults +
593
- scaling.categories.other;
594
- const freeSpace = Math.max(0, contextWindow - used - autocompactBuffer);
595
-
623
+ const observation: ContextObservation = {
624
+ ...fallback,
625
+ scaling: applyScaling(
626
+ categories,
627
+ capacity.contextUsage?.tokens ?? null,
628
+ rawTotal,
629
+ capacity.contextUsage,
630
+ ),
631
+ snapshot: capacity.snapshot,
632
+ };
596
633
  const promptOptions = deriveOptionsFromSystemPrompt(ctx, cachedOptions);
597
634
  const breakdown = computeSystemPromptBreakdown(
598
635
  promptOptions,
599
- systemPromptText,
600
- scaling.categories.systemPrompt,
636
+ observation.systemPromptText,
637
+ observation.scaling.categories.systemPrompt,
601
638
  ctx.cwd,
602
639
  );
603
- const injectedFiles = extractInjectedContextFiles(apiView.messages);
640
+ const injectedFiles = extractInjectedContextFiles(observation.messages);
604
641
  const toolDefinitions = computeToolDefinitions(pi);
605
- const compaction = detectCompaction(branch);
606
-
607
- const guidelineBullets = extractGuidelineBullets(extractGuidelinesSection(systemPromptText));
642
+ const guidelineBullets = extractGuidelineBullets(
643
+ extractGuidelinesSection(observation.systemPromptText),
644
+ );
608
645
 
609
646
  return {
610
- modelName: ctx.model?.name ?? ctx.model?.id ?? "No model selected",
611
- contextWindow,
612
- totalTokens: scaling.totalTokens,
613
- scaled: scaling.scaled,
614
- approximationNote: scaling.approximationNote,
615
- full,
616
- categories: {
617
- ...scaling.categories,
618
- autocompactBuffer,
619
- freeSpace,
620
- },
647
+ ...observation.snapshot,
648
+ scaled: observation.scaling.scaled,
649
+ categories: observation.scaling.categories,
621
650
  systemPromptBreakdown: breakdown,
622
651
  injectedFiles,
623
652
  skills: breakdown.skills,
@@ -626,7 +655,6 @@ export function analyzeContext(
626
655
  guidelineSources: breakdown.guidelineSources,
627
656
  toolSnippetDetails: breakdown.toolSnippetDetails,
628
657
  toolDefinitions,
629
- compaction,
630
658
  providerSections: collectProviderData(),
631
659
  };
632
660
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A small, point-in-time capacity reading without diagnostic attribution.
3
+ */
4
+ export interface ContextCapacity {
5
+ contextWindow: number | null;
6
+ usedTokens: number;
7
+ usagePercent: number | null;
8
+ compactionEnabled: boolean;
9
+ reserveTokens: number;
10
+ headroomTokens: number | null;
11
+ pressurePercent: number | null;
12
+ compacted: boolean;
13
+ approximationNote: string | null;
14
+ }
15
+
16
+ /** Agent-facing, constant-shape reading of current context pressure. */
17
+ export interface ContextPressureSnapshot extends ContextCapacity {
18
+ modelName: string;
19
+ }
20
+
21
+ /** Inputs that the shared capacity analysis derives from session state. */
22
+ export interface ContextCapacityInput {
23
+ contextWindow: number | null;
24
+ usedTokens: number;
25
+ compactionEnabled: boolean;
26
+ configuredReserveTokens: number;
27
+ compacted: boolean;
28
+ approximationNote: string | null;
29
+ }
30
+
31
+ function roundPercentage(value: number): number {
32
+ return Math.round(value * 10) / 10;
33
+ }
34
+
35
+ /**
36
+ * Derive reserve-adjusted capacity without inspecting diagnostic inventories.
37
+ *
38
+ * The configured reserve affects capacity only while auto-compaction is enabled.
39
+ * This lets report and snapshot callers share exactly the same Active Context
40
+ * Limit, Headroom, and Pressure Percentage semantics.
41
+ */
42
+ export function analyzeContextCapacity(input: ContextCapacityInput): ContextCapacity {
43
+ const contextWindow = input.contextWindow && input.contextWindow > 0 ? input.contextWindow : null;
44
+ const reserveTokens = input.compactionEnabled ? Math.max(0, input.configuredReserveTokens) : 0;
45
+ const activeLimit = contextWindow === null ? null : contextWindow - reserveTokens;
46
+ const hasUsableActiveLimit = activeLimit !== null && activeLimit > 0;
47
+
48
+ return {
49
+ contextWindow,
50
+ usedTokens: input.usedTokens,
51
+ usagePercent:
52
+ contextWindow === null ? null : roundPercentage((input.usedTokens / contextWindow) * 100),
53
+ compactionEnabled: input.compactionEnabled,
54
+ reserveTokens,
55
+ headroomTokens: activeLimit === null ? null : Math.max(0, activeLimit - input.usedTokens),
56
+ pressurePercent: hasUsableActiveLimit
57
+ ? roundPercentage((input.usedTokens / activeLimit) * 100)
58
+ : null,
59
+ compacted: input.compacted,
60
+ approximationNote: input.approximationNote,
61
+ };
62
+ }
63
+
64
+ /** Create the exact agent-facing Context Pressure Snapshot shape. */
65
+ export function createContextPressureSnapshot(
66
+ modelName: string,
67
+ capacity: ContextCapacity,
68
+ ): ContextPressureSnapshot {
69
+ return {
70
+ modelName,
71
+ contextWindow: capacity.contextWindow,
72
+ usedTokens: capacity.usedTokens,
73
+ usagePercent: capacity.usagePercent,
74
+ compactionEnabled: capacity.compactionEnabled,
75
+ reserveTokens: capacity.reserveTokens,
76
+ headroomTokens: capacity.headroomTokens,
77
+ pressurePercent: capacity.pressurePercent,
78
+ compacted: capacity.compacted,
79
+ approximationNote: capacity.approximationNote,
80
+ };
81
+ }