1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! [`ContextualFactory`] — runtime-bounded dynamic tool registration.
//!
//! Unlike [`AnyToolFactory`](super::AnyToolFactory), which is object-safe and
//! discovered via `inventory` at compile time, `ContextualFactory` is
//! monomorphized at the call site and registered explicitly with runtime context.
//!
//! # Motivation
//!
//! `register_type::<T>(prefix)` generates tool schemas from `schemars::schema_for!(T)` —
//! a static macro with no access to runtime values. There is no way to reflect a
//! runtime constraint (e.g., `"maximum": player_bankroll`) into the schema.
//!
//! `ContextualFactory` solves this: the factory receives a typed `Context` at
//! registration time and can embed any runtime value into the schema or the set
//! of tools it generates.
//!
//! # Example
//!
//! ```rust,ignore
//! use elicitation::dynamic::{ContextualFactory, DynamicToolDescriptor};
//! use rmcp::ErrorData;
//! use serde_json::json;
//! use std::sync::Arc;
//! use futures::FutureExt;
//!
//! pub struct BetConstraints { pub min: u64, pub max: u64 }
//!
//! pub struct BetAmountFactory;
//!
//! impl ContextualFactory for BetAmountFactory {
//! type Context = BetConstraints;
//!
//! fn instantiate(
//! &self,
//! prefix: &str,
//! ctx: &BetConstraints,
//! ) -> Result<Vec<DynamicToolDescriptor>, ErrorData> {
//! let max = ctx.max;
//! let name = format!("{prefix}__place");
//! Ok(vec![DynamicToolDescriptor {
//! name: name.clone(),
//! description: format!("Place a bet (max {max})"),
//! schema: json!({
//! "type": "object",
//! "properties": {
//! "amount": { "type": "integer", "minimum": ctx.min, "maximum": max }
//! },
//! "required": ["amount"]
//! }),
//! handler: Arc::new(move |args| {
//! Box::pin(async move {
//! // validate and handle ...
//! Ok(rmcp::model::CallToolResult::success(vec![]))
//! })
//! }),
//! }])
//! }
//! }
//!
//! // At betting phase start — re-register when bankroll changes:
//! let registry = registry.register_contextual(
//! "bet",
//! BetAmountFactory,
//! BetConstraints { min: 1, max: player.bankroll() },
//! );
//! ```
//!
//! # Re-registration on phase transitions
//!
//! Calling `register_contextual` a second time with the same prefix **replaces**
//! the previously generated tools. Pair this with
//! [`DynamicToolRegistry::notify_tool_list_changed`](crate::DynamicToolRegistry::notify_tool_list_changed) to push the updated tool
//! list to connected agents immediately.
//!
//! # Relationship to `AnyToolFactory`
//!
//! | Property | `AnyToolFactory` | `ContextualFactory` |
//! |---|---|---|
//! | Discovery | `inventory` (compile time) | Explicit (registration time) |
//! | Object safe | Yes (`&'static dyn`) | No (monomorphized) |
//! | Context | None | Typed associated type |
//! | Schema bounds | `schemars::schema_for!` | Fully runtime |
//! | `Context = ()` | — | Degenerates to no-op context |
use ErrorData;
use instrument;
use DynamicToolDescriptor;
/// A tool factory that requires typed runtime context to instantiate tools.
///
/// Implement this trait for factories whose tool schemas or tool sets depend on
/// values not available at compile time (e.g., per-session limits, database
/// records, user permissions).
///
/// Use `type Context = ()` for factories that need no runtime data — the
/// compiler optimises away the unit reference and the call site is identical.
/// Erased contextual entry stored in the registry.
///
/// Holds the pre-instantiated descriptors produced at registration time.
/// Re-registration replaces the entry entirely.
pub