Skip to main content

lc_core/runnables/
config.rs

1// src/core/runnables/config.rs
2//! Runnable execution configuration.
3
4use serde_json::Value;
5use std::collections::HashMap;
6use std::sync::Arc;
7use uuid::Uuid;
8
9use lc_callbacks::{CallbackManager, RunTree, RunType};
10
11use super::cancellation::CancellationToken;
12
13/// Reserved metadata key (UUID string): id of the parent run.
14///
15/// The enclosing chain/agent stamps it on the [`RunnableConfig`] handed to a
16/// planning LLM call so the provider-built [`RunTree`] joins the caller's trace
17/// tree instead of becoming a trace root. The key is reserved — downstream
18/// components strip it from the run's user-visible metadata.
19pub const RUN_META_PARENT_RUN_ID: &str = "__lc_parent_run_id";
20
21/// Reserved metadata key (UUID string): id of the trace root run.
22///
23/// See [`RUN_META_PARENT_RUN_ID`] for the linkage contract.
24pub const RUN_META_TRACE_ID: &str = "__lc_trace_id";
25
26/// Builds the [`RunTree`] a component dispatches its lifecycle callbacks
27/// against, applying the config every provider is expected to honor.
28///
29/// Behavior:
30///
31/// * tags and user metadata are copied from `config`;
32/// * the reserved keys [`RUN_META_PARENT_RUN_ID`] / [`RUN_META_TRACE_ID`] set
33///   the run's parent / trace linkage and are **not** copied into the run's
34///   own metadata (unparseable ids are ignored with a warning);
35/// * with no config the result is a plain root run.
36///
37/// Every chat provider builds its LLM run through this helper, so the
38/// observability contract is defined in one place.
39pub fn run_tree_from_config(
40    name: impl Into<String>,
41    run_type: RunType,
42    inputs: serde_json::Value,
43    config: Option<&RunnableConfig>,
44) -> RunTree {
45    let mut run = RunTree::new(name, run_type, inputs);
46    let Some(cfg) = config else {
47        return run;
48    };
49
50    for tag in &cfg.tags {
51        run = run.with_tag(tag.clone());
52    }
53    for (key, value) in &cfg.metadata {
54        if key == RUN_META_PARENT_RUN_ID || key == RUN_META_TRACE_ID {
55            continue;
56        }
57        run = run.with_metadata(key.clone(), value.clone());
58    }
59
60    if let Some(parent) = cfg
61        .metadata
62        .get(RUN_META_PARENT_RUN_ID)
63        .and_then(|v| v.as_str())
64    {
65        match Uuid::parse_str(parent) {
66            Ok(id) => run.parent_run_id = Some(id),
67            Err(_) => {
68                log::warn!("ignoring invalid {RUN_META_PARENT_RUN_ID} '{parent}' in RunnableConfig")
69            }
70        }
71    }
72    if let Some(trace) = cfg.metadata.get(RUN_META_TRACE_ID).and_then(|v| v.as_str()) {
73        match Uuid::parse_str(trace) {
74            Ok(id) => run.trace_id = Some(id),
75            Err(_) => {
76                log::warn!("ignoring invalid {RUN_META_TRACE_ID} '{trace}' in RunnableConfig")
77            }
78        }
79    }
80
81    run
82}
83
84/// Runnable execution configuration.
85#[derive(Debug, Clone, Default)]
86pub struct RunnableConfig {
87    /// Tags for filtering and tracking.
88    pub tags: Vec<String>,
89
90    /// Metadata - custom data (JSON serializable).
91    pub metadata: HashMap<String, Value>,
92
93    /// Max concurrency for batch operations.
94    pub max_concurrency: Option<usize>,
95
96    /// Run ID for tracking.
97    pub run_id: Option<Uuid>,
98
99    /// Run name for debugging.
100    pub run_name: Option<String>,
101
102    /// Callback manager for tracing and monitoring.
103    pub callbacks: Option<Arc<CallbackManager>>,
104
105    /// Cancellation token for aborting long-running operations.
106    pub cancellation_token: Option<CancellationToken>,
107
108    /// Sampling temperature override for the current call.
109    ///
110    /// When set, takes precedence over the model's own configured
111    /// temperature. This is how wrapper layers (e.g. `LLMClient`) make
112    /// `with_temperature` effective through a trait object (providers Q2).
113    pub temperature: Option<f32>,
114
115    /// Max-tokens override for the current call.
116    ///
117    /// When set, takes precedence over the model's own configured max
118    /// tokens (providers Q2).
119    pub max_tokens: Option<usize>,
120
121    /// Configurable values — the Rust counterpart of Python LCEL's
122    /// `config["configurable"]`.
123    ///
124    /// Runtime selection keys live here: `configurable_alternatives` reads
125    /// its `which` key from this map, and `RunnableWithMessageHistory`
126    /// (session mode) reads `session_id` from it.
127    pub configurable: HashMap<String, Value>,
128}
129
130impl RunnableConfig {
131    /// Creates an empty configuration.
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// Adds a tag.
137    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
138        self.tags.push(tag.into());
139        self
140    }
141
142    /// Adds metadata.
143    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
144        self.metadata.insert(key.into(), value);
145        self
146    }
147
148    /// Sets max concurrency.
149    pub fn with_max_concurrency(mut self, max: usize) -> Self {
150        self.max_concurrency = Some(max);
151        self
152    }
153
154    /// Sets run ID.
155    pub fn with_run_id(mut self, id: Uuid) -> Self {
156        self.run_id = Some(id);
157        self
158    }
159
160    /// Sets run name.
161    pub fn with_run_name(mut self, name: impl Into<String>) -> Self {
162        self.run_name = Some(name.into());
163        self
164    }
165
166    /// Sets callback manager.
167    pub fn with_callbacks(mut self, callbacks: Arc<CallbackManager>) -> Self {
168        self.callbacks = Some(callbacks);
169        self
170    }
171
172    /// Sets cancellation token.
173    pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self {
174        self.cancellation_token = Some(token);
175        self
176    }
177
178    /// Sets a sampling temperature override for the call.
179    pub fn with_temperature(mut self, temperature: f32) -> Self {
180        self.temperature = Some(temperature);
181        self
182    }
183
184    /// Sets a max-tokens override for the call.
185    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
186        self.max_tokens = Some(max_tokens);
187        self
188    }
189
190    /// Sets a configurable value — the Rust counterpart of Python LCEL's
191    /// `config["configurable"][key] = value`.
192    ///
193    /// # Example
194    ///
195    /// ```rust,ignore
196    /// // 给 session 记忆选槽 / 给 configurable_alternatives 路由
197    /// let config = RunnableConfig::new().with_configurable("session_id", json!("s1"));
198    /// ```
199    pub fn with_configurable(mut self, key: impl Into<String>, value: Value) -> Self {
200        self.configurable.insert(key.into(), value);
201        self
202    }
203
204    /// Reads a configurable value by key.
205    pub fn configurable_value(&self, key: &str) -> Option<&Value> {
206        self.configurable.get(key)
207    }
208
209    /// Checks if cancellation has been requested.
210    pub fn is_cancelled(&self) -> bool {
211        self.cancellation_token
212            .as_ref()
213            .is_some_and(|t| t.is_cancelled())
214    }
215
216    /// Merges two configurations (later overrides earlier).
217    pub fn merge(mut self, other: RunnableConfig) -> Self {
218        // Merge tags as an order-preserving union (dedup without sorting).
219        for tag in other.tags {
220            if !self.tags.iter().any(|existing| existing == &tag) {
221                self.tags.push(tag);
222            }
223        }
224
225        // Merge metadata (override)
226        self.metadata.extend(other.metadata);
227
228        // Merge configurable values (override)
229        self.configurable.extend(other.configurable);
230
231        // Override other fields
232        if other.max_concurrency.is_some() {
233            self.max_concurrency = other.max_concurrency;
234        }
235        if other.run_id.is_some() {
236            self.run_id = other.run_id;
237        }
238        if other.run_name.is_some() {
239            self.run_name = other.run_name;
240        }
241        if other.cancellation_token.is_some() {
242            self.cancellation_token = other.cancellation_token;
243        }
244        if other.temperature.is_some() {
245            self.temperature = other.temperature;
246        }
247        if other.max_tokens.is_some() {
248            self.max_tokens = other.max_tokens;
249        }
250
251        // Merge callbacks: append `other`'s handlers instead of replacing the
252        // whole manager (Q8), so observers configured at different layers of
253        // the pipeline all keep firing.
254        if let (Some(self_cb), Some(other_cb)) = (&self.callbacks, &other.callbacks) {
255            self.callbacks = Some(Arc::new(self_cb.merge_with(other_cb)));
256        } else if other.callbacks.is_some() {
257            self.callbacks = other.callbacks;
258        }
259
260        self
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use serde_json::json;
268
269    #[test]
270    fn configurable_roundtrip() {
271        let cfg = RunnableConfig::new().with_configurable("session_id", json!("s1"));
272        assert_eq!(cfg.configurable_value("session_id"), Some(&json!("s1")));
273        assert_eq!(cfg.configurable_value("missing"), None);
274    }
275
276    #[test]
277    fn configurable_merge_overrides() {
278        let base = RunnableConfig::new().with_configurable("which", json!("a"));
279        let other = RunnableConfig::new().with_configurable("which", json!("b"));
280        let merged = base.merge(other);
281        assert_eq!(merged.configurable_value("which"), Some(&json!("b")));
282        // 不存在的键不进 merge 结果
283        assert_eq!(merged.configurable_value("none"), None);
284    }
285}