@@ -22,6 +22,218 @@ const GEMINI_BUNDLE = join(REPO_ROOT, 'bundle', 'gemini.js');
2222const LOG_DIR = join ( REPO_ROOT , 'logs' , 'agent' ) ;
2323const DEBUG_AGENT = process . env . DEBUG_AGENT === 'true' ;
2424
25+ let latestUsageRecord = null ;
26+ let aggregateUsage = null ;
27+
28+ const emptyDecisions = ( ) => ( {
29+ accept : 0 ,
30+ reject : 0 ,
31+ modify : 0 ,
32+ auto_accept : 0 ,
33+ } ) ;
34+
35+ const deepClone = ( value ) => {
36+ if ( typeof globalThis . structuredClone === 'function' ) {
37+ return globalThis . structuredClone ( value ) ;
38+ }
39+ return JSON . parse ( JSON . stringify ( value ) ) ;
40+ } ;
41+
42+ const normalizeModelStats = ( stats ) => ( {
43+ api : {
44+ totalRequests : stats ?. api ?. totalRequests ?? 0 ,
45+ totalErrors : stats ?. api ?. totalErrors ?? 0 ,
46+ totalLatencyMs : stats ?. api ?. totalLatencyMs ?? 0 ,
47+ } ,
48+ tokens : {
49+ prompt : stats ?. tokens ?. prompt ?? 0 ,
50+ candidates : stats ?. tokens ?. candidates ?? 0 ,
51+ total : stats ?. tokens ?. total ?? 0 ,
52+ cached : stats ?. tokens ?. cached ?? 0 ,
53+ thoughts : stats ?. tokens ?. thoughts ?? 0 ,
54+ tool : stats ?. tokens ?. tool ?? 0 ,
55+ } ,
56+ } ) ;
57+
58+ const normalizeToolStats = ( stats ) => ( {
59+ count : stats ?. count ?? 0 ,
60+ success : stats ?. success ?? 0 ,
61+ fail : stats ?. fail ?? 0 ,
62+ durationMs : stats ?. durationMs ?? 0 ,
63+ decisions : {
64+ accept : stats ?. decisions ?. accept ?? 0 ,
65+ reject : stats ?. decisions ?. reject ?? 0 ,
66+ modify : stats ?. decisions ?. modify ?? 0 ,
67+ auto_accept : stats ?. decisions ?. auto_accept ?? 0 ,
68+ } ,
69+ } ) ;
70+
71+ const mergeModelStats = ( target , source ) => {
72+ for ( const [ modelName , modelStats ] of Object . entries ( source ?. models ?? { } ) ) {
73+ const existing = target . models [ modelName ]
74+ ? normalizeModelStats ( target . models [ modelName ] )
75+ : normalizeModelStats ( ) ;
76+ const incoming = normalizeModelStats ( modelStats ) ;
77+ existing . api . totalRequests += incoming . api . totalRequests ;
78+ existing . api . totalErrors += incoming . api . totalErrors ;
79+ existing . api . totalLatencyMs += incoming . api . totalLatencyMs ;
80+ existing . tokens . prompt += incoming . tokens . prompt ;
81+ existing . tokens . candidates += incoming . tokens . candidates ;
82+ existing . tokens . total += incoming . tokens . total ;
83+ existing . tokens . cached += incoming . tokens . cached ;
84+ existing . tokens . thoughts += incoming . tokens . thoughts ;
85+ existing . tokens . tool += incoming . tokens . tool ;
86+ target . models [ modelName ] = existing ;
87+ }
88+ } ;
89+
90+ const mergeToolStats = ( target , source ) => {
91+ const incomingTools = source ?. tools ;
92+ if ( ! incomingTools ) return ;
93+
94+ target . tools . totalCalls += incomingTools . totalCalls ?? 0 ;
95+ target . tools . totalSuccess += incomingTools . totalSuccess ?? 0 ;
96+ target . tools . totalFail += incomingTools . totalFail ?? 0 ;
97+ target . tools . totalDurationMs += incomingTools . totalDurationMs ?? 0 ;
98+
99+ const incomingDecisions = incomingTools . totalDecisions ?? emptyDecisions ( ) ;
100+ target . tools . totalDecisions . accept += incomingDecisions . accept ?? 0 ;
101+ target . tools . totalDecisions . reject += incomingDecisions . reject ?? 0 ;
102+ target . tools . totalDecisions . modify += incomingDecisions . modify ?? 0 ;
103+ target . tools . totalDecisions . auto_accept += incomingDecisions . auto_accept ?? 0 ;
104+
105+ for ( const [ toolName , stats ] of Object . entries ( incomingTools . byName ?? { } ) ) {
106+ const existing = target . tools . byName [ toolName ]
107+ ? normalizeToolStats ( target . tools . byName [ toolName ] )
108+ : normalizeToolStats ( ) ;
109+ const incoming = normalizeToolStats ( stats ) ;
110+ existing . count += incoming . count ;
111+ existing . success += incoming . success ;
112+ existing . fail += incoming . fail ;
113+ existing . durationMs += incoming . durationMs ;
114+ existing . decisions . accept += incoming . decisions . accept ;
115+ existing . decisions . reject += incoming . decisions . reject ;
116+ existing . decisions . modify += incoming . decisions . modify ;
117+ existing . decisions . auto_accept += incoming . decisions . auto_accept ;
118+ target . tools . byName [ toolName ] = existing ;
119+ }
120+ } ;
121+
122+ const mergeFileStats = ( target , source ) => {
123+ target . files . totalLinesAdded += source ?. files ?. totalLinesAdded ?? 0 ;
124+ target . files . totalLinesRemoved += source ?. files ?. totalLinesRemoved ?? 0 ;
125+ } ;
126+
127+ const mergeStats = ( target , source ) => {
128+ mergeModelStats ( target , source ) ;
129+ mergeToolStats ( target , source ) ;
130+ mergeFileStats ( target , source ) ;
131+ } ;
132+
133+ const computeSummary = ( stats ) => {
134+ const summary = {
135+ totalRequests : 0 ,
136+ totalPromptTokens : 0 ,
137+ totalOutputTokens : 0 ,
138+ totalTokens : 0 ,
139+ totalCachedTokens : 0 ,
140+ models : [ ] ,
141+ } ;
142+
143+ for ( const [ name , model ] of Object . entries ( stats ?. models ?? { } ) ) {
144+ const requests = model . api ?. totalRequests ?? 0 ;
145+ const promptTokens = model . tokens ?. prompt ?? 0 ;
146+ const outputTokens = model . tokens ?. candidates ?? 0 ;
147+ const totalTokens = model . tokens ?. total ?? promptTokens + outputTokens ;
148+ const cachedTokens = model . tokens ?. cached ?? 0 ;
149+
150+ summary . totalRequests += requests ;
151+ summary . totalPromptTokens += promptTokens ;
152+ summary . totalOutputTokens += outputTokens ;
153+ summary . totalTokens += totalTokens ;
154+ summary . totalCachedTokens += cachedTokens ;
155+
156+ summary . models . push ( {
157+ name,
158+ requests,
159+ promptTokens,
160+ outputTokens,
161+ totalTokens,
162+ cachedTokens,
163+ } ) ;
164+ }
165+
166+ return summary ;
167+ } ;
168+
169+ const recordUsageStats = ( stats ) => {
170+ if ( ! stats ) return ;
171+
172+ const timestamp = new Date ( ) . toISOString ( ) ;
173+ const clonedStats = deepClone ( stats ) ;
174+ const summary = computeSummary ( clonedStats ) ;
175+
176+ latestUsageRecord = {
177+ stats : clonedStats ,
178+ summary,
179+ recordedAt : timestamp ,
180+ } ;
181+
182+ if ( ! aggregateUsage ) {
183+ aggregateUsage = {
184+ stats : deepClone ( clonedStats ) ,
185+ summary : computeSummary ( clonedStats ) ,
186+ sessions : 1 ,
187+ since : timestamp ,
188+ updatedAt : timestamp ,
189+ } ;
190+ return ;
191+ }
192+
193+ mergeStats ( aggregateUsage . stats , stats ) ;
194+ aggregateUsage . summary = computeSummary ( aggregateUsage . stats ) ;
195+ aggregateUsage . sessions += 1 ;
196+ aggregateUsage . updatedAt = timestamp ;
197+ } ;
198+
199+ const parseGeminiJsonOutput = ( rawOutput ) => {
200+ if ( ! rawOutput ) return null ;
201+ const trimmed = rawOutput . trim ( ) ;
202+ if ( ! trimmed ) return null ;
203+
204+ const firstBraceIndex = trimmed . indexOf ( '{' ) ;
205+ if ( firstBraceIndex === - 1 ) return null ;
206+
207+ const jsonCandidate = trimmed . slice ( firstBraceIndex ) ;
208+
209+ try {
210+ return JSON . parse ( jsonCandidate ) ;
211+ } catch ( error ) {
212+ console . warn ( '⚠️ Failed to parse Gemini JSON output:' , error ) ;
213+ return null ;
214+ }
215+ } ;
216+
217+ export const getUsageSummary = ( ) => {
218+ const latest = latestUsageRecord
219+ ? {
220+ ...latestUsageRecord ,
221+ stats : deepClone ( latestUsageRecord . stats ) ,
222+ summary : { ...latestUsageRecord . summary } ,
223+ }
224+ : null ;
225+
226+ const aggregate = aggregateUsage
227+ ? {
228+ ...aggregateUsage ,
229+ stats : deepClone ( aggregateUsage . stats ) ,
230+ summary : { ...aggregateUsage . summary } ,
231+ }
232+ : null ;
233+
234+ return { latest, aggregate } ;
235+ } ;
236+
25237function ensureNodeVersion ( ) {
26238 const [ major ] = process . versions . node . split ( '.' ) ;
27239 const majorNumber = Number ( major ) ;
@@ -112,7 +324,7 @@ Please apply the requested update and include DONE at the very end.`;
112324 '--prompt' ,
113325 promptWithContext ,
114326 '--output-format' ,
115- 'text ' ,
327+ 'json ' ,
116328 ] ;
117329
118330 return new Promise ( ( resolve , reject ) => {
@@ -151,6 +363,13 @@ Please apply the requested update and include DONE at the very end.`;
151363
152364 if ( code === 0 ) {
153365 const trimmedOutput = output . trim ( ) ;
366+ const parsed = parseGeminiJsonOutput ( trimmedOutput ) ;
367+ const responseText = parsed ?. response ?? trimmedOutput ;
368+ const stats = parsed ?. stats ;
369+
370+ if ( stats ) {
371+ recordUsageStats ( stats ) ;
372+ }
154373
155374 if ( / E r r o r w h e n t a l k i n g t o G e m i n i A P I / i. test ( errorOutput ) ) {
156375 const logPath = await writeDebugLog (
@@ -165,7 +384,7 @@ Please apply the requested update and include DONE at the very end.`;
165384 return ;
166385 }
167386
168- if ( ! trimmedOutput ) {
387+ if ( ! responseText . trim ( ) ) {
169388 const logPath = await writeDebugLog (
170389 `${ new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' ) } -empty-output.log` ,
171390 `User command: ${ userCommand } \nArgs: ${ JSON . stringify ( args ) } \nStdout empty.\nStderr:\n${ errorOutput } \n` ,
@@ -176,7 +395,7 @@ Please apply the requested update and include DONE at the very end.`;
176395 return ;
177396 }
178397
179- if ( ! / D O N E \b / . test ( trimmedOutput ) ) {
398+ if ( ! / D O N E \b / . test ( responseText ) ) {
180399 console . warn ( '⚠️ Gemini agent finished without DONE confirmation.' ) ;
181400 }
182401
@@ -188,7 +407,9 @@ Please apply the requested update and include DONE at the very end.`;
188407 console . log ( '✅ Gemini agent completed successfully' ) ;
189408 resolve ( {
190409 success : true ,
191- output,
410+ output : responseText ,
411+ rawOutput : output ,
412+ stats : stats ? deepClone ( stats ) : null ,
192413 modifiedFile : GENERATED_CONTENT_PATH ,
193414 debugLogPath : logPath ,
194415 } ) ;
0 commit comments