Skip to main content

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}