adk_core/context.rs
1use crate::identity::{AdkIdentity, AppName, ExecutionIdentity, InvocationId, SessionId, UserId};
2use crate::{AdkError, Agent, Result, 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 /// Append content to conversation history (for sequential agent support)
261 fn append_to_history(&self, _content: Content) {
262 // Default no-op - implementations can override to track history
263 }
264
265 /// Returns the application name as a typed [`AppName`].
266 ///
267 /// Parses the value returned by [`app_name()`](Self::app_name). Returns an
268 /// error if the raw string fails validation (empty, null bytes, or exceeds
269 /// the maximum length).
270 ///
271 /// # Errors
272 ///
273 /// Returns an error when the
274 /// underlying string is not a valid identifier.
275 fn try_app_name(&self) -> Result<AppName> {
276 Ok(AppName::try_from(self.app_name())?)
277 }
278
279 /// Returns the user identifier as a typed [`UserId`].
280 ///
281 /// Parses the value returned by [`user_id()`](Self::user_id). Returns an
282 /// error if the raw string fails validation.
283 ///
284 /// # Errors
285 ///
286 /// Returns an error when the
287 /// underlying string is not a valid identifier.
288 fn try_user_id(&self) -> Result<UserId> {
289 Ok(UserId::try_from(self.user_id())?)
290 }
291
292 /// Returns the session identifier as a typed [`SessionId`].
293 ///
294 /// Parses the value returned by [`id()`](Self::id). Returns an error if
295 /// the raw string fails validation.
296 ///
297 /// # Errors
298 ///
299 /// Returns an error when the
300 /// underlying string is not a valid identifier.
301 fn try_session_id(&self) -> Result<SessionId> {
302 Ok(SessionId::try_from(self.id())?)
303 }
304
305 /// Returns the stable session-scoped [`AdkIdentity`] triple.
306 ///
307 /// Combines [`try_app_name()`](Self::try_app_name),
308 /// [`try_user_id()`](Self::try_user_id), and
309 /// [`try_session_id()`](Self::try_session_id) into a single composite
310 /// identity value.
311 ///
312 /// # Errors
313 ///
314 /// Returns an error if any of the three constituent identifiers fail
315 /// validation.
316 fn try_identity(&self) -> Result<AdkIdentity> {
317 Ok(AdkIdentity {
318 app_name: self.try_app_name()?,
319 user_id: self.try_user_id()?,
320 session_id: self.try_session_id()?,
321 })
322 }
323}
324
325/// Structured metadata about a completed tool execution.
326///
327/// Available via [`CallbackContext::tool_outcome()`] in after-tool callbacks,
328/// plugins, and telemetry hooks. Provides structured access to execution
329/// results without requiring JSON error parsing.
330///
331/// # Fields
332///
333/// - `tool_name` — Name of the tool that was executed.
334/// - `tool_args` — Arguments passed to the tool as a JSON value.
335/// - `success` — Whether the tool execution succeeded. Derived from the
336/// Rust `Result` / timeout path, never from JSON content inspection.
337/// - `duration` — Wall-clock duration of the tool execution.
338/// - `error_message` — Error message if the tool failed; `None` on success.
339/// - `attempt` — Retry attempt number (0 = first attempt, 1 = first retry, etc.).
340/// Always 0 when retries are not configured.
341#[derive(Debug, Clone)]
342pub struct ToolOutcome {
343 /// Name of the tool that was executed.
344 pub tool_name: String,
345 /// Arguments passed to the tool (JSON value).
346 pub tool_args: serde_json::Value,
347 /// Whether the tool execution succeeded.
348 pub success: bool,
349 /// Wall-clock duration of the tool execution.
350 pub duration: std::time::Duration,
351 /// Error message if the tool failed. `None` on success.
352 pub error_message: Option<String>,
353 /// Retry attempt number (0 = first attempt, 1 = first retry, etc.).
354 /// Always 0 when retries are not configured.
355 pub attempt: u32,
356}
357
358/// Context available to agent lifecycle callbacks.
359///
360/// Extends [`ReadonlyContext`] with access to artifacts and tool execution metadata.
361#[async_trait]
362pub trait CallbackContext: ReadonlyContext {
363 /// Returns the artifact store, if one is configured.
364 fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
365
366 /// Returns structured metadata about the most recent tool execution.
367 /// Available in after-tool callbacks and plugin hooks.
368 /// Returns `None` when not in a tool execution context.
369 fn tool_outcome(&self) -> Option<ToolOutcome> {
370 None // default for backward compatibility
371 }
372
373 /// Returns the name of the tool about to be executed.
374 /// Available in before-tool and after-tool callback contexts.
375 fn tool_name(&self) -> Option<&str> {
376 None
377 }
378
379 /// Returns the input arguments for the tool about to be executed.
380 /// Available in before-tool and after-tool callback contexts.
381 fn tool_input(&self) -> Option<&serde_json::Value> {
382 None
383 }
384
385 /// Returns the shared state for parallel agent coordination.
386 /// Returns `None` when not running inside a `ParallelAgent` with shared state enabled.
387 fn shared_state(&self) -> Option<Arc<crate::SharedState>> {
388 None
389 }
390}
391
392/// Wraps a [`CallbackContext`] to inject tool name and input for before-tool
393/// and after-tool callbacks.
394///
395/// Used by the agent runtime to provide tool context to `BeforeToolCallback`
396/// and `AfterToolCallback` invocations.
397///
398/// # Example
399///
400/// ```rust,ignore
401/// let tool_ctx = Arc::new(ToolCallbackContext::new(
402/// ctx.clone(),
403/// "search".to_string(),
404/// serde_json::json!({"query": "hello"}),
405/// ));
406/// callback(tool_ctx as Arc<dyn CallbackContext>).await;
407/// ```
408pub struct ToolCallbackContext {
409 /// The inner callback context to delegate to.
410 pub inner: Arc<dyn CallbackContext>,
411 /// The name of the tool being executed.
412 pub tool_name: String,
413 /// The input arguments for the tool being executed.
414 pub tool_input: serde_json::Value,
415}
416
417impl ToolCallbackContext {
418 /// Creates a new `ToolCallbackContext` wrapping the given inner context.
419 pub fn new(
420 inner: Arc<dyn CallbackContext>,
421 tool_name: String,
422 tool_input: serde_json::Value,
423 ) -> Self {
424 Self { inner, tool_name, tool_input }
425 }
426}
427
428#[async_trait]
429impl ReadonlyContext for ToolCallbackContext {
430 fn invocation_id(&self) -> &str {
431 self.inner.invocation_id()
432 }
433
434 fn agent_name(&self) -> &str {
435 self.inner.agent_name()
436 }
437
438 fn user_id(&self) -> &str {
439 self.inner.user_id()
440 }
441
442 fn app_name(&self) -> &str {
443 self.inner.app_name()
444 }
445
446 fn session_id(&self) -> &str {
447 self.inner.session_id()
448 }
449
450 fn branch(&self) -> &str {
451 self.inner.branch()
452 }
453
454 fn user_content(&self) -> &Content {
455 self.inner.user_content()
456 }
457}
458
459#[async_trait]
460impl CallbackContext for ToolCallbackContext {
461 fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
462 self.inner.artifacts()
463 }
464
465 fn tool_outcome(&self) -> Option<ToolOutcome> {
466 self.inner.tool_outcome()
467 }
468
469 fn tool_name(&self) -> Option<&str> {
470 Some(&self.tool_name)
471 }
472
473 fn tool_input(&self) -> Option<&serde_json::Value> {
474 Some(&self.tool_input)
475 }
476
477 fn shared_state(&self) -> Option<Arc<crate::SharedState>> {
478 self.inner.shared_state()
479 }
480}
481
482/// Full invocation context available to agents during execution.
483///
484/// Extends [`CallbackContext`] with access to the agent itself, memory,
485/// session, and run configuration.
486#[async_trait]
487pub trait InvocationContext: CallbackContext {
488 /// Returns the agent being executed.
489 fn agent(&self) -> Arc<dyn Agent>;
490 /// Returns the memory service, if one is configured.
491 fn memory(&self) -> Option<Arc<dyn Memory>>;
492 /// Returns the current session.
493 fn session(&self) -> &dyn Session;
494 /// Returns the run configuration for this invocation.
495 fn run_config(&self) -> &RunConfig;
496 /// Signals that this invocation should end after the current turn.
497 fn end_invocation(&self);
498 /// Returns whether the invocation has been ended.
499 fn ended(&self) -> bool;
500
501 /// Returns the scopes granted to the current user for this invocation.
502 ///
503 /// When a [`RequestContext`](crate::RequestContext) is present (set by the
504 /// server's auth middleware bridge), this returns the scopes from that
505 /// context. The default returns an empty vec (no scopes granted).
506 fn user_scopes(&self) -> Vec<String> {
507 vec![]
508 }
509
510 /// Returns the request metadata from the auth middleware bridge, if present.
511 ///
512 /// This provides access to custom key-value pairs extracted from the HTTP
513 /// request by the [`RequestContextExtractor`](crate::RequestContext).
514 fn request_metadata(&self) -> HashMap<String, serde_json::Value> {
515 HashMap::new()
516 }
517
518 /// Retrieve a secret by name from the configured secret provider.
519 ///
520 /// Returns `Ok(Some(value))` when a provider is configured and the secret
521 /// exists, `Ok(None)` when no provider is configured, or an error on
522 /// provider failure. The default returns `Ok(None)`.
523 async fn get_secret(&self, _name: &str) -> Result<Option<String>> {
524 Ok(None)
525 }
526}
527
528// Placeholder service traits
529/// Binary artifact storage for agents.
530#[async_trait]
531pub trait Artifacts: Send + Sync {
532 /// Saves a binary artifact and returns its version number.
533 async fn save(&self, name: &str, data: &crate::Part) -> Result<i64>;
534 /// Loads a binary artifact by name.
535 async fn load(&self, name: &str) -> Result<crate::Part>;
536 /// Lists all artifact names.
537 async fn list(&self) -> Result<Vec<String>>;
538}
539
540/// Semantic memory search for agents.
541#[async_trait]
542pub trait Memory: Send + Sync {
543 /// Searches memory for entries matching the query.
544 async fn search(&self, query: &str) -> Result<Vec<MemoryEntry>>;
545
546 /// Verify backend connectivity.
547 ///
548 /// The default implementation succeeds, which is suitable for in-memory
549 /// implementations and adapters without an external dependency.
550 async fn health_check(&self) -> Result<()> {
551 Ok(())
552 }
553
554 /// Add a single memory entry.
555 ///
556 /// The default implementation returns an "not implemented" error, which is
557 /// suitable for read-only memory backends.
558 async fn add(&self, entry: MemoryEntry) -> Result<()> {
559 let _ = entry;
560 Err(AdkError::memory("add not implemented"))
561 }
562
563 /// Delete entries matching a query. Returns count of deleted entries.
564 ///
565 /// The default implementation returns an "not implemented" error, which is
566 /// suitable for read-only memory backends.
567 async fn delete(&self, query: &str) -> Result<u64> {
568 let _ = query;
569 Err(AdkError::memory("delete not implemented"))
570 }
571
572 /// Search for memories within a specific project.
573 /// Returns global entries + entries for the given project.
574 /// Default delegates to `search` (global-only results).
575 async fn search_in_project(&self, query: &str, project_id: &str) -> Result<Vec<MemoryEntry>> {
576 let _ = project_id;
577 self.search(query).await
578 }
579
580 /// Add a memory entry scoped to a specific project.
581 /// Default delegates to `add` (global entry).
582 async fn add_to_project(&self, entry: MemoryEntry, project_id: &str) -> Result<()> {
583 let _ = project_id;
584 self.add(entry).await
585 }
586}
587
588/// Trait for retrieving secrets at runtime.
589///
590/// This is the core-level abstraction used by [`ToolContext::get_secret`] and
591/// [`InvocationContext::get_secret`]. Concrete implementations (e.g., AWS
592/// Secrets Manager, Azure Key Vault, GCP Secret Manager) live in `adk-auth`
593/// behind feature flags and implement this trait via the `SecretProvider`
594/// adapter.
595///
596/// # Example
597///
598/// ```rust,ignore
599/// use adk_core::SecretService;
600///
601/// struct EnvSecretService;
602///
603/// #[async_trait::async_trait]
604/// impl SecretService for EnvSecretService {
605/// async fn get_secret(&self, name: &str) -> adk_core::Result<String> {
606/// std::env::var(name).map_err(|_| adk_core::AdkError::not_found(
607/// format!("secret '{name}' not found in environment"),
608/// ))
609/// }
610/// }
611/// ```
612#[async_trait]
613pub trait SecretService: Send + Sync {
614 /// Retrieve a secret value by name.
615 ///
616 /// Returns the secret string on success, or an [`AdkError`] on failure.
617 async fn get_secret(&self, name: &str) -> Result<String>;
618}
619
620/// A single entry returned from memory search.
621#[derive(Debug, Clone)]
622pub struct MemoryEntry {
623 /// The content of this memory entry.
624 pub content: Content,
625 /// The author who created this memory entry.
626 pub author: String,
627}
628
629/// Streaming mode for agent responses.
630/// Matches ADK Python/Go specification.
631#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
632pub enum StreamingMode {
633 /// No streaming; responses delivered as complete units.
634 /// Agent collects all chunks internally and yields a single final event.
635 None,
636 /// Server-Sent Events streaming; one-way streaming from server to client.
637 /// Agent yields each chunk as it arrives with stable event ID.
638 #[default]
639 SSE,
640 /// Bidirectional streaming; simultaneous communication in both directions.
641 /// Used for realtime audio/video agents.
642 Bidi,
643}
644
645/// Controls what parts of prior conversation history is received by llmagent
646#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
647pub enum IncludeContents {
648 /// The llmagent operates solely on its current turn (latest user input + any following agent events)
649 None,
650 /// Default - The llmagent receives the relevant conversation history
651 #[default]
652 Default,
653}
654
655/// Decision applied when a tool execution requires human confirmation.
656#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
657#[serde(rename_all = "snake_case")]
658pub enum ToolConfirmationDecision {
659 /// Approve the tool execution.
660 Approve,
661 /// Deny the tool execution.
662 Deny,
663}
664
665/// Policy defining which tools require human confirmation before execution.
666#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
667#[serde(rename_all = "snake_case")]
668pub enum ToolConfirmationPolicy {
669 /// No tool confirmation is required.
670 #[default]
671 Never,
672 /// Every tool call requires confirmation.
673 Always,
674 /// Only the listed tool names require confirmation.
675 PerTool(BTreeSet<String>),
676}
677
678impl ToolConfirmationPolicy {
679 /// Returns true when the given tool name must be confirmed before execution.
680 pub fn requires_confirmation(&self, tool_name: &str) -> bool {
681 match self {
682 Self::Never => false,
683 Self::Always => true,
684 Self::PerTool(tools) => tools.contains(tool_name),
685 }
686 }
687
688 /// Add one tool name to the confirmation policy (converts `Never` to `PerTool`).
689 pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
690 let tool_name = tool_name.into();
691 match &mut self {
692 Self::Never => {
693 let mut tools = BTreeSet::new();
694 tools.insert(tool_name);
695 Self::PerTool(tools)
696 }
697 Self::Always => Self::Always,
698 Self::PerTool(tools) => {
699 tools.insert(tool_name);
700 self
701 }
702 }
703 }
704}
705
706/// Payload describing a tool call awaiting human confirmation.
707#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
708#[serde(rename_all = "camelCase")]
709pub struct ToolConfirmationRequest {
710 /// Name of the tool awaiting confirmation.
711 pub tool_name: String,
712 /// The function call ID from the LLM, if available.
713 #[serde(skip_serializing_if = "Option::is_none")]
714 pub function_call_id: Option<String>,
715 /// Arguments the tool would be called with.
716 pub args: Value,
717}
718
719/// Configuration for a single agent run.
720///
721/// Controls streaming behavior, tool confirmation, caching, transfer targets,
722/// and concurrency settings. Use [`RunConfig::builder()`] to construct from
723/// external crates (struct is `#[non_exhaustive]`).
724#[non_exhaustive]
725#[derive(Debug, Clone)]
726pub struct RunConfig {
727 /// The streaming mode for agent responses.
728 pub streaming_mode: StreamingMode,
729 /// Optional per-tool confirmation decisions for the current run.
730 /// Keys are tool names.
731 pub tool_confirmation_decisions: HashMap<String, ToolConfirmationDecision>,
732 /// Optional cached content name for automatic prompt caching.
733 /// When set by the runner's cache lifecycle manager, agents should attach
734 /// this name to their `GenerateContentConfig` so the LLM provider can
735 /// reuse cached system instructions and tool definitions.
736 pub cached_content: Option<String>,
737 /// Valid agent names this agent can transfer to (parent, peers, children).
738 /// Set by the runner when invoking agents in a multi-agent tree.
739 /// When non-empty, the `transfer_to_agent` tool is injected and validation
740 /// uses this list instead of only checking `sub_agents`.
741 pub transfer_targets: Vec<String>,
742 /// The name of the parent agent, if this agent was invoked via transfer.
743 /// Used by the agent to apply `disallow_transfer_to_parent` filtering.
744 pub parent_agent: Option<String>,
745 /// Enable automatic prompt caching for all providers that support it.
746 ///
747 /// When `true` (the default), the runner enables provider-level caching:
748 /// - Anthropic: sets `prompt_caching = true` on the config
749 /// - Bedrock: sets `prompt_caching = Some(BedrockCacheConfig::default())`
750 /// - OpenAI / DeepSeek: no action needed (caching is automatic)
751 /// - Gemini: handled separately via `ContextCacheConfig`
752 pub auto_cache: bool,
753 /// Maximum number of recent persisted events to load at the start of a run.
754 ///
755 /// `None` preserves the previous behavior and loads the full session
756 /// history. Set this for chat surfaces that already summarize older turns
757 /// and need predictable startup latency.
758 pub history_max_events: Option<usize>,
759 /// Tool concurrency configuration controlling parallel tool dispatch limits,
760 /// per-tool overrides, and backpressure behavior.
761 ///
762 /// The default (`ToolConcurrencyConfig::default()`) imposes no limits,
763 /// preserving backward compatibility with the previous `max_tool_concurrency: None`.
764 pub tool_concurrency: ToolConcurrencyConfig,
765 /// Whether tracing spans may include full request, response, and tool
766 /// payloads when the `record-payloads` crate feature is enabled.
767 pub record_payloads: bool,
768 /// Maximum serialized bytes recorded for tracing payload fields when full
769 /// payload recording is disabled.
770 pub trace_payload_max_bytes: usize,
771}
772
773impl Default for RunConfig {
774 fn default() -> Self {
775 Self {
776 streaming_mode: StreamingMode::SSE,
777 tool_confirmation_decisions: HashMap::new(),
778 cached_content: None,
779 transfer_targets: Vec::new(),
780 parent_agent: None,
781 auto_cache: true,
782 history_max_events: None,
783 tool_concurrency: ToolConcurrencyConfig::default(),
784 record_payloads: false,
785 trace_payload_max_bytes: 2048,
786 }
787 }
788}
789
790impl RunConfig {
791 /// Creates a new [`RunConfigBuilder`] initialized with default values.
792 ///
793 /// Use the builder to construct a `RunConfig` when struct literal syntax
794 /// is unavailable (e.g., from external crates due to `#[non_exhaustive]`).
795 ///
796 /// # Example
797 ///
798 /// ```rust
799 /// use adk_core::{RunConfig, StreamingMode};
800 ///
801 /// let config = RunConfig::builder()
802 /// .streaming_mode(StreamingMode::None)
803 /// .auto_cache(false)
804 /// .build();
805 ///
806 /// assert_eq!(config.streaming_mode, StreamingMode::None);
807 /// assert!(!config.auto_cache);
808 /// ```
809 pub fn builder() -> RunConfigBuilder {
810 RunConfigBuilder::default()
811 }
812}
813
814/// Builder for [`RunConfig`].
815///
816/// Provides a fluent API for constructing `RunConfig` instances. All fields
817/// start with their default values and can be overridden individually.
818///
819/// # Example
820///
821/// ```rust
822/// use adk_core::{RunConfig, RunConfigBuilder, StreamingMode, ToolConcurrencyConfig};
823///
824/// let config = RunConfigBuilder::default()
825/// .streaming_mode(StreamingMode::Bidi)
826/// .history_max_events(Some(50))
827/// .build();
828/// ```
829#[derive(Debug, Clone, Default)]
830pub struct RunConfigBuilder {
831 config: RunConfig,
832}
833
834impl RunConfigBuilder {
835 /// Sets the streaming mode for the run.
836 pub fn streaming_mode(mut self, mode: StreamingMode) -> Self {
837 self.config.streaming_mode = mode;
838 self
839 }
840
841 /// Sets per-tool confirmation decisions for the current run.
842 pub fn tool_confirmation_decisions(
843 mut self,
844 decisions: HashMap<String, ToolConfirmationDecision>,
845 ) -> Self {
846 self.config.tool_confirmation_decisions = decisions;
847 self
848 }
849
850 /// Sets the cached content name for automatic prompt caching.
851 pub fn cached_content(mut self, name: impl Into<String>) -> Self {
852 self.config.cached_content = Some(name.into());
853 self
854 }
855
856 /// Sets the valid agent names this agent can transfer to.
857 pub fn transfer_targets(mut self, targets: Vec<String>) -> Self {
858 self.config.transfer_targets = targets;
859 self
860 }
861
862 /// Sets the parent agent name.
863 pub fn parent_agent(mut self, name: impl Into<String>) -> Self {
864 self.config.parent_agent = Some(name.into());
865 self
866 }
867
868 /// Enables or disables automatic prompt caching for supported providers.
869 pub fn auto_cache(mut self, enabled: bool) -> Self {
870 self.config.auto_cache = enabled;
871 self
872 }
873
874 /// Sets the maximum number of recent persisted events to load at run start.
875 pub fn history_max_events(mut self, max: Option<usize>) -> Self {
876 self.config.history_max_events = max;
877 self
878 }
879
880 /// Sets the tool concurrency configuration.
881 pub fn tool_concurrency(mut self, config: ToolConcurrencyConfig) -> Self {
882 self.config.tool_concurrency = config;
883 self
884 }
885
886 /// Enables or disables full payload recording in tracing spans.
887 pub fn record_payloads(mut self, enabled: bool) -> Self {
888 self.config.record_payloads = enabled;
889 self
890 }
891
892 /// Sets the maximum serialized bytes for tracing payload fields.
893 pub fn trace_payload_max_bytes(mut self, max: usize) -> Self {
894 self.config.trace_payload_max_bytes = max;
895 self
896 }
897
898 /// Consumes the builder and returns the configured [`RunConfig`].
899 pub fn build(self) -> RunConfig {
900 self.config
901 }
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907
908 #[test]
909 fn test_run_config_default() {
910 let config = RunConfig::default();
911 assert_eq!(config.streaming_mode, StreamingMode::SSE);
912 assert_eq!(config.history_max_events, None);
913 assert_eq!(config.tool_concurrency.max_concurrency, None);
914 assert!(config.tool_concurrency.per_tool.is_empty());
915 assert_eq!(config.tool_concurrency.backpressure, BackpressurePolicy::Queue);
916 assert!(!config.record_payloads);
917 assert_eq!(config.trace_payload_max_bytes, 2048);
918 assert!(config.tool_confirmation_decisions.is_empty());
919 }
920
921 #[test]
922 fn test_streaming_mode() {
923 assert_eq!(StreamingMode::SSE, StreamingMode::SSE);
924 assert_ne!(StreamingMode::SSE, StreamingMode::None);
925 assert_ne!(StreamingMode::None, StreamingMode::Bidi);
926 }
927
928 #[test]
929 fn test_tool_confirmation_policy() {
930 let policy = ToolConfirmationPolicy::default();
931 assert!(!policy.requires_confirmation("search"));
932
933 let policy = policy.with_tool("search");
934 assert!(policy.requires_confirmation("search"));
935 assert!(!policy.requires_confirmation("write_file"));
936
937 assert!(ToolConfirmationPolicy::Always.requires_confirmation("any_tool"));
938 }
939
940 #[test]
941 fn test_validate_state_key_valid() {
942 assert!(validate_state_key("user_name").is_ok());
943 assert!(validate_state_key("app:config").is_ok());
944 assert!(validate_state_key("temp:data").is_ok());
945 assert!(validate_state_key("a").is_ok());
946 }
947
948 #[test]
949 fn test_validate_state_key_empty() {
950 assert_eq!(validate_state_key(""), Err("state key must not be empty"));
951 }
952
953 #[test]
954 fn test_validate_state_key_too_long() {
955 let long_key = "a".repeat(MAX_STATE_KEY_LEN + 1);
956 assert!(validate_state_key(&long_key).is_err());
957 }
958
959 #[test]
960 fn test_validate_state_key_path_traversal() {
961 assert!(validate_state_key("../etc/passwd").is_err());
962 assert!(validate_state_key("foo/bar").is_err());
963 assert!(validate_state_key("foo\\bar").is_err());
964 assert!(validate_state_key("..").is_err());
965 }
966
967 #[test]
968 fn test_validate_state_key_null_byte() {
969 assert!(validate_state_key("foo\0bar").is_err());
970 }
971
972 #[test]
973 fn test_run_config_builder_defaults() {
974 let config = RunConfig::builder().build();
975 let default = RunConfig::default();
976 assert_eq!(config.streaming_mode, default.streaming_mode);
977 assert_eq!(config.auto_cache, default.auto_cache);
978 assert_eq!(config.history_max_events, default.history_max_events);
979 assert_eq!(config.record_payloads, default.record_payloads);
980 assert_eq!(config.trace_payload_max_bytes, default.trace_payload_max_bytes);
981 assert!(config.tool_confirmation_decisions.is_empty());
982 assert!(config.transfer_targets.is_empty());
983 assert!(config.cached_content.is_none());
984 assert!(config.parent_agent.is_none());
985 }
986
987 #[test]
988 fn test_run_config_builder_all_fields() {
989 let mut decisions = HashMap::new();
990 decisions.insert("delete".to_string(), ToolConfirmationDecision::Approve);
991
992 let config = RunConfig::builder()
993 .streaming_mode(StreamingMode::None)
994 .tool_confirmation_decisions(decisions.clone())
995 .cached_content("my-cache")
996 .transfer_targets(vec!["agent_a".to_string(), "agent_b".to_string()])
997 .parent_agent("parent")
998 .auto_cache(false)
999 .history_max_events(Some(50))
1000 .tool_concurrency(ToolConcurrencyConfig {
1001 max_concurrency: Some(4),
1002 per_tool: HashMap::new(),
1003 backpressure: BackpressurePolicy::Fail,
1004 })
1005 .record_payloads(true)
1006 .trace_payload_max_bytes(4096)
1007 .build();
1008
1009 assert_eq!(config.streaming_mode, StreamingMode::None);
1010 assert_eq!(config.tool_confirmation_decisions, decisions);
1011 assert_eq!(config.cached_content.as_deref(), Some("my-cache"));
1012 assert_eq!(config.transfer_targets, vec!["agent_a", "agent_b"]);
1013 assert_eq!(config.parent_agent.as_deref(), Some("parent"));
1014 assert!(!config.auto_cache);
1015 assert_eq!(config.history_max_events, Some(50));
1016 assert_eq!(config.tool_concurrency.max_concurrency, Some(4));
1017 assert_eq!(config.tool_concurrency.backpressure, BackpressurePolicy::Fail);
1018 assert!(config.record_payloads);
1019 assert_eq!(config.trace_payload_max_bytes, 4096);
1020 }
1021}