Skip to main content

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    /// Task activation ownership, captured at admission when using skills.
58    pub task: Option<crate::tasks::TaskOwner>,
59    /// Cancelled when the call should stop: the user barged in, or the
60    /// session is ending. A long tool can check it between steps; the
61    /// runtime also drops the call's future when it fires.
62    pub cancel: CancellationToken,
63}
64
65impl ToolContext {
66    /// A context over `state`, with no call id and a token nothing cancels.
67    pub fn new(state: State) -> Self {
68        Self {
69            state,
70            call_id: None,
71            task: None,
72            cancel: CancellationToken::new(),
73        }
74    }
75
76    /// A context for a call made outside any session (a direct
77    /// [`ToolFunction::call`], a unit test): fresh, empty state.
78    pub fn detached() -> Self {
79        Self::new(State::new())
80    }
81
82    /// With the model's call id.
83    pub fn with_call_id(mut self, id: impl Into<String>) -> Self {
84        self.call_id = Some(id.into());
85        self
86    }
87
88    /// Bind execution and tracing to its original task activation.
89    pub fn with_task(mut self, owner: crate::tasks::TaskOwner) -> Self {
90        self.task = Some(owner);
91        self
92    }
93
94    /// With a cancellation token.
95    pub fn with_cancel(mut self, cancel: CancellationToken) -> Self {
96        self.cancel = cancel;
97        self
98    }
99}
100
101type ContextFn = Arc<
102    dyn Fn(Value, ToolContext) -> Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send>>
103        + Send
104        + Sync,
105>;
106
107/// A tool from a closure that receives the [`ToolContext`].
108pub struct ContextTool {
109    name: String,
110    description: String,
111    parameters: Option<Value>,
112    run: ContextFn,
113}
114
115impl ContextTool {
116    /// A tool named `name` running `f(args, ctx)`. `parameters` is the JSON
117    /// Schema of its arguments (`None` for none).
118    pub fn new<F, Fut>(
119        name: impl Into<String>,
120        description: impl Into<String>,
121        parameters: Option<Value>,
122        f: F,
123    ) -> Self
124    where
125        F: Fn(Value, ToolContext) -> Fut + Send + Sync + 'static,
126        Fut: Future<Output = Result<Value, ToolError>> + Send + 'static,
127    {
128        let f = Arc::new(f);
129        Self {
130            name: name.into(),
131            description: description.into(),
132            parameters,
133            run: Arc::new(move |args, ctx| {
134                let f = f.clone();
135                Box::pin(async move { f(args, ctx).await })
136            }),
137        }
138    }
139}
140
141#[async_trait]
142impl ToolFunction for ContextTool {
143    fn name(&self) -> &str {
144        &self.name
145    }
146
147    fn description(&self) -> &str {
148        &self.description
149    }
150
151    fn parameters(&self) -> Option<Value> {
152        self.parameters.clone()
153    }
154
155    async fn call(&self, args: Value) -> Result<Value, ToolError> {
156        self.call_with_context(args, ToolContext::detached()).await
157    }
158
159    async fn call_with_context(&self, args: Value, ctx: ToolContext) -> Result<Value, ToolError> {
160        (self.run)(args, ctx).await
161    }
162}