@@ -370,6 +370,60 @@ async function loadRecordedAgentOSUsage(
370370 return getRecordedAgentOSUsage ( options ) ;
371371}
372372
373+ /**
374+ * Build a zeroed usage aggregate. Used to seed the per-agent and per-session
375+ * in-memory tallies so callers see real numbers from `agent.usage()` /
376+ * `session.usage()` without having to enable the persisted ledger.
377+ */
378+ function createEmptyUsageAggregate ( sessionId ?: string ) : AgentOSUsageAggregate {
379+ return {
380+ sessionId,
381+ personaId : undefined ,
382+ promptTokens : 0 ,
383+ completionTokens : 0 ,
384+ totalTokens : 0 ,
385+ costUSD : 0 ,
386+ calls : 0 ,
387+ } ;
388+ }
389+
390+ /**
391+ * Fold a single generation's `TokenUsage` into a running `AgentOSUsageAggregate`.
392+ * Mutates the target. Cost is accumulated when present on the source.
393+ */
394+ function accumulateUsage (
395+ target : AgentOSUsageAggregate ,
396+ usage : { promptTokens ?: number ; completionTokens ?: number ; totalTokens ?: number ; costUSD ?: number } | undefined ,
397+ ) : void {
398+ if ( ! usage ) return ;
399+ if ( typeof usage . promptTokens === 'number' ) target . promptTokens += usage . promptTokens ;
400+ if ( typeof usage . completionTokens === 'number' ) target . completionTokens += usage . completionTokens ;
401+ if ( typeof usage . totalTokens === 'number' ) {
402+ target . totalTokens += usage . totalTokens ;
403+ } else {
404+ target . totalTokens += ( usage . promptTokens ?? 0 ) + ( usage . completionTokens ?? 0 ) ;
405+ }
406+ if ( typeof usage . costUSD === 'number' ) target . costUSD = ( target . costUSD ?? 0 ) + usage . costUSD ;
407+ target . calls += 1 ;
408+ }
409+
410+ /**
411+ * Merge two aggregates field-wise. Used to combine the in-memory tally with
412+ * the persisted ledger total so cross-process history rolls up alongside the
413+ * current process's tally.
414+ */
415+ function mergeAggregates ( a : AgentOSUsageAggregate , b : AgentOSUsageAggregate ) : AgentOSUsageAggregate {
416+ return {
417+ sessionId : a . sessionId ?? b . sessionId ,
418+ personaId : a . personaId ?? b . personaId ,
419+ promptTokens : a . promptTokens + b . promptTokens ,
420+ completionTokens : a . completionTokens + b . completionTokens ,
421+ totalTokens : a . totalTokens + b . totalTokens ,
422+ costUSD : ( a . costUSD ?? 0 ) + ( b . costUSD ?? 0 ) ,
423+ calls : a . calls + b . calls ,
424+ } ;
425+ }
426+
373427/**
374428 * Convert HEXACO trait values (0-1) into behavioral descriptions the LLM can act on.
375429 *
@@ -472,6 +526,13 @@ function buildSystemPrompt(opts: AgentOptions): string | undefined {
472526 */
473527export function agent ( opts : AgentOptions ) : Agent {
474528 const sessions = new Map < string , Message [ ] > ( ) ;
529+ // In-memory usage tally per session and per agent. Populated synchronously
530+ // after every generate/send/stream call so `agent.usage()` and
531+ // `session.usage()` work even when the persisted ledger is disabled (the
532+ // common case). The persisted ledger is still merged in at read time so
533+ // cross-process / historical totals continue to roll up correctly.
534+ const sessionUsageTallies = new Map < string , AgentOSUsageAggregate > ( ) ;
535+ const agentUsageTally : AgentOSUsageAggregate = createEmptyUsageAggregate ( ) ;
475536 let avatarBindingOverrides : Record < string , unknown > = { } ;
476537 const useMemory = opts . memory !== false ;
477538
@@ -546,7 +607,9 @@ export function agent(opts: AgentOptions): Agent {
546607 } else {
547608 genOpts . messages = [ ...( genOpts . messages ?? [ ] ) , { role : 'user' , content : prompt } ] ;
548609 }
549- return generateText ( genOpts as GenerateTextOptions ) ;
610+ const result = await generateText ( genOpts as GenerateTextOptions ) ;
611+ accumulateUsage ( agentUsageTally , result . usage ) ;
612+ return result ;
550613 } ,
551614
552615 stream ( prompt : MessageContent , extra ?: Partial < GenerateTextOptions > ) : StreamTextResult {
@@ -567,13 +630,21 @@ export function agent(opts: AgentOptions): Agent {
567630 } else {
568631 streamOpts . messages = [ ...( streamOpts . messages ?? [ ] ) , { role : 'user' , content : prompt } ] ;
569632 }
570- return streamText ( streamOpts as GenerateTextOptions ) ;
633+ const result = streamText ( streamOpts as GenerateTextOptions ) ;
634+ void result . usage
635+ . then ( ( usage ) => accumulateUsage ( agentUsageTally , usage ) )
636+ . catch ( ( ) => { /* stream errored; usage tally unchanged */ } ) ;
637+ return result ;
571638 } ,
572639
573640 session ( id ?: string ) : AgentSession {
574641 const sessionId = id ?? crypto . randomUUID ( ) ;
575642 if ( ! sessions . has ( sessionId ) ) sessions . set ( sessionId , [ ] ) ;
643+ if ( ! sessionUsageTallies . has ( sessionId ) ) {
644+ sessionUsageTallies . set ( sessionId , createEmptyUsageAggregate ( sessionId ) ) ;
645+ }
576646 const history = sessions . get ( sessionId ) ! ;
647+ const sessionUsageTally = sessionUsageTallies . get ( sessionId ) ! ;
577648
578649 const session = {
579650 id : sessionId ,
@@ -640,6 +711,8 @@ export function agent(opts: AgentOptions): Agent {
640711 ) ;
641712
642713 const result = await generateText ( wrappedOpts as GenerateTextOptions ) ;
714+ accumulateUsage ( sessionUsageTally , result . usage ) ;
715+ accumulateUsage ( agentUsageTally , result . usage ) ;
643716
644717 // Validate + parse when a schema was supplied. Native enforcement
645718 // guarantees a valid shape on every successful response, so a
@@ -701,6 +774,12 @@ export function agent(opts: AgentOptions): Agent {
701774 ) ;
702775
703776 const result = streamText ( wrappedOpts as GenerateTextOptions ) ;
777+ void result . usage
778+ . then ( ( usage ) => {
779+ accumulateUsage ( sessionUsageTally , usage ) ;
780+ accumulateUsage ( agentUsageTally , usage ) ;
781+ } )
782+ . catch ( ( ) => { /* stream errored; usage tally unchanged */ } ) ;
704783
705784 // Capture text for history when done. Memory observe runs inside
706785 // applyMemoryProvider's onAfterGeneration wrapper so it's not
@@ -723,11 +802,12 @@ export function agent(opts: AgentOptions): Agent {
723802 } ,
724803
725804 async usage ( ) : Promise < AgentOSUsageAggregate > {
726- return loadRecordedAgentOSUsage ( {
805+ const persisted = await loadRecordedAgentOSUsage ( {
727806 enabled : baseOpts . usageLedger ?. enabled ,
728807 path : baseOpts . usageLedger ?. path ,
729808 sessionId,
730809 } ) ;
810+ return mergeAggregates ( sessionUsageTally , persisted ) ;
731811 } ,
732812
733813 clear ( ) {
@@ -742,11 +822,17 @@ export function agent(opts: AgentOptions): Agent {
742822 } ,
743823
744824 async usage ( sessionId ?: string ) : Promise < AgentOSUsageAggregate > {
745- return loadRecordedAgentOSUsage ( {
825+ const persisted = await loadRecordedAgentOSUsage ( {
746826 enabled : baseOpts . usageLedger ?. enabled ,
747827 path : baseOpts . usageLedger ?. path ,
748828 sessionId,
749829 } ) ;
830+ // When a sessionId is requested, only that session's tally is in scope.
831+ // When none is requested, return the agent-wide tally.
832+ const inMemory = sessionId
833+ ? sessionUsageTallies . get ( sessionId ) ?? createEmptyUsageAggregate ( sessionId )
834+ : agentUsageTally ;
835+ return mergeAggregates ( inMemory , persisted ) ;
750836 } ,
751837
752838 async close ( ) {
0 commit comments