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}