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 /// Retrieve a secret by name from the configured secret provider.
554 ///
555 /// Returns `Ok(Some(value))` when a provider is configured and the secret
556 /// exists, `Ok(None)` when no provider is configured, or an error on
557 /// provider failure. The default returns `Ok(None)`.
558 async fn get_secret(&self, _name: &str) -> Result<Option<String>> {
559 Ok(None)
560 }
561
562 /// Resolves a secret for a described access.
563 ///
564 /// A wrapper context must forward this, and a tool context builds the request from
565 /// the identity the framework gave it. The default drops the description and calls
566 /// [`InvocationContext::get_secret`], which keeps a context that predates the
567 /// request object working.
568 async fn get_secret_for(&self, request: &SecretRequest) -> Result<Option<String>> {
569 self.get_secret(&request.name).await
570 }
571}
572
573// Placeholder service traits
574/// Binary artifact storage for agents.
575#[async_trait]
576pub trait Artifacts: Send + Sync {
577 /// Saves a binary artifact and returns its version number.
578 async fn save(&self, name: &str, data: &crate::Part) -> Result<i64>;
579 /// Loads a binary artifact by name.
580 async fn load(&self, name: &str) -> Result<crate::Part>;
581 /// Lists all artifact names.
582 async fn list(&self) -> Result<Vec<String>>;
583}
584
585/// Semantic memory search for agents.
586#[async_trait]
587pub trait Memory: Send + Sync {
588 /// Searches memory for entries matching the query.
589 async fn search(&self, query: &str) -> Result<Vec<MemoryEntry>>;
590
591 /// Verify backend connectivity.
592 ///
593 /// The default implementation succeeds, which is suitable for in-memory
594 /// implementations and adapters without an external dependency.
595 async fn health_check(&self) -> Result<()> {
596 Ok(())
597 }
598
599 /// Add a single memory entry.
600 ///
601 /// The default implementation returns an "not implemented" error, which is
602 /// suitable for read-only memory backends.
603 async fn add(&self, entry: MemoryEntry) -> Result<()> {
604 let _ = entry;
605 Err(AdkError::memory("add not implemented"))
606 }
607
608 /// Delete entries matching a query. Returns count of deleted entries.
609 ///
610 /// The default implementation returns an "not implemented" error, which is
611 /// suitable for read-only memory backends.
612 async fn delete(&self, query: &str) -> Result<u64> {
613 let _ = query;
614 Err(AdkError::memory("delete not implemented"))
615 }
616
617 /// Whether this memory keeps project-scoped entries isolated.
618 ///
619 /// Returns `false` by default, so a caller can tell real isolation apart from a
620 /// memory that has no project support instead of inferring it from data.
621 fn supports_project_scoping(&self) -> bool {
622 false
623 }
624
625 /// Searches memories within a specific project.
626 ///
627 /// # Errors
628 ///
629 /// The default implementation returns an error. Delegating to the global search
630 /// would return entries the project boundary is meant to exclude, and nothing in
631 /// the result would say the boundary was ignored.
632 async fn search_in_project(&self, query: &str, project_id: &str) -> Result<Vec<MemoryEntry>> {
633 let _ = (query, project_id);
634 Err(AdkError::memory(
635 "this memory does not implement project scoping, so `search_in_project` cannot \
636 honour the project boundary; check `supports_project_scoping` first, or call \
637 `search` if global scope is intended",
638 ))
639 }
640
641 /// Adds a memory entry scoped to a specific project.
642 ///
643 /// # Errors
644 ///
645 /// Returns an error by default. Writing the entry globally would make data
646 /// intended for one project visible everywhere under the same app and user.
647 async fn add_to_project(&self, entry: MemoryEntry, project_id: &str) -> Result<()> {
648 let _ = (entry, project_id);
649 Err(AdkError::memory(
650 "this memory does not implement project scoping, so `add_to_project` cannot honour \
651 the project boundary; check `supports_project_scoping` first, or call `add` if \
652 global scope is intended",
653 ))
654 }
655}
656
657/// Trait for retrieving secrets at runtime.
658///
659/// This is the core-level abstraction used by `ToolContext::get_secret` and
660/// `InvocationContext::get_secret`. Concrete implementations (e.g., AWS
661/// Secrets Manager, Azure Key Vault, GCP Secret Manager) live in `adk-auth`
662/// behind feature flags and implement this trait via the `SecretProvider`
663/// adapter.
664///
665/// # Example
666///
667/// ```rust,ignore
668/// use adk_core::SecretService;
669///
670/// struct EnvSecretService;
671///
672/// #[async_trait::async_trait]
673/// impl SecretService for EnvSecretService {
674/// async fn get_secret(&self, name: &str) -> adk_core::Result<String> {
675/// std::env::var(name).map_err(|_| adk_core::AdkError::not_found(
676/// format!("secret '{name}' not found in environment"),
677/// ))
678/// }
679/// }
680/// ```
681#[async_trait]
682pub trait SecretService: Send + Sync {
683 /// Retrieve a secret value by name.
684 ///
685 /// Returns the secret string on success, or an [`AdkError`] on failure.
686 async fn get_secret(&self, name: &str) -> Result<String>;
687
688 /// Retrieve a secret for a described access.
689 ///
690 /// This is the form an authorizing service implements: the request carries who is
691 /// asking and why, so a decision can be made before the value is fetched. The
692 /// default implementation ignores the context and calls
693 /// [`SecretService::get_secret`], which is correct for a service that has no
694 /// policy of its own.
695 ///
696 /// Every field on [`SecretRequest`] is set by the framework at the call site, not
697 /// supplied by the tool, so a tool cannot present another tool's identity.
698 async fn get_secret_for(&self, request: &SecretRequest) -> Result<String> {
699 self.get_secret(&request.name).await
700 }
701}
702
703/// A described secret access.
704///
705/// Carries the requested name plus the identity the framework observed at the call
706/// site, so a [`SecretService`] can authorize and audit rather than being handed a
707/// bare name with no context.
708///
709/// # Example
710///
711/// ```rust
712/// use adk_core::SecretRequest;
713///
714/// let request = SecretRequest::new("payments-api-key")
715/// .with_tool_name("charge_card")
716/// .with_purpose("authorize a customer payment");
717///
718/// assert_eq!(request.tool_name.as_deref(), Some("charge_card"));
719/// ```
720#[derive(Debug, Clone, Default, PartialEq, Eq)]
721pub struct SecretRequest {
722 /// Name of the requested secret.
723 pub name: String,
724 /// The tool making the request, when the access came from a tool.
725 ///
726 /// Set by the framework from the tool it dispatched, never from a value the tool
727 /// provided.
728 pub tool_name: Option<String>,
729 /// Application the run belongs to.
730 pub app_name: Option<String>,
731 /// Authenticated user the run belongs to.
732 pub user_id: Option<String>,
733 /// Session the run belongs to.
734 pub session_id: Option<String>,
735 /// Invocation the access happened in, for correlating audit records.
736 pub invocation_id: Option<String>,
737 /// Why the secret is needed, when the caller states it.
738 pub purpose: Option<String>,
739}
740
741impl SecretRequest {
742 /// Creates a request for `name` with no identity attached.
743 pub fn new(name: impl Into<String>) -> Self {
744 Self { name: name.into(), ..Default::default() }
745 }
746
747 /// Attaches the requesting tool's name.
748 #[must_use]
749 pub fn with_tool_name(mut self, tool_name: impl Into<String>) -> Self {
750 self.tool_name = Some(tool_name.into());
751 self
752 }
753
754 /// Attaches the run's identity.
755 #[must_use]
756 pub fn with_identity(
757 mut self,
758 app_name: impl Into<String>,
759 user_id: impl Into<String>,
760 session_id: impl Into<String>,
761 ) -> Self {
762 self.app_name = Some(app_name.into());
763 self.user_id = Some(user_id.into());
764 self.session_id = Some(session_id.into());
765 self
766 }
767
768 /// Attaches the invocation the access happened in.
769 #[must_use]
770 pub fn with_invocation_id(mut self, invocation_id: impl Into<String>) -> Self {
771 self.invocation_id = Some(invocation_id.into());
772 self
773 }
774
775 /// Attaches a stated purpose.
776 #[must_use]
777 pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
778 self.purpose = Some(purpose.into());
779 self
780 }
781}
782
783/// A single entry returned from memory search.
784#[derive(Debug, Clone)]
785pub struct MemoryEntry {
786 /// The content of this memory entry.
787 pub content: Content,
788 /// The author who created this memory entry.
789 pub author: String,
790}
791
792/// Streaming mode for agent responses.
793/// Matches ADK Python/Go specification.
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
795pub enum StreamingMode {
796 /// No streaming; responses delivered as complete units.
797 /// Agent collects all chunks internally and yields a single final event.
798 None,
799 /// Server-Sent Events streaming; one-way streaming from server to client.
800 /// Agent yields each chunk as it arrives with stable event ID.
801 #[default]
802 SSE,
803 /// Bidirectional streaming; simultaneous communication in both directions.
804 /// Used for realtime audio/video agents.
805 Bidi,
806}
807
808/// Controls what parts of prior conversation history is received by llmagent
809#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
810pub enum IncludeContents {
811 /// The llmagent operates solely on its current turn (latest user input + any following agent events)
812 None,
813 /// Default - The llmagent receives the relevant conversation history
814 #[default]
815 Default,
816}
817
818/// Decision applied when a tool execution requires human confirmation.
819#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
820#[serde(rename_all = "snake_case")]
821pub enum ToolConfirmationDecision {
822 /// Approve the tool execution.
823 Approve,
824 /// Deny the tool execution.
825 Deny,
826}
827
828/// Produces a canonical fingerprint of a tool call.
829///
830/// The fingerprint is the tool name followed by its arguments in canonical JSON
831/// form, with object keys sorted at every level so that two structurally equal
832/// argument sets always produce the same string. It is deliberately readable
833/// rather than hashed, so a mismatch can be diagnosed by inspection.
834///
835/// Use it with
836/// [`RunConfig::tool_confirmation_fingerprints`](RunConfig::tool_confirmation_fingerprints)
837/// to bind an approval to the exact arguments it was granted for.
838///
839/// # Example
840///
841/// ```rust
842/// use adk_core::tool_call_fingerprint;
843/// use serde_json::json;
844///
845/// // Key order does not change the fingerprint.
846/// let a = tool_call_fingerprint("delete_file", &json!({ "path": "/tmp/a", "force": true }));
847/// let b = tool_call_fingerprint("delete_file", &json!({ "force": true, "path": "/tmp/a" }));
848/// assert_eq!(a, b);
849///
850/// // A different path does not.
851/// let c = tool_call_fingerprint("delete_file", &json!({ "path": "/etc/passwd", "force": true }));
852/// assert_ne!(a, c);
853/// ```
854pub fn tool_call_fingerprint(tool_name: &str, args: &Value) -> String {
855 let mut out = String::with_capacity(tool_name.len() + 32);
856 out.push_str(tool_name);
857 out.push('\u{1f}');
858 write_canonical(args, &mut out);
859 out
860}
861
862/// Writes `value` as canonical JSON, with object keys sorted at every level.
863fn write_canonical(value: &Value, out: &mut String) {
864 match value {
865 Value::Object(map) => {
866 let mut keys: Vec<&String> = map.keys().collect();
867 keys.sort();
868 out.push('{');
869 for (i, key) in keys.iter().enumerate() {
870 if i > 0 {
871 out.push(',');
872 }
873 out.push_str(&Value::String((*key).clone()).to_string());
874 out.push(':');
875 write_canonical(&map[*key], out);
876 }
877 out.push('}');
878 }
879 Value::Array(items) => {
880 out.push('[');
881 for (i, item) in items.iter().enumerate() {
882 if i > 0 {
883 out.push(',');
884 }
885 write_canonical(item, out);
886 }
887 out.push(']');
888 }
889 other => out.push_str(&other.to_string()),
890 }
891}
892
893/// Policy defining which tools require human confirmation before execution.
894#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
895#[serde(rename_all = "snake_case")]
896pub enum ToolConfirmationPolicy {
897 /// No tool confirmation is required.
898 #[default]
899 Never,
900 /// Every tool call requires confirmation.
901 Always,
902 /// Only the listed tool names require confirmation.
903 PerTool(BTreeSet<String>),
904}
905
906impl ToolConfirmationPolicy {
907 /// Returns true when the given tool name must be confirmed before execution.
908 pub fn requires_confirmation(&self, tool_name: &str) -> bool {
909 match self {
910 Self::Never => false,
911 Self::Always => true,
912 Self::PerTool(tools) => tools.contains(tool_name),
913 }
914 }
915
916 /// Add one tool name to the confirmation policy (converts `Never` to `PerTool`).
917 pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
918 let tool_name = tool_name.into();
919 match &mut self {
920 Self::Never => {
921 let mut tools = BTreeSet::new();
922 tools.insert(tool_name);
923 Self::PerTool(tools)
924 }
925 Self::Always => Self::Always,
926 Self::PerTool(tools) => {
927 tools.insert(tool_name);
928 self
929 }
930 }
931 }
932}
933
934/// Payload describing a tool call awaiting human confirmation.
935#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
936#[serde(rename_all = "camelCase")]
937pub struct ToolConfirmationRequest {
938 /// Name of the tool awaiting confirmation.
939 pub tool_name: String,
940 /// The function call ID from the LLM, if available.
941 #[serde(skip_serializing_if = "Option::is_none")]
942 pub function_call_id: Option<String>,
943 /// Arguments the tool would be called with.
944 pub args: Value,
945}
946
947/// Asynchronous decision source for tool calls that require confirmation.
948///
949/// Front ends and protocol adapters can implement this trait to pause an
950/// invocation while a person or an external policy service reviews the exact
951/// tool call. When no handler is configured, agents preserve the existing
952/// behavior and emit an interrupted confirmation event for a later run.
953#[async_trait]
954pub trait ToolConfirmationHandler: std::fmt::Debug + Send + Sync {
955 /// Approve or deny one pending tool call.
956 async fn decide(&self, request: &ToolConfirmationRequest) -> Result<ToolConfirmationDecision>;
957}
958
959/// A toolset attached to one runner invocation rather than compiled into the
960/// agent definition.
961///
962/// Protocol adapters use this wrapper for session-scoped capabilities such as
963/// MCP servers supplied by an ACP client. The wrapper keeps [`RunConfig`]
964/// debuggable without requiring every toolset implementation to implement
965/// [`std::fmt::Debug`].
966#[derive(Clone)]
967pub struct RuntimeToolset(Arc<dyn Toolset>);
968
969impl RuntimeToolset {
970 /// Wrap a toolset for use during one runner invocation.
971 pub fn new(toolset: Arc<dyn Toolset>) -> Self {
972 Self(toolset)
973 }
974
975 /// Borrow the wrapped toolset.
976 pub fn toolset(&self) -> &Arc<dyn Toolset> {
977 &self.0
978 }
979}
980
981impl std::fmt::Debug for RuntimeToolset {
982 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
983 formatter.debug_tuple("RuntimeToolset").field(&self.0.name()).finish()
984 }
985}
986
987/// Configuration for a single agent run.
988///
989/// Controls streaming behavior, tool confirmation, caching, transfer targets,
990/// and concurrency settings. Use [`RunConfig::builder()`] to construct from
991#[derive(Debug, Clone)]
992pub struct RunConfig {
993 /// The streaming mode for agent responses.
994 pub streaming_mode: StreamingMode,
995 /// Static confirmation decisions for the current run, keyed by **function
996 /// call ID**.
997 ///
998 /// The ID is the one reported on
999 /// [`ToolConfirmationRequest::function_call_id`], so a decision authorizes the
1000 /// exact call it was requested for. A decision under a tool *name* is not
1001 /// consulted, because one name can cover materially different calls — a
1002 /// `delete_file` approval for a scratch path must not authorize a call that
1003 /// targets a different path.
1004 ///
1005 /// Use [`tool_confirmation_fingerprints`](Self::tool_confirmation_fingerprints)
1006 /// to additionally bind a decision to the arguments it was granted for. For
1007 /// name-wide or policy-driven decisions, supply a
1008 /// [`tool_confirmation_handler`](Self::tool_confirmation_handler) instead.
1009 pub tool_confirmation_decisions: HashMap<String, ToolConfirmationDecision>,
1010 /// Optional argument binding for entries in
1011 /// [`tool_confirmation_decisions`](Self::tool_confirmation_decisions), keyed by
1012 /// the same function call ID.
1013 ///
1014 /// The value is the fingerprint produced by [`tool_call_fingerprint`] for the
1015 /// call the decision was granted for. When an entry is present and the actual
1016 /// call does not match it, the decision is ignored and the call is treated as
1017 /// unconfirmed — the safe direction. Use this when a decision travels through
1018 /// an untrusted round trip, such as a browser, where the arguments could be
1019 /// changed while the call ID is replayed.
1020 pub tool_confirmation_fingerprints: HashMap<String, String>,
1021 /// Optional live decision source for confirmations that have no static
1022 /// entry in [`tool_confirmation_decisions`](Self::tool_confirmation_decisions).
1023 pub tool_confirmation_handler: Option<Arc<dyn ToolConfirmationHandler>>,
1024 /// Toolsets made available only for this invocation.
1025 pub runtime_toolsets: Vec<RuntimeToolset>,
1026 /// Optional cached content name for automatic prompt caching.
1027 /// When set by the runner's cache lifecycle manager, agents should attach
1028 /// this name to their `GenerateContentConfig` so the LLM provider can
1029 /// reuse cached system instructions and tool definitions.
1030 pub cached_content: Option<String>,
1031 /// Valid agent names this agent can transfer to (parent, peers, children).
1032 /// Set by the runner when invoking agents in a multi-agent tree.
1033 /// When non-empty, the `transfer_to_agent` tool is injected and validation
1034 /// uses this list instead of only checking `sub_agents`.
1035 pub transfer_targets: Vec<String>,
1036 /// The name of the parent agent, if this agent was invoked via transfer.
1037 /// Used by the agent to apply `disallow_transfer_to_parent` filtering.
1038 pub parent_agent: Option<String>,
1039 /// Enable automatic prompt caching for all providers that support it.
1040 ///
1041 /// When `true` (the default), the runner enables provider-level caching:
1042 /// - Anthropic: sets `prompt_caching = true` on the config
1043 /// - Bedrock: sets `prompt_caching = Some(BedrockCacheConfig::default())`
1044 /// - OpenAI / DeepSeek: no action needed (caching is automatic)
1045 /// - Gemini: handled separately via `ContextCacheConfig`
1046 pub auto_cache: bool,
1047 /// Maximum number of recent persisted events to load at the start of a run.
1048 ///
1049 /// `None` preserves the previous behavior and loads the full session
1050 /// history. Set this for chat surfaces that already summarize older turns
1051 /// and need predictable startup latency.
1052 pub history_max_events: Option<usize>,
1053 /// Tool concurrency configuration controlling parallel tool dispatch limits,
1054 /// per-tool overrides, and backpressure behavior.
1055 ///
1056 /// The default (`ToolConcurrencyConfig::default()`) imposes no limits,
1057 /// preserving backward compatibility with the previous `max_tool_concurrency: None`.
1058 pub tool_concurrency: ToolConcurrencyConfig,
1059 /// Whether tracing spans may include full request, response, and tool
1060 /// payloads when the `record-payloads` crate feature is enabled.
1061 pub record_payloads: bool,
1062 /// Maximum serialized bytes recorded for tracing payload fields when full
1063 /// payload recording is disabled.
1064 pub trace_payload_max_bytes: usize,
1065 /// Maximum number of agent-to-agent transfers allowed in a single run.
1066 ///
1067 /// Prevents infinite transfer loops when agents transfer back and forth.
1068 /// Defaults to 10 when `None`.
1069 pub max_transfer_depth: Option<u32>,
1070}
1071
1072impl Default for RunConfig {
1073 fn default() -> Self {
1074 Self {
1075 streaming_mode: StreamingMode::SSE,
1076 tool_confirmation_decisions: HashMap::new(),
1077 tool_confirmation_fingerprints: HashMap::new(),
1078 tool_confirmation_handler: None,
1079 runtime_toolsets: Vec::new(),
1080 cached_content: None,
1081 transfer_targets: Vec::new(),
1082 parent_agent: None,
1083 auto_cache: true,
1084 history_max_events: None,
1085 tool_concurrency: ToolConcurrencyConfig::default(),
1086 record_payloads: false,
1087 trace_payload_max_bytes: 2048,
1088 max_transfer_depth: None,
1089 }
1090 }
1091}
1092
1093impl RunConfig {
1094 /// Creates a new [`RunConfigBuilder`] initialized with default values.
1095 ///
1096 /// Use the builder to construct a `RunConfig` when struct literal syntax
1097 ///
1098 /// # Example
1099 ///
1100 /// ```rust
1101 /// use adk_core::{RunConfig, StreamingMode};
1102 ///
1103 /// let config = RunConfig::builder()
1104 /// .streaming_mode(StreamingMode::None)
1105 /// .auto_cache(false)
1106 /// .build();
1107 ///
1108 /// assert_eq!(config.streaming_mode, StreamingMode::None);
1109 /// assert!(!config.auto_cache);
1110 /// ```
1111 pub fn builder() -> RunConfigBuilder {
1112 RunConfigBuilder::default()
1113 }
1114}
1115
1116/// Builder for [`RunConfig`].
1117///
1118/// Provides a fluent API for constructing `RunConfig` instances. All fields
1119/// start with their default values and can be overridden individually.
1120///
1121/// # Example
1122///
1123/// ```rust
1124/// use adk_core::{RunConfig, RunConfigBuilder, StreamingMode, ToolConcurrencyConfig};
1125///
1126/// let config = RunConfigBuilder::default()
1127/// .streaming_mode(StreamingMode::Bidi)
1128/// .history_max_events(Some(50))
1129/// .build();
1130/// ```
1131#[derive(Debug, Clone, Default)]
1132pub struct RunConfigBuilder {
1133 config: RunConfig,
1134}
1135
1136impl RunConfigBuilder {
1137 /// Sets the streaming mode for the run.
1138 pub fn streaming_mode(mut self, mode: StreamingMode) -> Self {
1139 self.config.streaming_mode = mode;
1140 self
1141 }
1142
1143 /// Sets static confirmation decisions for the current run, keyed by function
1144 /// call ID.
1145 ///
1146 /// The ID is the one carried on `ToolConfirmationRequest::function_call_id`.
1147 pub fn tool_confirmation_decisions(
1148 mut self,
1149 decisions: HashMap<String, ToolConfirmationDecision>,
1150 ) -> Self {
1151 self.config.tool_confirmation_decisions = decisions;
1152 self
1153 }
1154
1155 /// Binds confirmation decisions to the arguments they were granted for.
1156 ///
1157 /// Keys are function call IDs and values are fingerprints from
1158 /// [`tool_call_fingerprint`]. A decision whose fingerprint does not match the
1159 /// actual call is ignored and the call is treated as unconfirmed.
1160 pub fn tool_confirmation_fingerprints(mut self, fingerprints: HashMap<String, String>) -> Self {
1161 self.config.tool_confirmation_fingerprints = fingerprints;
1162 self
1163 }
1164
1165 /// Sets an asynchronous tool confirmation handler for the current run.
1166 pub fn tool_confirmation_handler(mut self, handler: Arc<dyn ToolConfirmationHandler>) -> Self {
1167 self.config.tool_confirmation_handler = Some(handler);
1168 self
1169 }
1170
1171 /// Adds a toolset that is resolved only for this runner invocation.
1172 pub fn runtime_toolset(mut self, toolset: Arc<dyn Toolset>) -> Self {
1173 self.config.runtime_toolsets.push(RuntimeToolset::new(toolset));
1174 self
1175 }
1176
1177 /// Adds several toolsets that are resolved only for this runner invocation.
1178 pub fn runtime_toolsets(
1179 mut self,
1180 toolsets: impl IntoIterator<Item = Arc<dyn Toolset>>,
1181 ) -> Self {
1182 self.config.runtime_toolsets.extend(toolsets.into_iter().map(RuntimeToolset::new));
1183 self
1184 }
1185
1186 /// Sets the cached content name for automatic prompt caching.
1187 pub fn cached_content(mut self, name: impl Into<String>) -> Self {
1188 self.config.cached_content = Some(name.into());
1189 self
1190 }
1191
1192 /// Sets the valid agent names this agent can transfer to.
1193 pub fn transfer_targets(mut self, targets: Vec<String>) -> Self {
1194 self.config.transfer_targets = targets;
1195 self
1196 }
1197
1198 /// Sets the parent agent name.
1199 pub fn parent_agent(mut self, name: impl Into<String>) -> Self {
1200 self.config.parent_agent = Some(name.into());
1201 self
1202 }
1203
1204 /// Enables or disables automatic prompt caching for supported providers.
1205 pub fn auto_cache(mut self, enabled: bool) -> Self {
1206 self.config.auto_cache = enabled;
1207 self
1208 }
1209
1210 /// Sets the maximum number of recent persisted events to load at run start.
1211 pub fn history_max_events(mut self, max: Option<usize>) -> Self {
1212 self.config.history_max_events = max;
1213 self
1214 }
1215
1216 /// Sets the tool concurrency configuration.
1217 pub fn tool_concurrency(mut self, config: ToolConcurrencyConfig) -> Self {
1218 self.config.tool_concurrency = config;
1219 self
1220 }
1221
1222 /// Enables or disables full payload recording in tracing spans.
1223 pub fn record_payloads(mut self, enabled: bool) -> Self {
1224 self.config.record_payloads = enabled;
1225 self
1226 }
1227
1228 /// Sets the maximum serialized bytes for tracing payload fields.
1229 pub fn trace_payload_max_bytes(mut self, max: usize) -> Self {
1230 self.config.trace_payload_max_bytes = max;
1231 self
1232 }
1233
1234 /// Sets the maximum number of agent-to-agent transfers allowed in a single run.
1235 ///
1236 /// Prevents infinite transfer loops. Defaults to 10 when `None`.
1237 pub fn max_transfer_depth(mut self, depth: u32) -> Self {
1238 self.config.max_transfer_depth = Some(depth);
1239 self
1240 }
1241
1242 /// Consumes the builder and returns the configured [`RunConfig`].
1243 pub fn build(self) -> RunConfig {
1244 self.config
1245 }
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250 use super::*;
1251
1252 #[test]
1253 fn test_run_config_default() {
1254 let config = RunConfig::default();
1255 assert_eq!(config.streaming_mode, StreamingMode::SSE);
1256 assert_eq!(config.history_max_events, None);
1257 assert_eq!(config.tool_concurrency.max_concurrency, None);
1258 assert!(config.tool_concurrency.per_tool.is_empty());
1259 assert_eq!(config.tool_concurrency.backpressure, BackpressurePolicy::Queue);
1260 assert!(!config.record_payloads);
1261 assert_eq!(config.trace_payload_max_bytes, 2048);
1262 assert!(config.tool_confirmation_decisions.is_empty());
1263 assert_eq!(config.max_transfer_depth, None);
1264 }
1265
1266 #[test]
1267 fn test_streaming_mode() {
1268 assert_eq!(StreamingMode::SSE, StreamingMode::SSE);
1269 assert_ne!(StreamingMode::SSE, StreamingMode::None);
1270 assert_ne!(StreamingMode::None, StreamingMode::Bidi);
1271 }
1272
1273 #[test]
1274 fn test_tool_confirmation_policy() {
1275 let policy = ToolConfirmationPolicy::default();
1276 assert!(!policy.requires_confirmation("search"));
1277
1278 let policy = policy.with_tool("search");
1279 assert!(policy.requires_confirmation("search"));
1280 assert!(!policy.requires_confirmation("write_file"));
1281
1282 assert!(ToolConfirmationPolicy::Always.requires_confirmation("any_tool"));
1283 }
1284
1285 #[test]
1286 fn test_validate_state_key_valid() {
1287 assert!(validate_state_key("user_name").is_ok());
1288 assert!(validate_state_key("app:config").is_ok());
1289 assert!(validate_state_key("temp:data").is_ok());
1290 assert!(validate_state_key("a").is_ok());
1291 }
1292
1293 #[test]
1294 fn test_validate_state_key_empty() {
1295 assert_eq!(validate_state_key(""), Err("state key must not be empty"));
1296 }
1297
1298 #[test]
1299 fn test_validate_state_key_too_long() {
1300 let long_key = "a".repeat(MAX_STATE_KEY_LEN + 1);
1301 assert!(validate_state_key(&long_key).is_err());
1302 }
1303
1304 #[test]
1305 fn test_validate_state_key_path_traversal() {
1306 assert!(validate_state_key("../etc/passwd").is_err());
1307 assert!(validate_state_key("foo/bar").is_err());
1308 assert!(validate_state_key("foo\\bar").is_err());
1309 assert!(validate_state_key("..").is_err());
1310 }
1311
1312 #[test]
1313 fn test_validate_state_key_null_byte() {
1314 assert!(validate_state_key("foo\0bar").is_err());
1315 }
1316
1317 #[test]
1318 fn test_run_config_builder_defaults() {
1319 let config = RunConfig::builder().build();
1320 let default = RunConfig::default();
1321 assert_eq!(config.streaming_mode, default.streaming_mode);
1322 assert_eq!(config.auto_cache, default.auto_cache);
1323 assert_eq!(config.history_max_events, default.history_max_events);
1324 assert_eq!(config.record_payloads, default.record_payloads);
1325 assert_eq!(config.trace_payload_max_bytes, default.trace_payload_max_bytes);
1326 assert!(config.tool_confirmation_decisions.is_empty());
1327 assert!(config.transfer_targets.is_empty());
1328 assert!(config.cached_content.is_none());
1329 assert!(config.parent_agent.is_none());
1330 }
1331
1332 #[test]
1333 fn test_run_config_builder_all_fields() {
1334 let mut decisions = HashMap::new();
1335 decisions.insert("delete".to_string(), ToolConfirmationDecision::Approve);
1336
1337 let config = RunConfig::builder()
1338 .streaming_mode(StreamingMode::None)
1339 .tool_confirmation_decisions(decisions.clone())
1340 .cached_content("my-cache")
1341 .transfer_targets(vec!["agent_a".to_string(), "agent_b".to_string()])
1342 .parent_agent("parent")
1343 .auto_cache(false)
1344 .history_max_events(Some(50))
1345 .tool_concurrency(ToolConcurrencyConfig {
1346 max_concurrency: Some(4),
1347 per_tool: HashMap::new(),
1348 backpressure: BackpressurePolicy::Fail,
1349 })
1350 .record_payloads(true)
1351 .trace_payload_max_bytes(4096)
1352 .build();
1353
1354 assert_eq!(config.streaming_mode, StreamingMode::None);
1355 assert_eq!(config.tool_confirmation_decisions, decisions);
1356 assert_eq!(config.cached_content.as_deref(), Some("my-cache"));
1357 assert_eq!(config.transfer_targets, vec!["agent_a", "agent_b"]);
1358 assert_eq!(config.parent_agent.as_deref(), Some("parent"));
1359 assert!(!config.auto_cache);
1360 assert_eq!(config.history_max_events, Some(50));
1361 assert_eq!(config.tool_concurrency.max_concurrency, Some(4));
1362 assert_eq!(config.tool_concurrency.backpressure, BackpressurePolicy::Fail);
1363 assert!(config.record_payloads);
1364 assert_eq!(config.trace_payload_max_bytes, 4096);
1365 }
1366}