gemini_adk_rs/tool/
context.rs

1//! What a tool knows about the call it serves.
2//!
3//! A tool is usually a pure function of its arguments. Some need more: the
4//! session [`State`] (the caller's account, a slot captured earlier), the
5//! call's id (for logs and idempotency), or a way to notice the user barged
6//! in and stop early. The runtime hands every call a [`ToolContext`]. A tool
7//! that wants it asks for it:
8//!
9//! - `#[tool]`: add a `ctx: ToolContext` parameter. It is filled by the
10//!   runtime and never shown to the model.
11//! - A closure: [`ContextTool`], or `T::contextual` in the fluent crate.
12//! - A hand-written [`ToolFunction`]: override
13//!   [`call_with_context`](ToolFunction::call_with_context).
14//!
15//! ```
16//! use gemini_adk_rs::tool::{ContextTool, ToolContext, ToolFunction};
17//! use gemini_adk_rs::State;
18//! use serde_json::json;
19//!
20//! # tokio_test::block_on(async {
21//! let balance = ContextTool::new("balance", "The caller's balance", None, |_args, ctx: ToolContext| async move {
22//!     let account: String = ctx.state.get("account_id").unwrap_or_default();
23//!     Ok(json!({ "account": account, "cents": 1200 }))
24//! });
25//!
26//! let state = State::new();
27//! state.set("account_id", "A-17").unwrap();
28//! let out = balance
29//!     .call_with_context(json!({}), ToolContext::new(state).with_call_id("call-1"))
30//!     .await
31//!     .unwrap();
32//! assert_eq!(out["account"], "A-17");
33//! # });
34//! ```
35
36use std::future::Future;
37use std::pin::Pin;
38use std::sync::Arc;
39
40use async_trait::async_trait;
41use serde_json::Value;
42use tokio_util::sync::CancellationToken;
43
44use super::ToolFunction;
45use crate::error::ToolError;
46use crate::state::State;
47
48/// The session a tool call runs in.
49#[derive(Debug, Clone)]
50#[non_exhaustive]
51pub struct ToolContext {
52    /// The session state: read what the conversation captured, write what
53    /// the tool learned.
54    pub state: State,
55    /// The model's id for this call, when it gave one.
56    pub call_id: Option<String>,
57    /// Cancelled when the call should stop: the user barged in, or the
58    /// session is ending. A long tool can check it between steps; the
59    /// runtime also drops the call's future when it fires.
60    pub cancel: CancellationToken,
61}
62
63impl ToolContext {
64    /// A context over `state`, with no call id and a token nothing cancels.
65    pub fn new(state: State) -> Self {
66        Self {
67            state,
68            call_id: None,
69            cancel: CancellationToken::new(),
70        }
71    }
72
73    /// A context for a call made outside any session (a direct
74    /// [`ToolFunction::call`], a unit test): fresh, empty state.
75    pub fn detached() -> Self {
76        Self::new(State::new())
77    }
78
79    /// With the model's call id.
80    pub fn with_call_id(mut self, id: impl Into<String>) -> Self {
81        self.call_id = Some(id.into());
82        self
83    }
84
85    /// With a cancellation token.
86    pub fn with_cancel(mut self, cancel: CancellationToken) -> Self {
87        self.cancel = cancel;
88        self
89    }
90}
91
92type ContextFn = Arc<
93    dyn Fn(Value, ToolContext) -> Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send>>
94        + Send
95        + Sync,
96>;
97
98/// A tool from a closure that receives the [`ToolContext`].
99pub struct ContextTool {
100    name: String,
101    description: String,
102    parameters: Option<Value>,
103    run: ContextFn,
104}
105
106impl ContextTool {
107    /// A tool named `name` running `f(args, ctx)`. `parameters` is the JSON
108    /// Schema of its arguments (`None` for none).
109    pub fn new<F, Fut>(
110        name: impl Into<String>,
111        description: impl Into<String>,
112        parameters: Option<Value>,
113        f: F,
114    ) -> Self
115    where
116        F: Fn(Value, ToolContext) -> Fut + Send + Sync + 'static,
117        Fut: Future<Output = Result<Value, ToolError>> + Send + 'static,
118    {
119        let f = Arc::new(f);
120        Self {
121            name: name.into(),
122            description: description.into(),
123            parameters,
124            run: Arc::new(move |args, ctx| {
125                let f = f.clone();
126                Box::pin(async move { f(args, ctx).await })
127            }),
128        }
129    }
130}
131
132#[async_trait]
133impl ToolFunction for ContextTool {
134    fn name(&self) -> &str {
135        &self.name
136    }
137
138    fn description(&self) -> &str {
139        &self.description
140    }
141
142    fn parameters(&self) -> Option<Value> {
143        self.parameters.clone()
144    }
145
146    async fn call(&self, args: Value) -> Result<Value, ToolError> {
147        self.call_with_context(args, ToolContext::detached()).await
148    }
149
150    async fn call_with_context(&self, args: Value, ctx: ToolContext) -> Result<Value, ToolError> {
151        (self.run)(args, ctx).await
152    }
153}