an archetype's prompt prefills the new agent's agents.md in the create form
This commit is contained in:
parent
2acde41e54
commit
a5470d82ae
1 changed files with 179 additions and 12 deletions
|
|
@ -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!({
|
||||
|
|
|
|||
Loading…
Reference in a new issue