feat(subagent): persona + toolFilter become real; structured runtime collapses to scoped registrations

SubagentStartRequest gains persona (capability-gated like toolFilter); the
in-process driver composes the child's scoped world in the factory's setup
window — persona as a scoped shadowing deployment:persona section,
toolFilter as a scoped tools.restrict() (loud unknown-name validation),
outputSchema as the scoped structured runtime. spawn/fork now advertise
every start-time capability; ACP stays all-false. A parent-scope teardown
effect links each child to its parent through the memoized handle, so a
disposed parent reaches its whole subtree even if the delegating tool's
finally never runs; subagent/start|end dispatch in the delegating parent's
scope.

structured.ts loses the placeholder schema, the final-assembly swap/strip,
the refcounted root runtime, and the WeakMap state: each child registers
its OWN capture tool (real schema), instruction section, and enforcement
listeners on child.ctx, riding the child's fiber. The commit listener is
call-keyed (a stale stage from a short-circuited post-execute chain is
dropped, never promoted on a later call), and one scoped prepend re-assert
listener preserves the final-assembly guarantee against a stripping global
listener.

tool-subagent gains persona/toolFilter/maxDepth passthrough config —
deny-listing the delegation tool (or maxDepth) is how a deployment bounds
recursion; the omitted-toolFilter schema key is forced absent (a
materialized {} would mean an empty allow-list, i.e. deny-everything).
This commit is contained in:
Tianyi Cui
2026-07-09 02:10:06 +08:00
parent 67cb9a591d
commit 15f4d1cd03
15 changed files with 398 additions and 475 deletions

View File

@@ -182,9 +182,9 @@ describe('dsh-subagent-fork', () => {
await run.dispose()
})
it('advertises depthLimit and outputSchema but not toolFilter', async () => {
it('advertises every start-time capability (depthLimit, outputSchema, toolFilter, persona)', async () => {
const { ctx } = await setup([])
expect(ctx.subagents.getProvider('fork')!.capabilities).toEqual({ outputSchema: true, depthLimit: true, toolFilter: false })
expect(ctx.subagents.getProvider('fork')!.capabilities).toEqual({ outputSchema: true, depthLimit: true, toolFilter: true, persona: true })
})
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {