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}