Skip to content

0.4.3 → 0.4.4 — Tool-trigger Filtering Fix (DEV-631)¤

Summary¤

This release fixes a silent cross-agent capability leak in ToolReference._resolve_single. Per-agent trigger filtering no longer leaks across agents sharing a setup_id: the SDK tool cache stops trimming ToolModuleInfo.tools by per-selection triggers; the full catalogue is always stored, and per-agent filtering happens at the consumer site.

The cache contract changes shape (full catalogue per setup_id instead of trimmed lists) but the public API is unchanged. Persisted resolved_tools written by <= 0.4.3 may still hold incorrectly trimmed lists — they are automatically flushed by the startup clear introduced in 0.4.3, but the first mission run after upgrading may want a setup-version bump to be absolutely safe. See the Migration section below.

Bug — capability leak across agents (DEV-631)¤

Symptom¤

In archetype-isaac, a single setup can host a main_agent, a team with N members and a workflow with M steps. Each agent independently selects which trigger protocols of a shared SDK tool (one setup_id) it can call, via a tools field carrying a list of ToolSelection({setup_id, triggers: {name: bool}}).

When several agents reference the same setup_id with different trigger booleans, the first agent walked by the SDK silently dictated which tools were loaded for every other agent. Example with three agents sharing one setup_id (a Knowledge Graph tool with 5 triggers):

Agent Configured triggers Effective tools (before fix)
main_agent all 5 true all 5
member "search only" {search: true, rest: false} all 5
member "edit only" {edit: true, rest: false} all 5

…and worse, if main_agent had edit: false, the "edit only" member would lose access to edit entirely — its own trigger config was ignored.

Root cause¤

ToolReference._resolve_single previously trimmed the resolved ToolModuleInfo.tools in place, based on a single ToolSelection's triggers:

if enabled_triggers := {name for name, enabled in entry.triggers.items() if enabled}:
    tool_info.tools = [t for t in tool_info.tools if t.name in enabled_triggers]

SetupModel._collect_from_tool_ref keys the resolution cache by setup_id only and short-circuits via has_uncached. Subsequent ToolReference instances pointing at the same setup_id therefore skipped resolution and reused the first writer's trimmed object. The trimmed ToolModuleInfo then populated context.tool_cache.entries, which downstream toolkit code (e.g. archetype-isaac's ModuleToolkit) reads from. Per-agent allowed_tools filters could only shrink that already-trimmed list, never re-expand it — so any trigger the first resolver dropped became permanently invisible to later agents.

resolved_tools is also persisted across mission runs (invalidated only on setup-version bumps prior to 0.4.3, or on module startup since 0.4.3), so a stale trimmed cache survived runs until the user edited the setup or restarted the module.

Fix¤

ToolReference._resolve_single no longer touches tool_info.tools. It returns the full module catalog, unchanged. The full canonical ToolModuleInfo is stored under tool_cache.entries[setup_id] and re-used by every agent that references the setup.

Per-agent filtering is the consumer's responsibility — for example archetype-isaac builds one ModuleToolkit(allowed_tools=...) per agent with allowed_tools derived from that agent's own ToolSelection.triggers. Filtering after the cache, on each toolkit instance, means three agents on the same setup_id with {search: true}, {edit: true} and {everything: true} each end up with exactly the tools they declared, even though the underlying cache entry holds the full catalog.

Architecture (after fix)¤

ToolReference.resolve
    └─ _resolve_single(entry)        # no triggers filter
         └─ registry.discover_by_id  # returns full ModuleInfo
              └─ module_info_to_tool_module_info → ToolModuleInfo (full tools)
                   └─ tool_cache.entries[setup_id] = info   # shared, untrimmed

Consumer (e.g. archetype-isaac)
    └─ ToolkitMixin.create_toolkit_for_selection(setup_id, triggers)
         ├─ allowed_tools = {name for name, enabled in triggers.items() if enabled}
         └─ ModuleToolkit(tool_module_info, allowed_tools=allowed_tools)
              └─ filters Function objects per agent, never mutates the cache

Migration¤

  • No code changes required in consumers. The ToolReference, ToolSelection, ToolModuleInfo and ToolCache types are unchanged in shape.
  • One-time data hygiene: persisted resolved_tools entries written by <= 0.4.3 may still hold incorrectly trimmed tools lists. The startup clear introduced in 0.4.3 already flushes them on the next module boot, so a vanilla restart after upgrading is enough. If you want belt-and-braces certainty (e.g. for shared production setups whose resolved_tools was written before the 0.4.3 startup clear landed), bump the affected setup's version once to force a clean re-resolution.
  • Archetypes relying on the previous (buggy) behaviour of having the first agent's triggers silently restrict all peers must instead set the desired triggers explicitly on each ToolSelection — every agent is now independently honoured.

Verification¤

The SDK ships with new regression coverage in tests/modules/test_tool_reference.py (class TestSharedSetupIdAcrossAgents):

  • two ToolReferences sharing a setup_id with disjoint triggers ({search: true, edit: false} and {search: false, edit: true}) both observe the full tool catalogue in tool_cache.entries;
  • a second resolution call does not progressively trim the cache.

archetype-isaac mirrors this with three agents (main, search-only, edit-only) sharing a single setup_id, asserting that each toolkit holds exactly its agent's enabled triggers while the shared cache entry remains untouched (tests/toolkits/test_toolkit_mixin.py::TestSharedSetupAcrossAgents).