Skip to main content

prospero_core/
model.rs

1//! Prospero's fleet domain model: `Host -> Workspace -> [Agent]`.
2//!
3//! The read-model DTOs (`Agent`, `Workspace`, `FleetSnapshot`, `AgentStatus`,
4//! `WorkspaceHealth`, `Readiness`, `AgentId`) now live in [`prospero_types`] so
5//! the WASM dashboard can share them (prospero #98); they are re-exported here
6//! from their original path. The control-plane types below (`TaskSpec`,
7//! `AgentHandle`, `DrainPolicy`, `FleetChange`) stay in `prospero-core` — they
8//! reference core-only types (`SpawnRequest`, `Endpoint`).
9
10use serde::{Deserialize, Serialize};
11
12pub use prospero_types::{
13    Agent, AgentId, AgentStatus, FleetSnapshot, Readiness, Workspace, WorkspaceHealth,
14};
15
16/// Desired state for one agent — the provider-agnostic spec `ensure_agent` takes.
17/// Generalizes today's `(workspace, SpawnRequest)` pair (fleet.rs:618).
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct TaskSpec {
20    pub workspace: String,
21    pub request: crate::fleet::SpawnRequest,
22}
23
24/// Handle to a provisioned agent, resolved when it is attachable.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct AgentHandle {
27    pub id: AgentId,
28    pub workspace: String,
29    /// Endpoint the agent's per-agent socket is reachable at. `None` until the
30    /// backend has resolved one — e.g. a k8s agent between spawn and Running.
31    pub endpoint: Option<crate::caliband::wire::Endpoint>,
32    /// Whether this call actually started a new agent (#190).
33    ///
34    /// `ensure_agent` is idempotent, and under k8s the `CalibanTask` name is
35    /// derived from the spec — so re-submitting an identical prompt resolves to
36    /// the *existing* agent. That is the right behaviour, but the caller must be
37    /// able to tell, or the UI reports a launch that never happened.
38    ///
39    /// `false` from [`crate::k8s::fleet::handle_from`], which only ever observes
40    /// a `CalibanTask` that already exists.
41    pub created: bool,
42}
43
44/// How to stop an agent. `Kill` preserves today's unconditional behavior.
45#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum DrainPolicy {
48    #[default]
49    Kill,
50    Graceful {
51        timeout_ms: u64,
52    },
53}
54
55/// A change in the observed fleet — the item type of `watch_fleet`.
56/// Mirrors the poll-diff variants `reconcile` already emits (fleet.rs:811).
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58#[serde(tag = "kind", rename_all = "snake_case")]
59pub enum FleetChange {
60    Discovered {
61        id: AgentId,
62        workspace: String,
63        agent: Agent,
64    },
65    StatusChanged {
66        id: AgentId,
67        workspace: String,
68        from: AgentStatus,
69        to: AgentStatus,
70    },
71    Gone {
72        id: AgentId,
73        workspace: String,
74    },
75    WorkspaceHealth {
76        workspace: String,
77        health: WorkspaceHealth,
78    },
79}
80
81#[cfg(test)]
82mod fleet_provider_types_tests {
83    use super::*;
84
85    #[test]
86    fn drain_policy_defaults_to_kill() {
87        assert!(matches!(DrainPolicy::default(), DrainPolicy::Kill));
88    }
89    #[test]
90    fn fleet_change_serdes() {
91        let c = FleetChange::StatusChanged {
92            id: AgentId::from("a1"),
93            workspace: "r".into(),
94            from: AgentStatus::Spawning,
95            to: AgentStatus::Running,
96        };
97        let j = serde_json::to_string(&c).unwrap();
98        let back: FleetChange = serde_json::from_str(&j).unwrap();
99        assert_eq!(back, c);
100    }
101}