Skip to main content

agent_sdk_tools/
seed.rs

1//! Durable reconstruction types for worker-context recovery.
2//!
3//! [`ToolContextSeed`] captures the durable state a worker needs to
4//! reconstruct a [`crate::tools::ToolContext`] deterministically.  Host-provided runtime
5//! dependencies are represented by [`HostDependencies`], and the
6//! [`ExecutionContextFactory`] trait abstracts how a host combines these
7//! to build a ready-to-use context.
8//!
9//! ## Design rationale
10//!
11//! The server must rebuild `ToolContext` from two sources:
12//!
13//! 1. **Durable task state** — thread identity, turn number, event-sequence
14//!    offset, user-defined metadata.  These survive restarts and are stored
15//!    in the task / thread record.  [`ToolContextSeed`] models this.
16//!
17//! 2. **Host-provided runtime deps** — event store, event authority,
18//!    cancellation tokens, concurrency limiters.  These are created fresh
19//!    by the orchestration layer each time a worker is started.
20//!    [`HostDependencies`] models this.
21//!
22//! By keeping these separate the Phase 4 root worker and Phase 5
23//! tool-runtime worker can both depend on this contract directly, and the
24//! server never has to infer context shape from SDK internals.
25
26use crate::stores::EventStore;
27use agent_sdk_foundation::types::ThreadId;
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30use std::sync::Arc;
31use tokio_util::sync::CancellationToken;
32
33// ---------------------------------------------------------------------------
34// Durable reconstruction payload
35// ---------------------------------------------------------------------------
36
37/// Durable inputs needed to reconstruct a [`crate::tools::ToolContext`].
38///
39/// Every field is recoverable from the task / thread record in durable
40/// storage.  The server serialises this when a task is created and
41/// deserialises it when a worker starts (or restarts).
42///
43/// This is the *stable* reconstruction contract — later phases depend on
44/// its shape, so changing it requires a version bump.
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46pub struct ToolContextSeed {
47    /// Thread identity for the current conversation.
48    pub thread_id: ThreadId,
49    /// Turn number within the thread (1-based).
50    pub turn: usize,
51    /// Sequence offset for event ordering continuity across turns.
52    ///
53    /// The event authority uses this to resume numbering where the
54    /// previous turn left off, so events within a thread form a
55    /// single monotonic sequence even across worker restarts.
56    pub sequence_offset: u64,
57    /// Arbitrary key-value metadata forwarded to the tool context.
58    #[serde(default)]
59    pub metadata: HashMap<String, serde_json::Value>,
60}
61
62impl ToolContextSeed {
63    /// Create a seed for a fresh first turn with no prior state.
64    #[must_use]
65    pub fn first_turn(thread_id: ThreadId) -> Self {
66        Self {
67            thread_id,
68            turn: 1,
69            sequence_offset: 0,
70            metadata: HashMap::new(),
71        }
72    }
73
74    /// Builder-style setter for metadata.
75    #[must_use]
76    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
77        self.metadata.insert(key.into(), value);
78        self
79    }
80}
81
82// ---------------------------------------------------------------------------
83// Host-provided runtime dependencies
84// ---------------------------------------------------------------------------
85
86/// Runtime dependencies created by the host when a worker is started.
87///
88/// These are *not* durable — they are constructed fresh each time and do
89/// not survive worker restarts.  The orchestration layer is responsible
90/// for providing them.
91///
92/// Note: the event authority is intentionally absent — [`crate::tools::ToolContext::from_seed`]
93/// constructs it internally from [`ToolContextSeed::sequence_offset`] to
94/// guarantee monotonic sequencing.
95pub struct HostDependencies {
96    /// Authoritative event store for persisting tool-originated events.
97    pub event_store: Arc<dyn EventStore>,
98    /// Token for cooperative cancellation.
99    pub cancel_token: CancellationToken,
100    /// Optional concurrency limiter for subagent spawning.
101    pub subagent_semaphore: Option<Arc<tokio::sync::Semaphore>>,
102}
103
104// ---------------------------------------------------------------------------
105// Factory trait
106// ---------------------------------------------------------------------------
107
108/// Abstraction for how hosts build a [`crate::tools::ToolContext`] from
109/// durable seeds and fresh runtime dependencies.
110///
111/// The SDK provides [`crate::tools::ToolContext::from_seed`] as the default
112/// implementation — hosts can call it directly or implement this trait to
113/// add additional ambient state (e.g. database connections wrapped in the
114/// application context `Ctx`).
115pub trait ExecutionContextFactory<Ctx>: Send + Sync {
116    /// Build a ready-to-use `ToolContext` from the durable seed, the
117    /// application context, and host-provided runtime dependencies.
118    fn build(
119        &self,
120        seed: &ToolContextSeed,
121        app: Ctx,
122        deps: HostDependencies,
123    ) -> crate::tools::ToolContext<Ctx>;
124}
125
126/// Default factory that delegates to [`crate::tools::ToolContext::from_seed`].
127///
128/// Hosts that do not need custom ambient injection can use this directly.
129pub struct DefaultContextFactory;
130
131impl<Ctx> ExecutionContextFactory<Ctx> for DefaultContextFactory {
132    fn build(
133        &self,
134        seed: &ToolContextSeed,
135        app: Ctx,
136        deps: HostDependencies,
137    ) -> crate::tools::ToolContext<Ctx> {
138        crate::tools::ToolContext::from_seed(seed, app, deps)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn seed_first_turn_defaults() {
148        let seed = ToolContextSeed::first_turn(ThreadId::from_string("t-1"));
149        assert_eq!(seed.thread_id, ThreadId::from_string("t-1"));
150        assert_eq!(seed.turn, 1);
151        assert_eq!(seed.sequence_offset, 0);
152        assert!(seed.metadata.is_empty());
153    }
154
155    #[test]
156    fn seed_with_metadata() {
157        let seed = ToolContextSeed::first_turn(ThreadId::new())
158            .with_metadata("user_id", serde_json::json!("u-42"));
159        assert_eq!(
160            seed.metadata.get("user_id"),
161            Some(&serde_json::json!("u-42"))
162        );
163    }
164
165    #[test]
166    fn seed_round_trips_through_json() -> anyhow::Result<()> {
167        let original = ToolContextSeed {
168            thread_id: ThreadId::from_string("t-round-trip"),
169            turn: 5,
170            sequence_offset: 42,
171            metadata: {
172                let mut m = HashMap::new();
173                m.insert("key".into(), serde_json::json!("value"));
174                m
175            },
176        };
177        let json = serde_json::to_string(&original)?;
178        let recovered: ToolContextSeed = serde_json::from_str(&json)?;
179        assert_eq!(recovered, original);
180        Ok(())
181    }
182}