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
@@ -113,3 +113,51 @@ pub async fn agents_for_template(
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// A claw already in this workspace that can take this template role again.
|
||||
///
|
||||
/// The workforce is meant to be KEPT: a mission that needs a `coder` should
|
||||
/// hire the one that already exists rather than mint a sixth. Without this,
|
||||
/// every zeroclaw mission added a whole team to the roster permanently — they
|
||||
/// are minted `lifecycle = 'permanent'` and nothing reaps them until the
|
||||
/// mission itself is deleted — while each member was used exactly once.
|
||||
///
|
||||
/// A claw currently 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; minting a second claw is much cheaper than
|
||||
/// reasoning about that.
|
||||
///
|
||||
/// Oldest first, so reuse concentrates on the same few claws and their brains
|
||||
/// actually accumulate, instead of spreading thinly across a growing pool.
|
||||
pub async fn reusable_claw(
|
||||
pool: &PgPool,
|
||||
workspace_id: uuid::Uuid,
|
||||
template_id: uuid::Uuid,
|
||||
role_slot: &str,
|
||||
) -> Result<Option<uuid::Uuid>, DbError> {
|
||||
let row: Option<(uuid::Uuid,)> = sqlx::query_as(
|
||||
"SELECT a.id
|
||||
FROM agents a
|
||||
JOIN agent_template_link l ON l.agent_id = a.id
|
||||
WHERE a.workspace_id = $1
|
||||
AND a.deleted_at IS NULL
|
||||
AND l.template_id = $2
|
||||
AND l.role_slot = $3
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM team_members tm
|
||||
JOIN mission_teams mt ON mt.team_id = tm.team_id
|
||||
JOIN missions m ON m.id = mt.mission_id
|
||||
WHERE tm.claw_id = a.id
|
||||
AND m.status = 'running'
|
||||
)
|
||||
ORDER BY a.created_at
|
||||
LIMIT 1",
|
||||
)
|
||||
.bind(workspace_id)
|
||||
.bind(template_id)
|
||||
.bind(role_slot)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row.map(|(id,)| id))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user