feat(workforce): missions hire the agents you already have, and name them by role
Every zeroclaw mission minted a fresh team of claws. They are created
`lifecycle = 'permanent'` and nothing reaps them until the MISSION is deleted,
so the roster grew by a whole team per mission while each member worked exactly
once — "My Workforce" was a list of strangers, and upskilling had nothing
durable to act on.
A mission now hires the claw that already does the job, matched on
`agent_template_link (template_id, role_slot)`, minting only what is missing.
Oldest first, so reuse concentrates on the same few claws and their brains
actually accumulate rather than spreading thinly across a growing pool.
A claw on a RUNNING mission is not offered. Two missions driving the same
ZeroClaw agent and the same `.brain` at once is a data race with a model on the
other end of it, and minting a second claw is much cheaper than reasoning about
that.
A reused claw is NOT re-seeded from the template's brain_seed — that would
overwrite what it learned with its starting point, which is precisely the
accumulation this exists for.
Names are the role now (`planner`), not
`"{mission} · {purpose} · {template} · {slot}"`. That produced
"verify: a repo-less research mission keeps its output · mission · Rust SDLC ·
planner" — unreadable in the roster, the API and every log line at once. Which
mission a claw is on is context a caller can join to; it is not its name.
And the half that makes reuse safe rather than destructive: deleting a mission
now purges only claws no OTHER mission still employs. Without it, tidying up one
mission deletes staff another one holds — presenting as the roster quietly
shrinking rather than as an error. A test asserts the guard exists inside the
reaper AND runs before the purge, because a check after it is decoration.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
895413509d
commit
fe2451fd60
@@ -463,10 +463,32 @@ async fn mint_team_from_template(
|
||||
template.roles.len(),
|
||||
));
|
||||
};
|
||||
// Hire the claw that already does this job, if it is free.
|
||||
//
|
||||
// Every zeroclaw mission used to mint a fresh set. They are created
|
||||
// `lifecycle = 'permanent'` and nothing reaps them until the MISSION is
|
||||
// deleted, so the roster grew by a whole team per mission while each
|
||||
// member worked once — and "My Workforce" was a list of strangers.
|
||||
let reused = cm_db::repo::agent_template_link::reusable_claw(
|
||||
pool,
|
||||
workspace_id.as_uuid().to_owned(),
|
||||
template.template.id,
|
||||
&role.slot,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("look up a reusable claw for {}: {e}", role.slot))?;
|
||||
|
||||
let agent = Agent {
|
||||
id: cm_domain::AgentId::new(),
|
||||
workspace_id,
|
||||
name: format!("{} · {}", team_name, role.slot),
|
||||
// The ROLE, not the mission.
|
||||
//
|
||||
// This was `"{mission title} · {purpose} · {template} · {slot}"`,
|
||||
// which produced names like "verify: a repo-less research mission
|
||||
// keeps its output · mission · Rust SDLC · planner" — unreadable in
|
||||
// the roster, the API and every log line at once. Which mission a
|
||||
// claw is on is context a caller can join to; it is not its name.
|
||||
name: role.slot.clone(),
|
||||
job_title: role.slot.clone(),
|
||||
// This is the ONLY consumer of the templates' `system_prompt` prose,
|
||||
// and it feeds the *chat* path, not missions: it lands in
|
||||
@@ -483,10 +505,23 @@ async fn mint_team_from_template(
|
||||
managed_by: user_id,
|
||||
status: AgentStatus::Online,
|
||||
};
|
||||
cm_db::repo::agents::insert(pool, &agent, &AccessPolicy::default())
|
||||
.await
|
||||
.map_err(|e| format!("insert agent {}: {e}", role.slot))?;
|
||||
let claw_id = agent.id.as_uuid();
|
||||
let claw_id = match reused {
|
||||
Some(existing) => {
|
||||
eprintln!(
|
||||
"mission_orchestrator: reusing claw {existing} for role {} \
|
||||
(template {})",
|
||||
role.slot, template.template.id
|
||||
);
|
||||
existing
|
||||
}
|
||||
None => {
|
||||
cm_db::repo::agents::insert(pool, &agent, &AccessPolicy::default())
|
||||
.await
|
||||
.map_err(|e| format!("insert agent {}: {e}", role.slot))?;
|
||||
agent.id.as_uuid()
|
||||
}
|
||||
};
|
||||
let agent_id = cm_domain::AgentId::from(claw_id);
|
||||
|
||||
// The ROLE's model when the template names one, else the mint's default.
|
||||
// Before migration 0071 there was no role model at all, so every claw of
|
||||
@@ -498,7 +533,7 @@ async fn mint_team_from_template(
|
||||
.map(str::trim)
|
||||
.filter(|m| !m.is_empty())
|
||||
.unwrap_or(default_model);
|
||||
cm_db::repo::agents::set_model_binding(pool, agent.id, role_model)
|
||||
cm_db::repo::agents::set_model_binding(pool, agent_id, role_model)
|
||||
.await
|
||||
.map_err(|e| format!("set_model_binding {claw_id}: {e}"))?;
|
||||
|
||||
@@ -518,7 +553,7 @@ async fn mint_team_from_template(
|
||||
.provision_claw(claw_id, role_model, &template.template.risk_profile)
|
||||
.await
|
||||
{
|
||||
Ok(_) => provisioned_claws.push(agent.id),
|
||||
Ok(_) => provisioned_claws.push(agent_id),
|
||||
Err(e) => eprintln!(
|
||||
"mission_orchestrator: provision claw {claw_id} failed (continuing): {e}"
|
||||
),
|
||||
@@ -527,6 +562,10 @@ async fn mint_team_from_template(
|
||||
|
||||
// Ingest brain seed (Slice 3.5d). Non-fatal on failure —
|
||||
// agent still works from system_prompt alone.
|
||||
// Seed only a NEW claw. A reused one carries what it learned on earlier
|
||||
// missions, and re-seeding would overwrite that with the template's
|
||||
// starting point — which is precisely the accumulation reuse exists for.
|
||||
if reused.is_none() {
|
||||
if let Some(seed) = role.brain_seed.as_deref().filter(|s| !s.trim().is_empty()) {
|
||||
if let Err(e) =
|
||||
crate::brain_seed::ingest(claw_id, seed.to_string(), role.system_prompt.clone())
|
||||
@@ -537,6 +576,7 @@ async fn mint_team_from_template(
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record lineage (Slice 3.5d) so the MCP skills server can
|
||||
// merge template default skills with per-agent overrides.
|
||||
@@ -565,9 +605,10 @@ async fn mint_team_from_template(
|
||||
cm_db::repo::audit::Actor::User(user_id),
|
||||
"agent.created",
|
||||
"agent",
|
||||
&agent.id.to_string(),
|
||||
&agent_id.to_string(),
|
||||
serde_json::json!({
|
||||
"name": agent.name,
|
||||
"reused": reused.is_some(),
|
||||
"job_title": agent.job_title,
|
||||
"source": "mission_orchestrator",
|
||||
"template_id": template.template.id.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user