//! An agent's `AGENTS.md`, as the swarm sees it: a read mirror in the //! `agents-md` KV bucket, and the edit command that changes the file. //! //! **The file in the agent's state dir is the truth.** The agent's harness is //! the only writer of both the file and its key here: it publishes the file's //! text and [`crate::agents_md::Mirror::revision`] when it starts and whenever //! the file changes, so the swarm can show the text while the agent is //! stopped. An edit from the swarm is a [`crate::agents_md::Apply`] sent on the //! agent's control subject, which the harness applies to the file itself and //! answers with a [`crate::agents_md::ApplyReply`]. //! //! Keyed by the agent alone, for the reason [`crate::agent_icon`] gives. The //! auth callout grants each agent's credential `$KV.agents-md.` and no //! other key; the writer publishes to [`crate::agents_md::subject`] without //! opening the bucket, and swarm-controller creates it. #[cfg(feature = "kv")] use crate::Error; /// The KV bucket the mirrors are published into, keyed by agent name. pub const BUCKET: &str = "agents-md"; /// The subject `agent`'s mirror is written on: `$KV..`. #[must_use] pub fn subject(agent: &str) -> String { format!("$KV.{BUCKET}.{agent}") } /// The value under an agent's key: its `AGENTS.md` as last published. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Mirror { /// The file's text. An agent with no `AGENTS.md` publishes `""`. pub content: String, /// The git blob id of the file's bytes, what `git hash-object` prints for /// it. An [`Apply`] names the revision its diff was made against. pub revision: String, } /// An edit to the agent's `AGENTS.md`: a unified diff, as `git diff` writes /// it (`a/AGENTS.md` and `b/AGENTS.md`), against the text at /// [`Self::base_revision`]. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Apply { /// The [`Mirror::revision`] the diff was made against. The harness /// refuses the edit when the file is no longer at it. pub base_revision: String, /// The diff. pub diff: String, } /// What the harness answers an [`Apply`] with. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case", tag = "outcome")] pub enum ApplyReply { /// The diff is applied; the file is now at `revision`. Applied { revision: String }, /// The file is not at the base revision, so nothing was applied. It is at /// `revision`. Stale { revision: String }, /// `git apply` refused the diff, or the diff touches something other than /// `AGENTS.md`. The file is unchanged; `error` is git's own message or /// says what the diff touched. Rejected { error: String }, /// The harness could not read or write the file, or run git. Failed { error: String }, } /// Open the bucket, creating it if nothing has yet. `history: 1`, as for /// [`crate::agent_icon::open_or_create`]: the swarm wants the current text. #[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, "agents-md", "Each agent's AGENTS.md, as its harness last published it".to_owned(), ) .await } #[cfg(test)] mod tests { use super::{ApplyReply, subject}; /// One token after the bucket, the agent name alone: what the per-agent /// grant `$KV.agents-md.{agent}` expands to. #[test] fn the_published_subject_carries_the_agent_alone() { assert_eq!(subject("iris"), "$KV.agents-md.iris"); } #[test] fn a_reply_names_its_outcome() { let wire = serde_json::to_string(&ApplyReply::Stale { revision: "abc".to_owned(), }) .expect("encodes"); assert_eq!(wire, r#"{"outcome":"stale","revision":"abc"}"#); } }