Skip to main content

atomr_agents_channel_core/
target.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use atomr_agents_callable::CallableHandle;
5
6use crate::content::{InboundMessage, MessageContent};
7use crate::error::Result;
8
9/// What a thread invokes when an inbound message arrives.
10///
11/// `Callable` works for [`AgentRef`](atomr_agents_core), `Team`,
12/// `WorkflowStep`, plain [`FnCallable`](atomr_agents_callable::FnCallable)
13/// — anything that already implements the trait.
14///
15/// `Harness` is special: [`HarnessRef::call`](atomr_agents_callable)
16/// ignores its input, so binding a harness via raw `Callable` would
17/// silently drop every inbound message. Instead, the orchestrator
18/// applies the inbound through a user-supplied [`HarnessInputAdapter`]
19/// (which mutates whatever state the harness reads on its next run)
20/// and then optionally triggers `harness.run()`.
21#[derive(Clone)]
22pub enum ThreadTarget {
23    Callable(CallableHandle),
24    Harness {
25        callable: CallableHandle,
26        adapter: Arc<dyn HarnessInputAdapter>,
27    },
28}
29
30impl ThreadTarget {
31    pub fn callable(handle: CallableHandle) -> Self {
32        Self::Callable(handle)
33    }
34
35    pub fn harness(callable: CallableHandle, adapter: Arc<dyn HarnessInputAdapter>) -> Self {
36        Self::Harness { callable, adapter }
37    }
38
39    pub fn label(&self) -> &str {
40        match self {
41            Self::Callable(c) => c.label(),
42            Self::Harness { callable, .. } => callable.label(),
43        }
44    }
45
46    pub fn kind(&self) -> &'static str {
47        match self {
48            Self::Callable(_) => "callable",
49            Self::Harness { .. } => "harness",
50        }
51    }
52}
53
54impl std::fmt::Debug for ThreadTarget {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("ThreadTarget")
57            .field("kind", &self.kind())
58            .field("label", &self.label())
59            .finish()
60    }
61}
62
63/// Bridge an inbound channel message into the state a harness reads on
64/// its next run.
65///
66/// Example: a meetings harness that reads from an STT
67/// `ConversationStore`. The adapter would append the inbound's text as
68/// a new turn under the harness's source `conversation_id`, then return
69/// — the orchestrator will trigger `harness.run()` next.
70#[async_trait]
71pub trait HarnessInputAdapter: Send + Sync + 'static {
72    async fn apply(&self, msg: &InboundMessage) -> Result<()>;
73
74    /// If `true` (default), the orchestrator runs the harness once per
75    /// inbound. If `false`, the harness is expected to be driven
76    /// externally; the orchestrator only calls `apply`.
77    fn one_shot(&self) -> bool {
78        true
79    }
80
81    /// Map the harness's `run()` result into an outbound reply. Default
82    /// implementation extracts a `"text"` field if present.
83    fn reply_from_result(&self, value: &serde_json::Value) -> Option<MessageContent> {
84        if let Some(s) = value.get("text").and_then(|v| v.as_str()) {
85            if !s.is_empty() {
86                return Some(MessageContent::text(s));
87            }
88        }
89        if let Some(s) = value.as_str() {
90            if !s.is_empty() {
91                return Some(MessageContent::text(s));
92            }
93        }
94        None
95    }
96}