Watch
0
0
Fork
You've already forked hyperhive
0

set an agent's initial agents.md at create

This commit is contained in:
atlas 2026-10-18 06:39:19 +02:00 • committed by mara
commit 456cf9a7cb
19 changed files with 708 additions and 19 deletions

View file

@ -16,8 +16,8 @@ use crate::jobq::{SwarmNodeKind, SwarmResourceKind, declare_agent_job};
use crate::routes::hives::HiveEntry;
use crate::routes::{AppState, StatusUnavailable, error_problem, wanted_error_status};
use crate::{
agent_archetype, agent_identity, agent_limits, agent_permissions, agent_status, auth,
inference, schedules, wanted,
agent_archetype, agent_identity, agent_limits, agent_permissions, agent_status,
agents_md_initial, auth, inference, schedules, wanted,
};
/// The swarm's agent roster — every agent the swarm holds an identity for.
@ -214,6 +214,7 @@ pub(crate) async fn set_agent_state(
revoke_or_complain(&hive, &agent).await;
agent_permissions::clear_on_destroy(&state, &hive, &agent).await;
agent_archetype::clear_on_destroy(&state, &hive, &agent).await;
agents_md_initial::clear_on_destroy(&state, &hive, &agent).await;
}
Ok(Json(render(&declaration)))
}
@ -516,6 +517,13 @@ pub(crate) struct CreateAgentRequest {
#[serde(default)]
#[schema(value_type = Option<Object>, example = json!({"source": "hyperhive", "name": "reviewer"}))]
pub(crate) archetype: Option<swarm_queue_client::agent_archetype::ArchetypeRef>,
/// The text the agent's `AGENTS.md` starts with, at most
/// `swarm_queue_client::agents_md_initial::MAX_BYTES` bytes. The agent's
/// harness writes it on its first start, and only when the agent has no
/// `AGENTS.md` yet: an existing file is never replaced. Absent leaves the
/// file to the harness.
#[serde(default)]
pub(crate) agents_md: Option<String>,
}
/// Queue the whole agent-creation job graph for `name` — two independent
@ -549,10 +557,10 @@ pub(crate) struct CreateAgentRequest {
request_body = CreateAgentRequest,
responses(
(status = 200, description = "job chain queued", body = CreateAgentResponse),
(status = 400, description = "`name` or `hive` is not a valid identifier, `name` is new and reserved or refused by the forge, `hive` is not in this swarm, `limits` or `inference` holds a value the matching `PUT` refuses, a permission is listed twice, or `archetype` is not one the stored archetype list holds (problem+json)", body = String),
(status = 400, description = "`name` or `hive` is not a valid identifier, `name` is new and reserved or refused by the forge, `hive` is not in this swarm, `limits` or `inference` holds a value the matching `PUT` refuses, a permission is listed twice, `archetype` is not one the stored archetype list holds, or `agents_md` is over the size limit (problem+json)", body = String),
(status = 409, description = "the swarm has already placed `name` on a different hive (problem+json)", body = String),
(status = 422, description = "an unknown field in the body, in `limits`, or in `inference`, an `effort` that is not one of the five levels, or a `permissions` entry that is not a permission name"),
(status = 503, description = "the swarm queue is not connected, so whether `name` is placed on another hive is unknown, or `limits`, `inference`, `permissions` or `archetype` is set and cannot be stored (problem+json)", body = String),
(status = 503, description = "the swarm queue is not connected, so whether `name` is placed on another hive is unknown, or `limits`, `inference`, `permissions`, `archetype` or `agents_md` is set and cannot be stored (problem+json)", body = String),
(status = 500, description = "the job chain could not be queued, or another hive's wanted state could not be read (problem+json)", body = String),
),
tag = "agents"
@ -567,7 +575,7 @@ pub(crate) async fn create_agent(
// Settings are checked before anything is created, so a bad value stops
// the request rather than leaving an agent without them.
let settings = CreateSettings::parse(req.limits, req.inference, req.permissions, req.archetype)
let settings = CreateSettings::parse(&req)
.map_err(|why| error_problem(axum::http::StatusCode::BAD_REQUEST, &why))?;
// Two checks, and the second is the one that makes the field worth
@ -665,6 +673,7 @@ pub(crate) async fn create_agent(
settings.inference.as_ref(),
settings.permissions.as_ref(),
settings.archetype.as_ref(),
settings.agents_md.as_deref(),
)
.await?
}
@ -706,18 +715,25 @@ struct CreateSettings {
inference: Option<swarm_queue_client::agent_inference::AgentInference>,
permissions: Option<swarm_queue_client::agent_permissions::AgentPermissions>,
archetype: Option<swarm_queue_client::agent_archetype::ArchetypeRef>,
agents_md: Option<String>,
}
impl CreateSettings {
/// Each setting given, or why its `PUT` would refuse the first bad one.
/// An archetype is checked here for its shape only; whether the swarm
/// lists it is [`agent_archetype::check`].
fn parse(
limits: Option<agent_limits::Caps>,
inference: Option<inference::InferenceSettings>,
permissions: Option<Vec<hive_sh4re::permissions::Permission>>,
archetype: Option<swarm_queue_client::agent_archetype::ArchetypeRef>,
) -> Result<Self, String> {
fn parse(req: &CreateAgentRequest) -> Result<Self, String> {
let CreateAgentRequest {
limits,
inference,
permissions,
archetype,
agents_md,
..
} = req.clone();
if let Some(text) = &agents_md {
agents_md_initial::check(text)?;
}
if let Some(a) = &archetype {
for part in [&a.source, &a.name] {
hive_types::Ident::parse(part)
@ -731,6 +747,7 @@ impl CreateSettings {
.transpose()?,
permissions: permissions.map(agent_permissions::at_create).transpose()?,
archetype,
agents_md,
})
}
@ -740,6 +757,7 @@ impl CreateSettings {
|| self.inference.is_some()
|| self.permissions.is_some()
|| self.archetype.is_some()
|| self.agents_md.is_some()
}
/// The queue to store the settings through, `None` when none is given.
@ -767,10 +785,11 @@ struct StoredSettings {
inference: inference::Replaced,
permissions: agent_permissions::Replaced,
archetype: agent_archetype::Replaced,
agents_md: agents_md_initial::Replaced,
}
impl StoredSettings {
/// Store whichever of `limits`, `permissions`, `inference` and `archetype` are
/// Store whichever of `limits`, `permissions`, `inference`, `archetype` and `agents_md` are
/// given, in that order. A failure on one puts back the ones stored before it.
async fn store(
client: &async_nats::Client,
@ -779,6 +798,7 @@ impl StoredSettings {
inference: Option<&swarm_queue_client::agent_inference::AgentInference>,
permissions: Option<&swarm_queue_client::agent_permissions::AgentPermissions>,
archetype: Option<&swarm_queue_client::agent_archetype::ArchetypeRef>,
agents_md: Option<&str>,
) -> Result<Self, problem_details::ProblemDetails> {
let mut stored = Self::default();
if let Some(limits) = limits {
@ -811,6 +831,15 @@ impl StoredSettings {
}
}
}
if let Some(text) = agents_md {
match agents_md_initial::store(client, agent, text).await {
Ok(replaced) => stored.agents_md = replaced,
Err(e) => {
stored.roll_back(client, agent).await;
return Err(e);
}
}
}
Ok(stored)
}
@ -819,6 +848,7 @@ impl StoredSettings {
inference::restore(client, agent, self.inference).await;
agent_permissions::restore(client, agent, self.permissions).await;
agent_archetype::restore(client, agent, self.archetype).await;
agents_md_initial::restore(client, agent, self.agents_md).await;
}
}
@ -1238,6 +1268,7 @@ mod tests {
inference: None,
permissions: None,
archetype: None,
agents_md: None,
}),
)
.await
@ -1280,6 +1311,7 @@ mod tests {
inference: None,
permissions: None,
archetype: None,
agents_md: None,
}),
)
.await
@ -1444,6 +1476,7 @@ mod tests {
Some(&inference),
None,
None,
None,
)
.await;
assert!(stored.is_err(), "{agent}: the inference write must fail");
@ -1583,6 +1616,7 @@ mod tests {
inference: None,
permissions: None,
archetype: None,
agents_md: None,
}),
)
.await