Skip to main content

adk_core/
context.rs

1use crate::identity::{AdkIdentity, AppName, ExecutionIdentity, InvocationId, SessionId, UserId};
2use crate::{AdkError, Agent, Result, Toolset, types::Content};
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::{BTreeSet, HashMap};
7use std::sync::Arc;
8
9/// Policy for handling excess tool calls when the concurrency limit is reached.
10///
11/// Determines whether tool calls that exceed the configured concurrency limit
12/// should wait in a queue or fail immediately.
13///
14/// # Example
15///
16/// ```rust
17/// use adk_core::BackpressurePolicy;
18///
19/// // Default is Queue
20/// let policy = BackpressurePolicy::default();
21/// assert!(matches!(policy, BackpressurePolicy::Queue));
22/// ```
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub enum BackpressurePolicy {
25    /// Queue excess calls until a permit becomes available.
26    ///
27    /// This is the default policy. Tool calls will await until a semaphore
28    /// permit is released by a completing tool execution.
29    #[default]
30    Queue,
31
32    /// Fail immediately with a concurrency limit error when no permit is available.
33    ///
34    /// Use this when latency is more important than throughput — callers receive
35    /// an immediate error rather than waiting indefinitely.
36    Fail,
37}
38
39/// Configuration for tool execution concurrency.
40///
41/// Controls how many tool calls can execute simultaneously, with support for
42/// global limits, per-tool overrides, and configurable backpressure behavior.
43///
44/// # Example
45///
46/// ```rust
47/// use adk_core::{BackpressurePolicy, ToolConcurrencyConfig};
48/// use std::collections::HashMap;
49///
50/// let config = ToolConcurrencyConfig {
51///     max_concurrency: Some(10),
52///     per_tool: HashMap::from([
53///         ("web_scraper".to_string(), 2),
54///         ("calculator".to_string(), 8),
55///     ]),
56///     backpressure: BackpressurePolicy::Fail,
57/// };
58///
59/// assert_eq!(config.max_concurrency, Some(10));
60/// assert_eq!(config.per_tool.get("web_scraper"), Some(&2));
61/// ```
62#[derive(Debug, Clone, Default)]
63pub struct ToolConcurrencyConfig {
64    /// Global maximum concurrent tool calls. `None` means unlimited.
65    pub max_concurrency: Option<usize>,
66
67    /// Per-tool concurrency overrides. When a tool name is present in this map,
68    /// its individual limit takes precedence over the global `max_concurrency`.
69    pub per_tool: HashMap<String, usize>,
70
71    /// What to do when the concurrency limit is reached.
72    pub backpressure: BackpressurePolicy,
73}
74
75/// Read-only access to invocation metadata.
76///
77/// Provides identity information (user, app, session, invocation) and the
78/// current user content. Implemented by all context types.
79#[async_trait]
80pub trait ReadonlyContext: Send + Sync {
81    /// Returns the current invocation identifier.
82    fn invocation_id(&self) -> &str;
83    /// Returns the name of the currently executing agent.
84    fn agent_name(&self) -> &str;
85    /// Returns the user identifier for this session.
86    fn user_id(&self) -> &str;
87    /// Returns the application name for this session.
88    fn app_name(&self) -> &str;
89    /// Returns the session identifier.
90    fn session_id(&self) -> &str;
91    /// Returns the current conversation branch.
92    fn branch(&self) -> &str;
93    /// Returns the user's input content for this invocation.
94    fn user_content(&self) -> &Content;
95
96    /// Returns the application name as a typed [`AppName`].
97    ///
98    /// Parses the value returned by [`app_name()`](Self::app_name). Returns an
99    /// error if the raw string fails validation (empty, null bytes, or exceeds
100    /// the maximum length).
101    ///
102    /// # Errors
103    ///
104    /// Returns an error when the
105    /// underlying string is not a valid identifier.
106    fn try_app_name(&self) -> Result<AppName> {
107        Ok(AppName::try_from(self.app_name())?)
108    }
109
110    /// Returns the user identifier as a typed [`UserId`].
111    ///
112    /// Parses the value returned by [`user_id()`](Self::user_id). Returns an
113    /// error if the raw string fails validation.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error when the
118    /// underlying string is not a valid identifier.
119    fn try_user_id(&self) -> Result<UserId> {
120        Ok(UserId::try_from(self.user_id())?)
121    }
122
123    /// Returns the session identifier as a typed [`SessionId`].
124    ///
125    /// Parses the value returned by [`session_id()`](Self::session_id).
126    /// Returns an error if the raw string fails validation.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error when the
131    /// underlying string is not a valid identifier.
132    fn try_session_id(&self) -> Result<SessionId> {
133        Ok(SessionId::try_from(self.session_id())?)
134    }
135
136    /// Returns the invocation identifier as a typed [`InvocationId`].
137    ///
138    /// Parses the value returned by [`invocation_id()`](Self::invocation_id).
139    /// Returns an error if the raw string fails validation.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error when the
144    /// underlying string is not a valid identifier.
145    fn try_invocation_id(&self) -> Result<InvocationId> {
146        Ok(InvocationId::try_from(self.invocation_id())?)
147    }
148
149    /// Returns the stable session-scoped [`AdkIdentity`] triple.
150    ///
151    /// Combines [`try_app_name()`](Self::try_app_name),
152    /// [`try_user_id()`](Self::try_user_id), and
153    /// [`try_session_id()`](Self::try_session_id) into a single composite
154    /// identity value.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if any of the three constituent identifiers fail
159    /// validation.
160    fn try_identity(&self) -> Result<AdkIdentity> {
161        Ok(AdkIdentity {
162            app_name: self.try_app_name()?,
163            user_id: self.try_user_id()?,
164            session_id: self.try_session_id()?,
165        })
166    }
167
168    /// Returns the full per-invocation [`ExecutionIdentity`].
169    ///
170    /// Combines [`try_identity()`](Self::try_identity) with the invocation,
171    /// branch, and agent name from this context.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if any of the four typed identifiers fail validation.
176    fn try_execution_identity(&self) -> Result<ExecutionIdentity> {
177        Ok(ExecutionIdentity {
178            adk: self.try_identity()?,
179            invocation_id: self.try_invocation_id()?,
180            branch: self.branch().to_string(),
181            agent_name: self.agent_name().to_string(),
182        })
183    }
184}
185
186// State management traits
187
188/// Maximum allowed length for state keys (256 bytes).
189pub const MAX_STATE_KEY_LEN: usize = 256;
190
191/// Validates a state key. Returns `Ok(())` if the key is safe, or an error message.
192///
193/// Rules:
194/// - Must not be empty
195/// - Must not exceed [`MAX_STATE_KEY_LEN`] bytes
196/// - Must not contain path separators (`/`, `\`) or `..`
197/// - Must not contain null bytes
198pub fn validate_state_key(key: &str) -> std::result::Result<(), &'static str> {
199    if key.is_empty() {
200        return Err("state key must not be empty");
201    }
202    if key.len() > MAX_STATE_KEY_LEN {
203        return Err("state key exceeds maximum length of 256 bytes");
204    }
205    if key.contains('/') || key.contains('\\') || key.contains("..") {
206        return Err("state key must not contain path separators or '..'");
207    }
208    if key.contains('\0') {
209        return Err("state key must not contain null bytes");
210    }
211    Ok(())
212}
213
214/// Mutable session state with key-value storage.
215///
216/// Implementations persist state across turns within a session.
217pub trait State: Send + Sync {
218    /// Returns the value for the given key, or `None` if not present.
219    fn get(&self, key: &str) -> Option<Value>;
220    /// Set a state value. Implementations should call [`validate_state_key`] and
221    /// reject invalid keys (e.g., by logging a warning or panicking).
222    fn set(&mut self, key: String, value: Value);
223    /// Returns all key-value pairs in the state.
224    fn all(&self) -> HashMap<String, Value>;
225}
226
227/// Read-only view of session state.
228pub trait ReadonlyState: Send + Sync {
229    /// Returns the value for the given key, or `None` if not present.
230    fn get(&self, key: &str) -> Option<Value>;
231    /// Returns all key-value pairs in the state.
232    fn all(&self) -> HashMap<String, Value>;
233}
234
235// Session trait
236/// Represents an active conversation session with identity and state.
237pub trait Session: Send + Sync {
238    /// Returns the session identifier.
239    fn id(&self) -> &str;
240    /// Returns the application name this session belongs to.
241    fn app_name(&self) -> &str;
242    /// Returns the user identifier for this session.
243    fn user_id(&self) -> &str;
244    /// Returns the mutable state associated with this session.
245    fn state(&self) -> &dyn State;
246    /// Returns the conversation history from this session as Content items
247    fn conversation_history(&self) -> Vec<Content>;
248    /// Returns conversation history filtered for a specific agent.
249    ///
250    /// When provided, events authored by other agents (not "user", not the
251    /// named agent, and not function/tool responses) are excluded. This
252    /// prevents a transferred sub-agent from seeing the parent's tool calls
253    /// mapped as "model" role, which would cause the LLM to think work is
254    /// already done.
255    ///
256    /// Default implementation delegates to [`conversation_history`](Self::conversation_history).
257    fn conversation_history_for_agent(&self, _agent_name: &str) -> Vec<Content> {
258        self.conversation_history()
259    }
260    /// Returns conversation history scoped to an agent and a conversation branch.
261    ///
262    /// `branch` is the invocation branch of the agent asking for history. An
263    /// event is visible when its branch equals that branch or is an *ancestor*
264    /// of it, so a sub-agent sees the conversation that led to it but not what
265    /// its siblings produced. `ParallelAgent` relies on this to keep concurrent
266    /// branches from contaminating each other's context, mirroring ADK Python's
267    /// `_is_event_belongs_to_branch` and ADK Go's `eventBelongsToBranch`.
268    ///
269    /// An empty `branch` on either side means "unscoped" and matches everything,
270    /// so implementations that never set [`crate::Event::branch`] are unaffected.
271    ///
272    /// Default implementation ignores `branch` and preserves the agent-name
273    /// filtering behaviour, so existing [`Session`] implementations keep working.
274    fn conversation_history_scoped(&self, agent_name: Option<&str>, _branch: &str) -> Vec<Content> {
275        match agent_name {
276            Some(name) => self.conversation_history_for_agent(name),
277            None => self.conversation_history(),
278        }
279    }
280    /// Append content to conversation history (for sequential agent support)
281    fn append_to_history(&self, _content: Content) {
282        // Default no-op - implementations can override to track history
283    }
284
285    /// Returns the application name as a typed [`AppName`].
286    ///
287    /// Parses the value returned by [`app_name()`](Self::app_name). Returns an
288    /// error if the raw string fails validation (empty, null bytes, or exceeds
289    /// the maximum length).
290    ///
291    /// # Errors
292    ///
293    /// Returns an error when the
294    /// underlying string is not a valid identifier.
295    fn try_app_name(&self) -> Result<AppName> {
296        Ok(AppName::try_from(self.app_name())?)
297    }
298
299    /// Returns the user identifier as a typed [`UserId`].
300    ///
301    /// Parses the value returned by [`user_id()`](Self::user_id). Returns an
302    /// error if the raw string fails validation.
303    ///
304    /// # Errors
305    ///
306    /// Returns an error when the
307    /// underlying string is not a valid identifier.
308    fn try_user_id(&self) -> Result<UserId> {
309        Ok(UserId::try_from(self.user_id())?)
310    }
311
312    /// Returns the session identifier as a typed [`SessionId`].
313    ///
314    /// Parses the value returned by [`id()`](Self::id). Returns an error if
315    /// the raw string fails validation.
316    ///
317    /// # Errors
318    ///
319    /// Returns an error when the
320    /// underlying string is not a valid identifier.
321    fn try_session_id(&self) -> Result<SessionId> {
322        Ok(SessionId::try_from(self.id())?)
323    }
324
325    /// Returns the stable session-scoped [`AdkIdentity`] triple.
326    ///
327    /// Combines [`try_app_name()`](Self::try_app_name),
328    /// [`try_user_id()`](Self::try_user_id), and
329    /// [`try_session_id()`](Self::try_session_id) into a single composite
330    /// identity value.
331    ///
332    /// # Errors
333    ///
334    /// Returns an error if any of the three constituent identifiers fail
335    /// validation.
336    fn try_identity(&self) -> Result<AdkIdentity> {
337        Ok(AdkIdentity {
338            app_name: self.try_app_name()?,
339            user_id: self.try_user_id()?,
340            session_id: self.try_session_id()?,
341        })
342    }
343}
344
345/// Structured metadata about a completed tool execution.
346///
347/// Available via [`CallbackContext::tool_outcome()`] in after-tool callbacks,
348/// plugins, and telemetry hooks. Provides structured access to execution
349/// results without requiring JSON error parsing.
350///
351/// # Fields
352///
353/// - `tool_name` — Name of the tool that was executed.
354/// - `tool_args` — Arguments passed to the tool as a JSON value.
355/// - `success` — Whether the tool execution succeeded. Derived from the
356///   Rust `Result` / timeout path, never from JSON content inspection.
357/// - `duration` — Wall-clock duration of the tool execution.
358/// - `error_message` — Error message if the tool failed; `None` on success.
359/// - `attempt` — Retry attempt number (0 = first attempt, 1 = first retry, etc.).
360///   Always 0 when retries are not configured.
361#[derive(Debug, Clone)]
362pub struct ToolOutcome {
363    /// Name of the tool that was executed.
364    pub tool_name: String,
365    /// Arguments passed to the tool (JSON value).
366    pub tool_args: serde_json::Value,
367    /// Whether the tool execution succeeded.
368    pub success: bool,
369    /// Wall-clock duration of the tool execution.
370    pub duration: std::time::Duration,
371    /// Error message if the tool failed. `None` on success.
372    pub error_message: Option<String>,
373    /// Retry attempt number (0 = first attempt, 1 = first retry, etc.).
374    /// Always 0 when retries are not configured.
375    pub attempt: u32,
376}
377
378/// Context available to agent lifecycle callbacks.
379///
380/// Extends [`ReadonlyContext`] with access to artifacts and tool execution metadata.
381#[async_trait]
382pub trait CallbackContext: ReadonlyContext {
383    /// Returns the artifact store, if one is configured.
384    fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
385
386    /// Returns structured metadata about the most recent tool execution.
387    /// Available in after-tool callbacks and plugin hooks.
388    /// Returns `None` when not in a tool execution context.
389    fn tool_outcome(&self) -> Option<ToolOutcome> {
390        None // default for backward compatibility
391    }
392
393    /// Returns the name of the tool about to be executed.
394    /// Available in before-tool and after-tool callback contexts.
395    fn tool_name(&self) -> Option<&str> {
396        None
397    }
398
399    /// Returns the input arguments for the tool about to be executed.
400    /// Available in before-tool and after-tool callback contexts.
401    fn tool_input(&self) -> Option<&serde_json::Value> {
402        None
403    }
404
405    /// Returns the shared state for parallel agent coordination.
406    /// Returns `None` when not running inside a `ParallelAgent` with shared state enabled.
407    fn shared_state(&self) -> Option<Arc<crate::SharedState>> {
408        None
409    }
410}
411
412/// Wraps a [`CallbackContext`] to inject tool name and input for before-tool
413/// and after-tool callbacks.
414///
415/// Used by the agent runtime to provide tool context to `BeforeToolCallback`
416/// and `AfterToolCallback` invocations.
417///
418/// # Example
419///
420/// ```rust,ignore
421/// let tool_ctx = Arc::new(ToolCallbackContext::new(
422///     ctx.clone(),
423///     "search".to_string(),
424///     serde_json::json!({"query": "hello"}),
425/// ));
426/// callback(tool_ctx as Arc<dyn CallbackContext>).await;
427/// ```
428pub struct ToolCallbackContext {
429    /// The inner callback context to delegate to.
430    pub inner: Arc<dyn CallbackContext>,
431    /// The name of the tool being executed.
432    pub tool_name: String,
433    /// The input arguments for the tool being executed.
434    pub tool_input: serde_json::Value,
435}
436
437impl ToolCallbackContext {
438    /// Creates a new `ToolCallbackContext` wrapping the given inner context.
439    pub fn new(
440        inner: Arc<dyn CallbackContext>,
441        tool_name: String,
442        tool_input: serde_json::Value,
443    ) -> Self {
444        Self { inner, tool_name, tool_input }
445    }
446}
447
448#[async_trait]
449impl ReadonlyContext for ToolCallbackContext {
450    fn invocation_id(&self) -> &str {
451        self.inner.invocation_id()
452    }
453
454    fn agent_name(&self) -> &str {
455        self.inner.agent_name()
456    }
457
458    fn user_id(&self) -> &str {
459        self.inner.user_id()
460    }
461
462    fn app_name(&self) -> &str {
463        self.inner.app_name()
464    }
465
466    fn session_id(&self) -> &str {
467        self.inner.session_id()
468    }
469
470    fn branch(&self) -> &str {
471        self.inner.branch()
472    }
473
474    fn user_content(&self) -> &Content {
475        self.inner.user_content()
476    }
477}
478
479#[async_trait]
480impl CallbackContext for ToolCallbackContext {
481    fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
482        self.inner.artifacts()
483    }
484
485    fn tool_outcome(&self) -> Option<ToolOutcome> {
486        self.inner.tool_outcome()
487    }
488
489    fn tool_name(&self) -> Option<&str> {
490        Some(&self.tool_name)
491    }
492
493    fn tool_input(&self) -> Option<&serde_json::Value> {
494        Some(&self.tool_input)
495    }
496
497    fn shared_state(&self) -> Option<Arc<crate::SharedState>> {
498        self.inner.shared_state()
499    }
500}
501
502/// Full invocation context available to agents during execution.
503///
504/// Extends [`CallbackContext`] with access to the agent itself, memory,
505/// session, and run configuration.
506#[async_trait]
507pub trait InvocationContext: CallbackContext {
508    /// Returns the agent being executed.
509    fn agent(&self) -> Arc<dyn Agent>;
510    /// Returns the memory service, if one is configured.
511    fn memory(&self) -> Option<Arc<dyn Memory>>;
512    /// Returns the current session.
513    fn session(&self) -> &dyn Session;
514    /// Returns the run configuration for this invocation.
515    fn run_config(&self) -> &RunConfig;
516    /// Signals that this invocation should end after the current turn.
517    fn end_invocation(&self);
518    /// Returns whether the invocation has been ended.
519    fn ended(&self) -> bool;
520
521    /// Returns `true` if this invocation has been cancelled.
522    ///
523    /// Agents and tools can poll this during long-running work (LLM streaming,
524    /// HTTP I/O, tool execution) to detect an external cancellation request —
525    /// for example, a user pressing "Stop" or a call to
526    /// [`Runner::interrupt`](https://docs.rs/adk-runner). Checking it at chunk
527    /// or tool boundaries lets an agent exit promptly and perform any graceful
528    /// cleanup instead of running to natural completion.
529    ///
530    /// The default returns `false`. The runtime sets the underlying token when
531    /// `Runner::interrupt()` is called or `RunConfig::cancellation_token` fires.
532    fn is_cancelled(&self) -> bool {
533        false
534    }
535
536    /// Returns the scopes granted to the current user for this invocation.
537    ///
538    /// When a [`RequestContext`](crate::RequestContext) is present (set by the
539    /// server's auth middleware bridge), this returns the scopes from that
540    /// context. The default returns an empty vec (no scopes granted).
541    fn user_scopes(&self) -> Vec<String> {
542        vec![]
543    }
544
545    /// Returns the request metadata from the auth middleware bridge, if present.
546    ///
547    /// This provides access to custom key-value pairs extracted from the HTTP
548    /// request by the [`RequestContextExtractor`](crate::RequestContext).
549    fn request_metadata(&self) -> HashMap<String, serde_json::Value> {
550        HashMap::new()
551    }
552
553    /// Whether the run's transfer target list replaces static sub-agent targets.
554    fn authoritative_transfer_targets(&self) -> bool {
555        false
556    }
557
558    /// Current nested agent-as-tool delegation depth.
559    fn delegation_depth(&self) -> u32 {
560        0
561    }
562
563    /// Maximum nested agent-as-tool delegation depth.
564    fn max_delegation_depth(&self) -> Option<u32> {
565        None
566    }
567
568    /// Returns the root invocation that owns this orchestration tree.
569    ///
570    /// Nested agent runs use this stable identifier to aggregate budgets,
571    /// traces, and execution receipts without conflating unrelated runs.
572    fn orchestration_root_invocation_id(&self) -> &str {
573        self.invocation_id()
574    }
575
576    /// Returns the causal relationship execution that started this run.
577    fn orchestration_edge_id(&self) -> Option<&str> {
578        None
579    }
580
581    /// Returns whether a runtime-injected tool requires confirmation.
582    ///
583    /// This additive hook lets composition layers protect relationship tools
584    /// without mutating the concrete agent that receives them.
585    fn requires_tool_confirmation(&self, _tool_name: &str) -> bool {
586        false
587    }
588
589    /// Retrieve a secret by name from the configured secret provider.
590    ///
591    /// Returns `Ok(Some(value))` when a provider is configured and the secret
592    /// exists, `Ok(None)` when no provider is configured, or an error on
593    /// provider failure. The default returns `Ok(None)`.
594    async fn get_secret(&self, _name: &str) -> Result<Option<String>> {
595        Ok(None)
596    }
597
598    /// Resolves a secret for a described access.
599    ///
600    /// A wrapper context must forward this, and a tool context builds the request from
601    /// the identity the framework gave it. The default drops the description and calls
602    /// [`InvocationContext::get_secret`], which keeps a context that predates the
603    /// request object working.
604    async fn get_secret_for(&self, request: &SecretRequest) -> Result<Option<String>> {
605        self.get_secret(&request.name).await
606    }
607}
608
609// Placeholder service traits
610/// Binary artifact storage for agents.
611#[async_trait]
612pub trait Artifacts: Send + Sync {
613    /// Saves a binary artifact and returns its version number.
614    async fn save(&self, name: &str, data: &crate::Part) -> Result<i64>;
615    /// Loads a binary artifact by name.
616    async fn load(&self, name: &str) -> Result<crate::Part>;
617    /// Lists all artifact names.
618    async fn list(&self) -> Result<Vec<String>>;
619}
620
621/// Semantic memory search for agents.
622#[async_trait]
623pub trait Memory: Send + Sync {
624    /// Searches memory for entries matching the query.
625    async fn search(&self, query: &str) -> Result<Vec<MemoryEntry>>;
626
627    /// Verify backend connectivity.
628    ///
629    /// The default implementation succeeds, which is suitable for in-memory
630    /// implementations and adapters without an external dependency.
631    async fn health_check(&self) -> Result<()> {
632        Ok(())
633    }
634
635    /// Add a single memory entry.
636    ///
637    /// The default implementation returns an "not implemented" error, which is
638    /// suitable for read-only memory backends.
639    async fn add(&self, entry: MemoryEntry) -> Result<()> {
640        let _ = entry;
641        Err(AdkError::memory("add not implemented"))
642    }
643
644    /// Delete entries matching a query. Returns count of deleted entries.
645    ///
646    /// The default implementation returns an "not implemented" error, which is
647    /// suitable for read-only memory backends.
648    async fn delete(&self, query: &str) -> Result<u64> {
649        let _ = query;
650        Err(AdkError::memory("delete not implemented"))
651    }
652
653    /// Whether this memory keeps project-scoped entries isolated.
654    ///
655    /// Returns `false` by default, so a caller can tell real isolation apart from a
656    /// memory that has no project support instead of inferring it from data.
657    fn supports_project_scoping(&self) -> bool {
658        false
659    }
660
661    /// Searches memories within a specific project.
662    ///
663    /// # Errors
664    ///
665    /// The default implementation returns an error. Delegating to the global search
666    /// would return entries the project boundary is meant to exclude, and nothing in
667    /// the result would say the boundary was ignored.
668    async fn search_in_project(&self, query: &str, project_id: &str) -> Result<Vec<MemoryEntry>> {
669        let _ = (query, project_id);
670        Err(AdkError::memory(
671            "this memory does not implement project scoping, so `search_in_project` cannot \
672             honour the project boundary; check `supports_project_scoping` first, or call \
673             `search` if global scope is intended",
674        ))
675    }
676
677    /// Adds a memory entry scoped to a specific project.
678    ///
679    /// # Errors
680    ///
681    /// Returns an error by default. Writing the entry globally would make data
682    /// intended for one project visible everywhere under the same app and user.
683    async fn add_to_project(&self, entry: MemoryEntry, project_id: &str) -> Result<()> {
684        let _ = (entry, project_id);
685        Err(AdkError::memory(
686            "this memory does not implement project scoping, so `add_to_project` cannot honour \
687             the project boundary; check `supports_project_scoping` first, or call `add` if \
688             global scope is intended",
689        ))
690    }
691}
692
693/// Trait for retrieving secrets at runtime.
694///
695/// This is the core-level abstraction used by `ToolContext::get_secret` and
696/// `InvocationContext::get_secret`. Concrete implementations (e.g., AWS
697/// Secrets Manager, Azure Key Vault, GCP Secret Manager) live in `adk-auth`
698/// behind feature flags and implement this trait via the `SecretProvider`
699/// adapter.
700///
701/// # Example
702///
703/// ```rust,ignore
704/// use adk_core::SecretService;
705///
706/// struct EnvSecretService;
707///
708/// #[async_trait::async_trait]
709/// impl SecretService for EnvSecretService {
710///     async fn get_secret(&self, name: &str) -> adk_core::Result<String> {
711///         std::env::var(name).map_err(|_| adk_core::AdkError::not_found(
712///             format!("secret '{name}' not found in environment"),
713///         ))
714///     }
715/// }
716/// ```
717#[async_trait]
718pub trait SecretService: Send + Sync {
719    /// Retrieve a secret value by name.
720    ///
721    /// Returns the secret string on success, or an [`AdkError`] on failure.
722    async fn get_secret(&self, name: &str) -> Result<String>;
723
724    /// Retrieve a secret for a described access.
725    ///
726    /// This is the form an authorizing service implements: the request carries who is
727    /// asking and why, so a decision can be made before the value is fetched. The
728    /// default implementation ignores the context and calls
729    /// [`SecretService::get_secret`], which is correct for a service that has no
730    /// policy of its own.
731    ///
732    /// Every field on [`SecretRequest`] is set by the framework at the call site, not
733    /// supplied by the tool, so a tool cannot present another tool's identity.
734    async fn get_secret_for(&self, request: &SecretRequest) -> Result<String> {
735        self.get_secret(&request.name).await
736    }
737}
738
739/// A described secret access.
740///
741/// Carries the requested name plus the identity the framework observed at the call
742/// site, so a [`SecretService`] can authorize and audit rather than being handed a
743/// bare name with no context.
744///
745/// # Example
746///
747/// ```rust
748/// use adk_core::SecretRequest;
749///
750/// let request = SecretRequest::new("payments-api-key")
751///     .with_tool_name("charge_card")
752///     .with_purpose("authorize a customer payment");
753///
754/// assert_eq!(request.tool_name.as_deref(), Some("charge_card"));
755/// ```
756#[derive(Debug, Clone, Default, PartialEq, Eq)]
757pub struct SecretRequest {
758    /// Name of the requested secret.
759    pub name: String,
760    /// The tool making the request, when the access came from a tool.
761    ///
762    /// Set by the framework from the tool it dispatched, never from a value the tool
763    /// provided.
764    pub tool_name: Option<String>,
765    /// Application the run belongs to.
766    pub app_name: Option<String>,
767    /// Authenticated user the run belongs to.
768    pub user_id: Option<String>,
769    /// Session the run belongs to.
770    pub session_id: Option<String>,
771    /// Invocation the access happened in, for correlating audit records.
772    pub invocation_id: Option<String>,
773    /// Why the secret is needed, when the caller states it.
774    pub purpose: Option<String>,
775}
776
777impl SecretRequest {
778    /// Creates a request for `name` with no identity attached.
779    pub fn new(name: impl Into<String>) -> Self {
780        Self { name: name.into(), ..Default::default() }
781    }
782
783    /// Attaches the requesting tool's name.
784    #[must_use]
785    pub fn with_tool_name(mut self, tool_name: impl Into<String>) -> Self {
786        self.tool_name = Some(tool_name.into());
787        self
788    }
789
790    /// Attaches the run's identity.
791    #[must_use]
792    pub fn with_identity(
793        mut self,
794        app_name: impl Into<String>,
795        user_id: impl Into<String>,
796        session_id: impl Into<String>,
797    ) -> Self {
798        self.app_name = Some(app_name.into());
799        self.user_id = Some(user_id.into());
800        self.session_id = Some(session_id.into());
801        self
802    }
803
804    /// Attaches the invocation the access happened in.
805    #[must_use]
806    pub fn with_invocation_id(mut self, invocation_id: impl Into<String>) -> Self {
807        self.invocation_id = Some(invocation_id.into());
808        self
809    }
810
811    /// Attaches a stated purpose.
812    #[must_use]
813    pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
814        self.purpose = Some(purpose.into());
815        self
816    }
817}
818
819/// A single entry returned from memory search.
820#[derive(Debug, Clone)]
821pub struct MemoryEntry {
822    /// The content of this memory entry.
823    pub content: Content,
824    /// The author who created this memory entry.
825    pub author: String,
826}
827
828/// Streaming mode for agent responses.
829/// Matches ADK Python/Go specification.
830#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
831pub enum StreamingMode {
832    /// No streaming; responses delivered as complete units.
833    /// Agent collects all chunks internally and yields a single final event.
834    None,
835    /// Server-Sent Events streaming; one-way streaming from server to client.
836    /// Agent yields each chunk as it arrives with stable event ID.
837    #[default]
838    SSE,
839    /// Bidirectional streaming; simultaneous communication in both directions.
840    /// Used for realtime audio/video agents.
841    Bidi,
842}
843
844/// Controls what parts of prior conversation history is received by llmagent
845#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
846pub enum IncludeContents {
847    /// The llmagent operates solely on its current turn (latest user input + any following agent events)
848    None,
849    /// Default - The llmagent receives the relevant conversation history
850    #[default]
851    Default,
852}
853
854/// Decision applied when a tool execution requires human confirmation.
855#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
856#[serde(rename_all = "snake_case")]
857pub enum ToolConfirmationDecision {
858    /// Approve the tool execution.
859    Approve,
860    /// Deny the tool execution.
861    Deny,
862}
863
864/// Produces a canonical fingerprint of a tool call.
865///
866/// The fingerprint is the tool name followed by its arguments in canonical JSON
867/// form, with object keys sorted at every level so that two structurally equal
868/// argument sets always produce the same string. It is deliberately readable
869/// rather than hashed, so a mismatch can be diagnosed by inspection.
870///
871/// Use it with
872/// [`RunConfig::tool_confirmation_fingerprints`](RunConfig::tool_confirmation_fingerprints)
873/// to bind an approval to the exact arguments it was granted for.
874///
875/// # Example
876///
877/// ```rust
878/// use adk_core::tool_call_fingerprint;
879/// use serde_json::json;
880///
881/// // Key order does not change the fingerprint.
882/// let a = tool_call_fingerprint("delete_file", &json!({ "path": "/tmp/a", "force": true }));
883/// let b = tool_call_fingerprint("delete_file", &json!({ "force": true, "path": "/tmp/a" }));
884/// assert_eq!(a, b);
885///
886/// // A different path does not.
887/// let c = tool_call_fingerprint("delete_file", &json!({ "path": "/etc/passwd", "force": true }));
888/// assert_ne!(a, c);
889/// ```
890pub fn tool_call_fingerprint(tool_name: &str, args: &Value) -> String {
891    let mut out = String::with_capacity(tool_name.len() + 32);
892    out.push_str(tool_name);
893    out.push('\u{1f}');
894    write_canonical(args, &mut out);
895    out
896}
897
898/// Writes `value` as canonical JSON, with object keys sorted at every level.
899fn write_canonical(value: &Value, out: &mut String) {
900    match value {
901        Value::Object(map) => {
902            let mut keys: Vec<&String> = map.keys().collect();
903            keys.sort();
904            out.push('{');
905            for (i, key) in keys.iter().enumerate() {
906                if i > 0 {
907                    out.push(',');
908                }
909                out.push_str(&Value::String((*key).clone()).to_string());
910                out.push(':');
911                write_canonical(&map[*key], out);
912            }
913            out.push('}');
914        }
915        Value::Array(items) => {
916            out.push('[');
917            for (i, item) in items.iter().enumerate() {
918                if i > 0 {
919                    out.push(',');
920                }
921                write_canonical(item, out);
922            }
923            out.push(']');
924        }
925        other => out.push_str(&other.to_string()),
926    }
927}
928
929/// Policy defining which tools require human confirmation before execution.
930#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
931#[serde(rename_all = "snake_case")]
932pub enum ToolConfirmationPolicy {
933    /// No tool confirmation is required.
934    #[default]
935    Never,
936    /// Every tool call requires confirmation.
937    Always,
938    /// Only the listed tool names require confirmation.
939    PerTool(BTreeSet<String>),
940}
941
942impl ToolConfirmationPolicy {
943    /// Returns true when the given tool name must be confirmed before execution.
944    pub fn requires_confirmation(&self, tool_name: &str) -> bool {
945        match self {
946            Self::Never => false,
947            Self::Always => true,
948            Self::PerTool(tools) => tools.contains(tool_name),
949        }
950    }
951
952    /// Add one tool name to the confirmation policy (converts `Never` to `PerTool`).
953    pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
954        let tool_name = tool_name.into();
955        match &mut self {
956            Self::Never => {
957                let mut tools = BTreeSet::new();
958                tools.insert(tool_name);
959                Self::PerTool(tools)
960            }
961            Self::Always => Self::Always,
962            Self::PerTool(tools) => {
963                tools.insert(tool_name);
964                self
965            }
966        }
967    }
968}
969
970/// Payload describing a tool call awaiting human confirmation.
971#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
972#[serde(rename_all = "camelCase")]
973pub struct ToolConfirmationRequest {
974    /// Name of the tool awaiting confirmation.
975    pub tool_name: String,
976    /// The function call ID from the LLM, if available.
977    #[serde(skip_serializing_if = "Option::is_none")]
978    pub function_call_id: Option<String>,
979    /// Arguments the tool would be called with.
980    pub args: Value,
981}
982
983/// Asynchronous decision source for tool calls that require confirmation.
984///
985/// Front ends and protocol adapters can implement this trait to pause an
986/// invocation while a person or an external policy service reviews the exact
987/// tool call. When no handler is configured, agents preserve the existing
988/// behavior and emit an interrupted confirmation event for a later run.
989#[async_trait]
990pub trait ToolConfirmationHandler: std::fmt::Debug + Send + Sync {
991    /// Approve or deny one pending tool call.
992    async fn decide(&self, request: &ToolConfirmationRequest) -> Result<ToolConfirmationDecision>;
993}
994
995/// A toolset attached to one runner invocation rather than compiled into the
996/// agent definition.
997///
998/// Protocol adapters use this wrapper for session-scoped capabilities such as
999/// MCP servers supplied by an ACP client. The wrapper keeps [`RunConfig`]
1000/// debuggable without requiring every toolset implementation to implement
1001/// [`std::fmt::Debug`].
1002#[derive(Clone)]
1003pub struct RuntimeToolset(Arc<dyn Toolset>);
1004
1005impl RuntimeToolset {
1006    /// Wrap a toolset for use during one runner invocation.
1007    pub fn new(toolset: Arc<dyn Toolset>) -> Self {
1008        Self(toolset)
1009    }
1010
1011    /// Borrow the wrapped toolset.
1012    pub fn toolset(&self) -> &Arc<dyn Toolset> {
1013        &self.0
1014    }
1015}
1016
1017impl std::fmt::Debug for RuntimeToolset {
1018    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1019        formatter.debug_tuple("RuntimeToolset").field(&self.0.name()).finish()
1020    }
1021}
1022
1023/// Configuration for a single agent run.
1024///
1025/// Controls streaming behavior, tool confirmation, caching, transfer targets,
1026/// and concurrency settings. Use [`RunConfig::builder()`] to construct from
1027#[derive(Debug, Clone)]
1028pub struct RunConfig {
1029    /// The streaming mode for agent responses.
1030    pub streaming_mode: StreamingMode,
1031    /// Static confirmation decisions for the current run, keyed by **function
1032    /// call ID**.
1033    ///
1034    /// The ID is the one reported on
1035    /// [`ToolConfirmationRequest::function_call_id`], so a decision authorizes the
1036    /// exact call it was requested for. A decision under a tool *name* is not
1037    /// consulted, because one name can cover materially different calls — a
1038    /// `delete_file` approval for a scratch path must not authorize a call that
1039    /// targets a different path.
1040    ///
1041    /// Use [`tool_confirmation_fingerprints`](Self::tool_confirmation_fingerprints)
1042    /// to additionally bind a decision to the arguments it was granted for. For
1043    /// name-wide or policy-driven decisions, supply a
1044    /// [`tool_confirmation_handler`](Self::tool_confirmation_handler) instead.
1045    pub tool_confirmation_decisions: HashMap<String, ToolConfirmationDecision>,
1046    /// Optional argument binding for entries in
1047    /// [`tool_confirmation_decisions`](Self::tool_confirmation_decisions), keyed by
1048    /// the same function call ID.
1049    ///
1050    /// The value is the fingerprint produced by [`tool_call_fingerprint`] for the
1051    /// call the decision was granted for. When an entry is present and the actual
1052    /// call does not match it, the decision is ignored and the call is treated as
1053    /// unconfirmed — the safe direction. Use this when a decision travels through
1054    /// an untrusted round trip, such as a browser, where the arguments could be
1055    /// changed while the call ID is replayed.
1056    pub tool_confirmation_fingerprints: HashMap<String, String>,
1057    /// Optional live decision source for confirmations that have no static
1058    /// entry in [`tool_confirmation_decisions`](Self::tool_confirmation_decisions).
1059    pub tool_confirmation_handler: Option<Arc<dyn ToolConfirmationHandler>>,
1060    /// Toolsets made available only for this invocation.
1061    pub runtime_toolsets: Vec<RuntimeToolset>,
1062    /// Optional cached content name for automatic prompt caching.
1063    /// When set by the runner's cache lifecycle manager, agents should attach
1064    /// this name to their `GenerateContentConfig` so the LLM provider can
1065    /// reuse cached system instructions and tool definitions.
1066    pub cached_content: Option<String>,
1067    /// Valid agent names this agent can transfer to (parent, peers, children).
1068    /// Set by the runner when invoking agents in a multi-agent tree.
1069    /// When non-empty, the `transfer_to_agent` tool is injected and validation
1070    /// uses this list instead of only checking `sub_agents`.
1071    pub transfer_targets: Vec<String>,
1072    /// The name of the parent agent, if this agent was invoked via transfer.
1073    /// Used by the agent to apply `disallow_transfer_to_parent` filtering.
1074    pub parent_agent: Option<String>,
1075    /// Enable automatic prompt caching for all providers that support it.
1076    ///
1077    /// When `true` (the default), the runner enables provider-level caching:
1078    /// - Anthropic: sets `prompt_caching = true` on the config
1079    /// - Bedrock: sets `prompt_caching = Some(BedrockCacheConfig::default())`
1080    /// - OpenAI / DeepSeek: no action needed (caching is automatic)
1081    /// - Gemini: handled separately via `ContextCacheConfig`
1082    pub auto_cache: bool,
1083    /// Maximum number of recent persisted events to load at the start of a run.
1084    ///
1085    /// `None` preserves the previous behavior and loads the full session
1086    /// history. Set this for chat surfaces that already summarize older turns
1087    /// and need predictable startup latency.
1088    pub history_max_events: Option<usize>,
1089    /// Tool concurrency configuration controlling parallel tool dispatch limits,
1090    /// per-tool overrides, and backpressure behavior.
1091    ///
1092    /// The default (`ToolConcurrencyConfig::default()`) imposes no limits,
1093    /// preserving backward compatibility with the previous `max_tool_concurrency: None`.
1094    pub tool_concurrency: ToolConcurrencyConfig,
1095    /// Whether tracing spans may include full request, response, and tool
1096    /// payloads when the `record-payloads` crate feature is enabled.
1097    pub record_payloads: bool,
1098    /// Maximum serialized bytes recorded for tracing payload fields when full
1099    /// payload recording is disabled.
1100    pub trace_payload_max_bytes: usize,
1101    /// Maximum number of agent-to-agent transfers allowed in a single run.
1102    ///
1103    /// Prevents infinite transfer loops when agents transfer back and forth.
1104    /// Defaults to 10 when `None`.
1105    pub max_transfer_depth: Option<u32>,
1106}
1107
1108impl Default for RunConfig {
1109    fn default() -> Self {
1110        Self {
1111            streaming_mode: StreamingMode::SSE,
1112            tool_confirmation_decisions: HashMap::new(),
1113            tool_confirmation_fingerprints: HashMap::new(),
1114            tool_confirmation_handler: None,
1115            runtime_toolsets: Vec::new(),
1116            cached_content: None,
1117            transfer_targets: Vec::new(),
1118            parent_agent: None,
1119            auto_cache: true,
1120            history_max_events: None,
1121            tool_concurrency: ToolConcurrencyConfig::default(),
1122            record_payloads: false,
1123            trace_payload_max_bytes: 2048,
1124            max_transfer_depth: None,
1125        }
1126    }
1127}
1128
1129impl RunConfig {
1130    /// Creates a new [`RunConfigBuilder`] initialized with default values.
1131    ///
1132    /// Use the builder to construct a `RunConfig` when struct literal syntax
1133    ///
1134    /// # Example
1135    ///
1136    /// ```rust
1137    /// use adk_core::{RunConfig, StreamingMode};
1138    ///
1139    /// let config = RunConfig::builder()
1140    ///     .streaming_mode(StreamingMode::None)
1141    ///     .auto_cache(false)
1142    ///     .build();
1143    ///
1144    /// assert_eq!(config.streaming_mode, StreamingMode::None);
1145    /// assert!(!config.auto_cache);
1146    /// ```
1147    pub fn builder() -> RunConfigBuilder {
1148        RunConfigBuilder::default()
1149    }
1150}
1151
1152/// Builder for [`RunConfig`].
1153///
1154/// Provides a fluent API for constructing `RunConfig` instances. All fields
1155/// start with their default values and can be overridden individually.
1156///
1157/// # Example
1158///
1159/// ```rust
1160/// use adk_core::{RunConfig, RunConfigBuilder, StreamingMode, ToolConcurrencyConfig};
1161///
1162/// let config = RunConfigBuilder::default()
1163///     .streaming_mode(StreamingMode::Bidi)
1164///     .history_max_events(Some(50))
1165///     .build();
1166/// ```
1167#[derive(Debug, Clone, Default)]
1168pub struct RunConfigBuilder {
1169    config: RunConfig,
1170}
1171
1172impl RunConfigBuilder {
1173    /// Sets the streaming mode for the run.
1174    pub fn streaming_mode(mut self, mode: StreamingMode) -> Self {
1175        self.config.streaming_mode = mode;
1176        self
1177    }
1178
1179    /// Sets static confirmation decisions for the current run, keyed by function
1180    /// call ID.
1181    ///
1182    /// The ID is the one carried on `ToolConfirmationRequest::function_call_id`.
1183    pub fn tool_confirmation_decisions(
1184        mut self,
1185        decisions: HashMap<String, ToolConfirmationDecision>,
1186    ) -> Self {
1187        self.config.tool_confirmation_decisions = decisions;
1188        self
1189    }
1190
1191    /// Binds confirmation decisions to the arguments they were granted for.
1192    ///
1193    /// Keys are function call IDs and values are fingerprints from
1194    /// [`tool_call_fingerprint`]. A decision whose fingerprint does not match the
1195    /// actual call is ignored and the call is treated as unconfirmed.
1196    pub fn tool_confirmation_fingerprints(mut self, fingerprints: HashMap<String, String>) -> Self {
1197        self.config.tool_confirmation_fingerprints = fingerprints;
1198        self
1199    }
1200
1201    /// Sets an asynchronous tool confirmation handler for the current run.
1202    pub fn tool_confirmation_handler(mut self, handler: Arc<dyn ToolConfirmationHandler>) -> Self {
1203        self.config.tool_confirmation_handler = Some(handler);
1204        self
1205    }
1206
1207    /// Adds a toolset that is resolved only for this runner invocation.
1208    pub fn runtime_toolset(mut self, toolset: Arc<dyn Toolset>) -> Self {
1209        self.config.runtime_toolsets.push(RuntimeToolset::new(toolset));
1210        self
1211    }
1212
1213    /// Adds several toolsets that are resolved only for this runner invocation.
1214    pub fn runtime_toolsets(
1215        mut self,
1216        toolsets: impl IntoIterator<Item = Arc<dyn Toolset>>,
1217    ) -> Self {
1218        self.config.runtime_toolsets.extend(toolsets.into_iter().map(RuntimeToolset::new));
1219        self
1220    }
1221
1222    /// Sets the cached content name for automatic prompt caching.
1223    pub fn cached_content(mut self, name: impl Into<String>) -> Self {
1224        self.config.cached_content = Some(name.into());
1225        self
1226    }
1227
1228    /// Sets the valid agent names this agent can transfer to.
1229    pub fn transfer_targets(mut self, targets: Vec<String>) -> Self {
1230        self.config.transfer_targets = targets;
1231        self
1232    }
1233
1234    /// Sets the parent agent name.
1235    pub fn parent_agent(mut self, name: impl Into<String>) -> Self {
1236        self.config.parent_agent = Some(name.into());
1237        self
1238    }
1239
1240    /// Enables or disables automatic prompt caching for supported providers.
1241    pub fn auto_cache(mut self, enabled: bool) -> Self {
1242        self.config.auto_cache = enabled;
1243        self
1244    }
1245
1246    /// Sets the maximum number of recent persisted events to load at run start.
1247    pub fn history_max_events(mut self, max: Option<usize>) -> Self {
1248        self.config.history_max_events = max;
1249        self
1250    }
1251
1252    /// Sets the tool concurrency configuration.
1253    pub fn tool_concurrency(mut self, config: ToolConcurrencyConfig) -> Self {
1254        self.config.tool_concurrency = config;
1255        self
1256    }
1257
1258    /// Enables or disables full payload recording in tracing spans.
1259    pub fn record_payloads(mut self, enabled: bool) -> Self {
1260        self.config.record_payloads = enabled;
1261        self
1262    }
1263
1264    /// Sets the maximum serialized bytes for tracing payload fields.
1265    pub fn trace_payload_max_bytes(mut self, max: usize) -> Self {
1266        self.config.trace_payload_max_bytes = max;
1267        self
1268    }
1269
1270    /// Sets the maximum number of agent-to-agent transfers allowed in a single run.
1271    ///
1272    /// Prevents infinite transfer loops. Defaults to 10 when `None`.
1273    pub fn max_transfer_depth(mut self, depth: u32) -> Self {
1274        self.config.max_transfer_depth = Some(depth);
1275        self
1276    }
1277
1278    /// Consumes the builder and returns the configured [`RunConfig`].
1279    pub fn build(self) -> RunConfig {
1280        self.config
1281    }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286    use super::*;
1287
1288    #[test]
1289    fn test_run_config_default() {
1290        let config = RunConfig::default();
1291        assert_eq!(config.streaming_mode, StreamingMode::SSE);
1292        assert_eq!(config.history_max_events, None);
1293        assert_eq!(config.tool_concurrency.max_concurrency, None);
1294        assert!(config.tool_concurrency.per_tool.is_empty());
1295        assert_eq!(config.tool_concurrency.backpressure, BackpressurePolicy::Queue);
1296        assert!(!config.record_payloads);
1297        assert_eq!(config.trace_payload_max_bytes, 2048);
1298        assert!(config.tool_confirmation_decisions.is_empty());
1299        assert_eq!(config.max_transfer_depth, None);
1300    }
1301
1302    #[test]
1303    fn test_streaming_mode() {
1304        assert_eq!(StreamingMode::SSE, StreamingMode::SSE);
1305        assert_ne!(StreamingMode::SSE, StreamingMode::None);
1306        assert_ne!(StreamingMode::None, StreamingMode::Bidi);
1307    }
1308
1309    #[test]
1310    fn test_tool_confirmation_policy() {
1311        let policy = ToolConfirmationPolicy::default();
1312        assert!(!policy.requires_confirmation("search"));
1313
1314        let policy = policy.with_tool("search");
1315        assert!(policy.requires_confirmation("search"));
1316        assert!(!policy.requires_confirmation("write_file"));
1317
1318        assert!(ToolConfirmationPolicy::Always.requires_confirmation("any_tool"));
1319    }
1320
1321    #[test]
1322    fn test_validate_state_key_valid() {
1323        assert!(validate_state_key("user_name").is_ok());
1324        assert!(validate_state_key("app:config").is_ok());
1325        assert!(validate_state_key("temp:data").is_ok());
1326        assert!(validate_state_key("a").is_ok());
1327    }
1328
1329    #[test]
1330    fn test_validate_state_key_empty() {
1331        assert_eq!(validate_state_key(""), Err("state key must not be empty"));
1332    }
1333
1334    #[test]
1335    fn test_validate_state_key_too_long() {
1336        let long_key = "a".repeat(MAX_STATE_KEY_LEN + 1);
1337        assert!(validate_state_key(&long_key).is_err());
1338    }
1339
1340    #[test]
1341    fn test_validate_state_key_path_traversal() {
1342        assert!(validate_state_key("../etc/passwd").is_err());
1343        assert!(validate_state_key("foo/bar").is_err());
1344        assert!(validate_state_key("foo\\bar").is_err());
1345        assert!(validate_state_key("..").is_err());
1346    }
1347
1348    #[test]
1349    fn test_validate_state_key_null_byte() {
1350        assert!(validate_state_key("foo\0bar").is_err());
1351    }
1352
1353    #[test]
1354    fn test_run_config_builder_defaults() {
1355        let config = RunConfig::builder().build();
1356        let default = RunConfig::default();
1357        assert_eq!(config.streaming_mode, default.streaming_mode);
1358        assert_eq!(config.auto_cache, default.auto_cache);
1359        assert_eq!(config.history_max_events, default.history_max_events);
1360        assert_eq!(config.record_payloads, default.record_payloads);
1361        assert_eq!(config.trace_payload_max_bytes, default.trace_payload_max_bytes);
1362        assert!(config.tool_confirmation_decisions.is_empty());
1363        assert!(config.transfer_targets.is_empty());
1364        assert!(config.cached_content.is_none());
1365        assert!(config.parent_agent.is_none());
1366    }
1367
1368    #[test]
1369    fn test_run_config_builder_all_fields() {
1370        let mut decisions = HashMap::new();
1371        decisions.insert("delete".to_string(), ToolConfirmationDecision::Approve);
1372
1373        let config = RunConfig::builder()
1374            .streaming_mode(StreamingMode::None)
1375            .tool_confirmation_decisions(decisions.clone())
1376            .cached_content("my-cache")
1377            .transfer_targets(vec!["agent_a".to_string(), "agent_b".to_string()])
1378            .parent_agent("parent")
1379            .auto_cache(false)
1380            .history_max_events(Some(50))
1381            .tool_concurrency(ToolConcurrencyConfig {
1382                max_concurrency: Some(4),
1383                per_tool: HashMap::new(),
1384                backpressure: BackpressurePolicy::Fail,
1385            })
1386            .record_payloads(true)
1387            .trace_payload_max_bytes(4096)
1388            .build();
1389
1390        assert_eq!(config.streaming_mode, StreamingMode::None);
1391        assert_eq!(config.tool_confirmation_decisions, decisions);
1392        assert_eq!(config.cached_content.as_deref(), Some("my-cache"));
1393        assert_eq!(config.transfer_targets, vec!["agent_a", "agent_b"]);
1394        assert_eq!(config.parent_agent.as_deref(), Some("parent"));
1395        assert!(!config.auto_cache);
1396        assert_eq!(config.history_max_events, Some(50));
1397        assert_eq!(config.tool_concurrency.max_concurrency, Some(4));
1398        assert_eq!(config.tool_concurrency.backpressure, BackpressurePolicy::Fail);
1399        assert!(config.record_payloads);
1400        assert_eq!(config.trace_payload_max_bytes, 4096);
1401    }
1402}