Commit 7124c6c
authored
fix(core): rebuild tool registry on subagent Config overrides so bound tools resolve to the subagent (#3873)
* fix(core): rebuild tool registry on subagent Config overrides so bound tools resolve to the subagent
PR-B (#3774) added per-Config FileReadCache isolation via Object.create
overrides at two subagent spawn sites — agent.ts:createApprovalModeOverride
and subagent-manager.ts:maybeOverrideContentGenerator. The override
shielded code that read FileReadCache directly through the Config
instance, but missed the bound-tool path: Config.createToolRegistry runs
once at parent initialise time, so the parent's EditTool / WriteFileTool
/ ReadFileTool instances are bound with `this.config = parent`. The
subagent's Object.create wrapper inherited getToolRegistry via the
prototype chain, reaching the parent registry whose bound tools then
read FileReadCache and approval mode from the parent.
This change closes that gap by rebuilding the tool registry on the
override at both sites — the same pattern InProcessBackend.createPerAgentConfig
already uses:
- override.createToolRegistry(undefined, { skipDiscovery: true })
- registry.copyDiscoveredToolsFrom(base.getToolRegistry())
- override.getToolRegistry = () => registry
createApprovalModeOverride becomes async; its single call site already
ran inside an async block. maybeOverrideContentGenerator skips the
rebuild when the upstream Config already has its own getToolRegistry
(real-world case: agent.ts wrapper passed through createAgentHeadless),
avoiding wasted work, listener accumulation on shared SubagentManager /
SkillManager, and a cache split where the bound tools' registry layer
diverges from the runtime context's lazy-init cache.
Includes regression tests in agent-override.test.ts and
subagent-manager-override.test.ts that exercise the bound-tool path:
they instantiate the lazy factories on the override registry and
assert that EditTool / WriteFileTool / ReadFileTool resolve
this.config to the override Config (and thus to the override's
FileReadCache / approval mode), not the parent.
* fix(core): close bound-tool gap on resumed background agents too
Follow-up audit on PR #3873 surfaced a duplicate, pre-rebuild copy of
`createApprovalModeOverride` living in `background-agent-resume.ts`
(L142-150). Resumed fork agents go through `createResumedForkSubagent`
which bypasses `SubagentManager.maybeOverrideContentGenerator` (where
the registry rebuild now lives), so the resumed fork's
`EditTool` / `WriteFileTool` / `ReadFileTool` were still resolving
`this.config` to the parent and reading the parent's `FileReadCache`.
The non-fork resume path went through `subagent-manager` and worked
correctly only because `maybeOverrideContentGenerator` saw no upstream
own-registry on `bgConfig` and rebuilt one — but with that fallback
the fork path could never benefit.
This change deletes the local copy and switches `background-agent-resume.ts`
to import the now-async exported `createApprovalModeOverride` from
`agent.ts`. Drops the previous `?: this.config` short-circuit so the
resumed agent ALWAYS gets a wrapper Config — the same behaviour
`agent.ts` already enforces; reusing the parent directly defeats the
per-Config FileReadCache isolation.
Updates `background-agent-resume.test.ts` mock config with the
`createToolRegistry` / `getToolRegistry` stubs the rebuild path now
exercises.
* fix(core): address bound-tool isolation review feedback
Three independent fixes from PR #3873 review feedback:
1. Switch the upstream-rebuild guard from
`hasOwnProperty(base, 'getToolRegistry')` to a Symbol-keyed marker
`TOOL_REGISTRY_REBUILT`. The own-property check missed the case
where the override is reached via an Object.create wrapper above
the rebuilt Config (e.g. `bgConfig = Object.create(agentConfig)` in
the agent.ts background path) — it would falsely report "no
upstream rebuild" and cause a redundant third rebuild that wastes
work and doubles the listener-leak surface. Symbol property reads
walk the prototype chain via normal lookup, so a marker stored on
any ancestor is correctly observed.
Extracts the shared rebuild logic into
`rebuildToolRegistryOnOverride(override, base)` so the three spawn
sites (agent.ts:createApprovalModeOverride, the inherits branch,
the non-inherit branch) cannot drift apart.
2. Stop the per-subagent ToolRegistry in the lifecycle finally blocks:
- agent.ts foreground finally (after the inner try wrapping
`runFramed`)
- agent.ts background bgBody finally (after `bgSubagent.execute`
resolves)
- background-agent-resume.ts resume body finally (same shape)
Without this, every AgentTool / SkillTool the model instantiates
from the per-subagent registry registers a change-listener on
shared SubagentManager / SkillManager, and repeated subagent runs
accumulate listeners for the rest of the session. Stop is
fire-and-forget, matching `InProcessBackend.cleanup` and
`stopAgent`.
3. Add bound-tool isolation tests for the non-inherit branch
(explicit-model selector). The original PR only covered the
inherits branch directly; the non-inherit branch now goes through
the same helper, but a dedicated test pins
`tool.config === override` and the FileReadCache binding so a
regression cannot leave explicit-model subagents reading the
parent's cache while existing model-override tests still pass.
Tests now exercise:
- Symbol marker propagation via Object.create chain (3 cases)
- Non-inherit rebuild + bound-tool isolation
- Non-inherit skip-rebuild when upstream wrapper has the marker
- Pre-existing inherits / chained-override / approval-mode propagation
- Mock configs in agent.test.ts / subagent-manager.test.ts /
background-agent-resume.test.ts gain `stop` and `tools: Map` stubs
to model the registry contract the override path now exercises.
`npx vitest run packages/core/src` — 268 files / 6943 passed.1 parent b9f56e4 commit 7124c6c
8 files changed
Lines changed: 862 additions & 41 deletions
File tree
- packages/core/src
- agents
- subagents
- tools/agent
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
61 | 72 | | |
62 | 73 | | |
63 | 74 | | |
| |||
72 | 83 | | |
73 | 84 | | |
74 | 85 | | |
| 86 | + | |
| 87 | + | |
75 | 88 | | |
76 | 89 | | |
77 | 90 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| |||
139 | 141 | | |
140 | 142 | | |
141 | 143 | | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | 144 | | |
153 | 145 | | |
154 | 146 | | |
| |||
527 | 519 | | |
528 | 520 | | |
529 | 521 | | |
530 | | - | |
531 | | - | |
532 | | - | |
533 | | - | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
534 | 534 | | |
535 | 535 | | |
536 | 536 | | |
| |||
771 | 771 | | |
772 | 772 | | |
773 | 773 | | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
774 | 784 | | |
775 | 785 | | |
776 | 786 | | |
| |||
0 commit comments