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    /// The bridge refused a resume while the durable session row exists (e.g.
292    /// a transcript-continuity rejection). The identity → session binding is
293    /// intact; the identity is degraded until a reconcile retry succeeds.
294    ResumeRejected,
295}
296
297/// A typed failure payload for broken continuity.
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299pub struct ContinuityFailure {
300    pub identity: AgentIdentity,
301    pub kind: ContinuityFailureKind,
302    pub record: Option<ContinuityRecord>,
303    pub detail: String,
304}
305
306// ---------------------------------------------------------------------------
307// ContinuityResolveState
308// ---------------------------------------------------------------------------
309
310/// The resolve result for a single identity from the continuity store.
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "snake_case", tag = "state")]
313pub enum ContinuityResolveState {
314    Uninitialized,
315    Ready { record: ContinuityRecord },
316    Broken { failure: ContinuityFailure },
317}
318
319// ---------------------------------------------------------------------------
320// LeaseGrant
321// ---------------------------------------------------------------------------
322
323/// A lease grant returned by `LeaseProvider`.
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325pub struct LeaseGrant {
326    pub identity: AgentIdentity,
327    pub fencing_token: FencingToken,
328    #[serde(
329        serialize_with = "serde_duration_ms::serialize",
330        deserialize_with = "serde_duration_ms::deserialize"
331    )]
332    pub ttl: std::time::Duration,
333}
334
335/// Custom serde for `Duration` as integer milliseconds.
336mod serde_duration_ms {
337    use serde::{Deserialize, Deserializer, Serializer};
338    use std::time::Duration;
339
340    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
341    where
342        S: Serializer,
343    {
344        let ms = duration.as_millis();
345        // u128 -> u64 is safe for any reasonable TTL
346        serializer.serialize_u64(ms as u64)
347    }
348
349    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
350    where
351        D: Deserializer<'de>,
352    {
353        let ms = u64::deserialize(deserializer)?;
354        Ok(Duration::from_millis(ms))
355    }
356}
357
358// ---------------------------------------------------------------------------
359// LeaseAcquireResult + LeaseRenewResult
360// ---------------------------------------------------------------------------
361
362/// Result of a lease acquisition attempt.
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(rename_all = "snake_case", tag = "result")]
365pub enum LeaseAcquireResult {
366    Acquired(LeaseGrant),
367    AlreadyHeld {
368        identity: AgentIdentity,
369        holder: String,
370    },
371}
372
373/// Result of a lease renewal attempt.
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375#[serde(rename_all = "snake_case", tag = "result")]
376pub enum LeaseRenewResult {
377    Renewed(LeaseGrant),
378    Lost { identity: AgentIdentity },
379}
380
381// ---------------------------------------------------------------------------
382// DispatchOrigin + DispatchInput
383// ---------------------------------------------------------------------------
384
385/// Origin of a dispatch request.
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(rename_all = "snake_case")]
388pub enum DispatchOrigin {
389    Connector,
390    Scheduler,
391    Policy,
392    Flow,
393    System,
394}
395
396/// Input for a dispatch operation.
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398pub struct DispatchInput {
399    pub content: meerkat_core::ContentInput,
400    pub origin: DispatchOrigin,
401    pub correlation_id: Option<CorrelationId>,
402    pub idempotency_key: Option<DispatchIdempotencyKey>,
403}
404
405impl DispatchInput {
406    /// System-origin dispatch from plain text. The common case.
407    pub fn system(text: impl Into<String>) -> Self {
408        Self {
409            content: meerkat_core::ContentInput::Text(text.into()),
410            origin: DispatchOrigin::System,
411            correlation_id: None,
412            idempotency_key: None,
413        }
414    }
415
416    /// Dispatch with an explicit origin from plain text.
417    pub fn with_origin(text: impl Into<String>, origin: DispatchOrigin) -> Self {
418        Self {
419            content: meerkat_core::ContentInput::Text(text.into()),
420            origin,
421            correlation_id: None,
422            idempotency_key: None,
423        }
424    }
425
426    /// Attach a correlation ID (builder pattern).
427    pub fn with_correlation(mut self, id: impl Into<String>) -> Self {
428        self.correlation_id = Some(CorrelationId::new(id));
429        self
430    }
431
432    /// Attach an idempotency key (builder pattern).
433    pub fn with_idempotency(mut self, key: impl Into<String>) -> Self {
434        self.idempotency_key = Some(DispatchIdempotencyKey::new(key));
435        self
436    }
437}
438
439// ---------------------------------------------------------------------------
440// ManagedPeerEdge
441// ---------------------------------------------------------------------------
442
443/// Error returned when constructing an invalid `ManagedPeerEdge`.
444#[derive(Debug, Clone, PartialEq, Eq)]
445pub enum ManagedPeerEdgeError {
446    SelfEdge,
447}
448
449impl fmt::Display for ManagedPeerEdgeError {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        match self {
452            Self::SelfEdge => write!(f, "self-edges are not allowed"),
453        }
454    }
455}
456
457impl std::error::Error for ManagedPeerEdgeError {}
458
459/// A managed dynamic topology edge between two agent identities.
460///
461/// Canonical ordering: `a < b`. Self-edges are rejected at construction time.
462/// Deserialization enforces the same invariant as `new()` via `TryFrom`.
463#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
464#[serde(into = "ManagedPeerEdgeRaw")]
465pub struct ManagedPeerEdge {
466    a: AgentIdentity,
467    b: AgentIdentity,
468}
469
470/// Raw deserialization target for `ManagedPeerEdge`.
471#[derive(Serialize, Deserialize)]
472struct ManagedPeerEdgeRaw {
473    a: AgentIdentity,
474    b: AgentIdentity,
475}
476
477impl From<ManagedPeerEdge> for ManagedPeerEdgeRaw {
478    fn from(edge: ManagedPeerEdge) -> Self {
479        Self {
480            a: edge.a,
481            b: edge.b,
482        }
483    }
484}
485
486impl TryFrom<ManagedPeerEdgeRaw> for ManagedPeerEdge {
487    type Error = ManagedPeerEdgeError;
488
489    fn try_from(raw: ManagedPeerEdgeRaw) -> Result<Self, Self::Error> {
490        Self::new(raw.a, raw.b)
491    }
492}
493
494impl<'de> Deserialize<'de> for ManagedPeerEdge {
495    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
496    where
497        D: serde::Deserializer<'de>,
498    {
499        let raw = ManagedPeerEdgeRaw::deserialize(deserializer)?;
500        Self::try_from(raw).map_err(serde::de::Error::custom)
501    }
502}
503
504impl ManagedPeerEdge {
505    /// Construct a managed peer edge with canonical ordering enforcement.
506    ///
507    /// # Errors
508    ///
509    /// Returns `ManagedPeerEdgeError::SelfEdge` if `a == b`.
510    pub fn new(a: AgentIdentity, b: AgentIdentity) -> Result<Self, ManagedPeerEdgeError> {
511        if a == b {
512            return Err(ManagedPeerEdgeError::SelfEdge);
513        }
514        if a < b {
515            Ok(Self { a, b })
516        } else {
517            Ok(Self { a: b, b: a })
518        }
519    }
520
521    #[must_use]
522    pub fn a(&self) -> &AgentIdentity {
523        &self.a
524    }
525
526    #[must_use]
527    pub fn b(&self) -> &AgentIdentity {
528        &self.b
529    }
530}
531
532// ---------------------------------------------------------------------------
533// NotAddressable error
534// ---------------------------------------------------------------------------
535
536/// Error returned when `send()` targets an `InternalOnly` agent.
537#[derive(Debug, Clone, PartialEq, Eq)]
538pub struct NotAddressable {
539    pub identity: AgentIdentity,
540    pub addressability: AgentAddressability,
541}
542
543impl fmt::Display for NotAddressable {
544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545        write!(
546            f,
547            "agent {:?} is not addressable (current: {:?})",
548            self.identity, self.addressability
549        )
550    }
551}
552
553impl std::error::Error for NotAddressable {}
554
555// ---------------------------------------------------------------------------
556// DurableAgentSpec
557// ---------------------------------------------------------------------------
558
559/// The preferred roster/spawn specification for identity-first continuity.
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561pub struct DurableAgentSpec {
562    pub identity: AgentIdentity,
563    pub profile: meerkat_mob::ProfileName,
564    #[serde(default)]
565    pub addressability: AgentAddressability,
566    pub display_name: Option<DisplayName>,
567    #[serde(default)]
568    pub labels: std::collections::BTreeMap<String, String>,
569    pub context: Option<serde_json::Value>,
570    #[serde(default)]
571    pub additional_instructions: Vec<String>,
572    #[serde(default)]
573    pub initial_message: Option<meerkat_core::ContentInput>,
574    #[serde(default)]
575    pub runtime_mode_override: Option<meerkat_mob::MobRuntimeMode>,
576    #[serde(default)]
577    pub backend: Option<meerkat_mob::MobBackendKind>,
578    #[serde(default)]
579    pub binding: Option<meerkat_contracts::WireRuntimeBinding>,
580}
581
582// ---------------------------------------------------------------------------
583// IdentityStatus + supporting types
584// ---------------------------------------------------------------------------
585
586/// Lifecycle state of an identity.
587#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
588#[serde(rename_all = "snake_case")]
589pub enum IdentityLifecycleState {
590    /// Identity metadata is registered and addressable, but no concrete mob
591    /// member/session has been spawned or resumed in this runtime yet.
592    Dormant,
593    /// Identity has a concrete mob member/session in this runtime.
594    Active,
595    Retiring,
596    Suspended,
597    /// Continuity exists but cannot currently be materialized safely.
598    Broken,
599    Uninitialized,
600}
601
602impl IdentityLifecycleState {
603    /// Canonical wire vocabulary for identity lifecycle states.
604    ///
605    /// meerkat 0.7 moved the member rows (`mobkit/get_member`,
606    /// `list_members`, `ensure_member`, `find_members`) to lowercase state
607    /// strings — matching the published SDK constants
608    /// (`MEMBER_STATE_ACTIVE = "active"`) and the console vocabulary. The
609    /// identity-first status/inspect surfaces must speak the same casing so
610    /// the two member-state surfaces never disagree on the same wire.
611    pub fn wire_str(self) -> &'static str {
612        match self {
613            Self::Dormant => "dormant",
614            Self::Active => "active",
615            Self::Retiring => "retiring",
616            Self::Suspended => "suspended",
617            Self::Broken => "broken",
618            Self::Uninitialized => "uninitialized",
619        }
620    }
621}
622
623/// Information about a held lease.
624#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
625pub struct LeaseInfo {
626    pub fencing_token: FencingToken,
627    #[serde(
628        serialize_with = "serde_duration_ms::serialize",
629        deserialize_with = "serde_duration_ms::deserialize"
630    )]
631    pub ttl_remaining: std::time::Duration,
632    pub healthy: bool,
633}
634
635/// Durability policy declared by the continuity store.
636#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
637#[serde(rename_all = "snake_case", tag = "kind")]
638pub enum DurabilityPolicy {
639    SyncWriteThrough,
640    AsyncReplicated,
641    BufferedExport { max_loss_window_ms: u64 },
642}
643
644/// Health of the continuity store for an identity.
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
646pub struct ContinuityHealth {
647    pub store_reachable: bool,
648    pub durability_policy: DurabilityPolicy,
649    pub last_checkpoint_version: Option<CheckpointVersion>,
650}
651
652/// Full status response for an identity.
653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654pub struct IdentityStatus {
655    pub identity: AgentIdentity,
656    pub state: IdentityLifecycleState,
657    pub agent_runtime_id: Option<AgentRuntimeId>,
658    pub session_id: Option<meerkat_core::types::SessionId>,
659    pub profile: Option<meerkat_mob::ProfileName>,
660    pub runtime_mode: Option<meerkat_mob::MobRuntimeMode>,
661    pub addressability: AgentAddressability,
662    pub display_name: Option<DisplayName>,
663    #[serde(default)]
664    pub labels: std::collections::BTreeMap<String, String>,
665    pub generation: Option<ContinuityGeneration>,
666    pub checkpoint_version: Option<CheckpointVersion>,
667    pub lease: Option<LeaseInfo>,
668    pub continuity_health: Option<ContinuityHealth>,
669}
670
671// ---------------------------------------------------------------------------
672// AgentBuildContext + AgentBuildDraft + ExternalToolDef
673// ---------------------------------------------------------------------------
674
675#[derive(Clone, Default)]
676pub struct AgentRuntimeServices {
677    mob_handle: Option<meerkat_mob::MobHandle>,
678}
679
680impl AgentRuntimeServices {
681    pub fn new(mob_handle: meerkat_mob::MobHandle) -> Self {
682        Self {
683            mob_handle: Some(mob_handle),
684        }
685    }
686
687    pub fn empty() -> Self {
688        Self { mob_handle: None }
689    }
690
691    pub fn mob_handle(&self) -> Option<meerkat_mob::MobHandle> {
692        self.mob_handle.clone()
693    }
694
695    pub fn has_mob_handle(&self) -> bool {
696        self.mob_handle.is_some()
697    }
698}
699
700impl std::fmt::Debug for AgentRuntimeServices {
701    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
702        f.debug_struct("AgentRuntimeServices")
703            .field("mob_handle", &self.mob_handle.is_some())
704            .finish()
705    }
706}
707
708impl PartialEq for AgentRuntimeServices {
709    fn eq(&self, other: &Self) -> bool {
710        self.mob_handle.is_some() == other.mob_handle.is_some()
711    }
712}
713
714impl Eq for AgentRuntimeServices {}
715
716/// Read-only context provided to `AgentCustomizer` at build time.
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718pub struct AgentBuildContext {
719    pub identity: AgentIdentity,
720    pub active_peers: Vec<AgentIdentity>,
721    pub managed_edges: Vec<ManagedPeerEdge>,
722    #[serde(default, skip)]
723    pub runtime_services: AgentRuntimeServices,
724}
725
726/// Tool definition for the customizer boundary.
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
728pub struct ExternalToolDef {
729    pub name: String,
730    pub description: String,
731    pub input_schema: serde_json::Value,
732}
733
734/// Mutable draft that `AgentCustomizer` modifies.
735#[derive(Clone, Default)]
736pub struct LocalExternalToolOverlay {
737    dispatcher: Option<Arc<dyn meerkat_core::agent::AgentToolDispatcher>>,
738}
739
740impl LocalExternalToolOverlay {
741    pub fn new(dispatcher: Arc<dyn meerkat_core::agent::AgentToolDispatcher>) -> Self {
742        Self {
743            dispatcher: Some(dispatcher),
744        }
745    }
746
747    pub fn empty() -> Self {
748        Self { dispatcher: None }
749    }
750
751    pub fn dispatcher(&self) -> Option<Arc<dyn meerkat_core::agent::AgentToolDispatcher>> {
752        self.dispatcher.clone()
753    }
754
755    pub fn is_some(&self) -> bool {
756        self.dispatcher.is_some()
757    }
758}
759
760impl std::fmt::Debug for LocalExternalToolOverlay {
761    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
762        f.debug_struct("LocalExternalToolOverlay")
763            .field("dispatcher", &self.dispatcher.is_some())
764            .finish()
765    }
766}
767
768impl PartialEq for LocalExternalToolOverlay {
769    fn eq(&self, other: &Self) -> bool {
770        self.dispatcher.is_some() == other.dispatcher.is_some()
771    }
772}
773
774impl Eq for LocalExternalToolOverlay {}
775
776/// Mutable draft that `AgentCustomizer` modifies.
777///
778/// `external_tools` remains the serializable SDK/gateway declaration surface.
779/// `local_external_tools` is intentionally skipped by serde and is the
780/// in-process Rust overlay for apps that can supply a real dispatcher.
781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
782pub struct AgentBuildDraft {
783    pub model: Option<String>,
784    pub system_prompt: Option<String>,
785    #[serde(default)]
786    pub additional_instructions: Vec<String>,
787    #[serde(default)]
788    pub labels: std::collections::BTreeMap<String, String>,
789    pub app_context: Option<serde_json::Value>,
790    #[serde(default)]
791    pub external_tools: Vec<ExternalToolDef>,
792    #[serde(default, skip)]
793    pub local_external_tools: LocalExternalToolOverlay,
794}
795
796// ---------------------------------------------------------------------------
797// SessionSnapshot
798// ---------------------------------------------------------------------------
799
800/// Opaque wrapper around serialized Meerkat session state.
801///
802/// Stored and loaded by `ContinuityStore`.
803/// Wire format (JSON-RPC): `{ "data": "<base64 string>" }`.
804#[derive(Debug, Clone, PartialEq, Eq)]
805pub struct SessionSnapshot {
806    pub data: Vec<u8>,
807}
808
809impl Serialize for SessionSnapshot {
810    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
811    where
812        S: serde::Serializer,
813    {
814        use base64::Engine;
815        use serde::ser::SerializeStruct;
816        let encoded = base64::engine::general_purpose::STANDARD.encode(&self.data);
817        let mut s = serializer.serialize_struct("SessionSnapshot", 1)?;
818        s.serialize_field("data", &encoded)?;
819        s.end()
820    }
821}
822
823impl<'de> Deserialize<'de> for SessionSnapshot {
824    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
825    where
826        D: serde::Deserializer<'de>,
827    {
828        use base64::Engine;
829
830        #[derive(Deserialize)]
831        struct Wrapper {
832            data: String,
833        }
834
835        let wrapper = Wrapper::deserialize(deserializer)?;
836        let data = base64::engine::general_purpose::STANDARD
837            .decode(&wrapper.data)
838            .map_err(serde::de::Error::custom)?;
839        Ok(Self { data })
840    }
841}
842
843// ---------------------------------------------------------------------------
844// RosterContext + TopologyContext
845// ---------------------------------------------------------------------------
846
847/// Context passed to `RosterProvider`.
848#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
849pub struct RosterContext {
850    pub mob_definition: Option<meerkat_mob::MobDefinition>,
851    pub previous_identities: Vec<AgentIdentity>,
852}
853
854/// Context passed to `TopologyProvider`.
855#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
856pub struct TopologyContext {
857    pub roster: Vec<DurableAgentSpec>,
858}
859
860// ---------------------------------------------------------------------------
861// Error types
862// ---------------------------------------------------------------------------
863
864/// Error from the continuity store.
865#[derive(Debug)]
866pub enum ContinuityStoreError {
867    StaleFencingToken {
868        identity: AgentIdentity,
869        presented: FencingToken,
870        current: FencingToken,
871    },
872    StaleCheckpointVersion {
873        identity: AgentIdentity,
874        presented: CheckpointVersion,
875        current: CheckpointVersion,
876    },
877    NotFound {
878        identity: AgentIdentity,
879    },
880    Io(String),
881    Corruption(String),
882}
883
884impl fmt::Display for ContinuityStoreError {
885    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
886        match self {
887            Self::StaleFencingToken {
888                identity,
889                presented,
890                current,
891            } => write!(
892                f,
893                "stale fencing token for {identity}: presented {presented}, current {current}"
894            ),
895            Self::StaleCheckpointVersion {
896                identity,
897                presented,
898                current,
899            } => write!(
900                f,
901                "stale checkpoint version for {identity}: presented {presented}, current {current}"
902            ),
903            Self::NotFound { identity } => {
904                write!(f, "continuity record not found for {identity}")
905            }
906            Self::Io(msg) => write!(f, "continuity store I/O error: {msg}"),
907            Self::Corruption(msg) => write!(f, "continuity store corruption: {msg}"),
908        }
909    }
910}
911
912impl std::error::Error for ContinuityStoreError {}
913
914/// Error from the lease provider.
915#[derive(Debug)]
916pub enum LeaseError {
917    ProviderUnavailable(String),
918    Io(String),
919}
920
921impl fmt::Display for LeaseError {
922    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
923        match self {
924            Self::ProviderUnavailable(msg) => {
925                write!(f, "lease provider unavailable: {msg}")
926            }
927            Self::Io(msg) => write!(f, "lease I/O error: {msg}"),
928        }
929    }
930}
931
932impl std::error::Error for LeaseError {}
933
934/// Error from the roster provider.
935#[derive(Debug)]
936pub enum RosterError {
937    ProviderUnavailable(String),
938    Io(String),
939}
940
941impl fmt::Display for RosterError {
942    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
943        match self {
944            Self::ProviderUnavailable(msg) => {
945                write!(f, "roster provider unavailable: {msg}")
946            }
947            Self::Io(msg) => write!(f, "roster I/O error: {msg}"),
948        }
949    }
950}
951
952impl std::error::Error for RosterError {}
953
954/// Error from the topology provider.
955#[derive(Debug)]
956pub enum TopologyError {
957    InvalidEdge(String),
958    ProviderUnavailable(String),
959}
960
961impl fmt::Display for TopologyError {
962    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
963        match self {
964            Self::InvalidEdge(msg) => write!(f, "invalid topology edge: {msg}"),
965            Self::ProviderUnavailable(msg) => {
966                write!(f, "topology provider unavailable: {msg}")
967            }
968        }
969    }
970}
971
972impl std::error::Error for TopologyError {}
973
974/// Error from the agent customizer.
975#[derive(Debug)]
976pub enum CustomizerError {
977    BuildFailed(String),
978    Io(String),
979}
980
981impl fmt::Display for CustomizerError {
982    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
983        match self {
984            Self::BuildFailed(msg) => write!(f, "customizer build failed: {msg}"),
985            Self::Io(msg) => write!(f, "customizer I/O error: {msg}"),
986        }
987    }
988}
989
990impl std::error::Error for CustomizerError {}