From 61c3e64595269791481f688f9d2fb0ac1a459f6f Mon Sep 17 00:00:00 2001 From: Haoran Date: Sun, 23 Aug 2026 11:28:14 +0800 Subject: [PATCH] fix: support bounded recursive glob across lessons --- s02_tool_use/README.ja.md | 7 +++++- s02_tool_use/README.md | 7 +++++- s02_tool_use/README.zh.md | 7 +++++- s02_tool_use/code.py | 16 +++++++----- s03_permission/code.py | 16 +++++++----- s04_hooks/code.py | 16 +++++++----- s05_todo_write/code.py | 16 +++++++----- s06_subagent/code.py | 16 +++++++----- s07_skill_loading/code.py | 16 +++++++----- s08_context_compact/code.py | 11 ++++++--- s09_memory/code.py | 13 ++++++---- s10_task_system/code.py | 13 ++++++---- s11_background_tasks/code.py | 13 ++++++---- s12_cron_scheduler/code.py | 13 ++++++---- s13_agent_teams/code.py | 7 ++++-- s14_mcp_plugin/code.py | 13 ++++++---- s15_integrated_harness/code.py | 20 +++++++++------ s17_goal_loop/code.py | 14 +++++++---- tests/test_agent_loop_boundaries.py | 38 +++++++++++++++++++++++++++++ 19 files changed, 189 insertions(+), 83 deletions(-) diff --git a/s02_tool_use/README.ja.md b/s02_tool_use/README.ja.md index 3981b83b..509735ae 100644 --- a/s02_tool_use/README.ja.md +++ b/s02_tool_use/README.ja.md @@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text): def run_glob(pattern): import glob as g - return "\n".join(g.glob(pattern, root_dir=WORKDIR)) + matches = sorted(set(g.glob( + pattern, root_dir=WORKDIR, recursive=True))) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) ``` --- diff --git a/s02_tool_use/README.md b/s02_tool_use/README.md index e61097bd..89fbe743 100644 --- a/s02_tool_use/README.md +++ b/s02_tool_use/README.md @@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text): def run_glob(pattern): import glob as g - return "\n".join(g.glob(pattern, root_dir=WORKDIR)) + matches = sorted(set(g.glob( + pattern, root_dir=WORKDIR, recursive=True))) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) ``` --- diff --git a/s02_tool_use/README.zh.md b/s02_tool_use/README.zh.md index fcefa2b9..f40f31b3 100644 --- a/s02_tool_use/README.zh.md +++ b/s02_tool_use/README.zh.md @@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text): def run_glob(pattern): import glob as g - return "\n".join(g.glob(pattern, root_dir=WORKDIR)) + matches = sorted(set(g.glob( + pattern, root_dir=WORKDIR, recursive=True))) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) ``` --- diff --git a/s02_tool_use/code.py b/s02_tool_use/code.py index 500cf19c..0cdfc033 100644 --- a/s02_tool_use/code.py +++ b/s02_tool_use/code.py @@ -110,11 +110,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: import glob as g try: - results = [] - for match in g.glob(pattern, root_dir=WORKDIR): - if (WORKDIR / match).resolve().is_relative_to(WORKDIR): - results.append(match) - return "\n".join(results) if results else "(no matches)" + matches = sorted({ + match for match in g.glob( + pattern, root_dir=WORKDIR, recursive=True) + if (WORKDIR / match).resolve().is_relative_to(WORKDIR) + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as e: return f"Error: {e}" @@ -130,7 +134,7 @@ TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, ] diff --git a/s03_permission/code.py b/s03_permission/code.py index b1fb9e7e..acbbe601 100644 --- a/s03_permission/code.py +++ b/s03_permission/code.py @@ -105,11 +105,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: import glob as g try: - results = [] - for match in g.glob(pattern, root_dir=WORKDIR): - if (WORKDIR / match).resolve().is_relative_to(WORKDIR): - results.append(match) - return "\n".join(results) if results else "(no matches)" + matches = sorted({ + match for match in g.glob( + pattern, root_dir=WORKDIR, recursive=True) + if (WORKDIR / match).resolve().is_relative_to(WORKDIR) + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as e: return f"Error: {e}" @@ -125,7 +129,7 @@ TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, ] diff --git a/s04_hooks/code.py b/s04_hooks/code.py index ee44bc2e..709febe3 100644 --- a/s04_hooks/code.py +++ b/s04_hooks/code.py @@ -91,11 +91,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: import glob as g try: - results = [] - for match in g.glob(pattern, root_dir=WORKDIR): - if (WORKDIR / match).resolve().is_relative_to(WORKDIR): - results.append(match) - return "\n".join(results) if results else "(no matches)" + matches = sorted({ + match for match in g.glob( + pattern, root_dir=WORKDIR, recursive=True) + if (WORKDIR / match).resolve().is_relative_to(WORKDIR) + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as e: return f"Error: {e}" @@ -108,7 +112,7 @@ TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, ] diff --git a/s05_todo_write/code.py b/s05_todo_write/code.py index 004d1d2a..f919b46e 100644 --- a/s05_todo_write/code.py +++ b/s05_todo_write/code.py @@ -96,11 +96,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: import glob as g try: - results = [] - for match in g.glob(pattern, root_dir=WORKDIR): - if (WORKDIR / match).resolve().is_relative_to(WORKDIR): - results.append(match) - return "\n".join(results) if results else "(no matches)" + matches = sorted({ + match for match in g.glob( + pattern, root_dir=WORKDIR, recursive=True) + if (WORKDIR / match).resolve().is_relative_to(WORKDIR) + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as e: return f"Error: {e}" @@ -186,7 +190,7 @@ TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, # s05: new tool {"name": "todo_write", "description": "Create and manage a task list for your current coding session.", diff --git a/s06_subagent/code.py b/s06_subagent/code.py index 9d093ab9..531fbe88 100644 --- a/s06_subagent/code.py +++ b/s06_subagent/code.py @@ -101,11 +101,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: import glob try: - matches = [] - for match in glob.glob(pattern, root_dir=WORKDIR): - if (WORKDIR / match).resolve().is_relative_to(WORKDIR): - matches.append(match) - return "\n".join(matches) if matches else "(no matches)" + matches = sorted({ + match for match in glob.glob( + pattern, root_dir=WORKDIR, recursive=True) + if (WORKDIR / match).resolve().is_relative_to(WORKDIR) + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as e: return f"Error: {e}" @@ -119,7 +123,7 @@ BASE_TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, ] diff --git a/s07_skill_loading/code.py b/s07_skill_loading/code.py index 50bfc17e..0735d19e 100644 --- a/s07_skill_loading/code.py +++ b/s07_skill_loading/code.py @@ -185,11 +185,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: import glob try: - matches = [] - for match in glob.glob(pattern, root_dir=WORKDIR): - if (WORKDIR / match).resolve().is_relative_to(WORKDIR): - matches.append(match) - return "\n".join(matches) if matches else "(no matches)" + matches = sorted({ + match for match in glob.glob( + pattern, root_dir=WORKDIR, recursive=True) + if (WORKDIR / match).resolve().is_relative_to(WORKDIR) + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as e: return f"Error: {e}" @@ -203,7 +207,7 @@ TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, {"name": "load_skill", "description": "Load the full SKILL.md content by skill name.", "input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}}, diff --git a/s08_context_compact/code.py b/s08_context_compact/code.py index f835e82b..b1c3f73d 100644 --- a/s08_context_compact/code.py +++ b/s08_context_compact/code.py @@ -118,11 +118,14 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: try: - matches = [ + matches = sorted({ match for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True) if (WORKDIR / match).resolve().is_relative_to(WORKDIR) - ] - return "\n".join(matches) if matches else "(no matches)" + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as error: return f"Error: {error}" @@ -136,7 +139,7 @@ BASE_TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, ] COMPACT_TOOL = { diff --git a/s09_memory/code.py b/s09_memory/code.py index e8c33158..0a2c7b57 100644 --- a/s09_memory/code.py +++ b/s09_memory/code.py @@ -579,12 +579,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: try: - matches = [ + matches = sorted({ match - for match in glob.glob(pattern, root_dir=WORKDIR) + for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True) if (WORKDIR / match).resolve().is_relative_to(WORKDIR) - ] - return "\n".join(matches) if matches else "(no matches)" + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as error: return f"Error: {error}" @@ -597,7 +600,7 @@ TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, ] diff --git a/s10_task_system/code.py b/s10_task_system/code.py index 4fcb84a4..1c8fadd1 100644 --- a/s10_task_system/code.py +++ b/s10_task_system/code.py @@ -325,12 +325,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: try: - matches = [ + matches = sorted({ match - for match in glob.glob(pattern, root_dir=WORKDIR) + for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True) if (WORKDIR / match).resolve().is_relative_to(WORKDIR) - ] - return "\n".join(matches) if matches else "(no matches)" + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as error: return f"Error: {error}" @@ -392,7 +395,7 @@ TOOLS = [ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, {"name": "edit_file", "description": "Replace exact text in a file once.", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, {"name": "create_task", "description": "Create a task and return its runtime-generated ID.", "input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"], "additionalProperties": False}}, diff --git a/s11_background_tasks/code.py b/s11_background_tasks/code.py index 8a33166e..ce3b9bc9 100644 --- a/s11_background_tasks/code.py +++ b/s11_background_tasks/code.py @@ -156,12 +156,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: try: - matches = [ + matches = sorted({ match - for match in glob.glob(pattern, root_dir=WORKDIR) + for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True) if (WORKDIR / match).resolve().is_relative_to(WORKDIR) - ] - return "\n".join(matches) if matches else "(no matches)" + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as error: return f"Error: {error}" @@ -189,7 +192,7 @@ TOOLS = [ "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, diff --git a/s12_cron_scheduler/code.py b/s12_cron_scheduler/code.py index 2c9e115b..f0db0358 100644 --- a/s12_cron_scheduler/code.py +++ b/s12_cron_scheduler/code.py @@ -106,12 +106,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: try: - matches = [ + matches = sorted({ match - for match in glob.glob(pattern, root_dir=WORKDIR) + for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True) if (WORKDIR / match).resolve().is_relative_to(WORKDIR) - ] - return "\n".join(matches) if matches else "(no matches)" + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as error: return f"Error: {error}" @@ -137,7 +140,7 @@ TOOLS = [ "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, diff --git a/s13_agent_teams/code.py b/s13_agent_teams/code.py index 786e611d..858853ce 100644 --- a/s13_agent_teams/code.py +++ b/s13_agent_teams/code.py @@ -727,7 +727,10 @@ def run_glob(pattern: str, cwd: Path | None = None) -> str: for path in sorted(base.glob(pattern)) if path.resolve().is_relative_to(base) ] - return "\n".join(matches[:200]) or "No files found" + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) or "No files found" except Exception as exc: return f"Error: {exc}" @@ -1519,7 +1522,7 @@ BASE_TOOLS = [ "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files by glob pattern.", + {"name": "glob", "description": "Find files by glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, diff --git a/s14_mcp_plugin/code.py b/s14_mcp_plugin/code.py index 12b0d14c..a23e556a 100644 --- a/s14_mcp_plugin/code.py +++ b/s14_mcp_plugin/code.py @@ -109,12 +109,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str: def run_glob(pattern: str) -> str: try: - matches = [ + matches = sorted({ match - for match in glob.glob(pattern, root_dir=WORKDIR) + for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True) if (WORKDIR / match).resolve().is_relative_to(WORKDIR.resolve()) - ] - return "\n".join(matches[:200]) if matches else "(no matches)" + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as exc: return f"Error: {exc}" @@ -140,7 +143,7 @@ BASE_TOOLS = [ "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files by glob pattern.", + {"name": "glob", "description": "Find files by glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, diff --git a/s15_integrated_harness/code.py b/s15_integrated_harness/code.py index 9263e216..88f5431e 100644 --- a/s15_integrated_harness/code.py +++ b/s15_integrated_harness/code.py @@ -972,11 +972,15 @@ def run_glob(pattern: str, cwd: Path | None = None) -> str: import glob as g try: base = (cwd or WORKDIR).resolve() - results = [] - for match in g.glob(pattern, root_dir=base): - if (base / match).resolve().is_relative_to(base): - results.append(match) - return "\n".join(results) if results else "(no matches)" + matches = sorted({ + match for match in g.glob( + pattern, root_dir=base, recursive=True) + if (base / match).resolve().is_relative_to(base) + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" except Exception as e: return f"Error: {e}" @@ -1487,7 +1491,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files by glob pattern.", + {"name": "glob", "description": "Find files by glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": { "pattern": {"type": "string"}}, @@ -1849,7 +1853,7 @@ SUB_TOOLS = [ "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, @@ -2776,7 +2780,7 @@ BUILTIN_TOOLS = [ "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, - {"name": "glob", "description": "Find files matching a glob pattern.", + {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, diff --git a/s17_goal_loop/code.py b/s17_goal_loop/code.py index 873cb9ab..f42eb2dc 100644 --- a/s17_goal_loop/code.py +++ b/s17_goal_loop/code.py @@ -515,7 +515,7 @@ TOOLS = [ }, { "name": "glob", - "description": "Find files matching a glob pattern.", + "description": "Find files matching a glob pattern; ** matches recursively.", "input_schema": { "type": "object", "properties": {"pattern": {"type": "string"}}, @@ -797,12 +797,16 @@ class AgentSession: return f"Edited {path.relative_to(self.workdir)}" if name == "glob": - matches = [ + matches = sorted({ match - for match in glob.glob(str(arguments["pattern"]), root_dir=self.workdir) + for match in glob.glob( + str(arguments["pattern"]), root_dir=self.workdir, recursive=True) if (self.workdir / match).resolve().is_relative_to(self.workdir) - ] - return "\n".join(matches[:200]) if matches else "(no matches)" + }) + shown = matches[:200] + if len(matches) > 200: + shown.append("... (more matches omitted; narrow the pattern)") + return "\n".join(shown) if shown else "(no matches)" raise GoalError(f"unknown tool '{name}'") diff --git a/tests/test_agent_loop_boundaries.py b/tests/test_agent_loop_boundaries.py index 0d4840a9..2de397fb 100644 --- a/tests/test_agent_loop_boundaries.py +++ b/tests/test_agent_loop_boundaries.py @@ -30,6 +30,8 @@ LESSONS = tuple( ) ) INTEGRATED_LESSON = ROOT / "s15_integrated_harness" / "code.py" +GOAL_LESSON = ROOT / "s17_goal_loop" / "code.py" +GLOB_LESSONS = (*LESSONS[1:], INTEGRATED_LESSON, GOAL_LESSON) class FakeMessagesApi: @@ -137,6 +139,42 @@ def bash_tool_call(): ) +def run_glob_tool(lesson, workdir: Path, pattern: str) -> str: + if hasattr(lesson, "run_glob"): + return lesson.run_glob(pattern) + session = object.__new__(lesson.AgentSession) + session.workdir = workdir.resolve() + return session._run_tool("glob", {"pattern": pattern}) + + +@pytest.mark.parametrize("lesson_path", GLOB_LESSONS, + ids=lambda path: path.parent.name) +def test_glob_double_star_matches_files_at_any_depth( + tmp_path: Path, lesson_path: Path): + (tmp_path / "root.py").write_text("") + (tmp_path / "one" / "two").mkdir(parents=True) + (tmp_path / "one" / "one.py").write_text("") + (tmp_path / "one" / "two" / "deep.py").write_text("") + lesson = load_lesson(tmp_path, lesson_path) + + matches = set(run_glob_tool(lesson, tmp_path, "**/*.py").splitlines()) + + assert matches == {"root.py", "one/one.py", "one/two/deep.py"} + + +@pytest.mark.parametrize("lesson_path", GLOB_LESSONS, + ids=lambda path: path.parent.name) +def test_glob_caps_large_result_sets(tmp_path: Path, lesson_path: Path): + for index in range(205): + (tmp_path / f"file-{index:03}.txt").write_text("") + lesson = load_lesson(tmp_path, lesson_path) + + lines = run_glob_tool(lesson, tmp_path, "*.txt").splitlines() + + assert len(lines) == 201 + assert lines[-1] == "... (more matches omitted; narrow the pattern)" + + @pytest.mark.parametrize("lesson_path", LESSONS, ids=lambda path: path.parent.name) @pytest.mark.parametrize("content", ([], None), ids=("empty-content", "empty-text")) def test_parent_loop_does_not_append_an_empty_tool_result_turn(