From f90bb63a9fb65666677fa46174b672bc9420cf13 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:06:48 +0200 Subject: [PATCH] Use prompt-declared Responses tool names Prefer explicit tool_name examples and first prompt headings when deriving native Responses function tool names, falling back to the prompt filename only when no callable name is declared. Add regression coverage for code_execution_tool, memory_load, call_subordinate, behaviour_adjustment, and filename-only fallback, and document the contract in responses_tools DOX. --- helpers/responses_tools.py | 35 ++++++++-- helpers/responses_tools.py.dox.md | 1 + tests/test_responses_tools.py | 103 ++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 tests/test_responses_tools.py diff --git a/helpers/responses_tools.py b/helpers/responses_tools.py index 84bf50661..e0d0e6fc3 100644 --- a/helpers/responses_tools.py +++ b/helpers/responses_tools.py @@ -10,6 +10,10 @@ from helpers import files, subagents FUNCTION_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$") +TOOL_NAME_EXAMPLE_PATTERN = re.compile( + r"""["']tool_name["']\s*:\s*["']([A-Za-z0-9_-]{1,64})["']""" +) +TOOL_HEADING_PATTERN = re.compile(r"^\s{0,3}#{1,6}\s+(.+?)\s*$", re.MULTILINE) TOOL_PROMPT_PREFIX = "agent.system.tool." TOOL_PROMPT_SUFFIX = ".md" MAX_TOOL_DESCRIPTION_CHARS = 1024 @@ -62,10 +66,8 @@ def _local_tool_prompts(agent: Any) -> list[tuple[str, str]]: result: list[tuple[str, str]] = [] for tool_file in tool_files: basename = os.path.basename(tool_file) - tool_name = _tool_name_from_prompt_basename(basename) - if not tool_name: - continue - if not _include_local_tool_prompt(agent, tool_name): + fallback_name = _tool_name_from_prompt_basename(basename) + if not fallback_name: continue try: prompt = agent.read_prompt(basename) @@ -74,6 +76,9 @@ def _local_tool_prompts(agent: Any) -> list[tuple[str, str]]: prompt = files.read_file(tool_file) except Exception: prompt = "" + tool_name = _tool_name_from_prompt(prompt, fallback=fallback_name) + if not _include_local_tool_prompt(agent, tool_name): + continue result.append((tool_name, prompt)) return result @@ -116,6 +121,28 @@ def _tool_name_from_prompt_basename(basename: str) -> str: return name +def _tool_name_from_prompt(prompt: str, *, fallback: str) -> str: + for match in TOOL_NAME_EXAMPLE_PATTERN.finditer(prompt or ""): + name = match.group(1).strip() + if FUNCTION_NAME_PATTERN.fullmatch(name): + return name + + for match in TOOL_HEADING_PATTERN.finditer(prompt or ""): + name = _tool_name_from_heading(match.group(1)) + if name: + return name + + return fallback + + +def _tool_name_from_heading(heading: str) -> str: + token = (heading or "").strip().split(None, 1)[0] if heading else "" + name = token.strip("`'\" :") + if FUNCTION_NAME_PATTERN.fullmatch(name): + return name + return "" + + def _native_tool_name(tool_name: str) -> str: if FUNCTION_NAME_PATTERN.fullmatch(tool_name): return tool_name diff --git a/helpers/responses_tools.py.dox.md b/helpers/responses_tools.py.dox.md index 8c92a0200..094f4b522 100644 --- a/helpers/responses_tools.py.dox.md +++ b/helpers/responses_tools.py.dox.md @@ -13,6 +13,7 @@ ## Local Contracts - Build local function tools from enabled `agent.system.tool.*.md` prompt files. +- Local prompt-derived function names prefer explicit `"tool_name"` examples, then the first prompt heading, and only fall back to the prompt filename when the prompt declares no callable name. - Preserve original Agent Zero tool names through the native Responses name map. - Keep MCP tool schemas merged after local prompt-derived tools. - Connector remote tools are advertised only when `_a0_connector` runtime metadata says the matching connected CLI capability is currently available. diff --git a/tests/test_responses_tools.py b/tests/test_responses_tools.py new file mode 100644 index 000000000..f6aa5155b --- /dev/null +++ b/tests/test_responses_tools.py @@ -0,0 +1,103 @@ +import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from helpers import responses_tools + + +class FakeAgent: + def __init__(self, prompt_root: Path): + self.prompt_root = prompt_root + + def read_prompt(self, file: str, **kwargs) -> str: + return (self.prompt_root / file).read_text(encoding="utf-8") + + +def _write_prompt(prompt_root: Path, basename: str, content: str) -> None: + (prompt_root / basename).write_text(content.strip() + "\n", encoding="utf-8") + + +def test_responses_function_tools_use_prompt_declared_names(monkeypatch, tmp_path): + prompt_root = tmp_path / "prompts" + prompt_root.mkdir() + _write_prompt( + prompt_root, + "agent.system.tool.code_exe.md", + """ + ### code_execution_tool + run terminal commands + ```json + {"tool_name": "code_execution_tool", "tool_args": {"runtime": "terminal"}} + ``` + """, + ) + _write_prompt( + prompt_root, + "agent.system.tool.memory.md", + """ + ## memory tools + durable memory operations + ```json + {"tool_name": "memory_load", "tool_args": {"query": "responses naming"}} + ``` + """, + ) + _write_prompt( + prompt_root, + "agent.system.tool.call_sub.md", + """ + ### call_subordinate + delegate a subtask + ```json + {"tool_name": "call_subordinate", "tool_args": {"message": "inspect"}} + ``` + """, + ) + _write_prompt( + prompt_root, + "agent.system.tool.behaviour.md", + """ + ### behaviour_adjustment + update persistent behavioral rules + """, + ) + _write_prompt( + prompt_root, + "agent.system.tool.filename_only.md", + "plain prompt with no declared callable name", + ) + + monkeypatch.setattr( + responses_tools.subagents, + "get_paths", + lambda *args, **kwargs: [str(prompt_root)], + ) + monkeypatch.setattr( + responses_tools, + "_include_local_tool_prompt", + lambda agent, tool_name: True, + ) + monkeypatch.setattr(responses_tools, "_mcp_tools", lambda agent: []) + + tools, name_map = responses_tools.build_responses_function_tools( + FakeAgent(prompt_root) + ) + + names = {tool["name"] for tool in tools} + assert { + "code_execution_tool", + "memory_load", + "call_subordinate", + "behaviour_adjustment", + "filename_only", + } <= names + assert not {"code_exe", "memory", "call_sub", "behaviour"} & names + assert name_map["code_execution_tool"] == "code_execution_tool" + assert name_map["memory_load"] == "memory_load" + assert name_map["call_subordinate"] == "call_subordinate" + assert name_map["behaviour_adjustment"] == "behaviour_adjustment" + assert name_map["filename_only"] == "filename_only"