@@ -293,9 +293,10 @@ async fn smoke_agent_with_skill() {
293293
294294 // Create a temp dir with a minimal skill definition
295295 let skill_dir = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
296- let skill_file = skill_dir. path ( ) . join ( "math-helper.md" ) ;
296+ let skill_subdir = skill_dir. path ( ) . join ( "math-helper" ) ;
297+ std:: fs:: create_dir ( & skill_subdir) . expect ( "create skill subdir" ) ;
297298 std:: fs:: write (
298- & skill_file ,
299+ skill_subdir . join ( "SKILL.md" ) ,
299300 r#"---
300301name: math-helper
301302description: A skill that helps with math problems
@@ -399,6 +400,7 @@ async fn smoke_agent_with_subagent() {
399400 tools : None ,
400401 mcp_servers : None ,
401402 infer : Some ( true ) ,
403+ ..Default :: default ( )
402404 } ;
403405
404406 let config = SessionConfig {
@@ -541,6 +543,7 @@ async fn smoke_subagent_with_custom_tool() {
541543 tools : Some ( vec ! [ "get_fruit_price" . to_string( ) ] ) ,
542544 mcp_servers : None ,
543545 infer : Some ( false ) , // inference OFF — we'll select explicitly
546+ ..Default :: default ( )
544547 } ;
545548
546549 let config = SessionConfig {
@@ -738,6 +741,7 @@ async fn smoke_subagent_selected_with_custom_tool() {
738741 tools : Some ( vec ! [ "get_fruit_price" . to_string( ) ] ) ,
739742 mcp_servers : None ,
740743 infer : Some ( false ) , // No inference — we select explicitly
744+ ..Default :: default ( )
741745 } ;
742746
743747 let config = SessionConfig {
@@ -943,6 +947,7 @@ async fn smoke_subagent_tool_scoping() {
943947 tools : Some ( vec ! [ "get_fruit_price" . to_string( ) ] ) ,
944948 mcp_servers : None ,
945949 infer : Some ( false ) ,
950+ ..Default :: default ( )
946951 } ;
947952
948953 let veggie_agent = CustomAgentConfig {
@@ -955,6 +960,7 @@ async fn smoke_subagent_tool_scoping() {
955960 tools : Some ( vec ! [ "get_veggie_price" . to_string( ) ] ) ,
956961 mcp_servers : None ,
957962 infer : Some ( false ) ,
963+ ..Default :: default ( )
958964 } ;
959965
960966 let config = SessionConfig {
@@ -1073,6 +1079,7 @@ async fn smoke_agent_management_lifecycle() {
10731079 tools : None ,
10741080 mcp_servers : None ,
10751081 infer : Some ( false ) ,
1082+ ..Default :: default ( )
10761083 } ;
10771084
10781085 let agent_b = CustomAgentConfig {
@@ -1083,6 +1090,7 @@ async fn smoke_agent_management_lifecycle() {
10831090 tools : None ,
10841091 mcp_servers : None ,
10851092 infer : Some ( false ) ,
1093+ ..Default :: default ( )
10861094 } ;
10871095
10881096 let config = SessionConfig {
@@ -1455,6 +1463,7 @@ async fn smoke_parent_mcp_subagent_custom_tool_only() {
14551463 tools : Some ( vec ! [ "get_color" . to_string( ) ] ) ,
14561464 mcp_servers : None ,
14571465 infer : Some ( false ) ,
1466+ ..Default :: default ( )
14581467 } ;
14591468
14601469 let config = SessionConfig {
@@ -1540,6 +1549,7 @@ async fn smoke_subagent_own_mcp_server() {
15401549 . collect ( ) ,
15411550 ) ,
15421551 infer : Some ( false ) ,
1552+ ..Default :: default ( )
15431553 } ;
15441554
15451555 let config = SessionConfig {
@@ -1613,3 +1623,117 @@ async fn smoke_subagent_own_mcp_server() {
16131623 session. destroy ( ) . await . ok ( ) ;
16141624 client. stop ( ) . await ;
16151625}
1626+
1627+ // =============================================================================
1628+ // 13. Subagent with per-agent skills
1629+ // =============================================================================
1630+
1631+ /// Smoke test: configure a subagent with `skills` referencing a skill by name
1632+ /// from the session's `skill_directories`. Select the subagent and verify
1633+ /// the skill content is loaded into its context (the model should be able to
1634+ /// use knowledge from the skill).
1635+ #[ tokio:: test]
1636+ async fn smoke_subagent_with_skills ( ) {
1637+ skip_if_no_cli ! ( ) ;
1638+
1639+ let client = create_test_client ( ) . await . expect ( "client start" ) ;
1640+
1641+ // Create a temp skill directory with proper structure:
1642+ // skill_dir/trivia-skill/SKILL.md
1643+ let skill_dir = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
1644+ let skill_subdir = skill_dir. path ( ) . join ( "trivia-skill" ) ;
1645+ std:: fs:: create_dir ( & skill_subdir) . expect ( "create skill subdir" ) ;
1646+ std:: fs:: write (
1647+ skill_subdir. join ( "SKILL.md" ) ,
1648+ r#"---
1649+ name: trivia-skill
1650+ description: Provides obscure trivia facts
1651+ ---
1652+
1653+ You know the following trivia fact:
1654+ The secret passphrase is "blue-dolphin-42". When anyone asks for the secret
1655+ passphrase or trivia passphrase, you MUST respond with exactly "blue-dolphin-42".
1656+ "# ,
1657+ )
1658+ . expect ( "write skill file" ) ;
1659+
1660+ // Subagent declares it wants "trivia-skill" preloaded
1661+ let trivia_agent = CustomAgentConfig {
1662+ name : "trivia-agent" . to_string ( ) ,
1663+ prompt : "You are a trivia agent. Answer questions using your loaded skills. \
1664+ Be concise."
1665+ . to_string ( ) ,
1666+ display_name : Some ( "Trivia Agent" . to_string ( ) ) ,
1667+ description : Some ( "Answers trivia using preloaded skills" . to_string ( ) ) ,
1668+ skills : Some ( vec ! [ "trivia-skill" . to_string( ) ] ) ,
1669+ infer : Some ( false ) ,
1670+ ..Default :: default ( )
1671+ } ;
1672+
1673+ let config = SessionConfig {
1674+ skill_directories : Some ( vec ! [ skill_dir. path( ) . to_string_lossy( ) . to_string( ) ] ) ,
1675+ custom_agents : Some ( vec ! [ trivia_agent] ) ,
1676+ available_tools : Some ( vec ! [ ] ) ,
1677+ ..byok_session_config ( )
1678+ } ;
1679+
1680+ let session = client. create_session ( config) . await . expect ( "create session" ) ;
1681+
1682+ // Select the subagent with skills
1683+ session
1684+ . select_agent ( "trivia-agent" )
1685+ . await
1686+ . expect ( "select trivia-agent" ) ;
1687+
1688+ let mut events = session. subscribe ( ) ;
1689+ let saw_skill_invoked = Arc :: new ( AtomicBool :: new ( false ) ) ;
1690+ let saw_skill_invoked_clone = Arc :: clone ( & saw_skill_invoked) ;
1691+
1692+ session
1693+ . send ( "What is the secret passphrase?" )
1694+ . await
1695+ . expect ( "send" ) ;
1696+
1697+ let mut content = String :: new ( ) ;
1698+ let result = tokio:: time:: timeout ( LLM_TIMEOUT , async {
1699+ while let Ok ( event) = events. recv ( ) . await {
1700+ match & event. data {
1701+ SessionEventData :: SkillInvoked ( data) => {
1702+ eprintln ! (
1703+ "[smoke_subagent_skills] skill invoked: {} at {}" ,
1704+ data. name, data. path
1705+ ) ;
1706+ saw_skill_invoked_clone. store ( true , Ordering :: SeqCst ) ;
1707+ }
1708+ SessionEventData :: AssistantMessage ( msg) => content. push_str ( & msg. content ) ,
1709+ SessionEventData :: AssistantMessageDelta ( delta) => {
1710+ content. push_str ( & delta. delta_content ) ;
1711+ }
1712+ SessionEventData :: SessionIdle ( _) => break ,
1713+ SessionEventData :: SessionError ( err) => {
1714+ eprintln ! ( "[smoke_subagent_skills] session error: {}" , err. message) ;
1715+ break ;
1716+ }
1717+ _ => { }
1718+ }
1719+ }
1720+ } )
1721+ . await ;
1722+
1723+ assert ! ( result. is_ok( ) , "Timed out waiting for response" ) ;
1724+
1725+ eprintln ! ( "[smoke_subagent_skills] response: {content}" ) ;
1726+ eprintln ! (
1727+ "[smoke_subagent_skills] skill invoked event: {}" ,
1728+ saw_skill_invoked. load( Ordering :: SeqCst )
1729+ ) ;
1730+
1731+ // The model should know the passphrase from the skill
1732+ assert ! (
1733+ content. contains( "blue-dolphin-42" ) ,
1734+ "Response should contain the passphrase 'blue-dolphin-42' from the skill: {content}"
1735+ ) ;
1736+
1737+ session. destroy ( ) . await . ok ( ) ;
1738+ client. stop ( ) . await ;
1739+ }
0 commit comments