Skip to content

feat(skill): namespace inner tools as {skill}__{tool} and detect co…#29

Open
berges99 wants to merge 1 commit into
mainfrom
feat/skill-tool-namespace
Open

feat(skill): namespace inner tools as {skill}__{tool} and detect co…#29
berges99 wants to merge 1 commit into
mainfrom
feat/skill-tool-namespace

Conversation

@berges99
Copy link
Copy Markdown
Collaborator

@berges99 berges99 commented May 8, 2026

…llisions eagerly

Default-on auto-namespacing for skill-internal tools, plus an eager construction-time check that catches every static tool-name collision the LLM could ever see. Two motivations:

  1. Weaker LLMs frequently confuse a skill name with a tool name and try to call the skill directly. Prefixing inner tools makes provenance explicit in the function list and steers the model toward the right call. The system prompt is updated to teach the convention when any namespaced skill is present.
  2. Inner tools could silently shadow top-level tools — _register only warned and dropped the duplicate. Namespacing eliminates the most common collision class structurally, and the eager check fails fast on anything that slips through (including two skills slugifying to the same prefix, e.g. my.skill vs my_skill).

Implementation notes:

  • Skill names are slugified to [a-zA-Z0-9_-], runs of _ collapsed. Combined {prefix}__{tool} is capped at 64 chars (OpenAI/Anthropic limit) and validated up front.
  • namespace_tools=False keeps the legacy flat names for callers who rely on the old behaviour or have already designed around it.
  • Renaming is idempotent across Skill() constructions via a class-level id(tool) -> base_name map. Required because sys.modules shares the imported Tool object across constructions; without it a second Skill(path=...) would compound the prefix to cars__cars__search_cars.
  • The full cached_property chain on Tool is invalidated whenever a tool is renamed (params_model, params_model_schema, _formatted_params_schema, anthropic_schema, openai_chat_completions_schema, openai_responses_schema). Normal agent construction never populates these before the rename, but toggling namespace_tools between two Skills for the same path was serving stale per-provider schemas — fixed and pinned by a regression test.
  • Agent.model_post_init enumerates every static-known LLM-visible name (top-level tools, skill-internal tools, read_skill) and raises ValueError on duplicates, naming both origins. Dynamic ToolSets whose resolve() runs at runtime are deferred to a defensive logger.error in _resolve_tools._register (demoted from warning; should be unreachable for static configs).

Tests cover both with- and without-namespace flows end-to-end:

  • All three provider schema serializers asserted under both modes.
  • Cache-invalidation regression: warm every cached schema on Skill feat: add OpenAI TTS functionality #1, reconstruct as flat, assert every schema reports the bare name.
  • Idempotency across re-construction (no cars__cars__ compounding).
  • Slugification, empty-prefix and >64-char name validation.
  • 5 collision scenarios in TestAgentEagerCollisionDetection (flat-skill vs top-level, two flat skills, two namespaced skills not colliding, two skills slugifying to same prefix, error message names both origins).
  • Codegen + integration trace tests updated to expect the prefixed paths (support_agent.payment_processing__process_refund, etc.).

Note

Medium Risk
Changes the public tool names exposed to the LLM (and thus user prompts, traces, and provider schemas) and adds new construction-time validation that can raise ValueError for previously-accepted configurations. The behavior is well-covered by tests but could be a breaking change for callers relying on flat skill tool names.

Overview
Skill-internal tools are now namespaced by default as {skill}__{tool}, including slugification/length validation to meet provider tool-name constraints and cache invalidation so provider schemas reflect the renamed tool.

Agent initialization now fails fast on any statically-detectable tool-name collision across top-level tools, skill-internal tools (including when namespace_tools=False), and read_skill; runtime duplicate registration is treated as an error log and dropped as a last-resort fallback.

Tests are updated/expanded to assert the new names in tool lists, provider schemas, and trace paths (standalone and workflow-wrapped), and to cover opt-out behavior and multiple collision/edge cases.

Reviewed by Cursor Bugbot for commit a722deb. Bugbot is set up for automated code reviews on this repo. Configure here.

…llisions eagerly

Default-on auto-namespacing for skill-internal tools, plus an eager
construction-time check that catches every static tool-name collision the
LLM could ever see. Two motivations:

1. Weaker LLMs frequently confuse a skill name with a tool name and try
   to call the skill directly. Prefixing inner tools makes provenance
   explicit in the function list and steers the model toward the right
   call. The system prompt is updated to teach the convention when any
   namespaced skill is present.
2. Inner tools could silently shadow top-level tools — `_register` only
   warned and dropped the duplicate. Namespacing eliminates the most
   common collision class structurally, and the eager check fails fast
   on anything that slips through (including two skills slugifying to
   the same prefix, e.g. `my.skill` vs `my_skill`).

Implementation notes:

- Skill names are slugified to `[a-zA-Z0-9_-]`, runs of `_` collapsed.
  Combined `{prefix}__{tool}` is capped at 64 chars (OpenAI/Anthropic
  limit) and validated up front.
- `namespace_tools=False` keeps the legacy flat names for callers who
  rely on the old behaviour or have already designed around it.
- Renaming is idempotent across `Skill()` constructions via a class-level
  `id(tool) -> base_name` map. Required because `sys.modules` shares the
  imported Tool object across constructions; without it a second
  `Skill(path=...)` would compound the prefix to `cars__cars__search_cars`.
- The full cached_property chain on Tool is invalidated whenever a tool
  is renamed (params_model, params_model_schema, _formatted_params_schema,
  anthropic_schema, openai_chat_completions_schema, openai_responses_schema).
  Normal agent construction never populates these before the rename, but
  toggling `namespace_tools` between two Skills for the same path was
  serving stale per-provider schemas — fixed and pinned by a regression
  test.
- `Agent.model_post_init` enumerates every static-known LLM-visible
  name (top-level tools, skill-internal tools, `read_skill`) and raises
  `ValueError` on duplicates, naming both origins. Dynamic ToolSets
  whose `resolve()` runs at runtime are deferred to a defensive
  `logger.error` in `_resolve_tools._register` (demoted from warning;
  should be unreachable for static configs).

Tests cover both with- and without-namespace flows end-to-end:
- All three provider schema serializers asserted under both modes.
- Cache-invalidation regression: warm every cached schema on Skill #1,
  reconstruct as flat, assert every schema reports the bare name.
- Idempotency across re-construction (no `cars__cars__` compounding).
- Slugification, empty-prefix and >64-char name validation.
- 5 collision scenarios in `TestAgentEagerCollisionDetection`
  (flat-skill vs top-level, two flat skills, two namespaced skills not
  colliding, two skills slugifying to same prefix, error message names
  both origins).
- Codegen + integration trace tests updated to expect the prefixed
  paths (`support_agent.payment_processing__process_refund`, etc.).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant