# adk-managed
Managed agent runtime for ADK-Rust — a provider-neutral, durable, resumable agent execution engine.
[](https://crates.io/crates/adk-managed)
[](https://docs.rs/adk-managed)
[](LICENSE)
## Overview
`adk-managed` provides the `ManagedAgentRuntime` trait and its default implementation. It takes a declarative `ManagedAgentDef`, builds a runnable agent, and operates it as a durable, resumable, event-streaming background session. The runtime composes existing shipping components behind a unified lifecycle trait.
This is the execution engine inside a managed agent service. It is a **library**, not a service — the platform hosts it.
## Key Capabilities
- **Provider-neutral**: Identical event sequences regardless of LLM provider (Gemini, OpenAI, Anthropic, Ollama, OpenAI-compatible)
- **Durable sessions**: Checkpoint after every event; survive process crashes with zero event loss
- **Resumable**: Rehydrate from checkpoint and continue from last consistent state
- **Event streaming**: Uniform `SessionEvent` stream with monotonic sequence numbers and SSE replay support
- **Custom tool parking**: Client-executed tools park the loop until results arrive (or timeout)
- **Composable**: Injected services (sessions, sandbox, memory) — no platform dependencies
- **Additive**: Feature-gated; existing `Runner`/`LlmAgent` unchanged when feature is off
## Architecture
```text
┌─────────────────────────────────────────────────────────────┐
│ Platform Layer (ep-* crates) │
│ HTTP Routes │ Auth │ Billing │ Multi-tenancy │
└──────────────────────────┬──────────────────────────────────┘
│ Rust trait calls (in-process)
▼
┌─────────────────────────────────────────────────────────────┐
│ Runtime Layer (adk-managed — this crate) │
│ │
│ ManagedAgentRuntime trait + DefaultManagedAgentRuntime │
│ ─────────────────────────────────────────────────── │
│ • Builds runnable agents from ManagedAgentDef │
│ • Runs supervised session loop (durable, resumable) │
│ • Emits provider-neutral SessionEvent stream │
│ • Manages custom tool parking, checkpoints, interrupts │
│ • Resolves ModelRef → Arc<dyn Llm> │
│ │
│ Composes existing crates: │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │adk-runner│ │adk-session│ │adk-model │ │adk-tool │ │
│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Quick Start
Add to your `Cargo.toml`:
```toml
[dependencies]
adk-managed = "1.0"
adk-session = "1.0"
adk-core = "1.0"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
async-trait = "0.1"
```
Or via the umbrella crate:
```toml
[dependencies]
adk-rust = { version = "1.0", features = ["managed-runtime"] }
```
### Minimal Example
```rust,ignore
use std::sync::Arc;
use adk_managed::{
DefaultManagedAgentRuntime, ManagedAgentRuntime, ModelResolver,
ScriptedLlm, ScriptedTurn,
resolver::ResolverResult,
types::{ContentBlock, ManagedAgentDef, ModelRef, UserEvent},
};
use adk_session::InMemorySessionService;
use async_trait::async_trait;
use futures::StreamExt;
// A resolver that returns a scripted LLM (no API key needed)
struct MockResolver { llm: Arc<dyn adk_core::Llm> }
#[async_trait]
impl ModelResolver for MockResolver {
async fn resolve(&self, _: &ModelRef) -> ResolverResult<Arc<dyn adk_core::Llm>> {
Ok(self.llm.clone())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create a scripted LLM (deterministic, offline, $0)
let llm = Arc::new(ScriptedLlm::new("test-model", vec![
ScriptedTurn { text: Some("Hello!".into()), tool_calls: vec![] },
]));
// 2. Build the runtime
let runtime = DefaultManagedAgentRuntime::new(
Arc::new(MockResolver { llm }),
Arc::new(InMemorySessionService::new()),
);
// 3. Create an agent from a declarative definition
let def = ManagedAgentDef::new("my-agent", ModelRef::Shorthand("test-model".into()))
.with_system("You are helpful.");
let agent = runtime.create(def).await?;
// 4. Start a session (initial status: Queued)
let session = runtime.start_session(&agent, None).await?;
// 5. Subscribe to events and send a message
let mut stream = runtime.stream_events(&session, None).await?;
runtime.send_event(&session, UserEvent::Message {
content: vec![ContentBlock::Text { text: "Hi!".into() }],
}).await?;
// 6. Collect events: status.running → agent.message → status.idle
while let Some(event) = stream.next().await {
println!("{event:?}");
}
Ok(())
}
```
## Core Types
### ManagedAgentRuntime Trait
The central async trait defining the full agent lifecycle:
| `create(def)` | Register an agent definition → `AgentHandle` |
| `start_session(agent, env?)` | Start a new session (initial status: `Queued`) |
| `send_event(session, event)` | Send a `UserEvent` to the session loop |
| `stream_events(session, from_seq?)` | Subscribe to `SessionEvent` stream |
| `interrupt(session)` | Stop at next boundary, emit `status.idle` |
| `pause(session)` | Checkpoint and pause processing |
| `resume(session)` | Resume from pause or process restart |
| `status(session)` | Query current `SessionStatus` |
| `archive(session)` | Terminal state (data retained for read) |
| `delete_session(session)` | Remove session and its data |
### ManagedAgentDef
Declarative agent definition with a builder API:
```rust,ignore
let def = ManagedAgentDef::new("my-agent", ModelRef::Shorthand("gemini-2.5-flash".into()))
.with_system("You are a helpful assistant.")
.with_description("Research agent with web search")
.with_tools(vec![ToolConfig::WebSearch {}]);
```
### SessionEvent (Agent → Client)
Provider-neutral event stream with monotonic `seq`:
| `agent.message` | Assistant text content |
| `agent.tool_use` | Built-in tool invocation (server-side) |
| `agent.custom_tool_use` | Client-executed custom tool (loop parks) |
| `agent.mcp_tool_use` | MCP tool invocation |
| `status.running` | Turn started |
| `status.idle` | Turn complete (includes `stop_reason`) |
| `error` | Execution error |
### UserEvent (Client → Agent)
| `user.message` | Send content to the agent |
| `user.interrupt` | Stop the current turn |
| `user.tool_confirmation` | Allow/deny tool execution |
| `user.custom_tool_result` | Return custom tool results |
| `user.tool_result` | Built-in tool result (self-hosted only) |
| `user.define_outcome` | Set success criteria |
### ModelRef
Provider-neutral model reference:
```rust,ignore
// Shorthand (provider inferred from name prefix)
ModelRef::Shorthand("gemini-2.5-flash".into()) // → Gemini
ModelRef::Shorthand("gpt-4.1".into()) // → OpenAI
ModelRef::Shorthand("claude-3.5-sonnet".into()) // → Anthropic
// Structured (explicit provider)
ModelRef::Structured {
provider: Provider::OpenaiCompatible,
model: ModelConfig::Compatible {
model: "deepseek-chat".into(),
base_url: "https://api.deepseek.com/v1".into(),
api_key: "sk-...".into(),
},
speed: None,
}
```
### SessionStatus
Lifecycle state machine:
```text
Queued → Running → Idle (per turn) → Running (next turn)
→ Rescheduling → Running (retry success) | Failed (exhaust)
→ Paused → Running (on resume)
→ Completed / Failed / Archived
```
## Features
### Durable Sessions
Every event is checkpointed atomically. On process crash, `resume()` rehydrates from the last consistent checkpoint with no event loss.
### Custom Tool Parking
When the agent emits `agent.custom_tool_use`, the session loop parks until the client sends `user.custom_tool_result` or a configurable timeout elapses (default: 5 minutes).
### Event Replay (SSE Reconnection)
```rust,ignore
// Reconnect from seq 42 — replays events 43, 44, ... then live tail
let stream = runtime.stream_events(&session, Some(42)).await?;
```
### Provider Parity
An identical `ManagedAgentDef` run against all five providers produces byte-identical event type sequences (verified by golden fixture F-8).
## Testing with ScriptedLlm
`ScriptedLlm` is a deterministic LLM test double that exercises the full runtime pipeline. Only the provider API call is replaced:
```rust,ignore
use adk_managed::testing::{ScriptedLlm, ScriptedTurn, ScriptedToolCall};
use serde_json::json;
let llm = ScriptedLlm::new("test", vec![
ScriptedTurn {
text: Some("I'll search for that.".into()),
tool_calls: vec![ScriptedToolCall {
name: "web_search".into(),
input: json!({"query": "rust agents"}),
id: Some("tc_1".into()),
}],
},
ScriptedTurn {
text: Some("Here are the results...".into()),
tool_calls: vec![],
},
]);
```
This is NOT a mock — it implements the real `Llm` trait and exercises the full runtime (parking, checkpoints, replay, event mapping). Per-commit gate, $0 cost.
## Golden Fixture Tests
Eight fixture JSON files (F-1 through F-8) define conformance scenarios:
| F-1 Hello | Basic message → response → idle |
| F-2 MCP Tool | MCP tool call flow |
| F-3 Custom Tool | Park → deliver → resume |
| F-4 Confirmation | Tool confirmation request → approve |
| F-5 Resume | Crash → resume from checkpoint |
| F-6 Replay | Historical event replay |
| F-7 Interrupt | Interrupt stops at boundary |
| F-8 Provider Parity | Identical sequences across providers |
Run conformance tests:
```bash
cargo test -p adk-managed --test fixture_conformance_tests
```
## Module Structure
```
adk-managed/src/
├── lib.rs # Feature gate, exports
├── runtime.rs # ManagedAgentRuntime trait
├── default_runtime.rs # DefaultManagedAgentRuntime implementation
├── types/
│ ├── mod.rs # Re-exports
│ ├── agent_def.rs # ManagedAgentDef, builder
│ ├── content.rs # ContentBlock
│ ├── model_ref.rs # ModelRef, Provider, ModelConfig
│ ├── tools.rs # ToolConfig, McpServerConfig, SkillRef, PermissionPolicy
│ ├── events.rs # UserEvent, SessionEvent, StopReason
│ ├── session.rs # SessionStatus
│ └── error.rs # RuntimeError
├── resolver.rs # ModelRef → Arc<dyn Llm>
├── agent_builder.rs # ManagedAgentDef → runnable agent
├── session_loop.rs # Supervised loop: run turns, park, checkpoint
├── parking.rs # Custom tool parking (channel-based wait)
├── checkpoint.rs # Atomic event+state persistence
├── sequence.rs # Monotonic seq counter per session
├── replay.rs # Event replay with from_seq
├── event_mapping.rs # Provider-neutral Runner → SessionEvent mapping
├── schema_normalization.rs # Cross-provider MCP schema normalization
├── usage.rs # Uniform usage reporting
└── testing.rs # ScriptedLlm, ScriptedTurn, ScriptedToolCall
```
## Feature Flags
| `sandbox` | Enable `adk-sandbox` integration for isolated tool execution |
| `memory` | Enable `adk-memory` integration for cross-session memory |
| `full` | Enable all optional features |
## Smoke Test Example
A standalone example crate is provided:
```bash
cargo run --manifest-path examples/managed_runtime_hello/Cargo.toml
```
Runs fixture F-1 end-to-end with `ScriptedLlm` (no API key required). Platform teams can clone this to smoke-test integration.
## Stability
> **EXPERIMENTAL** — This crate is additive and feature-gated behind `managed-runtime` on the umbrella crate. It does not affect existing `Runner`/`LlmAgent` APIs when disabled. The API surface may change in future releases.
## License
Apache-2.0
## Part of ADK-Rust
This crate is part of the [ADK-Rust](https://github.com/zavora-ai/adk-rust) framework for building AI agents in Rust.