set an agent's initial agents.md at create
This commit is contained in:
parent
a6147dc875
commit
456cf9a7cb
19 changed files with 708 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue