@@ -40,7 +40,7 @@ use sacp::schema::{
4040 SetSessionConfigOptionResponse , SetSessionModeRequest , SetSessionModeResponse ,
4141 SetSessionModelRequest , SetSessionModelResponse , StopReason , TextContent , TextResourceContents ,
4242 ToolCall , ToolCallContent , ToolCallId , ToolCallLocation , ToolCallStatus , ToolCallUpdate ,
43- ToolCallUpdateFields , ToolKind ,
43+ ToolCallUpdateFields , ToolKind , Usage , UsageUpdate ,
4444} ;
4545use sacp:: util:: MatchDispatchFrom ;
4646use sacp:: {
@@ -603,6 +603,22 @@ fn build_config_options(
603603 ]
604604}
605605
606+ fn to_nonnegative_u64 ( value : Option < i32 > ) -> Option < u64 > {
607+ value. and_then ( |v| u64:: try_from ( v) . ok ( ) )
608+ }
609+
610+ fn build_prompt_usage ( session : & Session ) -> Option < Usage > {
611+ let total = to_nonnegative_u64 ( session. total_tokens ) ?;
612+ let input = to_nonnegative_u64 ( session. input_tokens ) . unwrap_or ( 0 ) ;
613+ let output = to_nonnegative_u64 ( session. output_tokens ) . unwrap_or ( 0 ) ;
614+ Some ( Usage :: new ( total, input, output) )
615+ }
616+
617+ fn build_usage_update ( session : & Session , context_limit : usize ) -> UsageUpdate {
618+ let used = session. total_tokens . unwrap_or ( 0 ) . max ( 0 ) as u64 ;
619+ UsageUpdate :: new ( used, context_limit as u64 )
620+ }
621+
606622impl GooseAcpAgent {
607623 pub fn permission_manager ( & self ) -> Arc < PermissionManager > {
608624 Arc :: clone ( & self . permission_manager )
@@ -1383,27 +1399,38 @@ impl GooseAcpAgent {
13831399 // Resolve provider + model from config so we can include the current
13841400 // model in the response without waiting for the full agent setup.
13851401 let resolved = resolve_provider_and_model ( & self . config_dir , & goose_session) . await ;
1402+ let initial_usage_update = resolved
1403+ . as_ref ( )
1404+ . ok ( )
1405+ . map ( |( _, mc) | build_usage_update ( & goose_session, mc. context_limit ( ) ) ) ;
13861406 let ( model_state, config_options) =
13871407 build_eager_config ( & resolved, & mode_state, & goose_session) . await ;
1408+ let session_id = SessionId :: new ( thread_id. clone ( ) ) ;
13881409
13891410 self . spawn_agent_setup (
13901411 cx,
13911412 agent_tx,
13921413 AgentSetupRequest {
1393- session_id : SessionId :: new ( thread_id . clone ( ) ) ,
1414+ session_id : session_id . clone ( ) ,
13941415 goose_session,
13951416 mcp_servers : args. mcp_servers ,
1396- resolved_provider : resolved. ok ( ) ,
1417+ resolved_provider : resolved. as_ref ( ) . ok ( ) . cloned ( ) ,
13971418 } ,
13981419 ) ;
13991420
1400- let mut response = NewSessionResponse :: new ( SessionId :: new ( thread_id ) ) . modes ( mode_state) ;
1421+ let mut response = NewSessionResponse :: new ( session_id . clone ( ) ) . modes ( mode_state) ;
14011422 if let Some ( ms) = model_state {
14021423 response = response. models ( ms) ;
14031424 }
14041425 if let Some ( co) = config_options {
14051426 response = response. config_options ( co) ;
14061427 }
1428+ if let Some ( usage_update) = initial_usage_update {
1429+ cx. send_notification ( SessionNotification :: new (
1430+ session_id,
1431+ SessionUpdate :: UsageUpdate ( usage_update) ,
1432+ ) ) ?;
1433+ }
14071434 debug ! (
14081435 target: "perf" ,
14091436 sid = %sid,
@@ -1748,6 +1775,16 @@ impl GooseAcpAgent {
17481775 let mode_state = build_mode_state ( loaded_mode) ?;
17491776
17501777 let resolved = resolve_provider_and_model ( & self . config_dir , & goose_session) . await ;
1778+ let initial_usage_update = resolved
1779+ . as_ref ( )
1780+ . ok ( )
1781+ . map ( |( _, mc) | build_usage_update ( & goose_session, mc. context_limit ( ) ) )
1782+ . or_else ( || {
1783+ goose_session
1784+ . model_config
1785+ . as_ref ( )
1786+ . map ( |mc| build_usage_update ( & goose_session, mc. context_limit ( ) ) )
1787+ } ) ;
17511788 let ( model_state, config_options) =
17521789 build_eager_config ( & resolved, & mode_state, & goose_session) . await ;
17531790
@@ -1769,6 +1806,12 @@ impl GooseAcpAgent {
17691806 if let Some ( co) = config_options {
17701807 response = response. config_options ( co) ;
17711808 }
1809+ if let Some ( usage_update) = initial_usage_update {
1810+ cx. send_notification ( SessionNotification :: new (
1811+ args. session_id . clone ( ) ,
1812+ SessionUpdate :: UsageUpdate ( usage_update) ,
1813+ ) ) ?;
1814+ }
17721815 debug ! (
17731816 target: "perf" ,
17741817 sid = %sid,
@@ -1883,10 +1926,30 @@ impl GooseAcpAgent {
18831926 }
18841927 }
18851928
1886- let mut sessions = self . sessions . lock ( ) . await ;
1887- if let Some ( session) = sessions. get_mut ( & thread_id) {
1888- session. cancel_token = None ;
1929+ {
1930+ let mut sessions = self . sessions . lock ( ) . await ;
1931+ if let Some ( session) = sessions. get_mut ( & thread_id) {
1932+ session. cancel_token = None ;
1933+ }
18891934 }
1935+
1936+ let session = self
1937+ . session_manager
1938+ . get_session ( & internal_session_id, false )
1939+ . await
1940+ . map_err ( |e| {
1941+ sacp:: Error :: internal_error ( ) . data ( format ! ( "Failed to load session: {}" , e) )
1942+ } ) ?;
1943+ let provider = agent. provider ( ) . await . map_err ( |e| {
1944+ sacp:: Error :: internal_error ( ) . data ( format ! ( "Failed to get provider: {}" , e) )
1945+ } ) ?;
1946+ let usage_update =
1947+ build_usage_update ( & session, provider. get_model_config ( ) . context_limit ( ) ) ;
1948+ cx. send_notification ( SessionNotification :: new (
1949+ args. session_id . clone ( ) ,
1950+ SessionUpdate :: UsageUpdate ( usage_update) ,
1951+ ) ) ?;
1952+
18901953 debug ! (
18911954 target: "perf" ,
18921955 sid = %sid,
@@ -1895,11 +1958,17 @@ impl GooseAcpAgent {
18951958 cancelled = was_cancelled,
18961959 "perf: prompt done"
18971960 ) ;
1898- Ok ( PromptResponse :: new ( if was_cancelled {
1961+ let stop_reason = if was_cancelled {
18991962 StopReason :: Cancelled
19001963 } else {
19011964 StopReason :: EndTurn
1902- } ) )
1965+ } ;
1966+
1967+ let mut response = PromptResponse :: new ( stop_reason) ;
1968+ if let Some ( usage) = build_prompt_usage ( & session) {
1969+ response = response. usage ( usage) ;
1970+ }
1971+ Ok ( response)
19031972 }
19041973
19051974 async fn on_cancel ( & self , args : CancelNotification ) -> Result < ( ) , sacp:: Error > {
@@ -3487,6 +3556,80 @@ print(\"hello, world\")
34873556 . map ( |locs| locs. into_iter ( ) . map ( |loc| ( loc. path , loc. line ) ) . collect ( ) )
34883557 }
34893558
3559+ fn make_session_with_usage (
3560+ total_tokens : Option < i32 > ,
3561+ input_tokens : Option < i32 > ,
3562+ output_tokens : Option < i32 > ,
3563+ accumulated_total_tokens : Option < i32 > ,
3564+ accumulated_input_tokens : Option < i32 > ,
3565+ accumulated_output_tokens : Option < i32 > ,
3566+ ) -> Session {
3567+ Session {
3568+ id : "session-1" . to_string ( ) ,
3569+ working_dir : PathBuf :: from ( "/tmp" ) ,
3570+ name : "ACP Session" . to_string ( ) ,
3571+ user_set_name : false ,
3572+ session_type : SessionType :: Acp ,
3573+ created_at : Default :: default ( ) ,
3574+ updated_at : Default :: default ( ) ,
3575+ extension_data : goose:: session:: ExtensionData :: default ( ) ,
3576+ total_tokens,
3577+ input_tokens,
3578+ output_tokens,
3579+ accumulated_total_tokens,
3580+ accumulated_input_tokens,
3581+ accumulated_output_tokens,
3582+ schedule_id : None ,
3583+ recipe : None ,
3584+ user_recipe_values : None ,
3585+ conversation : None ,
3586+ message_count : 0 ,
3587+ provider_name : None ,
3588+ model_config : None ,
3589+ goose_mode : GooseMode :: default ( ) ,
3590+ thread_id : None ,
3591+ }
3592+ }
3593+
3594+ #[ test]
3595+ fn test_build_prompt_usage_uses_current_turn_tokens ( ) {
3596+ let session = make_session_with_usage (
3597+ Some ( 120 ) ,
3598+ Some ( 80 ) ,
3599+ Some ( 40 ) ,
3600+ Some ( 360 ) ,
3601+ Some ( 210 ) ,
3602+ Some ( 150 ) ,
3603+ ) ;
3604+ let usage = build_prompt_usage ( & session) . expect ( "usage should be present" ) ;
3605+ assert_eq ! ( usage. total_tokens, 120 ) ;
3606+ assert_eq ! ( usage. input_tokens, 80 ) ;
3607+ assert_eq ! ( usage. output_tokens, 40 ) ;
3608+ }
3609+
3610+ #[ test]
3611+ fn test_build_prompt_usage_falls_back_to_current_tokens ( ) {
3612+ let session = make_session_with_usage ( Some ( 120 ) , Some ( 80 ) , Some ( 40 ) , None , None , None ) ;
3613+ let usage = build_prompt_usage ( & session) . expect ( "usage should be present" ) ;
3614+ assert_eq ! ( usage. total_tokens, 120 ) ;
3615+ assert_eq ! ( usage. input_tokens, 80 ) ;
3616+ assert_eq ! ( usage. output_tokens, 40 ) ;
3617+ }
3618+
3619+ #[ test]
3620+ fn test_build_prompt_usage_requires_total_tokens ( ) {
3621+ let session = make_session_with_usage ( None , Some ( 80 ) , Some ( 40 ) , None , None , None ) ;
3622+ assert ! ( build_prompt_usage( & session) . is_none( ) ) ;
3623+ }
3624+
3625+ #[ test]
3626+ fn test_build_usage_update_clamps_negative_used_to_zero ( ) {
3627+ let session = make_session_with_usage ( Some ( -7 ) , Some ( 0 ) , Some ( 0 ) , None , None , None ) ;
3628+ let usage = build_usage_update ( & session, 258_000 ) ;
3629+ assert_eq ! ( usage. used, 0 ) ;
3630+ assert_eq ! ( usage. size, 258_000 ) ;
3631+ }
3632+
34903633 #[ test_case(
34913634 GooseMode :: Auto
34923635 => Ok ( SessionModeState :: new(
0 commit comments