@@ -169,7 +169,7 @@ function extractCostTotal(usage: any): number {
169169 return 0 ;
170170}
171171
172- function sumSessionUsage ( ctx : ExtensionCommandContext ) : {
172+ function sumSessionUsage ( ctx : ExtensionContext | ExtensionCommandContext ) : {
173173 input : number ;
174174 output : number ;
175175 cacheRead : number ;
@@ -206,6 +206,71 @@ function sumSessionUsage(ctx: ExtensionCommandContext): {
206206 } ;
207207}
208208
209+ function estimateToolDefinitionTokens ( pi : ExtensionAPI ) : { toolsTokens : number ; activeTools : number } {
210+ const TOOL_FUDGE = 1.5 ;
211+ const activeToolNames = pi . getActiveTools ( ) ;
212+ const toolInfoByName = new Map ( pi . getAllTools ( ) . map ( ( t ) => [ t . name , t ] as const ) ) ;
213+ let toolsTokens = 0 ;
214+ for ( const name of activeToolNames ) {
215+ const info = toolInfoByName . get ( name ) ;
216+ const blob = `${ name } \n${ info ?. description ?? "" } ` ;
217+ toolsTokens += estimateTokens ( blob ) ;
218+ }
219+ toolsTokens = Math . round ( toolsTokens * TOOL_FUDGE ) ;
220+ return { toolsTokens, activeTools : activeToolNames . length } ;
221+ }
222+
223+ type ContextUsageSnapshot = {
224+ generated_at : string ;
225+ session_id : string ;
226+ context_window_used_tokens ?: number ;
227+ context_window_limit_tokens ?: number ;
228+ context_window_used_pct ?: number ;
229+ session_total_tokens : number ;
230+ session_total_cost_usd : number ;
231+ message_tokens ?: number ;
232+ tools_tokens ?: number ;
233+ } ;
234+
235+ function buildContextUsageSnapshot ( pi : ExtensionAPI , ctx : ExtensionContext ) : ContextUsageSnapshot {
236+ const usage = ctx . getContextUsage ( ) ;
237+ const { toolsTokens } = estimateToolDefinitionTokens ( pi ) ;
238+ const messageTokens = usage ?. tokens ?? 0 ;
239+ const contextWindow = usage ?. contextWindow ?? 0 ;
240+ const effectiveTokens = Math . max ( 0 , messageTokens + toolsTokens ) ;
241+ const percent = contextWindow > 0 ? ( effectiveTokens / contextWindow ) * 100 : 0 ;
242+ const sessionUsage = sumSessionUsage ( ctx ) ;
243+
244+ const snapshot : ContextUsageSnapshot = {
245+ generated_at : new Date ( ) . toISOString ( ) ,
246+ session_id : ctx . sessionManager . getSessionId ( ) ,
247+ session_total_tokens : Math . max ( 0 , sessionUsage . totalTokens ) ,
248+ session_total_cost_usd : Math . max ( 0 , sessionUsage . totalCost ) ,
249+ } ;
250+
251+ if ( contextWindow > 0 ) {
252+ snapshot . context_window_used_tokens = effectiveTokens ;
253+ snapshot . context_window_limit_tokens = contextWindow ;
254+ snapshot . context_window_used_pct = percent ;
255+ snapshot . message_tokens = messageTokens ;
256+ snapshot . tools_tokens = toolsTokens ;
257+ }
258+
259+ return snapshot ;
260+ }
261+
262+ async function persistContextUsageSnapshot ( snapshot : ContextUsageSnapshot ) : Promise < void > {
263+ const snapshotPath = path . join ( os . homedir ( ) , ".pi" , "agent" , "context-usage.json" ) ;
264+ const tmpPath = `${ snapshotPath } .tmp` ;
265+ try {
266+ await fs . mkdir ( path . dirname ( snapshotPath ) , { recursive : true } ) ;
267+ await fs . writeFile ( tmpPath , `${ JSON . stringify ( snapshot , null , 2 ) } \n` , { mode : 0o600 } ) ;
268+ await fs . rename ( tmpPath , snapshotPath ) ;
269+ } catch {
270+ // Best-effort only. Observability should not disrupt agent runtime.
271+ }
272+ }
273+
209274function shortenPath ( p : string , cwd : string ) : string {
210275 const rp = path . resolve ( p ) ;
211276 const rc = path . resolve ( cwd ) ;
@@ -449,7 +514,14 @@ export default function contextExtension(pi: ExtensionAPI) {
449514 return best ?. name ?? null ;
450515 } ;
451516
517+ const persistSnapshotFromContext = async ( ctx : ExtensionContext ) : Promise < void > => {
518+ const snapshot = buildContextUsageSnapshot ( pi , ctx ) ;
519+ await persistContextUsageSnapshot ( snapshot ) ;
520+ } ;
521+
452522 pi . on ( "tool_result" , ( event : ToolResultEvent , ctx : ExtensionContext ) => {
523+ void persistSnapshotFromContext ( ctx ) ;
524+
453525 // Only count successful reads.
454526 if ( ( event as any ) . toolName !== "read" ) return ;
455527 if ( ( event as any ) . isError ) return ;
@@ -469,6 +541,14 @@ export default function contextExtension(pi: ExtensionAPI) {
469541 }
470542 } ) ;
471543
544+ pi . on ( "turn_end" , async ( _event , ctx : ExtensionContext ) => {
545+ await persistSnapshotFromContext ( ctx ) ;
546+ } ) ;
547+
548+ pi . on ( "session_start" , async ( _event , ctx : ExtensionContext ) => {
549+ await persistSnapshotFromContext ( ctx ) ;
550+ } ) ;
551+
472552 pi . registerCommand ( "context" , {
473553 description : "Show loaded context overview" ,
474554 handler : async ( _args , ctx : ExtensionCommandContext ) => {
@@ -503,18 +583,8 @@ export default function contextExtension(pi: ExtensionAPI) {
503583 const ctxWindow = usage ?. contextWindow ?? 0 ;
504584
505585 // Tool definitions are not part of ctx.getContextUsage() (it estimates message tokens).
506- // We approximate their token impact from tool name + description, and apply a fudge
507- // factor to account for parameters/schema/formatting.
508- const TOOL_FUDGE = 1.5 ;
509- const activeToolNames = pi . getActiveTools ( ) ;
510- const toolInfoByName = new Map ( pi . getAllTools ( ) . map ( ( t ) => [ t . name , t ] as const ) ) ;
511- let toolsTokens = 0 ;
512- for ( const name of activeToolNames ) {
513- const info = toolInfoByName . get ( name ) ;
514- const blob = `${ name } \n${ info ?. description ?? "" } ` ;
515- toolsTokens += estimateTokens ( blob ) ;
516- }
517- toolsTokens = Math . round ( toolsTokens * TOOL_FUDGE ) ;
586+ // We approximate their token impact from tool name + description.
587+ const { toolsTokens, activeTools } = estimateToolDefinitionTokens ( pi ) ;
518588
519589 const effectiveTokens = messageTokens + toolsTokens ;
520590 const percent = ctxWindow > 0 ? ( effectiveTokens / ctxWindow ) * 100 : 0 ;
@@ -533,7 +603,7 @@ export default function contextExtension(pi: ExtensionAPI) {
533603 lines . push ( "Window: (unknown)" ) ;
534604 }
535605 lines . push ( `System: ~${ systemPromptTokens . toLocaleString ( ) } tok (AGENTS ~${ agentTokens . toLocaleString ( ) } )` ) ;
536- lines . push ( `Tools: ~${ toolsTokens . toLocaleString ( ) } tok (${ activeToolNames . length } active)` ) ;
606+ lines . push ( `Tools: ~${ toolsTokens . toLocaleString ( ) } tok (${ activeTools } active)` ) ;
537607 lines . push ( `AGENTS: ${ agentFilePaths . length ? joinComma ( agentFilePaths ) : "(none)" } ` ) ;
538608 lines . push ( `Extensions (${ extensionFiles . length } ): ${ extensionFiles . length ? joinComma ( extensionFiles ) : "(none)" } ` ) ;
539609 lines . push ( `Skills (${ skills . length } ): ${ skills . length ? joinComma ( skills ) : "(none)" } ` ) ;
@@ -559,7 +629,7 @@ export default function contextExtension(pi: ExtensionAPI) {
559629 systemPromptTokens,
560630 agentTokens,
561631 toolsTokens,
562- activeTools : activeToolNames . length ,
632+ activeTools,
563633 }
564634 : null ,
565635 agentFiles : agentFilePaths ,
0 commit comments