| 1 | import sys |
| 2 | from pathlib import Path |
| 3 | from types import SimpleNamespace |
| 4 | |
| 5 | |
| 6 | PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| 7 | if str(PROJECT_ROOT) not in sys.path: |
| 8 | sys.path.insert(0, str(PROJECT_ROOT)) |
| 9 | |
| 10 | from helpers import responses_tools, tool_policy |
| 11 | |
| 12 | |
| 13 | class FakeAgent: |
| 14 | def __init__(self, prompt_root: Path, data=None): |
| 15 | self.prompt_root = prompt_root |
| 16 | self.data = data or {} |
| 17 | self.config = SimpleNamespace(profile="default") |
| 18 | self.context = SimpleNamespace(get_data=lambda *args, **kwargs: None) |
| 19 | |
| 20 | def read_prompt(self, file: str, **kwargs) -> str: |
| 21 | prompt = (self.prompt_root / file).read_text(encoding="utf-8") |
| 22 | for key, value in kwargs.items(): |
| 23 | prompt = prompt.replace("{{" + key + "}}", str(value)) |
| 24 | return prompt |
| 25 | |
| 26 | def get_data(self, key: str): |
| 27 | return self.data.get(key) |
| 28 | |
| 29 | |
| 30 | def _write_prompt(prompt_root: Path, basename: str, content: str) -> None: |
| 31 | (prompt_root / basename).write_text(content.strip() + "\n", encoding="utf-8") |
| 32 | |
| 33 | |
| 34 | def test_responses_function_tools_use_prompt_declared_names(monkeypatch, tmp_path): |
| 35 | prompt_root = tmp_path / "prompts" |
| 36 | prompt_root.mkdir() |
| 37 | _write_prompt( |
| 38 | prompt_root, |
| 39 | "agent.system.tool.code_exe.md", |
| 40 | """ |
| 41 | ### code_execution_tool |
| 42 | run terminal commands |
| 43 | ```json |
| 44 | {"tool_name": "code_execution_tool", "tool_args": {"runtime": "terminal"}} |
| 45 | ``` |
| 46 | """, |
| 47 | ) |
| 48 | _write_prompt( |
| 49 | prompt_root, |
| 50 | "agent.system.tool.memory.md", |
| 51 | """ |
| 52 | ## memory tools |
| 53 | durable memory operations |
| 54 | - `memory_load`: args `query`, optional `threshold`, `limit`, `filter` |
| 55 | - `memory_save`: args `text`, optional `area` |
| 56 | - `memory_delete`: arg `ids` |
| 57 | - `memory_forget`: args `query`, optional `threshold`, `filter` |
| 58 | ```json |
| 59 | {"tool_name": "memory_load", "tool_args": {"query": "responses naming"}} |
| 60 | ``` |
| 61 | """, |
| 62 | ) |
| 63 | _write_prompt( |
| 64 | prompt_root, |
| 65 | "agent.system.tool.call_sub.md", |
| 66 | """ |
| 67 | ### call_subordinate |
| 68 | delegate a subtask |
| 69 | ```json |
| 70 | {"tool_name": "call_subordinate", "tool_args": {"message": "inspect"}} |
| 71 | ``` |
| 72 | """, |
| 73 | ) |
| 74 | _write_prompt( |
| 75 | prompt_root, |
| 76 | "agent.system.tool.behaviour.md", |
| 77 | """ |
| 78 | ### behaviour_adjustment |
| 79 | update persistent behavioral rules |
| 80 | """, |
| 81 | ) |
| 82 | _write_prompt( |
| 83 | prompt_root, |
| 84 | "agent.system.tool.filename_only.md", |
| 85 | "plain prompt with no declared callable name", |
| 86 | ) |
| 87 | |
| 88 | monkeypatch.setattr( |
| 89 | responses_tools.subagents, |
| 90 | "get_paths", |
| 91 | lambda *args, **kwargs: [str(prompt_root)], |
| 92 | ) |
| 93 | monkeypatch.setattr( |
| 94 | responses_tools, |
| 95 | "_include_local_tool_prompt", |
| 96 | lambda agent, tool_name: True, |
| 97 | ) |
| 98 | monkeypatch.setattr(responses_tools, "_mcp_tools", lambda agent: []) |
| 99 | |
| 100 | tools, name_map = responses_tools.build_responses_function_tools( |
| 101 | FakeAgent(prompt_root) |
| 102 | ) |
| 103 | |
| 104 | names = {tool["name"] for tool in tools} |
| 105 | assert { |
| 106 | "code_execution_tool", |
| 107 | "memory_load", |
| 108 | "memory_save", |
| 109 | "memory_delete", |
| 110 | "memory_forget", |
| 111 | "call_subordinate", |
| 112 | "behaviour_adjustment", |
| 113 | "filename_only", |
| 114 | } <= names |
| 115 | assert not {"code_exe", "memory", "call_sub", "behaviour"} & names |
| 116 | assert name_map["code_execution_tool"] == "code_execution_tool" |
| 117 | assert name_map["memory_load"] == "memory_load" |
| 118 | assert name_map["memory_save"] == "memory_save" |
| 119 | assert name_map["memory_delete"] == "memory_delete" |
| 120 | assert name_map["memory_forget"] == "memory_forget" |
| 121 | assert name_map["call_subordinate"] == "call_subordinate" |
| 122 | assert name_map["behaviour_adjustment"] == "behaviour_adjustment" |
| 123 | assert name_map["filename_only"] == "filename_only" |
| 124 | assert all(isinstance(tool["parameters"].get("properties"), dict) for tool in tools) |
| 125 | |
| 126 | |
| 127 | def test_responses_function_tools_add_empty_properties_to_mcp_schemas( |
| 128 | monkeypatch, |
| 129 | tmp_path, |
| 130 | ): |
| 131 | prompt_root = tmp_path / "prompts" |
| 132 | prompt_root.mkdir() |
| 133 | |
| 134 | monkeypatch.setattr( |
| 135 | responses_tools.subagents, |
| 136 | "get_paths", |
| 137 | lambda *args, **kwargs: [str(prompt_root)], |
| 138 | ) |
| 139 | monkeypatch.setattr( |
| 140 | responses_tools, |
| 141 | "_mcp_tools", |
| 142 | lambda agent: [ |
| 143 | ( |
| 144 | "remote_noop", |
| 145 | { |
| 146 | "description": "Remote noop", |
| 147 | "input_schema": {"type": "object"}, |
| 148 | }, |
| 149 | ) |
| 150 | ], |
| 151 | ) |
| 152 | |
| 153 | tools, _name_map = responses_tools.build_responses_function_tools( |
| 154 | FakeAgent(prompt_root) |
| 155 | ) |
| 156 | |
| 157 | assert tools == [ |
| 158 | { |
| 159 | "type": "function", |
| 160 | "name": "remote_noop", |
| 161 | "description": "Remote noop", |
| 162 | "parameters": { |
| 163 | "type": "object", |
| 164 | "properties": {}, |
| 165 | "additionalProperties": True, |
| 166 | }, |
| 167 | } |
| 168 | ] |
| 169 | |
| 170 | |
| 171 | def test_response_tool_native_contract_stays_provider_neutral(monkeypatch): |
| 172 | prompt_root = PROJECT_ROOT / "agents" / "agent0" / "prompts" |
| 173 | prompt = (prompt_root / "agent.system.tool.response.md").read_text(encoding="utf-8") |
| 174 | |
| 175 | description = tool_policy.tool_prompt_description( |
| 176 | prompt, |
| 177 | "response", |
| 178 | fallback="response", |
| 179 | ) |
| 180 | monkeypatch.setattr( |
| 181 | responses_tools.subagents, |
| 182 | "get_paths", |
| 183 | lambda *args, **kwargs: [str(prompt_root)], |
| 184 | ) |
| 185 | monkeypatch.setattr( |
| 186 | responses_tools, |
| 187 | "_include_local_tool_prompt", |
| 188 | lambda agent, tool_name: True, |
| 189 | ) |
| 190 | monkeypatch.setattr(responses_tools, "_vision_tool_prompt", lambda agent: "") |
| 191 | monkeypatch.setattr(responses_tools, "_mcp_tools", lambda agent: []) |
| 192 | tools, _name_map = responses_tools.build_responses_function_tools( |
| 193 | FakeAgent(prompt_root) |
| 194 | ) |
| 195 | response_tool = next(tool for tool in tools if tool["name"] == "response") |
| 196 | |
| 197 | assert description == "final answer to user" |
| 198 | assert response_tool["parameters"] == responses_tools._schema_from_prompt(prompt) |
| 199 | assert "strict" not in response_tool |
| 200 | |
| 201 | |
| 202 | def test_complex_prompt_args_are_not_guessed_as_string_schemas(): |
| 203 | for path in ( |
| 204 | PROJECT_ROOT / "prompts" / "agent.system.tool.scheduler.md", |
| 205 | PROJECT_ROOT / "prompts" / "agent.system.tool.parallel.md", |
| 206 | ): |
| 207 | schema = responses_tools._schema_from_prompt(path.read_text(encoding="utf-8")) |
| 208 | |
| 209 | assert schema == { |
| 210 | "type": "object", |
| 211 | "properties": {}, |
| 212 | "additionalProperties": True, |
| 213 | } |
| 214 | |
| 215 | |
| 216 | def test_responses_function_tools_include_vision_prompt(monkeypatch, tmp_path): |
| 217 | prompt_root = tmp_path / "prompts" |
| 218 | prompt_root.mkdir() |
| 219 | _write_prompt( |
| 220 | prompt_root, |
| 221 | "agent.system.tools_vision.md", |
| 222 | """ |
| 223 | ## multimodal vision tools |
| 224 | ### vision_load |
| 225 | load images into the model for visual reasoning |
| 226 | args: `paths` list of absolute image paths |
| 227 | """, |
| 228 | ) |
| 229 | agent = FakeAgent(prompt_root) |
| 230 | |
| 231 | monkeypatch.setattr(responses_tools.subagents, "get_paths", lambda *args: []) |
| 232 | monkeypatch.setattr( |
| 233 | responses_tools, |
| 234 | "_vision_tool_prompt", |
| 235 | lambda _agent: agent.read_prompt("agent.system.tools_vision.md"), |
| 236 | ) |
| 237 | monkeypatch.setattr(responses_tools, "_mcp_tools", lambda _agent: []) |
| 238 | |
| 239 | tools, name_map = responses_tools.build_responses_function_tools(agent) |
| 240 | |
| 241 | assert [tool["name"] for tool in tools] == ["vision_load"] |
| 242 | assert tools[0]["description"] == "load images into the model for visual reasoning" |
| 243 | assert tools[0]["parameters"]["properties"] == {} |
| 244 | assert name_map == {"vision_load": "vision_load"} |
| 245 | |
| 246 | |
| 247 | def test_local_tool_prompts_use_registered_render_kwargs(monkeypatch, tmp_path): |
| 248 | prompt_root = tmp_path / "prompts" |
| 249 | prompt_root.mkdir() |
| 250 | basename = "agent.system.tool.text_editor.md" |
| 251 | _write_prompt( |
| 252 | prompt_root, |
| 253 | basename, |
| 254 | """ |
| 255 | ### text_editor |
| 256 | read {{default_line_count}} lines by default |
| 257 | """, |
| 258 | ) |
| 259 | agent = FakeAgent( |
| 260 | prompt_root, |
| 261 | data={ |
| 262 | responses_tools.TOOL_PROMPT_KWARGS_KEY: { |
| 263 | basename: {"default_line_count": 200} |
| 264 | } |
| 265 | }, |
| 266 | ) |
| 267 | |
| 268 | monkeypatch.setattr( |
| 269 | responses_tools.subagents, |
| 270 | "get_paths", |
| 271 | lambda *args, **kwargs: [str(prompt_root)], |
| 272 | ) |
| 273 | monkeypatch.setattr(responses_tools, "_vision_tool_prompt", lambda _agent: "") |
| 274 | monkeypatch.setattr( |
| 275 | responses_tools, |
| 276 | "_include_local_tool_prompt", |
| 277 | lambda _agent, _tool_name: True, |
| 278 | ) |
| 279 | |
| 280 | prompts = dict(responses_tools._local_tool_prompts(agent)) |
| 281 | |
| 282 | assert "{{default_line_count}}" not in prompts["text_editor"] |
| 283 | assert "read 200 lines by default" in prompts["text_editor"] |
| 284 | |
| 285 | |
| 286 | def test_explicit_tool_name_precedes_a_generic_heading(): |
| 287 | prompt = """## memory tools |
| 288 | durable memory operations |
| 289 | {"tool_name": "memory_load", "tool_args": {}} |
| 290 | """ |
| 291 | |
| 292 | assert responses_tools._tool_names_from_prompt( |
| 293 | prompt, fallback="memory" |
| 294 | ) == ["memory_load"] |
| 295 | |
| 296 | |
| 297 | def test_bundled_memory_prompt_exposes_every_memory_tool(): |
| 298 | prompt = ( |
| 299 | PROJECT_ROOT / "plugins/_memory/prompts/agent.system.tool.memory.md" |
| 300 | ).read_text(encoding="utf-8") |
| 301 | |
| 302 | assert responses_tools._tool_names_from_prompt(prompt, fallback="memory") == [ |
| 303 | "memory_load", |
| 304 | "memory_save", |
| 305 | "memory_delete", |
| 306 | "memory_forget", |
| 307 | ] |