@@ -4,10 +4,10 @@ use github_copilot_sdk::handler::{ApproveAllHandler, PermissionHandler, Permissi
44use github_copilot_sdk:: tool:: ToolHandler ;
55use github_copilot_sdk:: {
66 Error , PermissionRequestData , RequestId , SessionConfig , SessionId , Tool , ToolInvocation ,
7- ToolResult ,
7+ ToolResult , ToolSet ,
88} ;
99use serde_json:: json;
10- use tokio:: sync:: mpsc;
10+ use tokio:: sync:: { Mutex , mpsc} ;
1111
1212use super :: support:: { assistant_message_content, recv_with_timeout, with_e2e_context} ;
1313
@@ -73,6 +73,55 @@ async fn invokes_custom_tool() {
7373 . await ;
7474}
7575
76+ #[ tokio:: test]
77+ async fn low_level_tool_definition ( ) {
78+ with_e2e_context ( "tools" , "low_level_tool_definition" , |ctx| {
79+ Box :: pin ( async move {
80+ ctx. set_default_copilot_user ( ) ;
81+ let client = ctx. start_client ( ) . await ;
82+ let __perm = Arc :: new ( ApproveAllHandler ) ;
83+ let current_phase = Arc :: new ( Mutex :: new ( String :: new ( ) ) ) ;
84+ let tools = vec ! [
85+ set_current_phase_tool( current_phase. clone( ) ) ,
86+ search_items_tool( ) ,
87+ ] ;
88+ let available_tools = ToolSet :: new ( )
89+ . add_custom ( "*" )
90+ . expect ( "add custom wildcard" )
91+ . add_builtin ( "web_fetch" )
92+ . expect ( "add web_fetch" )
93+ . into_vec ( ) ;
94+ let session = client
95+ . create_session (
96+ SessionConfig :: default ( )
97+ . with_github_token ( super :: support:: DEFAULT_TEST_TOKEN )
98+ . with_permission_handler ( __perm)
99+ . with_tools ( tools)
100+ . with_available_tools ( available_tools) ,
101+ )
102+ . await
103+ . expect ( "create session" ) ;
104+
105+ let answer = session
106+ . send_and_wait (
107+ "First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and search results." ,
108+ )
109+ . await
110+ . expect ( "send" )
111+ . expect ( "assistant message" ) ;
112+ let content = assistant_message_content ( & answer) ;
113+ assert ! ( !content. is_empty( ) ) ;
114+ assert ! ( content. to_lowercase( ) . contains( "analyzing" ) ) ;
115+ assert ! ( content. contains( "item_alpha" ) || content. contains( "item_beta" ) ) ;
116+ assert_eq ! ( current_phase. lock( ) . await . clone( ) , "analyzing" ) ;
117+
118+ session. disconnect ( ) . await . expect ( "disconnect session" ) ;
119+ client. stop ( ) . await . expect ( "stop client" ) ;
120+ } )
121+ } )
122+ . await ;
123+ }
124+
76125#[ tokio:: test]
77126async fn handles_tool_calling_errors ( ) {
78127 with_e2e_context ( "tools" , "handles_tool_calling_errors" , |ctx| {
@@ -502,6 +551,69 @@ impl ToolHandler for ErrorTool {
502551
503552struct CustomGrepTool ;
504553
554+ struct SetCurrentPhaseTool {
555+ current_phase : Arc < Mutex < String > > ,
556+ }
557+
558+ fn set_current_phase_tool ( current_phase : Arc < Mutex < String > > ) -> Tool {
559+ Tool :: new ( "set_current_phase" )
560+ . with_description ( "Sets the current phase of the agent" )
561+ . with_parameters ( json ! ( {
562+ "type" : "object" ,
563+ "properties" : {
564+ "phase" : {
565+ "type" : "string" ,
566+ "description" : "Current phase" ,
567+ "pattern" : "^(searching|analyzing|done)$"
568+ }
569+ } ,
570+ "required" : [ "phase" ]
571+ } ) )
572+ . with_handler ( Arc :: new ( SetCurrentPhaseTool { current_phase } ) )
573+ }
574+
575+ #[ async_trait:: async_trait]
576+ impl ToolHandler for SetCurrentPhaseTool {
577+ async fn call ( & self , invocation : ToolInvocation ) -> Result < ToolResult , Error > {
578+ let phase = invocation
579+ . arguments
580+ . get ( "phase" )
581+ . and_then ( serde_json:: Value :: as_str)
582+ . unwrap_or_default ( )
583+ . to_string ( ) ;
584+ * self . current_phase . lock ( ) . await = phase. clone ( ) ;
585+ Ok ( ToolResult :: Text ( format ! ( "Phase set to {phase}" ) ) )
586+ }
587+ }
588+
589+ struct SearchItemsTool ;
590+
591+ fn search_items_tool ( ) -> Tool {
592+ Tool :: new ( "search_items" )
593+ . with_description ( "Search for items by keyword" )
594+ . with_parameters ( json ! ( {
595+ "type" : "object" ,
596+ "properties" : {
597+ "keyword" : { "type" : "string" }
598+ } ,
599+ "required" : [ "keyword" ]
600+ } ) )
601+ . with_handler ( Arc :: new ( SearchItemsTool ) )
602+ }
603+
604+ #[ async_trait:: async_trait]
605+ impl ToolHandler for SearchItemsTool {
606+ async fn call ( & self , invocation : ToolInvocation ) -> Result < ToolResult , Error > {
607+ let keyword = invocation
608+ . arguments
609+ . get ( "keyword" )
610+ . and_then ( serde_json:: Value :: as_str)
611+ . unwrap_or_default ( ) ;
612+ assert_eq ! ( keyword, "copilot" ) ;
613+ Ok ( ToolResult :: Text ( "Found: item_alpha, item_beta" . to_string ( ) ) )
614+ }
615+ }
616+
505617fn custom_grep_tool ( ) -> Tool {
506618 Tool :: new ( "grep" )
507619 . with_description ( "A custom grep implementation that overrides the built-in" )
0 commit comments