main
py 307 lines 9.27 KB
Raw
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 ]