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.

Alessandro committed Jun 15, 2026 at 15:06 UTC f90bb63a9fb65666677fa46174b672bc9420cf13
3 files changed +135 -4
helpers/responses_tools.py
+31 -4
@@ -10,6 +10,10 @@ from helpers import files, subagents
10
11
12 FUNCTION_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
13 +TOOL_NAME_EXAMPLE_PATTERN = re.compile(
14 + r"""["']tool_name["']\s*:\s*["']([A-Za-z0-9_-]{1,64})["']"""
15 +)
16 +TOOL_HEADING_PATTERN = re.compile(r"^\s{0,3}#{1,6}\s+(.+?)\s*$", re.MULTILINE)
17 TOOL_PROMPT_PREFIX = "agent.system.tool."
18 TOOL_PROMPT_SUFFIX = ".md"
19 MAX_TOOL_DESCRIPTION_CHARS = 1024
@@ -62,10 +66,8 @@ def _local_tool_prompts(agent: Any) -> list[tuple[str, str]]:
66 result: list[tuple[str, str]] = []
67 for tool_file in tool_files:
68 basename = os.path.basename(tool_file)
65 - tool_name = _tool_name_from_prompt_basename(basename)
66 - if not tool_name:
67 - continue
68 - if not _include_local_tool_prompt(agent, tool_name):
69 + fallback_name = _tool_name_from_prompt_basename(basename)
70 + if not fallback_name:
71 continue
72 try:
73 prompt = agent.read_prompt(basename)
@@ -74,6 +76,9 @@ def _local_tool_prompts(agent: Any) -> list[tuple[str, str]]:
76 prompt = files.read_file(tool_file)
77 except Exception:
78 prompt = ""
79 + tool_name = _tool_name_from_prompt(prompt, fallback=fallback_name)
80 + if not _include_local_tool_prompt(agent, tool_name):
81 + continue
82 result.append((tool_name, prompt))
83 return result
84
@@ -116,6 +121,28 @@ def _tool_name_from_prompt_basename(basename: str) -> str:
121 return name
122
123
124 +def _tool_name_from_prompt(prompt: str, *, fallback: str) -> str:
125 + for match in TOOL_NAME_EXAMPLE_PATTERN.finditer(prompt or ""):
126 + name = match.group(1).strip()
127 + if FUNCTION_NAME_PATTERN.fullmatch(name):
128 + return name
129 +
130 + for match in TOOL_HEADING_PATTERN.finditer(prompt or ""):
131 + name = _tool_name_from_heading(match.group(1))
132 + if name:
133 + return name
134 +
135 + return fallback
136 +
137 +
138 +def _tool_name_from_heading(heading: str) -> str:
139 + token = (heading or "").strip().split(None, 1)[0] if heading else ""
140 + name = token.strip("`'\" :")
141 + if FUNCTION_NAME_PATTERN.fullmatch(name):
142 + return name
143 + return ""
144 +
145 +
146 def _native_tool_name(tool_name: str) -> str:
147 if FUNCTION_NAME_PATTERN.fullmatch(tool_name):
148 return tool_name
helpers/responses_tools.py.dox.md
+1
@@ -13,6 +13,7 @@
13 ## Local Contracts
14
15 - Build local function tools from enabled `agent.system.tool.*.md` prompt files.
16 +- 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.
17 - Preserve original Agent Zero tool names through the native Responses name map.
18 - Keep MCP tool schemas merged after local prompt-derived tools.
19 - Connector remote tools are advertised only when `_a0_connector` runtime metadata says the matching connected CLI capability is currently available.
tests/test_responses_tools.py new
+103
@@ -0,0 +1,103 @@
1 +import sys
2 +from pathlib import Path
3 +
4 +
5 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
6 +if str(PROJECT_ROOT) not in sys.path:
7 + sys.path.insert(0, str(PROJECT_ROOT))
8 +
9 +from helpers import responses_tools
10 +
11 +
12 +class FakeAgent:
13 + def __init__(self, prompt_root: Path):
14 + self.prompt_root = prompt_root
15 +
16 + def read_prompt(self, file: str, **kwargs) -> str:
17 + return (self.prompt_root / file).read_text(encoding="utf-8")
18 +
19 +
20 +def _write_prompt(prompt_root: Path, basename: str, content: str) -> None:
21 + (prompt_root / basename).write_text(content.strip() + "\n", encoding="utf-8")
22 +
23 +
24 +def test_responses_function_tools_use_prompt_declared_names(monkeypatch, tmp_path):
25 + prompt_root = tmp_path / "prompts"
26 + prompt_root.mkdir()
27 + _write_prompt(
28 + prompt_root,
29 + "agent.system.tool.code_exe.md",
30 + """
31 + ### code_execution_tool
32 + run terminal commands
33 + ```json
34 + {"tool_name": "code_execution_tool", "tool_args": {"runtime": "terminal"}}
35 + ```
36 + """,
37 + )
38 + _write_prompt(
39 + prompt_root,
40 + "agent.system.tool.memory.md",
41 + """
42 + ## memory tools
43 + durable memory operations
44 + ```json
45 + {"tool_name": "memory_load", "tool_args": {"query": "responses naming"}}
46 + ```
47 + """,
48 + )
49 + _write_prompt(
50 + prompt_root,
51 + "agent.system.tool.call_sub.md",
52 + """
53 + ### call_subordinate
54 + delegate a subtask
55 + ```json
56 + {"tool_name": "call_subordinate", "tool_args": {"message": "inspect"}}
57 + ```
58 + """,
59 + )
60 + _write_prompt(
61 + prompt_root,
62 + "agent.system.tool.behaviour.md",
63 + """
64 + ### behaviour_adjustment
65 + update persistent behavioral rules
66 + """,
67 + )
68 + _write_prompt(
69 + prompt_root,
70 + "agent.system.tool.filename_only.md",
71 + "plain prompt with no declared callable name",
72 + )
73 +
74 + monkeypatch.setattr(
75 + responses_tools.subagents,
76 + "get_paths",
77 + lambda *args, **kwargs: [str(prompt_root)],
78 + )
79 + monkeypatch.setattr(
80 + responses_tools,
81 + "_include_local_tool_prompt",
82 + lambda agent, tool_name: True,
83 + )
84 + monkeypatch.setattr(responses_tools, "_mcp_tools", lambda agent: [])
85 +
86 + tools, name_map = responses_tools.build_responses_function_tools(
87 + FakeAgent(prompt_root)
88 + )
89 +
90 + names = {tool["name"] for tool in tools}
91 + assert {
92 + "code_execution_tool",
93 + "memory_load",
94 + "call_subordinate",
95 + "behaviour_adjustment",
96 + "filename_only",
97 + } <= names
98 + assert not {"code_exe", "memory", "call_sub", "behaviour"} & names
99 + assert name_map["code_execution_tool"] == "code_execution_tool"
100 + assert name_map["memory_load"] == "memory_load"
101 + assert name_map["call_subordinate"] == "call_subordinate"
102 + assert name_map["behaviour_adjustment"] == "behaviour_adjustment"
103 + assert name_map["filename_only"] == "filename_only"