@@ -145,6 +145,8 @@ pub struct Channel {
145145 pending_retrigger_metadata : HashMap < String , serde_json:: Value > ,
146146 /// Deadline for firing the pending retrigger (debounce timer).
147147 retrigger_deadline : Option < tokio:: time:: Instant > ,
148+ /// Optional send_agent_message tool (only when agent has active links).
149+ send_agent_message_tool : Option < crate :: tools:: SendAgentMessageTool > ,
148150}
149151
150152impl Channel {
@@ -226,6 +228,7 @@ impl Channel {
226228 pending_retrigger : false ,
227229 pending_retrigger_metadata : HashMap :: new ( ) ,
228230 retrigger_deadline : None ,
231+ send_agent_message_tool : None ,
229232 } ;
230233
231234 ( channel, message_tx)
@@ -623,10 +626,13 @@ impl Channel {
623626
624627 let available_channels = self . build_available_channels ( ) . await ;
625628
629+ let org_context = self . build_org_context ( & prompt_engine) . await ;
630+ let link_context = self . build_link_context ( & prompt_engine) . await ;
631+
626632 let empty_to_none = |s : String | if s. is_empty ( ) { None } else { Some ( s) } ;
627633
628634 prompt_engine
629- . render_channel_prompt (
635+ . render_channel_prompt_with_links (
630636 empty_to_none ( identity_context) ,
631637 empty_to_none ( memory_bulletin. to_string ( ) ) ,
632638 empty_to_none ( skills_prompt) ,
@@ -635,6 +641,8 @@ impl Channel {
635641 empty_to_none ( status_text) ,
636642 coalesce_hint,
637643 available_channels,
644+ org_context,
645+ link_context,
638646 )
639647 . expect ( "failed to render channel prompt" )
640648 }
@@ -674,6 +682,20 @@ impl Channel {
674682 Vec :: new ( )
675683 } ;
676684
685+ // Emit AgentMessageReceived event for internal agent-to-agent messages
686+ if message. source == "internal" {
687+ if let Some ( from_agent_id) = message. metadata . get ( "from_agent_id" ) . and_then ( |v| v. as_str ( ) ) {
688+ if let Some ( link_id) = message. metadata . get ( "link_id" ) . and_then ( |v| v. as_str ( ) ) {
689+ self . deps . event_tx . send ( ProcessEvent :: AgentMessageReceived {
690+ from_agent_id : Arc :: from ( from_agent_id) ,
691+ to_agent_id : self . deps . agent_id . clone ( ) ,
692+ link_id : link_id. to_string ( ) ,
693+ channel_id : self . id . clone ( ) ,
694+ } ) . ok ( ) ;
695+ }
696+ }
697+ }
698+
677699 // Persist user messages (skip system re-triggers)
678700 if message. source != "system" {
679701 let sender_name = message
@@ -794,6 +816,118 @@ impl Channel {
794816 prompt_engine. render_available_channels ( entries) . ok ( )
795817 }
796818
819+ /// Build org context showing the agent's position in the communication hierarchy.
820+ async fn build_org_context (
821+ & self ,
822+ prompt_engine : & crate :: prompts:: PromptEngine ,
823+ ) -> Option < String > {
824+ let link_store = self . deps . link_store . as_ref ( ) ?;
825+ let agent_id = self . deps . agent_id . as_ref ( ) ;
826+
827+ let links = match link_store. get_links_for_agent ( agent_id) . await {
828+ Ok ( links) => links,
829+ Err ( error) => {
830+ tracing:: warn!( %error, "failed to fetch links for org context" ) ;
831+ return None ;
832+ }
833+ } ;
834+
835+ if links. is_empty ( ) {
836+ return None ;
837+ }
838+
839+ let mut superiors = Vec :: new ( ) ;
840+ let mut subordinates = Vec :: new ( ) ;
841+ let mut peers = Vec :: new ( ) ;
842+
843+ for link in & links {
844+ if !link. enabled {
845+ continue ;
846+ }
847+
848+ let is_from = link. from_agent_id == agent_id;
849+ let other_agent_id = if is_from {
850+ & link. to_agent_id
851+ } else {
852+ & link. from_agent_id
853+ } ;
854+
855+ // Determine relationship from this agent's perspective
856+ let relationship = if is_from {
857+ link. relationship
858+ } else {
859+ link. relationship . inverse ( )
860+ } ;
861+
862+ // For one-way links where this agent is the to_agent (can't initiate),
863+ // still show them in the org context for awareness
864+ let agent_info = crate :: prompts:: engine:: LinkedAgent {
865+ name : other_agent_id. clone ( ) ,
866+ id : other_agent_id. clone ( ) ,
867+ } ;
868+
869+ match relationship {
870+ crate :: links:: LinkRelationship :: Superior => superiors. push ( agent_info) ,
871+ crate :: links:: LinkRelationship :: Subordinate => subordinates. push ( agent_info) ,
872+ crate :: links:: LinkRelationship :: Peer => peers. push ( agent_info) ,
873+ }
874+ }
875+
876+ if superiors. is_empty ( ) && subordinates. is_empty ( ) && peers. is_empty ( ) {
877+ return None ;
878+ }
879+
880+ let org_context = crate :: prompts:: engine:: OrgContext {
881+ superiors,
882+ subordinates,
883+ peers,
884+ } ;
885+
886+ prompt_engine. render_org_context ( org_context) . ok ( )
887+ }
888+
889+ /// Build link context for the current channel if it's an internal agent-to-agent channel.
890+ async fn build_link_context (
891+ & self ,
892+ prompt_engine : & crate :: prompts:: PromptEngine ,
893+ ) -> Option < String > {
894+ // Link channels have conversation IDs starting with "link:"
895+ let conversation_id = self . conversation_id . as_deref ( ) ?;
896+ let link_id = conversation_id. strip_prefix ( "link:" ) ?;
897+
898+ let link_store = self . deps . link_store . as_ref ( ) ?;
899+ let link = match link_store. get ( link_id) . await {
900+ Ok ( Some ( link) ) => link,
901+ Ok ( None ) => return None ,
902+ Err ( error) => {
903+ tracing:: warn!( %error, "failed to fetch link for channel context" ) ;
904+ return None ;
905+ }
906+ } ;
907+
908+ let agent_id = self . deps . agent_id . as_ref ( ) ;
909+ let is_from = link. from_agent_id == agent_id;
910+ let other_agent_id = if is_from {
911+ & link. to_agent_id
912+ } else {
913+ & link. from_agent_id
914+ } ;
915+
916+ // Relationship from the other agent's perspective toward this agent
917+ let relationship = if is_from {
918+ link. relationship
919+ } else {
920+ link. relationship . inverse ( )
921+ } ;
922+
923+ let link_context = crate :: prompts:: engine:: LinkContext {
924+ agent_name : other_agent_id. clone ( ) ,
925+ relationship : relationship. as_str ( ) . to_string ( ) ,
926+ } ;
927+
928+ prompt_engine. render_link_context ( link_context) . ok ( )
929+ }
930+
797931 /// Assemble the full system prompt using the PromptEngine.
798932 async fn build_system_prompt ( & self ) -> String {
799933 let rc = & self . deps . runtime_config ;
@@ -818,10 +952,13 @@ impl Channel {
818952
819953 let available_channels = self . build_available_channels ( ) . await ;
820954
955+ let org_context = self . build_org_context ( & prompt_engine) . await ;
956+ let link_context = self . build_link_context ( & prompt_engine) . await ;
957+
821958 let empty_to_none = |s : String | if s. is_empty ( ) { None } else { Some ( s) } ;
822959
823960 prompt_engine
824- . render_channel_prompt (
961+ . render_channel_prompt_with_links (
825962 empty_to_none ( identity_context) ,
826963 empty_to_none ( memory_bulletin. to_string ( ) ) ,
827964 empty_to_none ( skills_prompt) ,
@@ -830,6 +967,8 @@ impl Channel {
830967 empty_to_none ( status_text) ,
831968 None , // coalesce_hint - only set for batched messages
832969 available_channels,
970+ org_context,
971+ link_context,
833972 )
834973 . expect ( "failed to render channel prompt" )
835974 }
@@ -860,6 +999,7 @@ impl Channel {
860999 skip_flag. clone ( ) ,
8611000 replied_flag. clone ( ) ,
8621001 self . deps . cron_tool . clone ( ) ,
1002+ self . send_agent_message_tool . clone ( ) ,
8631003 )
8641004 . await
8651005 {
0 commit comments