cookie_agent_engine/runtime/
messaging_api.rs1use cookie_agent_protocol::{
19 ProducerDeliveryMode, ProducerId, ProducerIdempotencyKey, ProducerMessageId, ProducerOwner,
20 RunId, SessionId, SessionOrigin, SessionProducersParams, SessionStatus, ToolCallId,
21};
22
23use super::producers::{ProducerAuthority, ProducerCommand, producer_description};
24use super::{Engine, EngineError, SessionCommand};
25
26pub const MESSAGE_DISABLED: &str = "send_message:disabled";
30pub const MESSAGE_INVALID_BODY: &str = "send_message:invalid_body";
31pub const MESSAGE_INVALID_ARGUMENTS: &str = "send_message:invalid_arguments";
32pub const MESSAGE_UNKNOWN_SESSION: &str = "send_message:unknown_session";
33pub const MESSAGE_NOT_TREE_PEER: &str = "send_message:not_tree_peer";
34pub const MESSAGE_SELF_SEND: &str = "send_message:self_send";
35pub const MESSAGE_INBOX_FULL: &str = "send_message:inbox_full";
36pub const MESSAGE_MAX_HOPS_EXCEEDED: &str = "send_message:max_hops_exceeded";
37pub const MESSAGE_INFLIGHT_FULL: &str = "send_message:inflight_full";
38pub const MESSAGE_ENGINE_SHUTDOWN: &str = "send_message:engine_shutdown";
39
40#[derive(Clone, Debug)]
43pub struct AgentMessageInvocation {
44 pub sender_session_id: SessionId,
45 pub sender_run_id: RunId,
46 pub sender_tool_call_id: ToolCallId,
47 pub recipient_session_id: SessionId,
48 pub body: String,
49 pub mode: ProducerDeliveryMode,
50}
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum AgentRecipientState {
58 Running,
59 Queued,
60 WakingFinished,
61}
62
63impl AgentRecipientState {
64 #[must_use]
65 pub const fn as_str(self) -> &'static str {
66 match self {
67 Self::Running => "running",
68 Self::Queued => "queued",
69 Self::WakingFinished => "waking_finished",
70 }
71 }
72}
73
74#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct AgentMessageHandle {
79 pub message_id: ProducerMessageId,
80 pub mode: ProducerDeliveryMode,
81 pub recipient_state: AgentRecipientState,
82}
83
84fn messaging_error(code: &'static str) -> EngineError {
85 EngineError::Messaging(code.to_owned())
86}
87
88fn map_shutdown(error: EngineError) -> EngineError {
91 match error {
92 EngineError::ActorStopped => messaging_error(MESSAGE_ENGINE_SHUTDOWN),
93 other => other,
94 }
95}
96
97fn map_unknown_session(error: EngineError) -> EngineError {
101 match error {
102 EngineError::Session(crate::session::SessionError::Missing(_)) => {
103 messaging_error(MESSAGE_UNKNOWN_SESSION)
104 }
105 other => other,
106 }
107}
108
109pub(super) fn render_agent_message_envelope(
113 sender_session_id: SessionId,
114 sender_short_id: Option<&str>,
115 body: &str,
116 include_reply_hint: bool,
117) -> String {
118 let sender_label = sender_short_id
119 .map(str::to_owned)
120 .unwrap_or_else(|| sender_session_id.to_string());
121 let reply_hint = include_reply_hint
122 .then(|| format!("\n\n[use `send_message(to=\"{sender_label}\", ...)` to reply]"));
123 format!(
124 "<agent_message from=\"{sender_label}\">\n{}{}\n</agent_message>",
125 body,
126 reply_hint.unwrap_or_default(),
127 )
128}
129
130pub(crate) fn relationship_label(
135 sender_origin: &SessionOrigin,
136 recipient_origin: &SessionOrigin,
137 sender: SessionId,
138 recipient: SessionId,
139) -> &'static str {
140 let parent_of = |origin: &SessionOrigin| match origin {
141 SessionOrigin::Delegated {
142 parent_session_id, ..
143 } => Some(*parent_session_id),
144 SessionOrigin::Root => None,
145 };
146 let sender_parent = parent_of(sender_origin);
147 let recipient_parent = parent_of(recipient_origin);
148 if sender_parent == Some(recipient) {
149 "parent"
150 } else if recipient_parent == Some(sender) {
151 "child"
152 } else if sender_parent.is_some() && sender_parent == recipient_parent {
153 "sibling"
154 } else {
155 "*"
156 }
157}
158
159impl Engine {
160 pub(crate) fn send_message_tool_visible(
165 &self,
166 session: SessionId,
167 ) -> Result<bool, EngineError> {
168 let projection = self.messaging_projection(session)?;
169 let agent = crate::permissions::governing_agent_for_skills(&projection);
170 let visible = crate::permissions::PermissionPipeline::tool_visible_with_overlay(
171 &agent,
172 Some(&projection.permission_overlay),
173 "message",
174 );
175 if !visible {
176 return Ok(false);
177 }
178 let providers = self
179 .inner
180 .tools
181 .lock()
182 .unwrap_or_else(|poisoned| poisoned.into_inner())
183 .clone();
184 Ok(providers.iter().any(|provider| {
185 provider
186 .tools_for_session(&crate::tool_api::SessionToolContext::new(session))
187 .ok()
188 .is_some_and(|tools| tools.iter().any(|tool| tool.name == "send_message"))
189 }))
190 }
191
192 fn messaging_projection(
193 &self,
194 session: SessionId,
195 ) -> Result<crate::session::SessionProjection, EngineError> {
196 self.inner
197 .store
198 .get(session)
199 .map_err(|error| map_unknown_session(error.into()))
200 }
201
202 pub(crate) fn session_root(&self, session: SessionId) -> Result<SessionId, EngineError> {
204 Ok(match self.messaging_projection(session)?.meta.origin {
205 SessionOrigin::Root => session,
206 SessionOrigin::Delegated {
207 root_session_id, ..
208 } => root_session_id,
209 })
210 }
211
212 pub fn message_relationship(
220 &self,
221 sender: SessionId,
222 recipient: SessionId,
223 ) -> Result<&'static str, EngineError> {
224 let sender_origin = self.messaging_projection(sender)?.meta.origin;
225 let recipient_origin = self.messaging_projection(recipient)?.meta.origin;
226 Ok(relationship_label(
227 &sender_origin,
228 &recipient_origin,
229 sender,
230 recipient,
231 ))
232 }
233
234 pub(crate) fn ensure_tree_peer(
239 &self,
240 sender: SessionId,
241 recipient: SessionId,
242 ) -> Result<(), EngineError> {
243 if sender == recipient {
244 return Err(messaging_error(MESSAGE_SELF_SEND));
245 }
246 if self.session_root(sender)? != self.session_root(recipient)? {
247 return Err(messaging_error(MESSAGE_NOT_TREE_PEER));
248 }
249 Ok(())
250 }
251
252 pub(crate) fn agent_recipient_state(
253 &self,
254 recipient: SessionId,
255 ) -> Result<AgentRecipientState, EngineError> {
256 let projection = self.messaging_projection(recipient)?;
257 let running = projection.status == SessionStatus::Running
258 || self
259 .inner
260 .sessions
261 .active
262 .lock()
263 .unwrap_or_else(|error| error.into_inner())
264 .values()
265 .any(|active| active.session == recipient);
266 if running {
267 return Ok(AgentRecipientState::Running);
268 }
269 if matches!(
270 projection.status,
271 SessionStatus::Completed
272 | SessionStatus::Failed
273 | SessionStatus::Interrupted
274 | SessionStatus::Cancelled
275 ) {
276 return Ok(AgentRecipientState::WakingFinished);
277 }
278 Ok(AgentRecipientState::Queued)
282 }
283
284 pub async fn send_agent_message(
309 &self,
310 invocation: AgentMessageInvocation,
311 ) -> Result<AgentMessageHandle, EngineError> {
312 let messaging = &self.inner.config.runtime.messaging;
313 if !messaging.enabled {
314 return Err(messaging_error(MESSAGE_DISABLED));
315 }
316 if invocation.body.trim().is_empty() || invocation.body.len() > messaging.max_body_bytes {
317 return Err(messaging_error(MESSAGE_INVALID_BODY));
318 }
319 self.ensure_tree_peer(
320 invocation.sender_session_id,
321 invocation.recipient_session_id,
322 )?;
323 let recipient_state = self.agent_recipient_state(invocation.recipient_session_id)?;
324 let include_reply_hint = self.send_message_tool_visible(invocation.recipient_session_id)?;
325 let sender = self.messaging_projection(invocation.sender_session_id)?;
326 let sender_agent_type = sender.creation_agent.agent.to_string();
327 let hop = self
328 .goal_producer_projection(invocation.sender_session_id)?
329 .inherited_agent_hop(invocation.sender_run_id);
330 let authority = ProducerAuthority {
331 owner: ProducerOwner::Agent {
332 session_id: invocation.sender_session_id,
333 },
334 connection_epoch: None,
335 };
336 let producer_id = self
337 .find_or_register_agent_producer(invocation.recipient_session_id, &authority)
338 .await
339 .map_err(map_shutdown)?;
340 let key = ProducerIdempotencyKey::new(format!(
341 "agent-message:{}:{}:{}",
342 invocation.sender_session_id, invocation.sender_run_id, invocation.sender_tool_call_id
343 ))
344 .expect("agent message idempotency key fits the producer key bound");
345 let description = producer_description("Agent message from ", &sender_agent_type);
346 let message_id = self
347 .request(invocation.recipient_session_id, |reply| {
348 SessionCommand::Producer(ProducerCommand::SendAgentMessage {
349 authority,
350 producer_id,
351 mode: invocation.mode,
352 key,
353 description,
354 sender: invocation.sender_session_id,
355 sender_agent_type,
356 body: invocation.body,
357 include_reply_hint,
358 hop,
359 reply,
360 })
361 })
362 .await
363 .map_err(map_shutdown)?;
364 Ok(AgentMessageHandle {
365 message_id,
366 mode: invocation.mode,
367 recipient_state,
368 })
369 }
370
371 async fn find_or_register_agent_producer(
372 &self,
373 recipient: SessionId,
374 authority: &ProducerAuthority,
375 ) -> Result<ProducerId, EngineError> {
376 let producers = self
377 .session_producers(SessionProducersParams {
378 session_id: recipient,
379 })
380 .await?;
381 if let Some(registration) = producers
382 .producers
383 .into_iter()
384 .find(|registration| registration.producer_owner == authority.owner)
385 {
386 return Ok(registration.producer_id);
387 }
388 self.register_producer(recipient, authority.clone()).await
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::render_agent_message_envelope;
395 use cookie_agent_protocol::SessionId;
396
397 #[test]
398 fn agent_message_envelope_keeps_body_readable_and_reply_hint_optional() {
399 let sender = SessionId::new_v7();
400 let with_hint = render_agent_message_envelope(
401 sender,
402 Some("explore_1a2b3c4d"),
403 "Message written by the sending agent",
404 true,
405 );
406 assert_eq!(
407 with_hint,
408 "<agent_message from=\"explore_1a2b3c4d\">\nMessage written by the sending agent\n\n[use `send_message(to=\"explore_1a2b3c4d\", ...)` to reply]\n</agent_message>"
409 );
410
411 let without_hint = render_agent_message_envelope(
412 sender,
413 Some("explore_1a2b3c4d"),
414 "Message written by the sending agent",
415 false,
416 );
417 assert_eq!(
418 without_hint,
419 "<agent_message from=\"explore_1a2b3c4d\">\nMessage written by the sending agent\n</agent_message>"
420 );
421 }
422}