Skip to main content

serve/
cx.rs

1//! `Cx`: the one context type, handed to tools, schedules and evals.
2//!
3//! Like a Topcoat component fetching its own data through `&Cx`, a tool asks
4//! the context for what it needs (a connection, a secret, progress reporting)
5//! instead of having it threaded through the agent definition. Approvals are
6//! declared on the tool (`#[tool(needs_approval …)]`) and enforced by the
7//! everruns runtime, not asked for from inside the tool body.
8
9use std::any::Any;
10use std::future::{Future, IntoFuture};
11use std::pin::Pin;
12use std::sync::{Arc, Weak};
13
14use anyhow::anyhow;
15use everruns::ToolCallContext;
16use serde_json::Value;
17
18use crate::app::Mode;
19use crate::host::{Host, NewSession};
20
21/// The context of one tool call (inside tools) or of the app (in schedules).
22///
23/// Inside a tool it wraps the runtime's `everruns::ToolCallContext` (session,
24/// turn and tool call ids, progress) and adds host access: connections and
25/// secrets.
26#[derive(Clone)]
27pub struct Cx {
28    host: Weak<Host>,
29    agent: Option<&'static str>,
30    session: Option<String>,
31    call: Option<ToolCallContext>,
32}
33
34impl std::fmt::Debug for Cx {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("Cx")
37            .field("session", &self.session)
38            .field("agent", &self.agent)
39            .field("call", &self.call)
40            .finish()
41    }
42}
43
44impl Cx {
45    /// App-level context (schedules): no session.
46    pub(crate) fn app(host: &Arc<Host>) -> Self {
47        Self {
48            host: Arc::downgrade(host),
49            agent: None,
50            session: None,
51            call: None,
52        }
53    }
54
55    /// Context for one tool call.
56    pub(crate) fn tool(host: Weak<Host>, agent: &'static str, call: ToolCallContext) -> Self {
57        Self {
58            host,
59            agent: Some(agent),
60            session: Some(call.session_id().to_string()),
61            call: Some(call),
62        }
63    }
64
65    fn host(&self) -> crate::Result<Arc<Host>> {
66        self.host
67            .upgrade()
68            .ok_or_else(|| anyhow!("the serve host has shut down"))
69    }
70
71    /// The current session id, inside a tool.
72    pub fn session_id(&self) -> Option<&str> {
73        self.session.as_deref()
74    }
75
76    /// The current tool call id, inside a tool.
77    pub fn tool_call_id(&self) -> Option<&str> {
78        self.call.as_ref().map(ToolCallContext::tool_call_id)
79    }
80
81    /// The turn that made the current call, inside a tool.
82    pub fn turn_id(&self) -> Option<String> {
83        self.call.as_ref().and_then(ToolCallContext::turn_id)
84    }
85
86    /// The agent this context belongs to, inside a tool.
87    pub fn agent(&self) -> Option<&'static str> {
88        self.agent
89    }
90
91    /// How the binary is running.
92    pub fn mode(&self) -> Option<Mode> {
93        self.host.upgrade().map(|host| host.mode)
94    }
95
96    /// The `#[connection]` of type `T`. The value (and any credentials in it)
97    /// never reaches the model.
98    pub fn connection<T: Any + Send + Sync>(&self) -> crate::Result<Arc<T>> {
99        let host = self.host()?;
100        host.app
101            .inner
102            .connections
103            .iter()
104            .find_map(|entry| entry.value.value.clone().downcast::<T>().ok())
105            .ok_or_else(|| anyhow!("no #[connection] returns `{}`", std::any::type_name::<T>()))
106    }
107
108    /// A host-provided secret by name.
109    pub fn secret(&self, name: &'static str) -> crate::Result<String> {
110        crate::Secret::named(name).value()
111    }
112
113    /// Report progress. Appears as a canonical `tool.progress` event on the
114    /// session's stream and in the dev console. A no-op outside a tool.
115    pub async fn progress(&self, message: impl Into<String>) {
116        if let Some(call) = &self.call {
117            call.progress(message).await;
118        }
119    }
120
121    /// Start a new session, e.g. from a schedule. Awaiting it runs the first
122    /// turn and delivers the reply to [`deliver_to`](StartSession::deliver_to).
123    pub fn start_session(&self, input: impl Into<String>) -> StartSession {
124        StartSession {
125            cx: self.clone(),
126            input: input.into(),
127            agent: None,
128            deliver_to: None,
129            metadata: None,
130        }
131    }
132}
133
134/// Where a session's replies go: a channel and a target on it.
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub struct DeliveryTarget {
137    pub channel: String,
138    pub target: String,
139}
140
141impl DeliveryTarget {
142    pub fn new(channel: impl Into<String>, target: impl Into<String>) -> Self {
143        Self {
144            channel: channel.into(),
145            target: target.into(),
146        }
147    }
148
149    /// `channel:target`, as stored with the session.
150    pub(crate) fn encode(&self) -> String {
151        format!("{}:{}", self.channel, self.target)
152    }
153
154    pub(crate) fn decode(value: &str) -> Option<Self> {
155        let (channel, target) = value.split_once(':')?;
156        Some(Self::new(channel, target))
157    }
158}
159
160/// A session being started from code. Configure, then `.await`.
161#[must_use = "a StartSession does nothing until awaited"]
162pub struct StartSession {
163    cx: Cx,
164    input: String,
165    agent: Option<String>,
166    deliver_to: Option<DeliveryTarget>,
167    metadata: Option<Value>,
168}
169
170impl StartSession {
171    /// Run on this agent instead of the default one.
172    pub fn agent(mut self, name: impl Into<String>) -> Self {
173        self.agent = Some(name.into());
174        self
175    }
176
177    /// Deliver every reply of this session to a channel.
178    pub fn deliver_to(mut self, target: DeliveryTarget) -> Self {
179        self.deliver_to = Some(target);
180        self
181    }
182
183    /// Attach metadata, visible on the session.
184    pub fn metadata(mut self, metadata: Value) -> Self {
185        self.metadata = Some(metadata);
186        self
187    }
188}
189
190impl IntoFuture for StartSession {
191    type Output = crate::Result;
192    type IntoFuture = Pin<Box<dyn Future<Output = crate::Result> + Send>>;
193
194    fn into_future(self) -> Self::IntoFuture {
195        Box::pin(async move {
196            let host = self.cx.host()?;
197            let session = host
198                .create_session(NewSession {
199                    agent: self.agent,
200                    metadata: self.metadata,
201                    deliver_to: self.deliver_to.map(|target| target.encode()),
202                    ..NewSession::default()
203                })
204                .await?;
205            let turn = host.send(&session, self.input).await?;
206            let outcome = turn.wait().await?;
207            if !outcome.success {
208                anyhow::bail!(
209                    "session {session} turn failed: {}",
210                    outcome.error.unwrap_or_default()
211                );
212            }
213            Ok(())
214        })
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn delivery_target_keeps_colons_in_the_target() {
224        let target = DeliveryTarget::new("slack", "C1:1700000000.0001");
225        let decoded = DeliveryTarget::decode(&target.encode()).unwrap();
226        assert_eq!(decoded, target);
227    }
228}