Settle subagents: it means delegate to, not borrow skills from
Some checks failed
CI / check (push) Has been cancelled

The key had two incompatible meanings running at once. CLAUDE.md §3 defines
subagents as "keys of other specs this may DELEGATE to" and §6 as a tool call
from the parent's perspective — the subagent runs its own turn, as the same
caller, out of the parent's budget, and returns an answer. The backend
implements exactly that.

This side did something else: agentSkillIds folded one level of subagent skills
into the parent's carried set, so a parent silently gained everything its
subagents carried, and the UI described it that way — "other agents whose
skills this one may also use", "borrowing them cannot reach data this page does
not hold".

Both are defensible readings. Only one is the specification, and running both
meant krow-workforce-agent carried eight delegation tools AND the flattened
skills of those same eight agents — able to answer a question directly or to
ask an agent that had already lent it the means to answer. Two ways to do one
thing, differing in cost and in what the trajectory records.

So an agent carries what it declares. Reaching another agent is delegation,
which the runtime does with its own budget and its own trajectory.

The blast radius was one check, which is the useful part of the answer: only
"a subagent cycle terminates" depended on the folding, because that traversal
was the only thing that could loop. Nothing walks subagents here now, so the
cycle question moved to where it belongs — refused at publish by
definition.FindSubagentCycle, bounded at run time by the depth cap. The
replacement checks assert the new meaning rather than deleting the old ones,
because the previous behaviour reads as perfectly reasonable and will be
reinvented otherwise.

The `agents` parameter stays on agentSkillIds and is no longer read. Removing
it is a wider edit for no behavioural gain, and agentScopedDisabledWith exists
precisely to pass it.

924/924 checks pass; production build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
This commit is contained in:
2026-08-29 15:29:09 +05:30
parent 1d35358dd6
commit 1a0dc7e5f1
3 changed files with 45 additions and 38 deletions

View File

@@ -3023,25 +3023,32 @@ record('a requested agent is never silently swapped for another',
/* ── Subagents ──────────────────────────────────────────────────────────── */
record('subagent skills are inherited one level deep',
runtime.agentSkillIds(rootAgent, ALL_AGENTS).length >= rootAgent.skills.length,
`${rootAgent.skills.length} own → ${runtime.agentSkillIds(rootAgent, ALL_AGENTS).length} with subagents`);
/* `subagents` means DELEGATE TO, not borrow from — CLAUDE.md §3 and §6. These
checks pin that, because the opposite behaviour lived here for a long time
and reads as reasonable: a parent silently carried one level of its
subagents' skills, which is a second and incompatible meaning for the key
the backend uses to spawn a delegated run. */
record('an unpublished subagent contributes nothing',
record('an agent carries only its own skills, never a subagent\'s',
(() => {
const draftSub = { ...agentReg.getAgent(ALL_AGENTS, 'analytics-agent'), status: 'draft' };
const others = ALL_AGENTS.map((a) => (a.id === 'analytics-agent' ? draftSub : a));
const withDraft = runtime.agentSkillIds(rootAgent, others);
const withPublished = runtime.agentSkillIds(rootAgent, ALL_AGENTS);
return withDraft.length <= withPublished.length;
})());
const a = { id: 'a', skills: ['s1'], subagents: ['b'], status: 'published' };
const b = { id: 'b', skills: ['s2'], subagents: [], status: 'published' };
const carried = runtime.agentSkillIds(a, [a, b]);
return carried.length === 1 && carried[0] === 's1';
})(),
'reaching another agent is delegation, which the runtime does with its own budget and trajectory');
record('a subagent cycle terminates',
record('the shipped root agent carries exactly what it declares',
runtime.agentSkillIds(rootAgent, ALL_AGENTS).length === new Set(rootAgent.skills).size,
`${new Set(rootAgent.skills).size} declared`);
record('a subagent cycle cannot be walked, because nothing walks subagents here',
(() => {
const a = { id: 'a', skills: ['s1'], subagents: ['b'], status: 'published' };
const b = { id: 'b', skills: ['s2'], subagents: ['a'], status: 'published' };
return runtime.agentSkillIds(a, [a, b]).length === 2;
})());
return runtime.agentSkillIds(a, [a, b]).length === 1;
})(),
'cycles are refused at publish (definition.FindSubagentCycle) and bounded at run time by the depth cap');
/* ── Starters ───────────────────────────────────────────────────────────── */