Watch
0
0
Fork
You've already forked hyperhive
0

an archetype's prompt prefills the new agent's agents.md in the create form

This commit is contained in:
atlas 2026-11-11 00:40:40 +01:00 • committed by mara
commit a5470d82ae

View file

@ -18,7 +18,9 @@ use problem_details::ProblemDetails;
use serde::{Deserialize, Serialize};
use swarm_controller_api::inference::InferenceSettings;
use swarm_controller_api::limits::Caps;
use swarm_queue_client::archetypes::{ArchetypeMeta, BUILTIN_SOURCE, SourceRecord};
use swarm_queue_client::archetypes::{
ArchetypeMeta, BUILTIN_SOURCE, MAX_PROMPT_BYTES, SourceRecord,
};
use swarm_wire::builder::{
ARCHETYPES_REPLY_DEADLINE, ARCHETYPES_SUBJECT, ArchetypesReply, EvalArchetypes,
};
@ -55,6 +57,8 @@ struct DeclaredMeta {
limits: Option<Caps>,
#[serde(default)]
inference: Option<InferenceSettings>,
#[serde(default)]
prompt: Option<String>,
}
/// `meta` as stored, or why a create would refuse it.
@ -64,6 +68,14 @@ fn check(name: &str, meta: serde_json::Value) -> Result<ArchetypeMeta, String> {
if declared.description.trim().is_empty() {
return Err("description is empty".to_owned());
}
if let Some(prompt) = &declared.prompt
&& prompt.len() > MAX_PROMPT_BYTES
{
return Err(format!(
"prompt is {} bytes; the limit is {MAX_PROMPT_BYTES}",
prompt.len()
));
}
let permissions = match declared.permissions {
Some(perms) => {
agent_permissions::unique(&perms)?;
@ -90,6 +102,7 @@ fn check(name: &str, meta: serde_json::Value) -> Result<ArchetypeMeta, String> {
.map(inference::into_stored)
.transpose()
.map_err(|e| format!("inference: {e}"))?,
prompt: declared.prompt.filter(|p| !p.trim().is_empty()),
})
}
@ -227,6 +240,9 @@ pub(crate) struct ArchetypeView {
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<Object>)]
inference: Option<swarm_wire::agent_inference::AgentInference>,
/// The text the create form prefills the agent's `AGENTS.md` with.
#[serde(skip_serializing_if = "Option::is_none")]
prompt: Option<String>,
}
/// One archetype a source declared and this daemon refused.
@ -253,6 +269,7 @@ fn view(key: &str, record: SourceRecord) -> Archetypes {
permissions: meta.permissions,
limits: meta.limits,
inference: meta.inference,
prompt: meta.prompt,
});
}
for (name, reason) in record.refused {
@ -344,6 +361,58 @@ mod tests {
assert!(meta.inference.as_ref().is_some_and(|i| i.effort.is_some()));
}
/// The prompt is stored as declared; one past the cap refuses the
/// archetype by name and the cap itself passes; a blank one is no prompt.
#[test]
fn the_prompt_is_stored_and_capped() {
let r = evaluated(json!({
"plain": { "description": "x" },
"with": { "description": "x", "prompt": "Review only." },
"blank": { "description": "x", "prompt": " \n" },
"at-cap": { "description": "x", "prompt": "a".repeat(MAX_PROMPT_BYTES) },
"over-cap": { "description": "x", "prompt": "a".repeat(MAX_PROMPT_BYTES + 1) },
"not-text": { "description": "x", "prompt": 3 }
}));
assert_eq!(r.archetypes["plain"].prompt, None);
assert_eq!(r.archetypes["with"].prompt.as_deref(), Some("Review only."));
assert_eq!(r.archetypes["blank"].prompt, None);
assert_eq!(
r.archetypes["at-cap"].prompt.as_deref().map(str::len),
Some(MAX_PROMPT_BYTES)
);
assert_eq!(
r.refused.keys().map(String::as_str).collect::<Vec<_>>(),
["not-text", "over-cap"]
);
assert!(
r.refused["over-cap"].contains(&MAX_PROMPT_BYTES.to_string()),
"{}",
r.refused["over-cap"]
);
}
/// `GET /api/archetypes` carries the prompt, and leaves it out when
/// there is none.
#[test]
fn the_view_carries_the_prompt() {
let r = evaluated(json!({
"with": { "description": "x", "prompt": "Review only." },
"plain": { "description": "x" }
}));
let wire = serde_json::to_value(view(BUILTIN_SOURCE, r)).unwrap();
let by_name = |name: &str| {
wire["archetypes"]
.as_array()
.unwrap()
.iter()
.find(|a| a["name"] == name)
.unwrap()
.clone()
};
assert_eq!(by_name("with")["prompt"], "Review only.");
assert!(by_name("plain").get("prompt").is_none());
}
#[test]
fn a_bad_archetype_is_refused_by_name_and_the_rest_stay() {
let r = evaluated(json!({