Skip to main content

meerkat_mobkit/identity_first/
types.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6// ---------------------------------------------------------------------------
7// Validation helpers
8// ---------------------------------------------------------------------------
9
10/// Error returned when an identity string fails validation.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct InvalidIdentity {
13    pub input: String,
14    pub reason: String,
15}
16
17impl fmt::Display for InvalidIdentity {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(f, "invalid identity {:?}: {}", self.input, self.reason)
20    }
21}
22
23impl std::error::Error for InvalidIdentity {}
24
25fn validate_identity_string(s: &str) -> Result<(), InvalidIdentity> {
26    if s.is_empty() {
27        return Err(InvalidIdentity {
28            input: s.to_string(),
29            reason: "must not be empty".to_string(),
30        });
31    }
32    if s.contains(char::is_whitespace) {
33        return Err(InvalidIdentity {
34            input: s.to_string(),
35            reason: "must not contain whitespace".to_string(),
36        });
37    }
38    if s.contains('/') {
39        return Err(InvalidIdentity {
40            input: s.to_string(),
41            reason: "must not contain slashes".to_string(),
42        });
43    }
44    Ok(())
45}
46
47// ---------------------------------------------------------------------------
48// Macro for validated string newtypes (AgentIdentity, AgentRuntimeId)
49// ---------------------------------------------------------------------------
50
51macro_rules! validated_string_newtype {
52    ($(#[$meta:meta])* $name:ident) => {
53        $(#[$meta])*
54        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
55        pub struct $name(String);
56
57        impl $name {
58            /// Parse and validate a string into this type.
59            ///
60            /// # Errors
61            ///
62            /// Returns `InvalidIdentity` if the input is empty, contains whitespace,
63            /// or contains slashes.
64            pub fn parse(s: &str) -> Result<Self, InvalidIdentity> {
65                validate_identity_string(s)?;
66                Ok(Self(s.to_string()))
67            }
68
69            #[must_use]
70            pub fn as_str(&self) -> &str {
71                &self.0
72            }
73        }
74
75        impl fmt::Display for $name {
76            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77                f.write_str(&self.0)
78            }
79        }
80
81        impl Serialize for $name {
82            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83            where
84                S: serde::Serializer,
85            {
86                serializer.serialize_str(&self.0)
87            }
88        }
89
90        impl<'de> Deserialize<'de> for $name {
91            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
92            where
93                D: serde::Deserializer<'de>,
94            {
95                let s = String::deserialize(deserializer)?;
96                Self::parse(&s).map_err(serde::de::Error::custom)
97            }
98        }
99    };
100}
101
102validated_string_newtype!(
103    /// The primary app-facing identity handle for all MobKit control-plane operations.
104    AgentIdentity
105);
106
107validated_string_newtype!(
108    /// Internal runtime-level ID minted at first-create.
109    AgentRuntimeId
110);
111
112// ---------------------------------------------------------------------------
113// AgentAddressability
114// ---------------------------------------------------------------------------
115
116/// Whether an agent accepts `send()` (addressable) or only `dispatch()` (internal-only).
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum AgentAddressability {
120    #[default]
121    Addressable,
122    InternalOnly,
123}
124
125// ---------------------------------------------------------------------------
126// DisplayName
127// ---------------------------------------------------------------------------
128
129/// Human-facing display name. Non-empty.
130#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131pub struct DisplayName(String);
132
133impl DisplayName {
134    /// # Errors
135    ///
136    /// Returns `InvalidIdentity` if the input is empty.
137    pub fn parse(s: &str) -> Result<Self, InvalidIdentity> {
138        if s.is_empty() {
139            return Err(InvalidIdentity {
140                input: s.to_string(),
141                reason: "display name must not be empty".to_string(),
142            });
143        }
144        Ok(Self(s.to_string()))
145    }
146
147    #[must_use]
148    pub fn as_str(&self) -> &str {
149        &self.0
150    }
151}
152
153impl fmt::Display for DisplayName {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        f.write_str(&self.0)
156    }
157}
158
159impl Serialize for DisplayName {
160    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
161    where
162        S: serde::Serializer,
163    {
164        serializer.serialize_str(&self.0)
165    }
166}
167
168impl<'de> Deserialize<'de> for DisplayName {
169    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
170    where
171        D: serde::Deserializer<'de>,
172    {
173        let s = String::deserialize(deserializer)?;
174        Self::parse(&s).map_err(serde::de::Error::custom)
175    }
176}
177
178// ---------------------------------------------------------------------------
179// Monotonic u64 newtypes
180// ---------------------------------------------------------------------------
181
182macro_rules! monotonic_u64_newtype {
183    ($(#[$meta:meta])* $name:ident) => {
184        $(#[$meta])*
185        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
186        #[serde(transparent)]
187        pub struct $name(u64);
188
189        impl $name {
190            #[must_use]
191            pub const fn new(value: u64) -> Self {
192                Self(value)
193            }
194
195            #[must_use]
196            pub const fn get(self) -> u64 {
197                self.0
198            }
199        }
200
201        impl fmt::Display for $name {
202            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203                write!(f, "{}", self.0)
204            }
205        }
206    };
207}
208
209monotonic_u64_newtype!(
210    /// Monotonic generation counter for continuity. Starts at 0, incremented by `reset()`.
211    ContinuityGeneration
212);
213
214monotonic_u64_newtype!(
215    /// Monotonic checkpoint counter scoped to `(AgentIdentity, ContinuityGeneration)`.
216    CheckpointVersion
217);
218
219monotonic_u64_newtype!(
220    /// Monotonic ownership token issued by `LeaseProvider`.
221    FencingToken
222);
223
224// ---------------------------------------------------------------------------
225// Lightweight string newtypes (no validation beyond serde)
226// ---------------------------------------------------------------------------
227
228macro_rules! string_newtype {
229    ($(#[$meta:meta])* $name:ident) => {
230        $(#[$meta])*
231        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
232        #[serde(transparent)]
233        pub struct $name(String);
234
235        impl $name {
236            #[must_use]
237            pub fn new(s: impl Into<String>) -> Self {
238                Self(s.into())
239            }
240
241            #[must_use]
242            pub fn as_str(&self) -> &str {
243                &self.0
244            }
245        }
246
247        impl fmt::Display for $name {
248            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249                f.write_str(&self.0)
250            }
251        }
252    };
253}
254
255string_newtype!(
256    /// Correlation ID for dispatch tracing.
257    CorrelationId
258);
259
260string_newtype!(
261    /// Idempotency key for dispatch deduplication.
262    DispatchIdempotencyKey
263);
264
265// ---------------------------------------------------------------------------
266// ContinuityRecord
267// ---------------------------------------------------------------------------
268
269/// The authoritative continuity record for a durable agent identity.
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
271pub struct ContinuityRecord {
272    pub identity: AgentIdentity,
273    pub agent_runtime_id: AgentRuntimeId,
274    pub session_id: meerkat_core::types::SessionId,
275    pub generation: ContinuityGeneration,
276    pub checkpoint_version: CheckpointVersion,
277}
278
279// ---------------------------------------------------------------------------
280// ContinuityFailure + ContinuityFailureKind
281// ---------------------------------------------------------------------------
282
283/// Kind of continuity failure.
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum ContinuityFailureKind {
287    SnapshotMissing,
288    SnapshotCorrupted,
289    GenerationMismatch,
290    StoreUnavailable,
291}
292
293/// A typed failure payload for broken continuity.
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct ContinuityFailure {
296    pub identity: AgentIdentity,
297    pub kind: ContinuityFailureKind,
298    pub record: Option<ContinuityRecord>,
299    pub detail: String,
300}
301
302// ---------------------------------------------------------------------------
303// ContinuityResolveState
304// ---------------------------------------------------------------------------
305
306/// The resolve result for a single identity from the continuity store.
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(rename_all = "snake_case", tag = "state")]
309pub enum ContinuityResolveState {
310    Uninitialized,
311    Ready { record: ContinuityRecord },
312    Broken { failure: ContinuityFailure },
313}
314
315// ---------------------------------------------------------------------------
316// LeaseGrant
317// ---------------------------------------------------------------------------
318
319/// A lease grant returned by `LeaseProvider`.
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321pub struct LeaseGrant {
322    pub identity: AgentIdentity,
323    pub fencing_token: FencingToken,
324    #[serde(
325        serialize_with = "serde_duration_ms::serialize",
326        deserialize_with = "serde_duration_ms::deserialize"
327    )]
328    pub ttl: std::time::Duration,
329}
330
331/// Custom serde for `Duration` as integer milliseconds.
332mod serde_duration_ms {
333    use serde::{Deserialize, Deserializer, Serializer};
334    use std::time::Duration;
335
336    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
337    where
338        S: Serializer,
339    {
340        let ms = duration.as_millis();
341        // u128 -> u64 is safe for any reasonable TTL
342        serializer.serialize_u64(ms as u64)
343    }
344
345    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
346    where
347        D: Deserializer<'de>,
348    {
349        let ms = u64::deserialize(deserializer)?;
350        Ok(Duration::from_millis(ms))
351    }
352}
353
354// ---------------------------------------------------------------------------
355// LeaseAcquireResult + LeaseRenewResult
356// ---------------------------------------------------------------------------
357
358/// Result of a lease acquisition attempt.
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(rename_all = "snake_case", tag = "result")]
361pub enum LeaseAcquireResult {
362    Acquired(LeaseGrant),
363    AlreadyHeld {
364        identity: AgentIdentity,
365        holder: String,
366    },
367}
368
369/// Result of a lease renewal attempt.
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "snake_case", tag = "result")]
372pub enum LeaseRenewResult {
373    Renewed(LeaseGrant),
374    Lost { identity: AgentIdentity },
375}
376
377// ---------------------------------------------------------------------------
378// DispatchOrigin + DispatchInput
379// ---------------------------------------------------------------------------
380
381/// Origin of a dispatch request.
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub enum DispatchOrigin {
385    Connector,
386    Scheduler,
387    Policy,
388    Flow,
389    System,
390}
391
392/// Input for a dispatch operation.
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
394pub struct DispatchInput {
395    pub content: meerkat_core::ContentInput,
396    pub origin: DispatchOrigin,
397    pub correlation_id: Option<CorrelationId>,
398    pub idempotency_key: Option<DispatchIdempotencyKey>,
399}
400
401impl DispatchInput {
402    /// System-origin dispatch from plain text. The common case.
403    pub fn system(text: impl Into<String>) -> Self {
404        Self {
405            content: meerkat_core::ContentInput::Text(text.into()),
406            origin: DispatchOrigin::System,
407            correlation_id: None,
408            idempotency_key: None,
409        }
410    }
411
412    /// Dispatch with an explicit origin from plain text.
413    pub fn with_origin(text: impl Into<String>, origin: DispatchOrigin) -> Self {
414        Self {
415            content: meerkat_core::ContentInput::Text(text.into()),
416            origin,
417            correlation_id: None,
418            idempotency_key: None,
419        }
420    }
421
422    /// Attach a correlation ID (builder pattern).
423    pub fn with_correlation(mut self, id: impl Into<String>) -> Self {
424        self.correlation_id = Some(CorrelationId::new(id));
425        self
426    }
427
428    /// Attach an idempotency key (builder pattern).
429    pub fn with_idempotency(mut self, key: impl Into<String>) -> Self {
430        self.idempotency_key = Some(DispatchIdempotencyKey::new(key));
431        self
432    }
433}
434
435// ---------------------------------------------------------------------------
436// ManagedPeerEdge
437// ---------------------------------------------------------------------------
438
439/// Error returned when constructing an invalid `ManagedPeerEdge`.
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub enum ManagedPeerEdgeError {
442    SelfEdge,
443}
444
445impl fmt::Display for ManagedPeerEdgeError {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        match self {
448            Self::SelfEdge => write!(f, "self-edges are not allowed"),
449        }
450    }
451}
452
453impl std::error::Error for ManagedPeerEdgeError {}
454
455/// A managed dynamic topology edge between two agent identities.
456///
457/// Canonical ordering: `a < b`. Self-edges are rejected at construction time.
458/// Deserialization enforces the same invariant as `new()` via `TryFrom`.
459#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
460#[serde(into = "ManagedPeerEdgeRaw")]
461pub struct ManagedPeerEdge {
462    a: AgentIdentity,
463    b: AgentIdentity,
464}
465
466/// Raw deserialization target for `ManagedPeerEdge`.
467#[derive(Serialize, Deserialize)]
468struct ManagedPeerEdgeRaw {
469    a: AgentIdentity,
470    b: AgentIdentity,
471}
472
473impl From<ManagedPeerEdge> for ManagedPeerEdgeRaw {
474    fn from(edge: ManagedPeerEdge) -> Self {
475        Self {
476            a: edge.a,
477            b: edge.b,
478        }
479    }
480}
481
482impl TryFrom<ManagedPeerEdgeRaw> for ManagedPeerEdge {
483    type Error = ManagedPeerEdgeError;
484
485    fn try_from(raw: ManagedPeerEdgeRaw) -> Result<Self, Self::Error> {
486        Self::new(raw.a, raw.b)
487    }
488}
489
490impl<'de> Deserialize<'de> for ManagedPeerEdge {
491    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
492    where
493        D: serde::Deserializer<'de>,
494    {
495        let raw = ManagedPeerEdgeRaw::deserialize(deserializer)?;
496        Self::try_from(raw).map_err(serde::de::Error::custom)
497    }
498}
499
500impl ManagedPeerEdge {
501    /// Construct a managed peer edge with canonical ordering enforcement.
502    ///
503    /// # Errors
504    ///
505    /// Returns `ManagedPeerEdgeError::SelfEdge` if `a == b`.
506    pub fn new(a: AgentIdentity, b: AgentIdentity) -> Result<Self, ManagedPeerEdgeError> {
507        if a == b {
508            return Err(ManagedPeerEdgeError::SelfEdge);
509        }
510        if a < b {
511            Ok(Self { a, b })
512        } else {
513            Ok(Self { a: b, b: a })
514        }
515    }
516
517    #[must_use]
518    pub fn a(&self) -> &AgentIdentity {
519        &self.a
520    }
521
522    #[must_use]
523    pub fn b(&self) -> &AgentIdentity {
524        &self.b
525    }
526}
527
528// ---------------------------------------------------------------------------
529// NotAddressable error
530// ---------------------------------------------------------------------------
531
532/// Error returned when `send()` targets an `InternalOnly` agent.
533#[derive(Debug, Clone, PartialEq, Eq)]
534pub struct NotAddressable {
535    pub identity: AgentIdentity,
536    pub addressability: AgentAddressability,
537}
538
539impl fmt::Display for NotAddressable {
540    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541        write!(
542            f,
543            "agent {:?} is not addressable (current: {:?})",
544            self.identity, self.addressability
545        )
546    }
547}
548
549impl std::error::Error for NotAddressable {}
550
551// ---------------------------------------------------------------------------
552// DurableAgentSpec
553// ---------------------------------------------------------------------------
554
555/// The preferred roster/spawn specification for identity-first continuity.
556#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
557pub struct DurableAgentSpec {
558    pub identity: AgentIdentity,
559    pub profile: meerkat_mob::ProfileName,
560    #[serde(default)]
561    pub addressability: AgentAddressability,
562    pub display_name: Option<DisplayName>,
563    #[serde(default)]
564    pub labels: std::collections::BTreeMap<String, String>,
565    pub context: Option<serde_json::Value>,
566    #[serde(default)]
567    pub additional_instructions: Vec<String>,
568    #[serde(default)]
569    pub initial_message: Option<meerkat_core::ContentInput>,
570    #[serde(default)]
571    pub runtime_mode_override: Option<meerkat_mob::MobRuntimeMode>,
572}
573
574// ---------------------------------------------------------------------------
575// IdentityStatus + supporting types
576// ---------------------------------------------------------------------------
577
578/// Lifecycle state of an identity.
579#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
580#[serde(rename_all = "snake_case")]
581pub enum IdentityLifecycleState {
582    Active,
583    Retiring,
584    Suspended,
585    Uninitialized,
586}
587
588/// Information about a held lease.
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
590pub struct LeaseInfo {
591    pub fencing_token: FencingToken,
592    #[serde(
593        serialize_with = "serde_duration_ms::serialize",
594        deserialize_with = "serde_duration_ms::deserialize"
595    )]
596    pub ttl_remaining: std::time::Duration,
597    pub healthy: bool,
598}
599
600/// Durability policy declared by the continuity store.
601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
602#[serde(rename_all = "snake_case", tag = "kind")]
603pub enum DurabilityPolicy {
604    SyncWriteThrough,
605    AsyncReplicated,
606    BufferedExport { max_loss_window_ms: u64 },
607}
608
609/// Health of the continuity store for an identity.
610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
611pub struct ContinuityHealth {
612    pub store_reachable: bool,
613    pub durability_policy: DurabilityPolicy,
614    pub last_checkpoint_version: Option<CheckpointVersion>,
615}
616
617/// Full status response for an identity.
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
619pub struct IdentityStatus {
620    pub identity: AgentIdentity,
621    pub state: IdentityLifecycleState,
622    pub agent_runtime_id: Option<AgentRuntimeId>,
623    pub session_id: Option<meerkat_core::types::SessionId>,
624    pub profile: Option<meerkat_mob::ProfileName>,
625    pub runtime_mode: Option<meerkat_mob::MobRuntimeMode>,
626    pub addressability: AgentAddressability,
627    pub display_name: Option<DisplayName>,
628    #[serde(default)]
629    pub labels: std::collections::BTreeMap<String, String>,
630    pub generation: Option<ContinuityGeneration>,
631    pub checkpoint_version: Option<CheckpointVersion>,
632    pub lease: Option<LeaseInfo>,
633    pub continuity_health: Option<ContinuityHealth>,
634}
635
636// ---------------------------------------------------------------------------
637// AgentBuildContext + AgentBuildDraft + ExternalToolDef
638// ---------------------------------------------------------------------------
639
640#[derive(Clone, Default)]
641pub struct AgentRuntimeServices {
642    mob_handle: Option<meerkat_mob::MobHandle>,
643}
644
645impl AgentRuntimeServices {
646    pub fn new(mob_handle: meerkat_mob::MobHandle) -> Self {
647        Self {
648            mob_handle: Some(mob_handle),
649        }
650    }
651
652    pub fn empty() -> Self {
653        Self { mob_handle: None }
654    }
655
656    pub fn mob_handle(&self) -> Option<meerkat_mob::MobHandle> {
657        self.mob_handle.clone()
658    }
659
660    pub fn has_mob_handle(&self) -> bool {
661        self.mob_handle.is_some()
662    }
663}
664
665impl std::fmt::Debug for AgentRuntimeServices {
666    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667        f.debug_struct("AgentRuntimeServices")
668            .field("mob_handle", &self.mob_handle.is_some())
669            .finish()
670    }
671}
672
673impl PartialEq for AgentRuntimeServices {
674    fn eq(&self, other: &Self) -> bool {
675        self.mob_handle.is_some() == other.mob_handle.is_some()
676    }
677}
678
679impl Eq for AgentRuntimeServices {}
680
681/// Read-only context provided to `AgentCustomizer` at build time.
682#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
683pub struct AgentBuildContext {
684    pub identity: AgentIdentity,
685    pub active_peers: Vec<AgentIdentity>,
686    pub managed_edges: Vec<ManagedPeerEdge>,
687    #[serde(default, skip)]
688    pub runtime_services: AgentRuntimeServices,
689}
690
691/// Tool definition for the customizer boundary.
692#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
693pub struct ExternalToolDef {
694    pub name: String,
695    pub description: String,
696    pub input_schema: serde_json::Value,
697}
698
699/// Mutable draft that `AgentCustomizer` modifies.
700#[derive(Clone, Default)]
701pub struct LocalExternalToolOverlay {
702    dispatcher: Option<Arc<dyn meerkat_core::agent::AgentToolDispatcher>>,
703}
704
705impl LocalExternalToolOverlay {
706    pub fn new(dispatcher: Arc<dyn meerkat_core::agent::AgentToolDispatcher>) -> Self {
707        Self {
708            dispatcher: Some(dispatcher),
709        }
710    }
711
712    pub fn empty() -> Self {
713        Self { dispatcher: None }
714    }
715
716    pub fn dispatcher(&self) -> Option<Arc<dyn meerkat_core::agent::AgentToolDispatcher>> {
717        self.dispatcher.clone()
718    }
719
720    pub fn is_some(&self) -> bool {
721        self.dispatcher.is_some()
722    }
723}
724
725impl std::fmt::Debug for LocalExternalToolOverlay {
726    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
727        f.debug_struct("LocalExternalToolOverlay")
728            .field("dispatcher", &self.dispatcher.is_some())
729            .finish()
730    }
731}
732
733impl PartialEq for LocalExternalToolOverlay {
734    fn eq(&self, other: &Self) -> bool {
735        self.dispatcher.is_some() == other.dispatcher.is_some()
736    }
737}
738
739impl Eq for LocalExternalToolOverlay {}
740
741/// Mutable draft that `AgentCustomizer` modifies.
742///
743/// `external_tools` remains the serializable SDK/gateway declaration surface.
744/// `local_external_tools` is intentionally skipped by serde and is the
745/// in-process Rust overlay for apps that can supply a real dispatcher.
746#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
747pub struct AgentBuildDraft {
748    pub model: Option<String>,
749    pub system_prompt: Option<String>,
750    #[serde(default)]
751    pub additional_instructions: Vec<String>,
752    #[serde(default)]
753    pub labels: std::collections::BTreeMap<String, String>,
754    pub app_context: Option<serde_json::Value>,
755    #[serde(default)]
756    pub external_tools: Vec<ExternalToolDef>,
757    #[serde(default, skip)]
758    pub local_external_tools: LocalExternalToolOverlay,
759}
760
761// ---------------------------------------------------------------------------
762// SessionSnapshot
763// ---------------------------------------------------------------------------
764
765/// Opaque wrapper around serialized Meerkat session state.
766///
767/// Stored and loaded by `ContinuityStore`.
768/// Wire format (JSON-RPC): `{ "data": "<base64 string>" }`.
769#[derive(Debug, Clone, PartialEq, Eq)]
770pub struct SessionSnapshot {
771    pub data: Vec<u8>,
772}
773
774impl Serialize for SessionSnapshot {
775    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
776    where
777        S: serde::Serializer,
778    {
779        use base64::Engine;
780        use serde::ser::SerializeStruct;
781        let encoded = base64::engine::general_purpose::STANDARD.encode(&self.data);
782        let mut s = serializer.serialize_struct("SessionSnapshot", 1)?;
783        s.serialize_field("data", &encoded)?;
784        s.end()
785    }
786}
787
788impl<'de> Deserialize<'de> for SessionSnapshot {
789    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
790    where
791        D: serde::Deserializer<'de>,
792    {
793        use base64::Engine;
794
795        #[derive(Deserialize)]
796        struct Wrapper {
797            data: String,
798        }
799
800        let wrapper = Wrapper::deserialize(deserializer)?;
801        let data = base64::engine::general_purpose::STANDARD
802            .decode(&wrapper.data)
803            .map_err(serde::de::Error::custom)?;
804        Ok(Self { data })
805    }
806}
807
808// ---------------------------------------------------------------------------
809// RosterContext + TopologyContext
810// ---------------------------------------------------------------------------
811
812/// Context passed to `RosterProvider`.
813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
814pub struct RosterContext {
815    pub mob_definition: Option<meerkat_mob::MobDefinition>,
816    pub previous_identities: Vec<AgentIdentity>,
817}
818
819/// Context passed to `TopologyProvider`.
820#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
821pub struct TopologyContext {
822    pub roster: Vec<DurableAgentSpec>,
823}
824
825// ---------------------------------------------------------------------------
826// Error types
827// ---------------------------------------------------------------------------
828
829/// Error from the continuity store.
830#[derive(Debug)]
831pub enum ContinuityStoreError {
832    StaleFencingToken {
833        identity: AgentIdentity,
834        presented: FencingToken,
835        current: FencingToken,
836    },
837    StaleCheckpointVersion {
838        identity: AgentIdentity,
839        presented: CheckpointVersion,
840        current: CheckpointVersion,
841    },
842    NotFound {
843        identity: AgentIdentity,
844    },
845    Io(String),
846    Corruption(String),
847}
848
849impl fmt::Display for ContinuityStoreError {
850    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
851        match self {
852            Self::StaleFencingToken {
853                identity,
854                presented,
855                current,
856            } => write!(
857                f,
858                "stale fencing token for {identity}: presented {presented}, current {current}"
859            ),
860            Self::StaleCheckpointVersion {
861                identity,
862                presented,
863                current,
864            } => write!(
865                f,
866                "stale checkpoint version for {identity}: presented {presented}, current {current}"
867            ),
868            Self::NotFound { identity } => {
869                write!(f, "continuity record not found for {identity}")
870            }
871            Self::Io(msg) => write!(f, "continuity store I/O error: {msg}"),
872            Self::Corruption(msg) => write!(f, "continuity store corruption: {msg}"),
873        }
874    }
875}
876
877impl std::error::Error for ContinuityStoreError {}
878
879/// Error from the lease provider.
880#[derive(Debug)]
881pub enum LeaseError {
882    ProviderUnavailable(String),
883    Io(String),
884}
885
886impl fmt::Display for LeaseError {
887    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
888        match self {
889            Self::ProviderUnavailable(msg) => {
890                write!(f, "lease provider unavailable: {msg}")
891            }
892            Self::Io(msg) => write!(f, "lease I/O error: {msg}"),
893        }
894    }
895}
896
897impl std::error::Error for LeaseError {}
898
899/// Error from the roster provider.
900#[derive(Debug)]
901pub enum RosterError {
902    ProviderUnavailable(String),
903    Io(String),
904}
905
906impl fmt::Display for RosterError {
907    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
908        match self {
909            Self::ProviderUnavailable(msg) => {
910                write!(f, "roster provider unavailable: {msg}")
911            }
912            Self::Io(msg) => write!(f, "roster I/O error: {msg}"),
913        }
914    }
915}
916
917impl std::error::Error for RosterError {}
918
919/// Error from the topology provider.
920#[derive(Debug)]
921pub enum TopologyError {
922    InvalidEdge(String),
923    ProviderUnavailable(String),
924}
925
926impl fmt::Display for TopologyError {
927    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
928        match self {
929            Self::InvalidEdge(msg) => write!(f, "invalid topology edge: {msg}"),
930            Self::ProviderUnavailable(msg) => {
931                write!(f, "topology provider unavailable: {msg}")
932            }
933        }
934    }
935}
936
937impl std::error::Error for TopologyError {}
938
939/// Error from the agent customizer.
940#[derive(Debug)]
941pub enum CustomizerError {
942    BuildFailed(String),
943    Io(String),
944}
945
946impl fmt::Display for CustomizerError {
947    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
948        match self {
949            Self::BuildFailed(msg) => write!(f, "customizer build failed: {msg}"),
950            Self::Io(msg) => write!(f, "customizer I/O error: {msg}"),
951        }
952    }
953}
954
955impl std::error::Error for CustomizerError {}