adk_managed/runtime.rs
1//! Core trait and handle types for the managed agent runtime.
2//!
3//! The [`ManagedAgentRuntime`] trait defines the full lifecycle interface for
4//! managed agents: create agents from declarative definitions, start sessions,
5//! send/receive events, pause/resume/interrupt, and archive.
6//!
7//! # Architecture
8//!
9//! The runtime is a **library**, not a service. The platform hosts it.
10//! This trait is provider-agnostic — it behaves identically for Gemini, OpenAI,
11//! Anthropic, Ollama, and OpenAI-compatible providers.
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use adk_managed::runtime::{ManagedAgentRuntime, AgentHandle, SessionHandle};
17//! use adk_managed::types::ManagedAgentDef;
18//!
19//! async fn example(runtime: &dyn ManagedAgentRuntime) {
20//! let def = ManagedAgentDef::default();
21//! let agent = runtime.create(def).await.unwrap();
22//! let session = runtime.start_session(&agent, &ManagedOwner::new("app", "user").unwrap(), None).await.unwrap();
23//! let status = runtime.status(&session).await.unwrap();
24//! println!("Session status: {status:?}");
25//! }
26//! ```
27
28use std::collections::HashMap;
29
30use async_trait::async_trait;
31use futures::stream::BoxStream;
32use serde::{Deserialize, Serialize};
33
34use crate::types::{ManagedAgentDef, RuntimeError, SessionEvent, SessionStatus, UserEvent};
35
36// ─── Handle Types ────────────────────────────────────────────────────────────
37
38/// Opaque agent handle.
39///
40/// The platform assigns the user-facing `agt_` prefixed ID; the runtime uses
41/// this internal handle for lookups. The inner string is an implementation detail.
42///
43/// # Example
44///
45/// ```
46/// use adk_managed::runtime::AgentHandle;
47///
48/// let handle = AgentHandle("agent_abc123".to_string());
49/// assert_eq!(handle.0, "agent_abc123");
50/// ```
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub struct AgentHandle(pub String);
53
54/// Opaque session handle.
55///
56/// Identifies an active or archived session within the runtime. The inner string
57/// is an implementation detail assigned by the runtime on session creation.
58///
59/// # Example
60///
61/// ```
62/// use adk_managed::runtime::SessionHandle;
63///
64/// let handle = SessionHandle("session_xyz789".to_string());
65/// assert_eq!(handle.0, "session_xyz789");
66/// ```
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
68pub struct SessionHandle(pub String);
69
70/// Who a managed session belongs to.
71///
72/// Every managed session was previously persisted under the constants `managed` /
73/// `managed_user`, so all sessions shared one logical namespace: session lookup, memory, and
74/// deletion could not be scoped to a caller, and audit could not attribute a session to
75/// anyone. The runtime needs this at creation because the underlying `SessionService` is
76/// addressed by app and user, and substituting constants to satisfy that contract is what
77/// erased the caller.
78///
79/// # Example
80///
81/// ```rust
82/// use adk_managed::ManagedOwner;
83///
84/// let owner = ManagedOwner::new("support-console", "user-42")?;
85/// assert_eq!(owner.app_name(), "support-console");
86///
87/// // Blank components are rejected, since they would silently re-create a shared namespace.
88/// assert!(ManagedOwner::new("", "user-42").is_err());
89/// # Ok::<(), adk_managed::types::RuntimeError>(())
90/// ```
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
92pub struct ManagedOwner {
93 app_name: String,
94 user_id: String,
95}
96
97impl ManagedOwner {
98 /// Validates and builds an owner identity.
99 ///
100 /// # Errors
101 ///
102 /// Returns [`RuntimeError::InvalidRequest`] when either component is empty or whitespace.
103 pub fn new(
104 app_name: impl Into<String>,
105 user_id: impl Into<String>,
106 ) -> Result<Self, RuntimeError> {
107 let app_name = app_name.into();
108 let user_id = user_id.into();
109
110 if app_name.trim().is_empty() {
111 return Err(RuntimeError::InvalidRequest {
112 message: "a managed session needs a non-empty app name to be addressable and \
113 auditable"
114 .to_string(),
115 param: Some("app_name".to_string()),
116 });
117 }
118 if user_id.trim().is_empty() {
119 return Err(RuntimeError::InvalidRequest {
120 message: "a managed session needs a non-empty user id; without one, sessions \
121 share a namespace and cannot be scoped to a caller"
122 .to_string(),
123 param: Some("user_id".to_string()),
124 });
125 }
126
127 Ok(Self { app_name, user_id })
128 }
129
130 /// The app this session belongs to.
131 pub fn app_name(&self) -> &str {
132 &self.app_name
133 }
134
135 /// The user this session belongs to.
136 pub fn user_id(&self) -> &str {
137 &self.user_id
138 }
139}
140
141// ─── EnvironmentConfig ───────────────────────────────────────────────────────
142
143/// Optional environment configuration for a session.
144///
145/// Provides environment variables and working directory context that the agent
146/// session can use during execution (e.g., for sandbox or tool execution).
147///
148/// # Example
149///
150/// ```
151/// use adk_managed::runtime::EnvironmentConfig;
152///
153/// let env = EnvironmentConfig {
154/// env_vars: [("API_KEY".to_string(), "secret".to_string())].into(),
155/// working_dir: Some("/workspace".to_string()),
156/// };
157/// assert_eq!(env.env_vars.get("API_KEY").unwrap(), "secret");
158/// ```
159#[derive(Debug, Clone, Default, Serialize, Deserialize)]
160pub struct EnvironmentConfig {
161 /// Environment variables available to the agent.
162 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
163 pub env_vars: HashMap<String, String>,
164
165 /// Optional working directory for the session.
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub working_dir: Option<String>,
168}
169
170// ─── ManagedAgentRuntime Trait ───────────────────────────────────────────────
171
172/// The central async trait defining the managed agent lifecycle.
173///
174/// Implementations of this trait encapsulate the full agent lifecycle:
175/// creating agents from declarative definitions, starting durable sessions,
176/// sending/receiving events, pausing/resuming, interrupting, and archiving.
177///
178/// The trait is provider-agnostic: it takes a [`ManagedAgentDef`] with a
179/// [`ModelRef`](crate::types::ModelRef) and behaves identically regardless
180/// of which LLM provider powers the agent.
181///
182/// # Implementors
183///
184/// - `DefaultManagedAgentRuntime` — the default implementation
185/// composed from `Runner` + pluggable `SessionService` + optional sandbox/memory.
186///
187/// # Design Notes
188///
189/// - All methods return `Result<_, RuntimeError>` for structured error handling.
190/// - `stream_events` returns a `BoxStream` for SSE-compatible event delivery.
191/// - `from_seq` on `stream_events` enables `Last-Event-ID` reconnection.
192/// - The runtime is `Send + Sync` for use across async task boundaries.
193#[async_trait]
194pub trait ManagedAgentRuntime: Send + Sync {
195 /// Create a managed agent from a declarative definition.
196 ///
197 /// Resolves the [`ModelRef`](crate::types::ModelRef), builds a runnable
198 /// agent, and stores it in the internal registry. Returns an opaque handle
199 /// for use with `start_session`.
200 async fn create(&self, def: ManagedAgentDef) -> Result<AgentHandle, RuntimeError>;
201
202 /// Start a new session for the given agent.
203 ///
204 /// Creates a session in `Queued` status, initializes the session loop,
205 /// and returns a handle for event interaction. The optional
206 /// [`EnvironmentConfig`] provides env vars and working directory.
207 async fn start_session(
208 &self,
209 agent: &AgentHandle,
210 owner: &ManagedOwner,
211 env: Option<EnvironmentConfig>,
212 ) -> Result<SessionHandle, RuntimeError>;
213
214 /// Send an event from the client to the agent session.
215 ///
216 /// Dispatches the [`UserEvent`] to the session loop. The event type
217 /// determines behavior:
218 /// - `user.message` — enqueues a message for processing
219 /// - `user.interrupt` — signals the session to stop at next boundary
220 /// - `user.custom_tool_result` — delivers a result to a parked tool call
221 /// - `user.tool_confirmation` — approves or denies a pending tool use
222 async fn send_event(
223 &self,
224 session: &SessionHandle,
225 event: UserEvent,
226 ) -> Result<(), RuntimeError>;
227
228 /// Subscribe to the session's event stream.
229 ///
230 /// Returns a stream of [`SessionEvent`]s. If `from_seq` is provided,
231 /// replays all events with `seq > from_seq` before attaching to the
232 /// live broadcast (enabling SSE `Last-Event-ID` reconnection).
233 async fn stream_events(
234 &self,
235 session: &SessionHandle,
236 from_seq: Option<u64>,
237 ) -> Result<BoxStream<'static, SessionEvent>, RuntimeError>;
238
239 /// Interrupt the session at the next safe boundary.
240 ///
241 /// Signals the session loop's cancellation token. The loop will stop
242 /// processing at the next inter-event boundary and emit `status.idle`.
243 async fn interrupt(&self, session: &SessionHandle) -> Result<(), RuntimeError>;
244
245 /// Pause the session, checkpointing current state.
246 ///
247 /// Stops consuming new input and persists the current run-state.
248 /// The session transitions to `Paused` status.
249 async fn pause(&self, session: &SessionHandle) -> Result<(), RuntimeError>;
250
251 /// Resume a paused session from its last checkpoint.
252 ///
253 /// Clears the pause flag, rehydrates state if needed, and returns
254 /// the session to active processing.
255 async fn resume(&self, session: &SessionHandle) -> Result<(), RuntimeError>;
256
257 /// Query the current status of a session.
258 async fn status(&self, session: &SessionHandle) -> Result<SessionStatus, RuntimeError>;
259
260 /// Archive a session (terminal state).
261 ///
262 /// Sets the session to `Archived` status and stops the session loop.
263 /// Archived sessions retain their event log for read access.
264 async fn archive(&self, session: &SessionHandle) -> Result<(), RuntimeError>;
265
266 /// Delete a session and its associated data.
267 ///
268 /// Archives the session (if not already terminal) and removes all
269 /// persisted data including events and checkpoints.
270 async fn delete_session(&self, session: &SessionHandle) -> Result<(), RuntimeError>;
271}