Skip to main content

meerkat_runtime/
comms_bridge.rs

1//! Runtime comms bridge helpers.
2//!
3//! These helpers translate drained comms interactions into the runtime-owned
4//! input families used by the comms classification bridge.
5
6use chrono::Utc;
7#[cfg(test)]
8use meerkat_core::comms::PeerId;
9use meerkat_core::interaction::{
10    InboxInteraction, InteractionContent, PeerIngressConvention, PeerIngressFact, PeerIngressKind,
11    PeerInputCandidate, PeerInputClass,
12};
13#[cfg(test)]
14use meerkat_core::interaction::{PeerIngressIdentity, ResponseStatus};
15use meerkat_core::lifecycle::InputId;
16
17use crate::identifiers::{CorrelationId, LogicalRuntimeId};
18use crate::input::{
19    ExternalEventInput, Input, InputDurability, InputHeader, InputOrigin, InputVisibility,
20    PeerConvention, PeerInput, ResponseProgressPhase, ResponseTerminalStatus,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
24pub enum PeerIngressProjectionError {
25    #[error(
26        "classified peer ingress {interaction_id} ({kind:?}) cannot project to a runtime PeerInput"
27    )]
28    UnsupportedPeerConvention {
29        interaction_id: meerkat_core::InteractionId,
30        kind: PeerIngressKind,
31    },
32    #[error("classified peer ingress {interaction_id} missing canonical peer id")]
33    MissingCanonicalPeerId {
34        interaction_id: meerkat_core::InteractionId,
35    },
36    #[error("classified peer response {interaction_id} missing machine response terminality")]
37    MissingResponseTerminality {
38        interaction_id: meerkat_core::InteractionId,
39    },
40    #[error(
41        "classified peer response {interaction_id} has unsupported machine response terminality"
42    )]
43    UnsupportedResponseTerminality {
44        interaction_id: meerkat_core::InteractionId,
45    },
46}
47
48/// Convert a classified comms interaction into the appropriate runtime-owned
49/// input family.
50pub fn classified_interaction_to_runtime_input(
51    classified: &PeerInputCandidate,
52    runtime_id: &LogicalRuntimeId,
53) -> Result<Input, PeerIngressProjectionError> {
54    let interaction = &classified.interaction;
55
56    if classified.class() == PeerInputClass::PlainEvent {
57        let source_name = classified
58            .ingress
59            .plain_event_source_name()
60            .unwrap_or("unknown");
61        let blocks = external_event_blocks(interaction);
62        return Ok(Input::ExternalEvent(ExternalEventInput {
63            header: InputHeader {
64                id: InputId::new(),
65                timestamp: Utc::now(),
66                source: InputOrigin::External {
67                    source_name: source_name.to_string(),
68                },
69                durability: InputDurability::Durable,
70                visibility: InputVisibility {
71                    transcript_eligible: true,
72                    operator_eligible: true,
73                },
74                idempotency_key: None,
75                supersession_key: None,
76                correlation_id: Some(CorrelationId::from_uuid(interaction.id.0)),
77            },
78            event_type: source_name.to_string(),
79            payload: external_event_payload(interaction),
80            blocks,
81            handling_mode: interaction.handling_mode,
82            render_metadata: interaction.render_metadata.clone(),
83            objective_id: interaction.objective_id,
84        }));
85    }
86
87    peer_candidate_to_peer_input(classified, runtime_id)
88}
89
90fn peer_candidate_to_peer_input(
91    classified: &PeerInputCandidate,
92    runtime_id: &LogicalRuntimeId,
93) -> Result<Input, PeerIngressProjectionError> {
94    peer_input_from_ingress_fact(
95        &classified.interaction,
96        runtime_id,
97        &classified.ingress,
98        classified.response_terminality,
99    )
100}
101
102fn peer_input_from_ingress_fact(
103    interaction: &InboxInteraction,
104    runtime_id: &LogicalRuntimeId,
105    ingress: &PeerIngressFact,
106    response_terminality: Option<meerkat_core::interaction::TerminalityClass>,
107) -> Result<Input, PeerIngressProjectionError> {
108    let convention = map_ingress_convention(interaction.id, ingress, response_terminality)?;
109    let transcript_correlation_id = transcript_correlation_id(interaction, &convention);
110    let durability = map_durability(&convention);
111    let handling_mode = match &convention {
112        PeerConvention::ResponseProgress { .. } => None,
113        _ => Some(interaction.handling_mode),
114    };
115    let peer_id = ingress.canonical_peer_id_string().ok_or(
116        PeerIngressProjectionError::MissingCanonicalPeerId {
117            interaction_id: interaction.id,
118        },
119    )?;
120    let display_identity = ingress
121        .route
122        .as_ref()
123        .map(meerkat_core::PeerRoute::label)
124        .or_else(|| ingress.display_label());
125
126    Ok(Input::Peer(PeerInput {
127        directed_interaction_id: None,
128        injected_context: Vec::new(),
129        header: InputHeader {
130            id: InputId::new(),
131            timestamp: Utc::now(),
132            source: InputOrigin::Peer {
133                peer_id,
134                display_identity,
135                runtime_id: Some(runtime_id.clone()),
136            },
137            durability,
138            visibility: InputVisibility {
139                transcript_eligible: true,
140                operator_eligible: true,
141            },
142            idempotency_key: None,
143            supersession_key: None,
144            correlation_id: Some(CorrelationId::from_uuid(transcript_correlation_id.0)),
145        },
146        convention: Some(convention),
147        content: match peer_blocks(interaction) {
148            Some(blocks) => meerkat_core::types::ContentInput::Blocks(blocks),
149            None => meerkat_core::types::ContentInput::Text(peer_rendered_body(interaction)),
150        },
151        payload: peer_payload(interaction),
152        handling_mode,
153        // Content-adjacent metadata from the classified interaction: the
154        // sender's signed taint declaration (when one was made) rides to the
155        // typed transcript notice.
156        sender_taint: interaction.sender_taint,
157        objective_id: interaction.objective_id,
158    }))
159}
160
161fn transcript_correlation_id(
162    interaction: &InboxInteraction,
163    convention: &PeerConvention,
164) -> meerkat_core::InteractionId {
165    match (convention, &interaction.content) {
166        (
167            PeerConvention::ResponseProgress { .. } | PeerConvention::ResponseTerminal { .. },
168            InteractionContent::Response { in_reply_to, .. },
169        ) => *in_reply_to,
170        _ => interaction.id,
171    }
172}
173
174fn map_ingress_convention(
175    interaction_id: meerkat_core::InteractionId,
176    ingress: &PeerIngressFact,
177    response_terminality: Option<meerkat_core::interaction::TerminalityClass>,
178) -> Result<PeerConvention, PeerIngressProjectionError> {
179    match &ingress.convention {
180        PeerIngressConvention::Message => Ok(PeerConvention::Message),
181        PeerIngressConvention::Request { request_id, intent } => Ok(PeerConvention::Request {
182            request_id: request_id.clone(),
183            intent: intent.clone(),
184        }),
185        PeerIngressConvention::Response {
186            in_reply_to,
187            status: _,
188        } => {
189            let terminality = response_terminality
190                .ok_or(PeerIngressProjectionError::MissingResponseTerminality { interaction_id })?;
191            map_response_convention(interaction_id, *in_reply_to, terminality)
192        }
193        PeerIngressConvention::Lifecycle { kind, .. } => Ok(PeerConvention::Request {
194            request_id: ingress.interaction_id.to_string(),
195            intent: kind.to_string(),
196        }),
197        PeerIngressConvention::Ack { .. } | PeerIngressConvention::PlainEvent { .. } => {
198            Err(PeerIngressProjectionError::UnsupportedPeerConvention {
199                interaction_id,
200                kind: ingress.kind,
201            })
202        }
203    }
204}
205
206fn map_response_convention(
207    interaction_id: meerkat_core::InteractionId,
208    in_reply_to: meerkat_core::InteractionId,
209    terminality: meerkat_core::interaction::TerminalityClass,
210) -> Result<PeerConvention, PeerIngressProjectionError> {
211    let request_id = in_reply_to.to_string();
212    Ok(match terminality {
213        meerkat_core::interaction::TerminalityClass::Progress => PeerConvention::ResponseProgress {
214            request_id,
215            phase: ResponseProgressPhase::Accepted,
216        },
217        meerkat_core::interaction::TerminalityClass::Terminal { disposition } => {
218            let term = match disposition {
219                meerkat_core::interaction::TerminalDisposition::Completed => {
220                    ResponseTerminalStatus::Completed
221                }
222                meerkat_core::interaction::TerminalDisposition::Failed => {
223                    ResponseTerminalStatus::Failed
224                }
225                _ => {
226                    return Err(PeerIngressProjectionError::UnsupportedResponseTerminality {
227                        interaction_id,
228                    });
229                }
230            };
231            PeerConvention::ResponseTerminal {
232                request_id,
233                status: term,
234            }
235        }
236        _ => {
237            return Err(PeerIngressProjectionError::UnsupportedResponseTerminality {
238                interaction_id,
239            });
240        }
241    })
242}
243
244fn peer_rendered_body(interaction: &InboxInteraction) -> String {
245    if !interaction.rendered_text.trim().is_empty() {
246        return interaction.rendered_text.clone();
247    }
248    match &interaction.content {
249        InteractionContent::Message { body, .. }
250        | InteractionContent::IncarnationFencedMessage { body, .. } => body.clone(),
251        InteractionContent::Request { params, .. } => {
252            serde_json::to_string(params).unwrap_or_default()
253        }
254        InteractionContent::Response { result, .. } => {
255            serde_json::to_string(result).unwrap_or_default()
256        }
257    }
258}
259
260fn peer_blocks(interaction: &InboxInteraction) -> Option<Vec<meerkat_core::types::ContentBlock>> {
261    match &interaction.content {
262        InteractionContent::Message { blocks, .. }
263        | InteractionContent::IncarnationFencedMessage { blocks, .. } => blocks.clone(),
264        InteractionContent::Request { blocks, .. } => blocks.clone(),
265        InteractionContent::Response { blocks, .. } => blocks.clone(),
266    }
267}
268
269fn peer_payload(interaction: &InboxInteraction) -> Option<serde_json::Value> {
270    match &interaction.content {
271        InteractionContent::Message { .. }
272        | InteractionContent::IncarnationFencedMessage { .. } => None,
273        InteractionContent::Request { params, .. } => Some(params.clone()),
274        InteractionContent::Response { result, .. } => Some(result.clone()),
275    }
276}
277
278fn external_event_payload(interaction: &InboxInteraction) -> serde_json::Value {
279    match &interaction.content {
280        InteractionContent::Message { body, .. }
281        | InteractionContent::IncarnationFencedMessage { body, .. } => {
282            serde_json::json!({ "body": body })
283        }
284        InteractionContent::Request { intent, params, .. } => {
285            serde_json::json!({ "intent": intent, "params": params })
286        }
287        InteractionContent::Response {
288            in_reply_to,
289            status,
290            result,
291            blocks,
292        } => serde_json::json!({
293            "in_reply_to": in_reply_to,
294            "status": status,
295            "result": result,
296            "blocks": blocks,
297        }),
298    }
299}
300
301fn external_event_blocks(
302    interaction: &InboxInteraction,
303) -> Option<Vec<meerkat_core::types::ContentBlock>> {
304    match &interaction.content {
305        InteractionContent::Message { blocks, .. }
306        | InteractionContent::IncarnationFencedMessage { blocks, .. } => blocks.clone(),
307        InteractionContent::Request { blocks, .. } => blocks.clone(),
308        _ => None,
309    }
310}
311
312fn map_durability(convention: &PeerConvention) -> InputDurability {
313    match convention {
314        PeerConvention::ResponseProgress { .. } => InputDurability::Ephemeral,
315        _ => InputDurability::Durable,
316    }
317}
318
319#[cfg(test)]
320#[allow(clippy::unwrap_used, clippy::panic)]
321mod tests {
322    use super::*;
323    use meerkat_core::interaction::{PeerIngressIdentity, ResponseStatus};
324
325    fn make_interaction_id() -> meerkat_core::interaction::InteractionId {
326        meerkat_core::interaction::InteractionId(meerkat_core::time_compat::new_uuid_v7())
327    }
328
329    fn plain_event_ingress(
330        id: meerkat_core::interaction::InteractionId,
331        source_name: &str,
332    ) -> PeerIngressFact {
333        PeerIngressFact::plain_event(
334            id,
335            source_name,
336            PeerInputClass::PlainEvent,
337            meerkat_core::PeerIngressKind::PlainEvent,
338        )
339    }
340
341    fn test_peer_id() -> PeerId {
342        PeerId::parse("22222222-2222-4222-8222-222222222222").expect("canonical test peer id")
343    }
344
345    fn peer_kind_for_convention(
346        convention: &PeerIngressConvention,
347    ) -> meerkat_core::PeerIngressKind {
348        match convention {
349            PeerIngressConvention::Message => meerkat_core::PeerIngressKind::Message,
350            PeerIngressConvention::Request { .. } | PeerIngressConvention::Lifecycle { .. } => {
351                meerkat_core::PeerIngressKind::Request
352            }
353            PeerIngressConvention::Response { .. } => meerkat_core::PeerIngressKind::Response,
354            PeerIngressConvention::Ack { .. } => meerkat_core::PeerIngressKind::Ack,
355            PeerIngressConvention::PlainEvent { .. } => meerkat_core::PeerIngressKind::PlainEvent,
356        }
357    }
358
359    fn peer_ingress(
360        id: meerkat_core::interaction::InteractionId,
361        peer_id: PeerId,
362        label: &str,
363        class: PeerInputClass,
364        convention: PeerIngressConvention,
365    ) -> PeerIngressFact {
366        let kind = peer_kind_for_convention(&convention);
367        PeerIngressFact::peer(
368            id,
369            class,
370            kind,
371            Some(meerkat_core::PeerIngressAuthDecision::Required),
372            PeerIngressIdentity::new(peer_id, label, convention),
373        )
374    }
375
376    fn candidate_for_interaction(interaction: InboxInteraction) -> PeerInputCandidate {
377        let peer_id = interaction.from_route.unwrap_or_else(test_peer_id);
378        crate::test_peer_input_candidate_from_interaction(interaction, peer_id)
379    }
380
381    fn peer_input_for_test(interaction: &InboxInteraction, runtime_id: &LogicalRuntimeId) -> Input {
382        let candidate = candidate_for_interaction(interaction.clone());
383        classified_interaction_to_runtime_input(&candidate, runtime_id)
384            .expect("test candidate should project to runtime input")
385    }
386
387    #[test]
388    fn message_to_peer_input() {
389        let interaction = InboxInteraction {
390            objective_id: None,
391            sender_taint: None,
392            from_route: None,
393            from: "peer-1".into(),
394            content: InteractionContent::Message {
395                body: "hello".into(),
396                blocks: None,
397            },
398            id: make_interaction_id(),
399            rendered_text: String::new(),
400            handling_mode: meerkat_core::types::HandlingMode::Queue,
401            render_metadata: None,
402        };
403        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
404        if let Input::Peer(p) = &input {
405            assert!(matches!(p.convention, Some(PeerConvention::Message)));
406            assert_eq!(p.content.text_content(), "hello");
407            assert_eq!(
408                p.header.correlation_id,
409                Some(CorrelationId::from_uuid(interaction.id.0)),
410                "plain peer messages must use the inbound interaction id as the live/history dedup key",
411            );
412            assert_eq!(p.header.durability, InputDurability::Durable);
413            assert_eq!(
414                p.handling_mode,
415                Some(meerkat_core::types::HandlingMode::Queue),
416                "explicit queue must survive comms -> runtime projection so it can suppress running-turn interruption"
417            );
418        } else {
419            panic!("Expected PeerInput");
420        }
421    }
422
423    /// Ask 5 gate: the classified interaction's sender-declared content taint
424    /// threads through to `PeerInput.sender_taint` as content-adjacent
425    /// metadata, and `None` (no declaration) survives as `None` — never
426    /// coalesced into `Clean`.
427    #[test]
428    fn message_sender_taint_threads_to_peer_input() {
429        use meerkat_core::comms::SenderContentTaint;
430
431        for declared in [
432            Some(SenderContentTaint::Tainted),
433            Some(SenderContentTaint::Clean),
434            None,
435        ] {
436            let interaction = InboxInteraction {
437                objective_id: None,
438                sender_taint: declared,
439                from_route: None,
440                from: "peer-1".into(),
441                content: InteractionContent::Message {
442                    body: "hello".into(),
443                    blocks: None,
444                },
445                id: make_interaction_id(),
446                rendered_text: String::new(),
447                handling_mode: meerkat_core::types::HandlingMode::Queue,
448                render_metadata: None,
449            };
450            let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
451            let Input::Peer(p) = &input else {
452                panic!("Expected PeerInput");
453            };
454            assert_eq!(
455                p.sender_taint, declared,
456                "sender taint must ride the classified projection unchanged"
457            );
458        }
459    }
460
461    #[test]
462    fn request_to_peer_input() {
463        let interaction = InboxInteraction {
464            objective_id: None,
465            sender_taint: None,
466            from_route: None,
467            from: "peer-1".into(),
468            content: InteractionContent::Request {
469                intent: "mob.peer_added".into(),
470                // K15: lifecycle-classed requests carry the typed peer
471                // subject; a missing subject is rejected at ingress.
472                params: serde_json::json!({"peer": "agent-1"}),
473                blocks: None,
474            },
475            id: make_interaction_id(),
476            rendered_text: String::new(),
477            handling_mode: meerkat_core::types::HandlingMode::Queue,
478            render_metadata: None,
479        };
480        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
481        if let Input::Peer(p) = &input {
482            assert!(matches!(p.convention, Some(PeerConvention::Request { .. })));
483            match p.convention.as_ref() {
484                Some(PeerConvention::Request { request_id, .. }) => {
485                    assert_eq!(request_id, &interaction.id.0.to_string());
486                }
487                other => panic!("Expected request convention, got {other:?}"),
488            }
489            assert_eq!(p.header.durability, InputDurability::Durable);
490            assert_eq!(
491                p.payload,
492                Some(serde_json::json!({"peer": "agent-1"})),
493                "request params must remain structured on PeerInput so runtime prompt projection does not depend on pre-rendered comms prose"
494            );
495            assert_eq!(
496                p.handling_mode,
497                Some(meerkat_core::types::HandlingMode::Queue),
498                "explicit queue request semantics must not collapse to default policy"
499            );
500        } else {
501            panic!("Expected PeerInput");
502        }
503    }
504
505    #[test]
506    fn classified_request_uses_canonical_peer_id_for_runtime_projection() {
507        let source_peer_id =
508            PeerId::parse("11111111-1111-4111-8111-111111111111").expect("canonical peer id");
509        let request_id = make_interaction_id();
510        let classified = PeerInputCandidate {
511            interaction: InboxInteraction {
512                objective_id: None,
513                sender_taint: None,
514                from_route: None,
515                from: "test-mob/lead/l-requester".into(),
516                content: InteractionContent::Request {
517                    intent: "interpret_image".into(),
518                    params: serde_json::json!({"description": "tower with a light"}),
519                    blocks: None,
520                },
521                id: request_id,
522                rendered_text: "stale helper prose".into(),
523                handling_mode: meerkat_core::types::HandlingMode::Steer,
524                render_metadata: None,
525            },
526            ingress: PeerIngressFact::peer(
527                request_id,
528                PeerInputClass::ActionableRequest,
529                meerkat_core::PeerIngressKind::Request,
530                Some(meerkat_core::PeerIngressAuthDecision::Required),
531                PeerIngressIdentity::new(
532                    source_peer_id,
533                    "test-mob/lead/l-requester",
534                    PeerIngressConvention::Request {
535                        request_id: request_id.to_string(),
536                        intent: "interpret_image".to_string(),
537                    },
538                ),
539            ),
540            lifecycle_peer: None,
541            response_terminality: None,
542        };
543
544        let input =
545            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("worker"))
546                .expect("classified request should project to peer input");
547        let Input::Peer(peer) = &input else {
548            panic!("Expected PeerInput");
549        };
550        let InputOrigin::Peer { peer_id, .. } = &peer.header.source else {
551            panic!("Expected peer source");
552        };
553        assert_eq!(peer_id, "11111111-1111-4111-8111-111111111111");
554        assert_eq!(peer.content.text_content(), "stale helper prose");
555
556        let prompt = crate::input::input_prompt_text(&input);
557        assert!(prompt.starts_with(
558            "Peer request from peer_id 11111111-1111-4111-8111-111111111111 (display_name: test-mob/lead/l-requester)."
559        ));
560        assert!(prompt.contains("\"peer_id\":\"11111111-1111-4111-8111-111111111111\""));
561        assert!(prompt.contains("\"display_name\":\"test-mob/lead/l-requester\""));
562        assert!(prompt.contains(&format!("\"in_reply_to\":\"{}\"", request_id.0)));
563        assert!(prompt.contains("\"status\":\"completed\""));
564        assert!(!prompt.contains("to=\""));
565    }
566
567    #[test]
568    fn plain_event_to_external_event_input() {
569        let id = make_interaction_id();
570        let classified = PeerInputCandidate {
571            lifecycle_peer: None,
572            response_terminality: None,
573            ingress: plain_event_ingress(id, "webhook"),
574            interaction: InboxInteraction {
575                objective_id: None,
576                sender_taint: None,
577                from_route: None,
578                from: "event:webhook".into(),
579                content: InteractionContent::Message {
580                    body: "{\"ok\":true}".into(),
581                    blocks: None,
582                },
583                id,
584                rendered_text: String::new(),
585                handling_mode: meerkat_core::types::HandlingMode::Queue,
586                render_metadata: None,
587            },
588        };
589        let input =
590            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
591                .expect("plain event should project to external event input");
592        match input {
593            Input::ExternalEvent(event) => {
594                assert_eq!(event.event_type, "webhook");
595                assert_eq!(event.payload["body"], "{\"ok\":true}");
596                assert_eq!(event.blocks, None);
597                assert_eq!(
598                    event.handling_mode,
599                    meerkat_core::types::HandlingMode::Queue
600                );
601                assert_eq!(event.render_metadata, None);
602            }
603            other => panic!("Expected ExternalEvent input, got {other:?}"),
604        }
605    }
606
607    #[test]
608    fn peer_named_event_prefix_stays_peer_without_plain_event_class() {
609        let id = make_interaction_id();
610        let classified = PeerInputCandidate {
611            lifecycle_peer: None,
612            response_terminality: None,
613            ingress: peer_ingress(
614                id,
615                test_peer_id(),
616                "event:webhook",
617                PeerInputClass::ActionableMessage,
618                PeerIngressConvention::Message,
619            ),
620            interaction: InboxInteraction {
621                objective_id: None,
622                sender_taint: None,
623                from_route: None,
624                from: "event:webhook".into(),
625                content: InteractionContent::Message {
626                    body: "hello".into(),
627                    blocks: None,
628                },
629                id,
630                rendered_text: "stale rendered text".into(),
631                handling_mode: meerkat_core::types::HandlingMode::Queue,
632                render_metadata: None,
633            },
634        };
635        let input =
636            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
637                .expect("classified peer event should project to peer input");
638        match input {
639            Input::Peer(peer) => {
640                assert_eq!(peer.content.text_content(), "stale rendered text");
641                match peer.header.source {
642                    InputOrigin::Peer { peer_id, .. } => {
643                        assert_eq!(peer_id, test_peer_id().as_str());
644                    }
645                    other => panic!("Expected peer source, got {other:?}"),
646                }
647            }
648            other => panic!("Expected Peer input, got {other:?}"),
649        }
650    }
651
652    #[test]
653    fn classified_peer_projection_uses_ingress_canonical_peer_id_not_display_from() {
654        let id = make_interaction_id();
655        let canonical_peer_id = meerkat_core::comms::PeerId::new();
656        let classified = PeerInputCandidate {
657            lifecycle_peer: None,
658            response_terminality: None,
659            ingress: PeerIngressFact::peer(
660                id,
661                PeerInputClass::ActionableRequest,
662                meerkat_core::PeerIngressKind::Request,
663                Some(meerkat_core::PeerIngressAuthDecision::Required),
664                PeerIngressIdentity::new(
665                    canonical_peer_id,
666                    "display-agent",
667                    PeerIngressConvention::Request {
668                        request_id: id.to_string(),
669                        intent: "review".to_string(),
670                    },
671                ),
672            ),
673            interaction: InboxInteraction {
674                objective_id: None,
675                sender_taint: None,
676                from_route: None,
677                from: "display-agent".into(),
678                content: InteractionContent::Request {
679                    intent: "review".into(),
680                    params: serde_json::json!({"pr": 42}),
681                    blocks: None,
682                },
683                id,
684                rendered_text: "stale rendered text".into(),
685                handling_mode: meerkat_core::types::HandlingMode::Queue,
686                render_metadata: None,
687            },
688        };
689
690        let input =
691            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
692                .expect("classified peer projection should use typed canonical id");
693        let Input::Peer(peer) = input else {
694            panic!("Expected Peer input");
695        };
696        match peer.header.source {
697            InputOrigin::Peer { peer_id, .. } => {
698                assert_eq!(peer_id, canonical_peer_id.as_str());
699                assert_ne!(peer_id, "display-agent");
700            }
701            other => panic!("Expected peer source, got {other:?}"),
702        }
703        assert_eq!(peer.content.text_content(), "stale rendered text");
704    }
705
706    #[test]
707    fn classified_peer_projection_rejects_display_only_ingress_identity() {
708        let id = make_interaction_id();
709        let classified = PeerInputCandidate {
710            lifecycle_peer: None,
711            response_terminality: None,
712            ingress: PeerIngressFact {
713                interaction_id: id,
714                class: PeerInputClass::ActionableMessage,
715                kind: meerkat_core::PeerIngressKind::Message,
716                canonical_peer_id: None,
717                display_name: meerkat_core::comms::PeerName::new("display-agent".to_string()).ok(),
718                signing_pubkey: None,
719                route: None,
720                declared_reply_endpoint: None,
721                auth: Some(meerkat_core::PeerIngressAuthDecision::Required),
722                convention: PeerIngressConvention::Message,
723            },
724            interaction: InboxInteraction {
725                objective_id: None,
726                sender_taint: None,
727                from_route: None,
728                from: "display-agent".into(),
729                content: InteractionContent::Message {
730                    body: "hello".into(),
731                    blocks: None,
732                },
733                id,
734                rendered_text: "stale rendered text".into(),
735                handling_mode: meerkat_core::types::HandlingMode::Queue,
736                render_metadata: None,
737            },
738        };
739
740        let result =
741            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"));
742        assert!(
743            matches!(
744                result,
745                Err(PeerIngressProjectionError::MissingCanonicalPeerId { interaction_id })
746                    if interaction_id == id
747            ),
748            "display-only ingress must fail closed, got {result:?}"
749        );
750    }
751
752    #[test]
753    fn request_body_preserves_rendered_text_and_structured_payload() {
754        let interaction = InboxInteraction {
755            objective_id: None,
756            sender_taint: None,
757            from_route: None,
758            from: "event:webhook".into(),
759            content: InteractionContent::Request {
760                intent: "mob.peer_added".into(),
761                params: serde_json::json!({"peer":"agent-1"}),
762                blocks: None,
763            },
764            id: make_interaction_id(),
765            rendered_text: "stale rendered text".into(),
766            handling_mode: meerkat_core::types::HandlingMode::Queue,
767            render_metadata: None,
768        };
769        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
770        if let Input::Peer(peer) = input {
771            assert_eq!(peer.content.text_content(), "stale rendered text");
772            assert_eq!(peer.payload, Some(serde_json::json!({"peer":"agent-1"})));
773        } else {
774            panic!("Expected PeerInput");
775        }
776    }
777
778    #[test]
779    fn message_blocks_are_preserved_on_peer_input() {
780        let blocks = vec![
781            meerkat_core::types::ContentBlock::Text {
782                text: "see image".into(),
783            },
784            meerkat_core::types::ContentBlock::Image {
785                media_type: "image/png".into(),
786                data: "abc".into(),
787            },
788        ];
789        let interaction = InboxInteraction {
790            objective_id: None,
791            sender_taint: None,
792            from_route: None,
793            from: "peer-1".into(),
794            content: InteractionContent::Message {
795                body: "see image".into(),
796                blocks: Some(blocks.clone()),
797            },
798            id: make_interaction_id(),
799            rendered_text: "stale rendered text".into(),
800            handling_mode: meerkat_core::types::HandlingMode::Queue,
801            render_metadata: None,
802        };
803        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
804        if let Input::Peer(peer) = input {
805            // Single-owner semantics: when typed blocks are present they ARE
806            // the content; the stale rendered text is not stored beside them.
807            assert_eq!(
808                peer.content,
809                meerkat_core::types::ContentInput::Blocks(blocks)
810            );
811        } else {
812            panic!("Expected PeerInput");
813        }
814    }
815
816    #[test]
817    fn request_blocks_are_preserved_on_peer_input() {
818        let blocks = vec![
819            meerkat_core::types::ContentBlock::Text {
820                text: "describe this image".into(),
821            },
822            meerkat_core::types::ContentBlock::Image {
823                media_type: "image/png".into(),
824                data: "abc".into(),
825            },
826        ];
827        let interaction_id = make_interaction_id();
828        let peer_id = PeerId::new();
829        let classified = PeerInputCandidate {
830            interaction: InboxInteraction {
831                objective_id: None,
832                sender_taint: None,
833                from_route: Some(peer_id),
834                from: "vision-peer".into(),
835                content: InteractionContent::Request {
836                    intent: "checksum_token".into(),
837                    params: serde_json::json!({"subject": "describe-image"}),
838                    blocks: Some(blocks.clone()),
839                },
840                id: interaction_id,
841                rendered_text: String::new(),
842                handling_mode: meerkat_core::types::HandlingMode::Steer,
843                render_metadata: None,
844            },
845            ingress: PeerIngressFact::peer(
846                interaction_id,
847                PeerInputClass::ActionableRequest,
848                PeerIngressKind::Request,
849                Some(meerkat_core::interaction::PeerIngressAuthDecision::Required),
850                PeerIngressIdentity::new(
851                    peer_id,
852                    "vision-peer",
853                    meerkat_core::interaction::PeerIngressConvention::Request {
854                        request_id: interaction_id.to_string(),
855                        intent: "checksum_token".to_string(),
856                    },
857                ),
858            ),
859            lifecycle_peer: None,
860            response_terminality: None,
861        };
862
863        let input = classified_interaction_to_runtime_input(
864            &classified,
865            &LogicalRuntimeId::new("runtime-a"),
866        )
867        .expect("classified request should project");
868        if let Input::Peer(peer) = input {
869            assert_eq!(
870                peer.content,
871                meerkat_core::types::ContentInput::Blocks(blocks)
872            );
873            assert_eq!(
874                peer.payload,
875                Some(serde_json::json!({"subject": "describe-image"}))
876            );
877        } else {
878            panic!("Expected PeerInput");
879        }
880    }
881
882    #[test]
883    fn multimodal_message_blocks_own_content_with_derived_text_projection() {
884        let blocks = vec![
885            meerkat_core::types::ContentBlock::Text {
886                text: "caption text".into(),
887            },
888            meerkat_core::types::ContentBlock::Image {
889                media_type: "image/png".into(),
890                data: "abc".into(),
891            },
892        ];
893        let interaction = InboxInteraction {
894            objective_id: None,
895            sender_taint: None,
896            from_route: None,
897            from: "peer-1".into(),
898            content: InteractionContent::Message {
899                body: "please inspect this image".into(),
900                blocks: Some(blocks.clone()),
901            },
902            id: make_interaction_id(),
903            rendered_text: "stale rendered text".into(),
904            handling_mode: meerkat_core::types::HandlingMode::Queue,
905            render_metadata: None,
906        };
907        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
908        if let Input::Peer(peer) = input {
909            // Single-owner semantics: typed blocks ARE the content; the text
910            // projection is derived from them at read time, never stored.
911            assert_eq!(
912                peer.content,
913                meerkat_core::types::ContentInput::Blocks(blocks)
914            );
915            assert_eq!(
916                peer.content.text_content(),
917                "caption text\n[image: image/png]"
918            );
919        } else {
920            panic!("Expected PeerInput");
921        }
922    }
923
924    #[test]
925    fn plain_event_blocks_are_preserved_on_external_event_input() {
926        let blocks = vec![
927            meerkat_core::types::ContentBlock::Text {
928                text: "see image".into(),
929            },
930            meerkat_core::types::ContentBlock::Image {
931                media_type: "image/png".into(),
932                data: "abc".into(),
933            },
934        ];
935        let id = make_interaction_id();
936        let classified = PeerInputCandidate {
937            lifecycle_peer: None,
938            response_terminality: None,
939            ingress: plain_event_ingress(id, "webhook"),
940            interaction: InboxInteraction {
941                objective_id: None,
942                sender_taint: None,
943                from_route: None,
944                from: "event:webhook".into(),
945                content: InteractionContent::Message {
946                    body: "see image".into(),
947                    blocks: Some(blocks.clone()),
948                },
949                id,
950                rendered_text: "stale rendered text".into(),
951                handling_mode: meerkat_core::types::HandlingMode::Queue,
952                render_metadata: None,
953            },
954        };
955        let input =
956            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
957                .expect("plain event with blocks should project");
958        match input {
959            Input::ExternalEvent(event) => {
960                assert_eq!(event.payload["body"], "see image");
961                assert!(event.payload.get("blocks").is_none());
962                assert_eq!(event.blocks, Some(blocks));
963                assert_eq!(
964                    event.handling_mode,
965                    meerkat_core::types::HandlingMode::Queue
966                );
967                assert_eq!(event.render_metadata, None);
968            }
969            other => panic!("Expected ExternalEvent input, got {other:?}"),
970        }
971    }
972
973    #[test]
974    fn plain_event_preserves_handling_mode_and_render_metadata() {
975        let render_metadata = meerkat_core::types::RenderMetadata {
976            class: meerkat_core::types::RenderClass::ExternalEvent,
977            salience: meerkat_core::types::RenderSalience::Urgent,
978        };
979        let id = make_interaction_id();
980        let classified = PeerInputCandidate {
981            lifecycle_peer: None,
982            response_terminality: None,
983            ingress: plain_event_ingress(id, "webhook"),
984            interaction: InboxInteraction {
985                objective_id: None,
986                sender_taint: None,
987                from_route: None,
988                from: "event:webhook".into(),
989                content: InteractionContent::Message {
990                    body: "urgent".into(),
991                    blocks: None,
992                },
993                id,
994                rendered_text: "stale rendered text".into(),
995                handling_mode: meerkat_core::types::HandlingMode::Steer,
996                render_metadata: Some(render_metadata.clone()),
997            },
998        };
999
1000        match classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
1001            .expect("plain event should preserve render metadata")
1002        {
1003            Input::ExternalEvent(event) => {
1004                assert_eq!(
1005                    event.handling_mode,
1006                    meerkat_core::types::HandlingMode::Steer
1007                );
1008                assert_eq!(event.render_metadata, Some(render_metadata));
1009            }
1010            other => panic!("Expected ExternalEvent input, got {other:?}"),
1011        }
1012    }
1013
1014    #[test]
1015    fn response_completed_to_terminal() {
1016        let in_reply_to = make_interaction_id();
1017        let route_id = meerkat_core::comms::PeerId::from_uuid(
1018            uuid::Uuid::parse_str("018f6f79-7a82-7c4e-a552-a3b86f9630f2").unwrap(),
1019        );
1020        let interaction = InboxInteraction {
1021            objective_id: None,
1022            sender_taint: None,
1023            from_route: Some(route_id),
1024            from: "Peer One".into(),
1025            content: InteractionContent::Response {
1026                status: ResponseStatus::Completed,
1027                result: serde_json::json!({"ok": true}),
1028                in_reply_to,
1029                blocks: None,
1030            },
1031            id: make_interaction_id(),
1032            rendered_text: String::new(),
1033            handling_mode: meerkat_core::types::HandlingMode::Queue,
1034            render_metadata: None,
1035        };
1036        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
1037        if let Input::Peer(p) = &input {
1038            match &p.header.source {
1039                InputOrigin::Peer {
1040                    peer_id,
1041                    display_identity,
1042                    ..
1043                } => {
1044                    assert_eq!(peer_id, &route_id.to_string());
1045                    assert_eq!(display_identity.as_deref(), Some("Peer One"));
1046                }
1047                other => panic!("Expected Peer source, got {other:?}"),
1048            }
1049            assert!(matches!(
1050                p.convention,
1051                Some(PeerConvention::ResponseTerminal {
1052                    status: ResponseTerminalStatus::Completed,
1053                    ..
1054                })
1055            ));
1056            assert_eq!(
1057                p.header.correlation_id,
1058                Some(CorrelationId::from_uuid(in_reply_to.0)),
1059                "terminal peer responses must use the request interaction id that InteractionComplete reports",
1060            );
1061            assert_eq!(p.header.durability, InputDurability::Durable);
1062            assert_eq!(
1063                p.payload,
1064                Some(serde_json::json!({"ok": true})),
1065                "terminal response result must remain structured on PeerInput so runtime prompt projection stays runtime-owned"
1066            );
1067        } else {
1068            panic!("Expected PeerInput");
1069        }
1070        let projection = crate::input::runtime_input_projection_for_machine_batch(&input);
1071        let context = projection
1072            .context_append
1073            .expect("terminal machine-batch context projection");
1074        let expected_key = format!("peer_response_terminal:{route_id}:{in_reply_to}");
1075        assert_eq!(context.key, expected_key);
1076        let meerkat_core::lifecycle::run_primitive::CoreRenderable::SystemNotice { blocks, .. } =
1077            context.content
1078        else {
1079            panic!("Expected terminal context notice");
1080        };
1081        assert!(matches!(
1082            blocks.first(),
1083            Some(meerkat_core::types::SystemNoticeBlock::Comms { peer, .. })
1084                if peer.as_ref().and_then(|peer| peer.display_name.as_deref()) == Some("Peer One")
1085        ));
1086    }
1087
1088    #[test]
1089    fn classified_response_uses_ingress_terminal_class() {
1090        let in_reply_to = make_interaction_id();
1091        let id = make_interaction_id();
1092        let classified = PeerInputCandidate {
1093            interaction: InboxInteraction {
1094                objective_id: None,
1095                sender_taint: None,
1096                from_route: None,
1097                from: "peer-1".into(),
1098                content: InteractionContent::Response {
1099                    status: ResponseStatus::Completed,
1100                    result: serde_json::json!({"ok": true}),
1101                    in_reply_to,
1102                    blocks: None,
1103                },
1104                id,
1105                rendered_text: String::new(),
1106                handling_mode: meerkat_core::types::HandlingMode::Queue,
1107                render_metadata: None,
1108            },
1109            ingress: PeerIngressFact::peer(
1110                id,
1111                PeerInputClass::ResponseProgress,
1112                meerkat_core::PeerIngressKind::Response,
1113                Some(meerkat_core::PeerIngressAuthDecision::Required),
1114                PeerIngressIdentity::new(
1115                    test_peer_id(),
1116                    "peer-1",
1117                    PeerIngressConvention::Response {
1118                        in_reply_to,
1119                        status: ResponseStatus::Completed,
1120                    },
1121                ),
1122            ),
1123            lifecycle_peer: None,
1124            response_terminality: Some(meerkat_core::TerminalityClass::Progress),
1125        };
1126
1127        let input =
1128            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
1129                .expect("classified response should project");
1130        if let Input::Peer(peer) = input {
1131            assert!(
1132                matches!(
1133                    peer.convention,
1134                    Some(PeerConvention::ResponseProgress { .. })
1135                ),
1136                "classified bridge must consume ingress-owned response class"
1137            );
1138        } else {
1139            panic!("Expected PeerInput");
1140        }
1141    }
1142
1143    #[test]
1144    fn classified_response_missing_machine_terminality_fails_closed() {
1145        let in_reply_to = make_interaction_id();
1146        let id = make_interaction_id();
1147        let classified = PeerInputCandidate {
1148            interaction: InboxInteraction {
1149                objective_id: None,
1150                sender_taint: None,
1151                from_route: None,
1152                from: "peer-1".into(),
1153                content: InteractionContent::Response {
1154                    status: ResponseStatus::Completed,
1155                    result: serde_json::json!({"ok": true}),
1156                    in_reply_to,
1157                    blocks: None,
1158                },
1159                id,
1160                rendered_text: String::new(),
1161                handling_mode: meerkat_core::types::HandlingMode::Queue,
1162                render_metadata: None,
1163            },
1164            ingress: PeerIngressFact::peer(
1165                id,
1166                PeerInputClass::ResponseTerminal,
1167                meerkat_core::PeerIngressKind::Response,
1168                Some(meerkat_core::PeerIngressAuthDecision::Required),
1169                PeerIngressIdentity::new(
1170                    test_peer_id(),
1171                    "peer-1",
1172                    PeerIngressConvention::Response {
1173                        in_reply_to,
1174                        status: ResponseStatus::Completed,
1175                    },
1176                ),
1177            ),
1178            lifecycle_peer: None,
1179            response_terminality: None,
1180        };
1181
1182        let result =
1183            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"));
1184        assert!(
1185            matches!(
1186                result,
1187                Err(PeerIngressProjectionError::MissingResponseTerminality { interaction_id })
1188                    if interaction_id == id
1189            ),
1190            "runtime projection must not infer public terminality from raw status: {result:?}"
1191        );
1192    }
1193
1194    #[test]
1195    fn response_terminal_without_canonical_peer_id_fails_typed_projection() {
1196        let in_reply_to = make_interaction_id();
1197        let interaction_id = make_interaction_id();
1198        let candidate = PeerInputCandidate {
1199            interaction: InboxInteraction {
1200                objective_id: None,
1201                sender_taint: None,
1202                from_route: None,
1203                from: "Peer One".into(),
1204                content: InteractionContent::Response {
1205                    status: ResponseStatus::Completed,
1206                    result: serde_json::json!({"ok": true}),
1207                    in_reply_to,
1208                    blocks: None,
1209                },
1210                id: interaction_id,
1211                rendered_text: String::new(),
1212                handling_mode: meerkat_core::types::HandlingMode::Queue,
1213                render_metadata: None,
1214            },
1215            ingress: PeerIngressFact {
1216                interaction_id,
1217                class: PeerInputClass::ResponseTerminal,
1218                kind: meerkat_core::PeerIngressKind::Response,
1219                canonical_peer_id: None,
1220                display_name: meerkat_core::comms::PeerName::new("Peer One".to_string()).ok(),
1221                signing_pubkey: None,
1222                route: None,
1223                declared_reply_endpoint: None,
1224                auth: Some(meerkat_core::PeerIngressAuthDecision::Required),
1225                convention: PeerIngressConvention::Response {
1226                    in_reply_to,
1227                    status: ResponseStatus::Completed,
1228                },
1229            },
1230            lifecycle_peer: None,
1231            response_terminality: Some(meerkat_core::TerminalityClass::Terminal {
1232                disposition: meerkat_core::TerminalDisposition::Completed,
1233            }),
1234        };
1235        let err =
1236            classified_interaction_to_runtime_input(&candidate, &LogicalRuntimeId::new("test"))
1237                .unwrap_err();
1238        assert!(matches!(
1239            err,
1240            PeerIngressProjectionError::MissingCanonicalPeerId { .. }
1241        ));
1242    }
1243
1244    #[test]
1245    fn response_failed_to_terminal() {
1246        let in_reply_to = make_interaction_id();
1247        let route_id = meerkat_core::comms::PeerId::from_uuid(
1248            uuid::Uuid::parse_str("018f6f79-7a82-7c4e-a552-a3b86f9630f3").unwrap(),
1249        );
1250        let interaction = InboxInteraction {
1251            objective_id: None,
1252            sender_taint: None,
1253            from_route: Some(route_id),
1254            from: "peer-1".into(),
1255            content: InteractionContent::Response {
1256                status: ResponseStatus::Failed,
1257                result: serde_json::json!({"error": "timeout"}),
1258                in_reply_to,
1259                blocks: None,
1260            },
1261            id: make_interaction_id(),
1262            rendered_text: String::new(),
1263            handling_mode: meerkat_core::types::HandlingMode::Queue,
1264            render_metadata: None,
1265        };
1266        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
1267        if let Input::Peer(p) = &input {
1268            assert!(matches!(
1269                p.convention,
1270                Some(PeerConvention::ResponseTerminal {
1271                    status: ResponseTerminalStatus::Failed,
1272                    ..
1273                })
1274            ));
1275        } else {
1276            panic!("Expected PeerInput");
1277        }
1278    }
1279
1280    #[test]
1281    fn response_accepted_to_progress() {
1282        let in_reply_to = make_interaction_id();
1283        let interaction = InboxInteraction {
1284            objective_id: None,
1285            sender_taint: None,
1286            from_route: None,
1287            from: "peer-1".into(),
1288            content: InteractionContent::Response {
1289                status: ResponseStatus::Accepted,
1290                result: serde_json::json!(null),
1291                in_reply_to,
1292                blocks: None,
1293            },
1294            id: make_interaction_id(),
1295            rendered_text: String::new(),
1296            handling_mode: meerkat_core::types::HandlingMode::Queue,
1297            render_metadata: None,
1298        };
1299        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
1300        if let Input::Peer(p) = &input {
1301            assert!(matches!(
1302                p.convention,
1303                Some(PeerConvention::ResponseProgress {
1304                    phase: ResponseProgressPhase::Accepted,
1305                    ..
1306                })
1307            ));
1308            assert_eq!(
1309                p.header.correlation_id,
1310                Some(CorrelationId::from_uuid(in_reply_to.0)),
1311                "progress peer responses must share the same request correlation as terminal response completion",
1312            );
1313            assert_eq!(p.header.durability, InputDurability::Ephemeral);
1314            assert!(
1315                p.handling_mode.is_none(),
1316                "ResponseProgress inputs must not carry handling_mode"
1317            );
1318        } else {
1319            panic!("Expected PeerInput");
1320        }
1321    }
1322
1323    #[test]
1324    fn classified_response_uses_ingress_terminality_over_raw_status() {
1325        let in_reply_to = make_interaction_id();
1326        let id = make_interaction_id();
1327        let classified = PeerInputCandidate {
1328            interaction: InboxInteraction {
1329                objective_id: None,
1330                sender_taint: None,
1331                from_route: None,
1332                from: "peer-1".into(),
1333                content: InteractionContent::Response {
1334                    status: ResponseStatus::Completed,
1335                    result: serde_json::json!({"ok": true}),
1336                    in_reply_to,
1337                    blocks: None,
1338                },
1339                id,
1340                rendered_text: String::new(),
1341                handling_mode: meerkat_core::types::HandlingMode::Queue,
1342                render_metadata: None,
1343            },
1344            ingress: PeerIngressFact::peer(
1345                id,
1346                PeerInputClass::ResponseProgress,
1347                meerkat_core::PeerIngressKind::Response,
1348                Some(meerkat_core::PeerIngressAuthDecision::Required),
1349                PeerIngressIdentity::new(
1350                    test_peer_id(),
1351                    "peer-1",
1352                    PeerIngressConvention::Response {
1353                        in_reply_to,
1354                        status: ResponseStatus::Completed,
1355                    },
1356                ),
1357            ),
1358            lifecycle_peer: None,
1359            response_terminality: Some(meerkat_core::TerminalityClass::Progress),
1360        };
1361
1362        let input =
1363            classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
1364                .expect("classified response should project");
1365
1366        if let Input::Peer(p) = &input {
1367            assert!(matches!(
1368                p.convention,
1369                Some(PeerConvention::ResponseProgress {
1370                    phase: ResponseProgressPhase::Accepted,
1371                    ..
1372                })
1373            ));
1374            assert_eq!(p.header.durability, InputDurability::Ephemeral);
1375            assert_eq!(p.handling_mode, None);
1376        } else {
1377            panic!("Expected PeerInput");
1378        }
1379    }
1380
1381    #[test]
1382    fn peer_source_includes_runtime_id() {
1383        let interaction = InboxInteraction {
1384            objective_id: None,
1385            sender_taint: None,
1386            from_route: None,
1387            from: "peer-1".into(),
1388            content: InteractionContent::Message {
1389                body: "hi".into(),
1390                blocks: None,
1391            },
1392            id: make_interaction_id(),
1393            rendered_text: String::new(),
1394            handling_mode: meerkat_core::types::HandlingMode::Queue,
1395            render_metadata: None,
1396        };
1397        let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("agent-runtime-1"));
1398        if let Input::Peer(p) = &input {
1399            if let InputOrigin::Peer {
1400                peer_id,
1401                display_identity,
1402                runtime_id,
1403                ..
1404            } = &p.header.source
1405            {
1406                assert_eq!(peer_id, &test_peer_id().as_str());
1407                assert_eq!(display_identity.as_deref(), Some("peer-1"));
1408                assert_eq!(runtime_id.as_ref().unwrap().0, "agent-runtime-1");
1409            } else {
1410                panic!("Expected Peer source");
1411            }
1412        } else {
1413            panic!("Expected PeerInput");
1414        }
1415    }
1416
1417    #[test]
1418    fn all_interaction_types_produce_valid_inputs() {
1419        let in_reply_to = make_interaction_id();
1420        let interactions = vec![
1421            InboxInteraction {
1422                objective_id: None,
1423                sender_taint: None,
1424                from_route: None,
1425                from: "p".into(),
1426                content: InteractionContent::Message {
1427                    body: "m".into(),
1428                    blocks: None,
1429                },
1430                id: make_interaction_id(),
1431                rendered_text: String::new(),
1432                handling_mode: meerkat_core::types::HandlingMode::Queue,
1433                render_metadata: None,
1434            },
1435            InboxInteraction {
1436                objective_id: None,
1437                sender_taint: None,
1438                from_route: None,
1439                from: "p".into(),
1440                content: InteractionContent::Request {
1441                    intent: "i".into(),
1442                    params: serde_json::json!({}),
1443                    blocks: None,
1444                },
1445                id: make_interaction_id(),
1446                rendered_text: String::new(),
1447                handling_mode: meerkat_core::types::HandlingMode::Queue,
1448                render_metadata: None,
1449            },
1450            InboxInteraction {
1451                objective_id: None,
1452                sender_taint: None,
1453                from_route: Some(meerkat_core::comms::PeerId::from_uuid(
1454                    uuid::Uuid::parse_str("018f6f79-7a82-7c4e-a552-a3b86f9630f6").unwrap(),
1455                )),
1456                from: "p".into(),
1457                content: InteractionContent::Response {
1458                    status: ResponseStatus::Completed,
1459                    result: serde_json::json!(null),
1460                    in_reply_to,
1461                    blocks: None,
1462                },
1463                id: make_interaction_id(),
1464                rendered_text: String::new(),
1465                handling_mode: meerkat_core::types::HandlingMode::Queue,
1466                render_metadata: None,
1467            },
1468        ];
1469
1470        let rid = LogicalRuntimeId::new("test");
1471        for interaction in &interactions {
1472            let input = peer_input_for_test(interaction, &rid);
1473            assert!(matches!(input, Input::Peer(_)));
1474        }
1475    }
1476}