//! The agent archetype KV bucket: one key per archetype source flake, holding //! the `meta` of every archetype that flake's `hyperhiveArchetypes` output //! declares, as swarm-controller last checked it. //! //! **swarm-controller is the only writer and the only reader.** The swarm //! builder evaluates the source and answers on //! `swarm_wire::builder::ARCHETYPES_SUBJECT`; the controller checks each //! archetype's settings the way `POST /api/agents` checks them and writes the //! result here, so the create screen lists archetypes from a value that a //! create would accept. use std::collections::BTreeMap; use swarm_wire::agent_inference::AgentInference; use swarm_wire::agent_limits::AgentCaps; /// The KV bucket archetype sources are published into, keyed by source name. pub const BUCKET: &str = "agent-archetypes"; /// The key of the archetypes the hyperhive flake itself ships, the one source /// there is so far. pub const BUILTIN_SOURCE: &str = "hyperhive"; /// The most bytes an archetype's `meta.prompt` may hold: the limit of an /// agent's initial `AGENTS.md`, which the prompt prefills. pub const MAX_PROMPT_BYTES: usize = crate::agents_md_initial::MAX_BYTES; /// One source's archetypes: the value under `BUCKET`/``. #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SourceRecord { /// The flake reference the builder evaluated. pub source: String, /// Every archetype whose `meta` passed the controller's checks, by name. #[serde(default)] pub archetypes: BTreeMap, /// Every archetype whose `meta` did not, by name, with the reason. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub refused: BTreeMap, } /// One archetype's `meta`, as checked: the settings a create takes, in the /// shapes the per-agent buckets store them in. #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct ArchetypeMeta { pub description: String, /// Permission names, `snake_case`. Absent leaves the default set. #[serde(default, skip_serializing_if = "Option::is_none")] pub permissions: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub limits: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub inference: Option, /// The text the create form prefills an agent's initial `AGENTS.md` with, /// at most [`MAX_PROMPT_BYTES`]. Absent prefills nothing. #[serde(default, skip_serializing_if = "Option::is_none")] pub prompt: Option, } /// Open the archetype bucket for writing, creating it if nothing has yet. /// /// **Controller-side only.** `history: 1`: a reader wants the current value. #[cfg(feature = "kv")] pub async fn open_or_create( client: &async_nats::Client, ) -> Result { let js = async_nats::jetstream::new(client.clone()); crate::kv::open_or_create( &js, BUCKET, "agent-archetypes", "Agent archetypes the swarm builder evaluated, one key per source flake".to_owned(), ) .await } /// Open the archetype bucket for reading, or `None` when nothing has created /// it yet, which reads the same as no archetypes. /// /// # Errors /// /// [`crate::Error::OpenBucket`] for every failure other than the bucket not /// existing. #[cfg(feature = "kv")] pub async fn open( client: &async_nats::Client, ) -> Result, crate::Error> { let js = async_nats::jetstream::new(client.clone()); match js.get_key_value(BUCKET).await { Ok(store) => Ok(Some(store)), Err(e) if crate::wanted::is_bucket_not_found(&e) => Ok(None), Err(source) => Err(crate::Error::OpenBucket { bucket: BUCKET.to_owned(), source, }), } } #[cfg(test)] mod tests { use super::*; /// An unset setting is left out on the wire, and so is an empty refusal /// list. #[test] fn unset_fields_are_absent() { let mut archetypes = BTreeMap::new(); archetypes.insert( "reviewer".to_owned(), ArchetypeMeta { description: "reads".to_owned(), permissions: Some(vec!["messaging".to_owned()]), ..ArchetypeMeta::default() }, ); let record = SourceRecord { source: "/nix/store/h".to_owned(), archetypes, refused: BTreeMap::new(), }; assert_eq!( serde_json::to_string(&record).unwrap(), r#"{"source":"/nix/store/h","archetypes":{"reviewer":{"description":"reads","permissions":["messaging"]}}}"# ); } /// A record stored before `prompt` existed still parses, with no prompt; /// one with a prompt round-trips it. #[test] fn prompt_is_optional_on_the_wire() { let old = r#"{"source":"s","archetypes":{"reviewer":{"description":"reads"}}}"#; let mut record: SourceRecord = serde_json::from_str(old).unwrap(); assert_eq!(record.archetypes["reviewer"].prompt, None); record.archetypes.get_mut("reviewer").unwrap().prompt = Some("be brief".to_owned()); let wire = serde_json::to_string(&record).unwrap(); assert!(wire.contains(r#""prompt":"be brief""#), "{wire}"); assert_eq!(serde_json::from_str::(&wire).unwrap(), record); } }