python-agent-harness 1.5.5.3__tar.gz → 1.5.5.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_agent_harness-1.5.5.3/python_agent_harness.egg-info → python_agent_harness-1.5.5.5}/PKG-INFO +21 -2
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/README.md +20 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/pyproject.toml +1 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/cli.py +5 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/persistence.py +3 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/agents/reviewer.md +10 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts.py +62 -14
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/session.py +53 -4
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/diffapply.py +2 -2
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/grep_win.py +12 -7
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/commands.py +74 -15
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5/python_agent_harness.egg-info}/PKG-INFO +21 -2
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_custom_agents.py +266 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_persistence.py +16 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/LICENSE +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/client.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/config.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/planmode.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/bash_win.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/edit_win.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/glob_win.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/grep_mac.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/setup.cfg +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_client.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_config.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_filesystem.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_scenarios.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_session.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_token_estimator.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_tools_misc.py +0 -0
{python_agent_harness-1.5.5.3/python_agent_harness.egg-info → python_agent_harness-1.5.5.5}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.5.
|
|
3
|
+
Version: 1.5.5.5
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
5
|
Author-email: Huming Chen <chenhuming@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -243,13 +243,32 @@ Custom commands from `prompts/commands/*.md` are registered as slash commands as
|
|
|
243
243
|
|
|
244
244
|
Agent prompt files are markdown files (`.md`) placed in the `prompts/agents/` directory. Each file becomes a switchable agent profile available via the `/agent` TUI command.
|
|
245
245
|
|
|
246
|
+
An agent file may carry YAML frontmatter with two optional keys:
|
|
247
|
+
|
|
248
|
+
- `name:` — override the agent name (defaults to the file stem)
|
|
249
|
+
- `exclude_tools:` — a list of tool names the agent must not see. An entry matches a tool by exact name, glob pattern (`mcp__git__*`), or `__`-delimited prefix (`mcp__git` hides `mcp__git__list_repos` but `Write` does NOT hide `TodoWrite`). The built-in `default` agent always sees all tools.
|
|
250
|
+
|
|
251
|
+
```markdown
|
|
252
|
+
---
|
|
253
|
+
name: assistant
|
|
254
|
+
exclude_tools:
|
|
255
|
+
- Bash
|
|
256
|
+
- Edit
|
|
257
|
+
- Write
|
|
258
|
+
- mcp__git__*
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
# Role and Behavior
|
|
262
|
+
You are a personal assistant. You do NOT modify files or run shell commands.
|
|
263
|
+
```
|
|
264
|
+
|
|
246
265
|
#### Commands vs Agents
|
|
247
266
|
|
|
248
267
|
| | Commands (`/review`) | Agents (`/agent reviewer`) |
|
|
249
268
|
|---|---|---|
|
|
250
269
|
| **Scope** | One-shot (prompt resets after run) | Persistent (stays until next `/agent`) |
|
|
251
270
|
| **Kickoff** | Hardcoded kickoff message | User types their own prompt |
|
|
252
|
-
| **Tools** | Can restrict (`allow_planexit=False`) | All tools |
|
|
271
|
+
| **Tools** | Can restrict (`allow_planexit=False`) | All tools, minus the agent's `exclude_tools` |
|
|
253
272
|
| **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
|
|
254
273
|
|
|
255
274
|
## Project layout
|
|
@@ -209,13 +209,32 @@ Custom commands from `prompts/commands/*.md` are registered as slash commands as
|
|
|
209
209
|
|
|
210
210
|
Agent prompt files are markdown files (`.md`) placed in the `prompts/agents/` directory. Each file becomes a switchable agent profile available via the `/agent` TUI command.
|
|
211
211
|
|
|
212
|
+
An agent file may carry YAML frontmatter with two optional keys:
|
|
213
|
+
|
|
214
|
+
- `name:` — override the agent name (defaults to the file stem)
|
|
215
|
+
- `exclude_tools:` — a list of tool names the agent must not see. An entry matches a tool by exact name, glob pattern (`mcp__git__*`), or `__`-delimited prefix (`mcp__git` hides `mcp__git__list_repos` but `Write` does NOT hide `TodoWrite`). The built-in `default` agent always sees all tools.
|
|
216
|
+
|
|
217
|
+
```markdown
|
|
218
|
+
---
|
|
219
|
+
name: assistant
|
|
220
|
+
exclude_tools:
|
|
221
|
+
- Bash
|
|
222
|
+
- Edit
|
|
223
|
+
- Write
|
|
224
|
+
- mcp__git__*
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
# Role and Behavior
|
|
228
|
+
You are a personal assistant. You do NOT modify files or run shell commands.
|
|
229
|
+
```
|
|
230
|
+
|
|
212
231
|
#### Commands vs Agents
|
|
213
232
|
|
|
214
233
|
| | Commands (`/review`) | Agents (`/agent reviewer`) |
|
|
215
234
|
|---|---|---|
|
|
216
235
|
| **Scope** | One-shot (prompt resets after run) | Persistent (stays until next `/agent`) |
|
|
217
236
|
| **Kickoff** | Hardcoded kickoff message | User types their own prompt |
|
|
218
|
-
| **Tools** | Can restrict (`allow_planexit=False`) | All tools |
|
|
237
|
+
| **Tools** | Can restrict (`allow_planexit=False`) | All tools, minus the agent's `exclude_tools` |
|
|
219
238
|
| **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
|
|
220
239
|
|
|
221
240
|
## Project layout
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-agent-harness"
|
|
7
|
-
version = "1.5.5.
|
|
7
|
+
version = "1.5.5.5"
|
|
8
8
|
description = "A lightweight, hackable mini-OpenCode written in Python."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Huming Chen", email = "chenhuming@gmail.com"}
|
|
@@ -125,7 +125,11 @@ def make_session(
|
|
|
125
125
|
)
|
|
126
126
|
if default_agent and default_agent != RESERVED_AGENT_NAME:
|
|
127
127
|
ok, msg = session.switch_agent(default_agent)
|
|
128
|
-
if
|
|
128
|
+
if ok:
|
|
129
|
+
# record the applied agent so saved sessions restore it;
|
|
130
|
+
# switch_agent("default") clears it again
|
|
131
|
+
session.store.agent = default_agent
|
|
132
|
+
else:
|
|
129
133
|
# A typo'd/missing default_agent must not silently start the
|
|
130
134
|
# session with the built-in prompt — record the config error
|
|
131
135
|
# so the TUI can surface it (see Tui.run).
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/persistence.py
RENAMED
|
@@ -112,9 +112,11 @@ class SessionPersistence:
|
|
|
112
112
|
max_tokens: int | None = None,
|
|
113
113
|
tool_names: list[str] | None = None,
|
|
114
114
|
round_times: list[float] | None = None,
|
|
115
|
+
agent: str | None = None,
|
|
115
116
|
) -> None:
|
|
116
117
|
self.project_dir = project_dir
|
|
117
118
|
self.model = model
|
|
119
|
+
self.agent = agent
|
|
118
120
|
self.system_prompt = system_prompt
|
|
119
121
|
self.temperature = temperature
|
|
120
122
|
self.max_tokens = max_tokens
|
|
@@ -171,6 +173,7 @@ class SessionPersistence:
|
|
|
171
173
|
pairs = [
|
|
172
174
|
("python-agent-harness--project-dir", self.project_dir),
|
|
173
175
|
("python-agent-harness--model", self.model),
|
|
176
|
+
("python-agent-harness--agent", self.agent),
|
|
174
177
|
]
|
|
175
178
|
for name, value in pairs:
|
|
176
179
|
if value is None:
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts.py
RENAMED
|
@@ -35,7 +35,7 @@ def read_prompt_file(name: str) -> str:
|
|
|
35
35
|
return path.read_text(encoding="utf-8")
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
_FRONTMATTER_RE = re.compile(r"\A---\n.*?\n---\n?", re.DOTALL)
|
|
38
|
+
_FRONTMATTER_RE = re.compile(r"\A---\r?\n.*?\r?\n---\r?\n?", re.DOTALL)
|
|
39
39
|
_SKILLS_PLACEHOLDER_RE = re.compile(r"\{\{\s*SKILLS\s*\}\}")
|
|
40
40
|
_SKILLS_FALLBACK = (
|
|
41
41
|
"Invoke with a skill name and optional args; the tool reports an "
|
|
@@ -509,30 +509,78 @@ def compacted_messages(summary: str, prompts: list[str]) -> list[Message]:
|
|
|
509
509
|
# ``prompts/agents/``. Each file becomes a switchable agent profile
|
|
510
510
|
# available via the ``/agent`` TUI command. The file's stem (e.g.
|
|
511
511
|
# ``reviewer.md`` → ``reviewer``) is the agent name; an optional YAML
|
|
512
|
-
# frontmatter ``name:`` can override it
|
|
513
|
-
#
|
|
514
|
-
#
|
|
512
|
+
# frontmatter ``name:`` can override it, and an optional
|
|
513
|
+
# ``exclude_tools:`` list restricts which tools the agent sees (an
|
|
514
|
+
# entry matches by exact name, glob pattern, or ``__``-delimited
|
|
515
|
+
# prefix, e.g. ``mcp__git`` or ``mcp__git__*``). The built-in
|
|
516
|
+
# ``default`` agent (``prompts/agent.md``) is always available and
|
|
517
|
+
# cannot be overridden by a file in this directory.
|
|
515
518
|
|
|
516
519
|
|
|
517
|
-
def
|
|
518
|
-
"""
|
|
519
|
-
|
|
520
|
+
def _agent_frontmatter(path: Path) -> dict[str, Any]:
|
|
521
|
+
"""Parse the YAML frontmatter of an agent prompt file.
|
|
522
|
+
|
|
523
|
+
Supports ``name:`` (string) and ``exclude_tools:`` (a YAML list —
|
|
524
|
+
either block form with ``- item`` lines or inline ``[a, b]``).
|
|
525
|
+
Unknown keys are ignored; a missing or unreadable frontmatter
|
|
526
|
+
yields an empty dict.
|
|
527
|
+
"""
|
|
520
528
|
try:
|
|
521
529
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
522
530
|
except OSError:
|
|
523
|
-
return
|
|
531
|
+
return {}
|
|
524
532
|
m = _FRONTMATTER_RE.match(text)
|
|
525
|
-
if m:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
533
|
+
if not m:
|
|
534
|
+
return {}
|
|
535
|
+
data: dict[str, Any] = {}
|
|
536
|
+
lines = m.group(0).splitlines()
|
|
537
|
+
i = 0
|
|
538
|
+
while i < len(lines):
|
|
539
|
+
line = lines[i]
|
|
540
|
+
if line.startswith("name:"):
|
|
541
|
+
data["name"] = line[len("name:") :].strip()
|
|
542
|
+
elif line.startswith("exclude_tools:"):
|
|
543
|
+
rest = line[len("exclude_tools:") :].strip()
|
|
544
|
+
tools: list[str] = []
|
|
545
|
+
if rest.startswith("[") and rest.endswith("]"):
|
|
546
|
+
tools = [
|
|
547
|
+
item.strip().strip("'\"") for item in rest[1:-1].split(",") if item.strip()
|
|
548
|
+
]
|
|
549
|
+
else:
|
|
550
|
+
j = i + 1
|
|
551
|
+
while j < len(lines) and lines[j].lstrip().startswith("- "):
|
|
552
|
+
tools.append(lines[j].lstrip()[2:].strip().strip("'\""))
|
|
553
|
+
j += 1
|
|
554
|
+
i = j - 1
|
|
555
|
+
data["exclude_tools"] = tools
|
|
556
|
+
i += 1
|
|
557
|
+
return data
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _agent_name_from_file(path: Path) -> str:
|
|
561
|
+
"""Derive agent name from a file: frontmatter ``name:`` if present,
|
|
562
|
+
otherwise the file stem (lowercased, non-alphanumerics → ``-``)."""
|
|
563
|
+
name = _agent_frontmatter(path).get("name", "")
|
|
564
|
+
if name:
|
|
565
|
+
return name
|
|
531
566
|
base = path.stem.lower()
|
|
532
567
|
base = re.sub(r"[^a-z0-9]+", "-", base)
|
|
533
568
|
return base.strip("-")
|
|
534
569
|
|
|
535
570
|
|
|
571
|
+
def agent_exclude_tools(path: Path | str) -> tuple[str, ...]:
|
|
572
|
+
"""Tool name substrings/patterns the agent profile excludes.
|
|
573
|
+
|
|
574
|
+
Reads the ``exclude_tools:`` frontmatter list of an agent prompt
|
|
575
|
+
file. An entry matches a tool by exact name, glob pattern
|
|
576
|
+
(``mcp__git__*``), or ``__``-delimited prefix (``mcp__git`` hides
|
|
577
|
+
``mcp__git__list_repos``); matching happens in
|
|
578
|
+
``Session.tool_specs``. Returns an empty tuple when the key is
|
|
579
|
+
absent.
|
|
580
|
+
"""
|
|
581
|
+
return tuple(_agent_frontmatter(Path(path)).get("exclude_tools", ()))
|
|
582
|
+
|
|
583
|
+
|
|
536
584
|
def discover_agents() -> dict[str, str]:
|
|
537
585
|
"""Discover all custom agents in the ``prompts/agents/`` directory.
|
|
538
586
|
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/session.py
RENAMED
|
@@ -9,6 +9,7 @@ provide interactive confirmations.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import contextlib
|
|
12
|
+
import fnmatch
|
|
12
13
|
import os
|
|
13
14
|
import threading
|
|
14
15
|
import time
|
|
@@ -31,6 +32,21 @@ from .tools.filesystem import cleanup_spooled_files
|
|
|
31
32
|
from .tools.mcp import mcp_tools_from_manager
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def _tool_excluded(pattern: str, name: str) -> bool:
|
|
36
|
+
"""True when *pattern* excludes tool *name*.
|
|
37
|
+
|
|
38
|
+
A pattern matches when it is the exact tool name, a glob pattern
|
|
39
|
+
(``mcp__git__*``), or a prefix delimited by ``__`` (``mcp__git``
|
|
40
|
+
hides ``mcp__git__list_repos`` but ``Write`` does NOT hide
|
|
41
|
+
``TodoWrite``).
|
|
42
|
+
"""
|
|
43
|
+
if "*" in pattern:
|
|
44
|
+
return fnmatch.fnmatchcase(name, pattern)
|
|
45
|
+
if pattern == name:
|
|
46
|
+
return True
|
|
47
|
+
return name.startswith(pattern + "__")
|
|
48
|
+
|
|
49
|
+
|
|
34
50
|
def find_skill_dir(project_dir: str, configured: str | None = None) -> str | None:
|
|
35
51
|
"""Locate the skill directory.
|
|
36
52
|
|
|
@@ -162,6 +178,12 @@ class Session:
|
|
|
162
178
|
# cache build simultaneously — double-checked locking prevents
|
|
163
179
|
# redundant filesystem scans and the data race on the cache tuple
|
|
164
180
|
self._skill_index_lock = threading.Lock()
|
|
181
|
+
# NOTE: the store does NOT record ``default_agent`` here: the
|
|
182
|
+
# store's agent must reflect the agent actually applied, and
|
|
183
|
+
# only a successful switch_agent() can guarantee that (a typo'd
|
|
184
|
+
# default_agent is reported as a startup warning and the session
|
|
185
|
+
# keeps the built-in prompt). cli.make_session applies the
|
|
186
|
+
# default_agent right after construction, which sets it.
|
|
165
187
|
self.store = SessionPersistence(
|
|
166
188
|
project_dir=project_dir,
|
|
167
189
|
model=model,
|
|
@@ -195,6 +217,11 @@ class Session:
|
|
|
195
217
|
# Original system prompt assembled at session start — used by
|
|
196
218
|
# /agent default to restore the original agent.md prompt.
|
|
197
219
|
self._default_system_prompt = system_prompt
|
|
220
|
+
# Tool exclusions from the active agent profile's
|
|
221
|
+
# ``exclude_tools`` frontmatter (substrings or glob patterns).
|
|
222
|
+
# The built-in default agent excludes nothing; switch_agent
|
|
223
|
+
# replaces this tuple when a custom agent is activated.
|
|
224
|
+
self._agent_excluded_tools: tuple[str, ...] = ()
|
|
198
225
|
# Monotonic cancel identity: cancel() bumps this counter, so a
|
|
199
226
|
# worker from a cancelled run can tell it was cancelled even
|
|
200
227
|
# after the next run clears the shared event.
|
|
@@ -245,9 +272,26 @@ class Session:
|
|
|
245
272
|
# tools
|
|
246
273
|
# ------------------------------------------------------------------
|
|
247
274
|
def tool_specs(self, exclude: tuple[str, ...] = ()) -> list:
|
|
248
|
-
"""Tool specs exposed to the model
|
|
249
|
-
|
|
250
|
-
|
|
275
|
+
"""Tool specs exposed to the model.
|
|
276
|
+
|
|
277
|
+
``exclude`` drops tools by name (e.g. one-shot/interactive tools
|
|
278
|
+
for sub-agent runs). The active agent profile's own
|
|
279
|
+
``exclude_tools`` frontmatter is applied on top: an entry
|
|
280
|
+
matches a tool by exact name, glob pattern (``mcp__git__*``),
|
|
281
|
+
or ``__``-delimited prefix (``mcp__git`` hides
|
|
282
|
+
``mcp__git__list_repos`` but ``Write`` does NOT hide
|
|
283
|
+
``TodoWrite``).
|
|
284
|
+
"""
|
|
285
|
+
excluded = set(exclude)
|
|
286
|
+
specs = self.registry.specs()
|
|
287
|
+
patterns = self._agent_excluded_tools
|
|
288
|
+
if not patterns:
|
|
289
|
+
return [spec for spec in specs if spec.name not in excluded]
|
|
290
|
+
return [
|
|
291
|
+
spec
|
|
292
|
+
for spec in specs
|
|
293
|
+
if spec.name not in excluded and not any(_tool_excluded(p, spec.name) for p in patterns)
|
|
294
|
+
]
|
|
251
295
|
|
|
252
296
|
def execute_tool(
|
|
253
297
|
self, name: str, args: dict[str, Any], call_id: str | None = None
|
|
@@ -731,6 +775,8 @@ class Session:
|
|
|
731
775
|
if name == RESERVED_AGENT_NAME:
|
|
732
776
|
self.system_prompt = self._default_system_prompt
|
|
733
777
|
self.store.system_prompt = self._default_system_prompt
|
|
778
|
+
self.store.agent = None
|
|
779
|
+
self._agent_excluded_tools = ()
|
|
734
780
|
return True, "switched to default agent"
|
|
735
781
|
|
|
736
782
|
agents = discover_agents()
|
|
@@ -738,17 +784,20 @@ class Session:
|
|
|
738
784
|
if prompt_file is None:
|
|
739
785
|
available = ", ".join(sorted([RESERVED_AGENT_NAME, *agents.keys()]))
|
|
740
786
|
return False, f"unknown agent: {name} (available: {available})"
|
|
741
|
-
from .prompts import assemble_agent_prompt, load_agent_prompt
|
|
787
|
+
from .prompts import agent_exclude_tools, assemble_agent_prompt, load_agent_prompt
|
|
742
788
|
|
|
743
789
|
agent_prompt = load_agent_prompt(prompt_file, skill_dir=self._skill_dir)
|
|
744
790
|
if agent_prompt is None:
|
|
745
791
|
return False, f"agent {name}: prompt file not found or empty: {prompt_file}"
|
|
792
|
+
new_exclusions = agent_exclude_tools(prompt_file)
|
|
746
793
|
self.system_prompt = assemble_agent_prompt(
|
|
747
794
|
self.project_dir,
|
|
748
795
|
agent_prompt,
|
|
749
796
|
context_path=self._configured_context_path,
|
|
750
797
|
)
|
|
751
798
|
self.store.system_prompt = self.system_prompt
|
|
799
|
+
self.store.agent = name
|
|
800
|
+
self._agent_excluded_tools = new_exclusions
|
|
752
801
|
return True, f"switched to {name}"
|
|
753
802
|
|
|
754
803
|
# ------------------------------------------------------------------
|
|
@@ -48,7 +48,7 @@ class _Hunk:
|
|
|
48
48
|
"""(kind, content-without-newline) for the lines that must exist
|
|
49
49
|
in the old file (' ' context and '-' removed lines)."""
|
|
50
50
|
return [
|
|
51
|
-
(kind, content.rstrip("\n")) for kind, content, _ in self.body if kind in (" ", "-")
|
|
51
|
+
(kind, content.rstrip("\r\n")) for kind, content, _ in self.body if kind in (" ", "-")
|
|
52
52
|
]
|
|
53
53
|
|
|
54
54
|
|
|
@@ -142,7 +142,7 @@ def _match_hunk(hunk: _Hunk, file_lines: list[str]) -> int | None:
|
|
|
142
142
|
mismatches = 0
|
|
143
143
|
ok = True
|
|
144
144
|
for i, (kind, content) in enumerate(old_seq):
|
|
145
|
-
file_content = file_lines[p + i].rstrip("\n")
|
|
145
|
+
file_content = file_lines[p + i].rstrip("\r\n")
|
|
146
146
|
if kind == "-":
|
|
147
147
|
if file_content != content:
|
|
148
148
|
ok = False
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/grep_win.py
RENAMED
|
@@ -98,12 +98,17 @@ class GrepWindows(Grep):
|
|
|
98
98
|
files = [Path(path)]
|
|
99
99
|
else:
|
|
100
100
|
root = Path(path)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for p in root.rglob("*")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
collected: list[Path] = []
|
|
102
|
+
try:
|
|
103
|
+
for p in root.rglob("*"):
|
|
104
|
+
if not p.is_file():
|
|
105
|
+
continue
|
|
106
|
+
if any(part.startswith(".") for part in p.relative_to(root).parts[:-1]):
|
|
107
|
+
continue
|
|
108
|
+
collected.append(p)
|
|
109
|
+
except OSError:
|
|
110
|
+
pass
|
|
111
|
+
files = collected
|
|
107
112
|
|
|
108
113
|
for file_path in files:
|
|
109
114
|
if match_count >= max_matches:
|
|
@@ -124,7 +129,7 @@ class GrepWindows(Grep):
|
|
|
124
129
|
start = max(0, i - 1 - context)
|
|
125
130
|
end = min(len(lines), i + context)
|
|
126
131
|
for j in range(start, end):
|
|
127
|
-
marker = "
|
|
132
|
+
marker = "-" if j + 1 != i else ":"
|
|
128
133
|
results.append(f"{file_path}:{j + 1}{marker}{lines[j].rstrip()}")
|
|
129
134
|
results.append("") # blank line between matches with context
|
|
130
135
|
else:
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/commands.py
RENAMED
|
@@ -119,7 +119,7 @@ class CommandMixin:
|
|
|
119
119
|
"/review [project] [commit|branch|PR] review code changes\n"
|
|
120
120
|
"/explain [project] [target] explain code\n"
|
|
121
121
|
"/sessions list saved sessions\n"
|
|
122
|
-
"/restore [path | title | --latest | latest] restore a saved session\n"
|
|
122
|
+
"/restore [path | title | --latest | latest] restore a saved session (conversation + agent + model)\n"
|
|
123
123
|
"/model [name] switch LLM model profile\n"
|
|
124
124
|
"/agent [name] switch agent system prompt\n"
|
|
125
125
|
"Ctrl-C cancels the current execution (app stays open); "
|
|
@@ -642,6 +642,62 @@ class CommandMixin:
|
|
|
642
642
|
title = title_from_filename(path)
|
|
643
643
|
if title:
|
|
644
644
|
self.session.store.title = title
|
|
645
|
+
# Restore agent if saved
|
|
646
|
+
agent = meta.get("python-agent-harness--agent")
|
|
647
|
+
if agent:
|
|
648
|
+
try:
|
|
649
|
+
success, msg = self.session.switch_agent(agent)
|
|
650
|
+
if success:
|
|
651
|
+
self.console.print(f"[green]{msg}[/green]")
|
|
652
|
+
else:
|
|
653
|
+
# Agent no longer exists: reset to default to avoid
|
|
654
|
+
# stale exclusions from the previously active agent
|
|
655
|
+
self.session.switch_agent("default")
|
|
656
|
+
self.console.print(f"[yellow]warning: {msg}[/yellow]")
|
|
657
|
+
except Exception as e:
|
|
658
|
+
self.console.print(f"[yellow]warning: could not restore agent: {e}[/yellow]")
|
|
659
|
+
# Restore model if saved: /model's profile switching is the
|
|
660
|
+
# canonical path — it also fixes base_url/api_key/temperature
|
|
661
|
+
# and other settings that a bare model-name change would leave
|
|
662
|
+
# pointing at the wrong endpoint. The metadata stores the raw
|
|
663
|
+
# model name, so first try it as a profile name, then fall back
|
|
664
|
+
# to a profile whose model matches; without a matching profile
|
|
665
|
+
# the current model stays active (profile names are not saved).
|
|
666
|
+
model = meta.get("python-agent-harness--model")
|
|
667
|
+
if model:
|
|
668
|
+
try:
|
|
669
|
+
if model == self.session.model:
|
|
670
|
+
# Already the active model: nothing to do, no noise.
|
|
671
|
+
pass
|
|
672
|
+
elif model == self.session.llm_settings.get("model"):
|
|
673
|
+
# The saved model is the session-start default: the
|
|
674
|
+
# "default" pseudo-profile restores exactly those
|
|
675
|
+
# original llm settings (profile names were not saved,
|
|
676
|
+
# so there is no better match).
|
|
677
|
+
success, msg = self.session.switch_model("default")
|
|
678
|
+
self.console.print(f"[green]{msg}[/green]")
|
|
679
|
+
else:
|
|
680
|
+
success, msg = self.session.switch_model(model)
|
|
681
|
+
if not success:
|
|
682
|
+
profile = next(
|
|
683
|
+
(
|
|
684
|
+
name
|
|
685
|
+
for name, p in self.session.model_profiles.items()
|
|
686
|
+
if p.get("model") == model
|
|
687
|
+
),
|
|
688
|
+
None,
|
|
689
|
+
)
|
|
690
|
+
if profile:
|
|
691
|
+
success, msg = self.session.switch_model(profile)
|
|
692
|
+
if success:
|
|
693
|
+
self.console.print(f"[green]{msg}[/green]")
|
|
694
|
+
else:
|
|
695
|
+
self.console.print(
|
|
696
|
+
f"[yellow]warning: model {model!r} has no matching profile, "
|
|
697
|
+
f"keeping current model[/yellow]"
|
|
698
|
+
)
|
|
699
|
+
except Exception as e:
|
|
700
|
+
self.console.print(f"[yellow]warning: could not restore model: {e}[/yellow]")
|
|
645
701
|
# Replace conversation history: a new generation. Invalidate any
|
|
646
702
|
# worker still winding down from a cancelled run so its salvaged
|
|
647
703
|
# history can't clobber the restored session.
|
|
@@ -650,11 +706,11 @@ class CommandMixin:
|
|
|
650
706
|
self.session.last_messages = list(messages)
|
|
651
707
|
self.session.clear_todos()
|
|
652
708
|
self._history_dirty = True
|
|
653
|
-
model = meta.get("python-agent-harness--model", "?")
|
|
654
709
|
project = meta.get("python-agent-harness--project-dir", "?")
|
|
710
|
+
agent_display = meta.get("python-agent-harness--agent", "default")
|
|
655
711
|
self.console.print(
|
|
656
712
|
f"[green]restored:[/green] {os.path.basename(path)} "
|
|
657
|
-
f"(model={model}, project={project}, {len(messages)} messages)"
|
|
713
|
+
f"(model={model}, agent={agent_display}, project={project}, {len(messages)} messages)"
|
|
658
714
|
)
|
|
659
715
|
|
|
660
716
|
@staticmethod
|
|
@@ -664,13 +720,15 @@ class CommandMixin:
|
|
|
664
720
|
The save format is markdown with **role**: content blocks
|
|
665
721
|
separated by blank lines.
|
|
666
722
|
|
|
667
|
-
``tool`` blocks are dropped: the saved markdown
|
|
668
|
-
``tool_call_id``/``name`` (assistant tool calls are
|
|
669
|
-
plain text), so a restored ``role="tool"`` message
|
|
670
|
-
API-invalid payload (a tool message with no
|
|
671
|
-
``tool_calls``).
|
|
672
|
-
|
|
673
|
-
|
|
723
|
+
``tool`` and ``system`` blocks are dropped: the saved markdown
|
|
724
|
+
does not keep ``tool_call_id``/``name`` (assistant tool calls are
|
|
725
|
+
flattened to plain text), so a restored ``role="tool"`` message
|
|
726
|
+
would form an API-invalid payload (a tool message with no
|
|
727
|
+
preceding assistant ``tool_calls``). A restored ``system``
|
|
728
|
+
message would duplicate the live system prompt the client
|
|
729
|
+
prepends on every request. The following assistant reply
|
|
730
|
+
already summarizes the results, so dropping them loses no
|
|
731
|
+
essential context.
|
|
674
732
|
|
|
675
733
|
Body lines that merely look like a block header are escaped by
|
|
676
734
|
the renderer (see `escape_role_headers`) and unescaped here, so
|
|
@@ -687,9 +745,10 @@ class CommandMixin:
|
|
|
687
745
|
header = split_role_header(line)
|
|
688
746
|
if header is not None:
|
|
689
747
|
role, rest = header
|
|
690
|
-
# Save the previous block (tool blocks
|
|
691
|
-
#
|
|
692
|
-
|
|
748
|
+
# Save the previous block (tool blocks lose their
|
|
749
|
+
# tool_call_id/name; system blocks would duplicate the
|
|
750
|
+
# live system prompt the client prepends per request)
|
|
751
|
+
if current_role is not None and current_role not in ("tool", "system"):
|
|
693
752
|
content = "\n".join(current_lines).strip()
|
|
694
753
|
if content:
|
|
695
754
|
messages.append(Message(role=current_role, content=content))
|
|
@@ -698,8 +757,8 @@ class CommandMixin:
|
|
|
698
757
|
continue
|
|
699
758
|
current_lines.append(unescape_role_header(line))
|
|
700
759
|
|
|
701
|
-
# Don't forget the last block (tool blocks
|
|
702
|
-
if current_role is not None and current_role
|
|
760
|
+
# Don't forget the last block (tool/system blocks dropped, see above)
|
|
761
|
+
if current_role is not None and current_role not in ("tool", "system"):
|
|
703
762
|
content = "\n".join(current_lines).strip()
|
|
704
763
|
if content:
|
|
705
764
|
messages.append(Message(role=current_role, content=content))
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5/python_agent_harness.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.5.
|
|
3
|
+
Version: 1.5.5.5
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
5
|
Author-email: Huming Chen <chenhuming@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -243,13 +243,32 @@ Custom commands from `prompts/commands/*.md` are registered as slash commands as
|
|
|
243
243
|
|
|
244
244
|
Agent prompt files are markdown files (`.md`) placed in the `prompts/agents/` directory. Each file becomes a switchable agent profile available via the `/agent` TUI command.
|
|
245
245
|
|
|
246
|
+
An agent file may carry YAML frontmatter with two optional keys:
|
|
247
|
+
|
|
248
|
+
- `name:` — override the agent name (defaults to the file stem)
|
|
249
|
+
- `exclude_tools:` — a list of tool names the agent must not see. An entry matches a tool by exact name, glob pattern (`mcp__git__*`), or `__`-delimited prefix (`mcp__git` hides `mcp__git__list_repos` but `Write` does NOT hide `TodoWrite`). The built-in `default` agent always sees all tools.
|
|
250
|
+
|
|
251
|
+
```markdown
|
|
252
|
+
---
|
|
253
|
+
name: assistant
|
|
254
|
+
exclude_tools:
|
|
255
|
+
- Bash
|
|
256
|
+
- Edit
|
|
257
|
+
- Write
|
|
258
|
+
- mcp__git__*
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
# Role and Behavior
|
|
262
|
+
You are a personal assistant. You do NOT modify files or run shell commands.
|
|
263
|
+
```
|
|
264
|
+
|
|
246
265
|
#### Commands vs Agents
|
|
247
266
|
|
|
248
267
|
| | Commands (`/review`) | Agents (`/agent reviewer`) |
|
|
249
268
|
|---|---|---|
|
|
250
269
|
| **Scope** | One-shot (prompt resets after run) | Persistent (stays until next `/agent`) |
|
|
251
270
|
| **Kickoff** | Hardcoded kickoff message | User types their own prompt |
|
|
252
|
-
| **Tools** | Can restrict (`allow_planexit=False`) | All tools |
|
|
271
|
+
| **Tools** | Can restrict (`allow_planexit=False`) | All tools, minus the agent's `exclude_tools` |
|
|
253
272
|
| **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
|
|
254
273
|
|
|
255
274
|
## Project layout
|
|
@@ -163,6 +163,25 @@ class TestSwitchAgent(unittest.TestCase):
|
|
|
163
163
|
session.switch_agent("default")
|
|
164
164
|
self.assertEqual(session.store.system_prompt, "original prompt")
|
|
165
165
|
|
|
166
|
+
def test_switch_updates_store_agent(self):
|
|
167
|
+
"""switch_agent must keep store.agent in sync so every auto-save
|
|
168
|
+
records the currently active agent in the metadata block."""
|
|
169
|
+
session = _make_session(system_prompt="original prompt")
|
|
170
|
+
self.assertIsNone(session.store.agent) # no custom agent yet
|
|
171
|
+
session.switch_agent("reviewer")
|
|
172
|
+
self.assertEqual(session.store.agent, "reviewer")
|
|
173
|
+
session.switch_agent("default")
|
|
174
|
+
self.assertIsNone(session.store.agent) # default = no custom agent
|
|
175
|
+
|
|
176
|
+
def test_failed_switch_leaves_store_agent_untouched(self):
|
|
177
|
+
"""A failed switch (unknown name) must not change store.agent:
|
|
178
|
+
the metadata must never record an agent that is not active."""
|
|
179
|
+
session = _make_session(system_prompt="original prompt")
|
|
180
|
+
session.switch_agent("reviewer")
|
|
181
|
+
ok, _ = session.switch_agent("nonexistent")
|
|
182
|
+
self.assertFalse(ok)
|
|
183
|
+
self.assertEqual(session.store.agent, "reviewer")
|
|
184
|
+
|
|
166
185
|
|
|
167
186
|
class TestDefaultAgentConfig(unittest.TestCase):
|
|
168
187
|
"""config.load_default_agent: reading the default_agent setting."""
|
|
@@ -286,6 +305,30 @@ class TestMakeSessionWithDefaultAgent(unittest.TestCase):
|
|
|
286
305
|
p.write_text(json.dumps({"default_agent": "reviewer"}), encoding="utf-8")
|
|
287
306
|
session = make_session("/tmp", config_path=str(p))
|
|
288
307
|
self.assertEqual(session.startup_warnings, [])
|
|
308
|
+
session.close()
|
|
309
|
+
|
|
310
|
+
def test_valid_default_agent_recorded_in_store(self):
|
|
311
|
+
"""A successfully applied default_agent is recorded in store.agent,
|
|
312
|
+
so saved sessions restore it. A typo'd one must NOT be recorded:
|
|
313
|
+
the metadata must never name an agent that is not active."""
|
|
314
|
+
from python_agent_harness.cli import make_session
|
|
315
|
+
|
|
316
|
+
with tempfile.TemporaryDirectory() as d:
|
|
317
|
+
p = Path(d) / "config.json"
|
|
318
|
+
p.write_text(json.dumps({"default_agent": "reviewer"}), encoding="utf-8")
|
|
319
|
+
session = make_session("/tmp", config_path=str(p))
|
|
320
|
+
try:
|
|
321
|
+
self.assertEqual(session.store.agent, "reviewer")
|
|
322
|
+
finally:
|
|
323
|
+
session.close()
|
|
324
|
+
with tempfile.TemporaryDirectory() as d:
|
|
325
|
+
p = Path(d) / "config.json"
|
|
326
|
+
p.write_text(json.dumps({"default_agent": "no-such-agent"}), encoding="utf-8")
|
|
327
|
+
session = make_session("/tmp", config_path=str(p))
|
|
328
|
+
try:
|
|
329
|
+
self.assertIsNone(session.store.agent)
|
|
330
|
+
finally:
|
|
331
|
+
session.close()
|
|
289
332
|
|
|
290
333
|
|
|
291
334
|
class TestMcpFailuresAsStartupWarnings(unittest.TestCase):
|
|
@@ -344,6 +387,229 @@ class TestMcpFailuresAsStartupWarnings(unittest.TestCase):
|
|
|
344
387
|
os.environ[k] = v
|
|
345
388
|
|
|
346
389
|
|
|
390
|
+
class TestAgentExcludeTools(unittest.TestCase):
|
|
391
|
+
"""Agent profile ``exclude_tools`` frontmatter: parsing and effect
|
|
392
|
+
on the tool specs exposed to the model."""
|
|
393
|
+
|
|
394
|
+
def _write_agent(
|
|
395
|
+
self, name: str, frontmatter: str, body: str = "You are a test agent."
|
|
396
|
+
) -> Path:
|
|
397
|
+
p = AGENTS_DIR / f"{name}.md"
|
|
398
|
+
p.write_text(f"---\n{frontmatter}---\n{body}", encoding="utf-8")
|
|
399
|
+
return p
|
|
400
|
+
|
|
401
|
+
def test_parse_exclude_tools(self):
|
|
402
|
+
"""agent_exclude_tools reads the frontmatter list."""
|
|
403
|
+
from python_agent_harness.prompts import agent_exclude_tools
|
|
404
|
+
|
|
405
|
+
p = self._write_agent(
|
|
406
|
+
"test-exclude-parse",
|
|
407
|
+
"name: test-exclude-parse\nexclude_tools:\n - Bash\n - Edit\n - mcp__git__*\n",
|
|
408
|
+
)
|
|
409
|
+
try:
|
|
410
|
+
self.assertEqual(
|
|
411
|
+
agent_exclude_tools(p),
|
|
412
|
+
("Bash", "Edit", "mcp__git__*"),
|
|
413
|
+
)
|
|
414
|
+
finally:
|
|
415
|
+
p.unlink(missing_ok=True)
|
|
416
|
+
|
|
417
|
+
def test_parse_exclude_tools_inline(self):
|
|
418
|
+
"""agent_exclude_tools reads an inline ``[a, b]`` list too."""
|
|
419
|
+
from python_agent_harness.prompts import agent_exclude_tools
|
|
420
|
+
|
|
421
|
+
p = self._write_agent(
|
|
422
|
+
"test-exclude-inline",
|
|
423
|
+
"name: test-exclude-inline\nexclude_tools: [Bash, mcp__git__*]\n",
|
|
424
|
+
)
|
|
425
|
+
try:
|
|
426
|
+
self.assertEqual(
|
|
427
|
+
agent_exclude_tools(p),
|
|
428
|
+
("Bash", "mcp__git__*"),
|
|
429
|
+
)
|
|
430
|
+
finally:
|
|
431
|
+
p.unlink(missing_ok=True)
|
|
432
|
+
|
|
433
|
+
def test_no_exclude_tools_returns_empty(self):
|
|
434
|
+
"""An agent without the key excludes nothing."""
|
|
435
|
+
from python_agent_harness.prompts import agent_exclude_tools
|
|
436
|
+
|
|
437
|
+
p = self._write_agent("test-exclude-none", "name: test-exclude-none\n")
|
|
438
|
+
try:
|
|
439
|
+
self.assertEqual(agent_exclude_tools(p), ())
|
|
440
|
+
finally:
|
|
441
|
+
p.unlink(missing_ok=True)
|
|
442
|
+
|
|
443
|
+
def test_switch_agent_applies_exclusions(self):
|
|
444
|
+
"""switch_agent activates the profile's excluded tools."""
|
|
445
|
+
p = self._write_agent(
|
|
446
|
+
"test-exclude-switch",
|
|
447
|
+
"name: test-exclude-switch\nexclude_tools:\n - Bash\n - Edit\n",
|
|
448
|
+
)
|
|
449
|
+
try:
|
|
450
|
+
session = _make_session()
|
|
451
|
+
ok, msg = session.switch_agent("test-exclude-switch")
|
|
452
|
+
self.assertTrue(ok, msg)
|
|
453
|
+
names = {s.name for s in session.tool_specs()}
|
|
454
|
+
self.assertNotIn("Bash", names)
|
|
455
|
+
self.assertNotIn("Edit", names)
|
|
456
|
+
self.assertIn("Read", names)
|
|
457
|
+
finally:
|
|
458
|
+
p.unlink(missing_ok=True)
|
|
459
|
+
|
|
460
|
+
def test_switch_to_default_clears_exclusions(self):
|
|
461
|
+
"""/agent default restores the full tool set."""
|
|
462
|
+
p = self._write_agent(
|
|
463
|
+
"test-exclude-default",
|
|
464
|
+
"name: test-exclude-default\nexclude_tools:\n - Bash\n",
|
|
465
|
+
)
|
|
466
|
+
try:
|
|
467
|
+
session = _make_session()
|
|
468
|
+
session.switch_agent("test-exclude-default")
|
|
469
|
+
self.assertNotIn("Bash", {s.name for s in session.tool_specs()})
|
|
470
|
+
ok, msg = session.switch_agent("default")
|
|
471
|
+
self.assertTrue(ok, msg)
|
|
472
|
+
self.assertIn("Bash", {s.name for s in session.tool_specs()})
|
|
473
|
+
finally:
|
|
474
|
+
p.unlink(missing_ok=True)
|
|
475
|
+
|
|
476
|
+
def test_glob_pattern_excludes_mcp_tools(self):
|
|
477
|
+
"""mcp__<server>__* patterns hide every tool of one server."""
|
|
478
|
+
from python_agent_harness.tools.base import Tool
|
|
479
|
+
|
|
480
|
+
class FakeMcpTool(Tool):
|
|
481
|
+
name = "mcp__git__list_repos"
|
|
482
|
+
description = "fake"
|
|
483
|
+
|
|
484
|
+
def run(self, args, ctx):
|
|
485
|
+
return ""
|
|
486
|
+
|
|
487
|
+
p = self._write_agent(
|
|
488
|
+
"test-exclude-glob",
|
|
489
|
+
"name: test-exclude-glob\nexclude_tools:\n - mcp__git__*\n",
|
|
490
|
+
)
|
|
491
|
+
try:
|
|
492
|
+
session = _make_session()
|
|
493
|
+
session.registry.register(FakeMcpTool())
|
|
494
|
+
self.assertIn("mcp__git__list_repos", {s.name for s in session.tool_specs()})
|
|
495
|
+
session.switch_agent("test-exclude-glob")
|
|
496
|
+
names = {s.name for s in session.tool_specs()}
|
|
497
|
+
self.assertNotIn("mcp__git__list_repos", names)
|
|
498
|
+
finally:
|
|
499
|
+
p.unlink(missing_ok=True)
|
|
500
|
+
|
|
501
|
+
def test_substring_pattern_excludes_mcp_tools(self):
|
|
502
|
+
"""A ``__``-delimited prefix (mcp__git) hides every tool of one
|
|
503
|
+
server, but does NOT accidentally match unrelated tools."""
|
|
504
|
+
from python_agent_harness.tools.base import Tool
|
|
505
|
+
|
|
506
|
+
class FakeMcpTool(Tool):
|
|
507
|
+
name = "mcp__git__list_repos"
|
|
508
|
+
description = "fake"
|
|
509
|
+
|
|
510
|
+
def run(self, args, ctx):
|
|
511
|
+
return ""
|
|
512
|
+
|
|
513
|
+
p = self._write_agent(
|
|
514
|
+
"test-exclude-substr",
|
|
515
|
+
"name: test-exclude-substr\nexclude_tools:\n - mcp__git\n",
|
|
516
|
+
)
|
|
517
|
+
try:
|
|
518
|
+
session = _make_session()
|
|
519
|
+
session.registry.register(FakeMcpTool())
|
|
520
|
+
self.assertIn("mcp__git__list_repos", {s.name for s in session.tool_specs()})
|
|
521
|
+
session.switch_agent("test-exclude-substr")
|
|
522
|
+
names = {s.name for s in session.tool_specs()}
|
|
523
|
+
self.assertNotIn("mcp__git__list_repos", names)
|
|
524
|
+
finally:
|
|
525
|
+
p.unlink(missing_ok=True)
|
|
526
|
+
|
|
527
|
+
def test_write_does_not_exclude_todowrite(self):
|
|
528
|
+
"""``Write`` must not accidentally hide ``TodoWrite``: matching
|
|
529
|
+
is by exact name or ``__``-delimited prefix, not raw substring."""
|
|
530
|
+
p = self._write_agent(
|
|
531
|
+
"test-exclude-no-false-match",
|
|
532
|
+
"name: test-exclude-no-false-match\nexclude_tools:\n - Write\n",
|
|
533
|
+
)
|
|
534
|
+
try:
|
|
535
|
+
session = _make_session()
|
|
536
|
+
session.switch_agent("test-exclude-no-false-match")
|
|
537
|
+
names = {s.name for s in session.tool_specs()}
|
|
538
|
+
self.assertNotIn("Write", names)
|
|
539
|
+
self.assertIn("TodoWrite", names)
|
|
540
|
+
finally:
|
|
541
|
+
p.unlink(missing_ok=True)
|
|
542
|
+
|
|
543
|
+
def test_explicit_exclude_still_works(self):
|
|
544
|
+
"""The caller's exclude argument composes with the profile's."""
|
|
545
|
+
p = self._write_agent(
|
|
546
|
+
"test-exclude-compose",
|
|
547
|
+
"name: test-exclude-compose\nexclude_tools:\n - Bash\n",
|
|
548
|
+
)
|
|
549
|
+
try:
|
|
550
|
+
session = _make_session()
|
|
551
|
+
session.switch_agent("test-exclude-compose")
|
|
552
|
+
names = {s.name for s in session.tool_specs(exclude=("Edit",))}
|
|
553
|
+
self.assertNotIn("Bash", names)
|
|
554
|
+
self.assertNotIn("Edit", names)
|
|
555
|
+
finally:
|
|
556
|
+
p.unlink(missing_ok=True)
|
|
557
|
+
|
|
558
|
+
def test_failed_switch_leaves_exclusions_untouched(self):
|
|
559
|
+
"""A failed switch (unknown name) must not change the active
|
|
560
|
+
exclusions: the caller's tool set must never silently shift."""
|
|
561
|
+
p = self._write_agent(
|
|
562
|
+
"test-exclude-fail",
|
|
563
|
+
"name: test-exclude-fail\nexclude_tools:\n - Bash\n",
|
|
564
|
+
)
|
|
565
|
+
try:
|
|
566
|
+
session = _make_session()
|
|
567
|
+
session.switch_agent("test-exclude-fail")
|
|
568
|
+
self.assertNotIn("Bash", {s.name for s in session.tool_specs()})
|
|
569
|
+
ok, _ = session.switch_agent("nonexistent")
|
|
570
|
+
self.assertFalse(ok)
|
|
571
|
+
self.assertNotIn("Bash", {s.name for s in session.tool_specs()})
|
|
572
|
+
finally:
|
|
573
|
+
p.unlink(missing_ok=True)
|
|
574
|
+
|
|
575
|
+
def test_quoted_items_stripped(self):
|
|
576
|
+
"""Quoted frontmatter items lose their quotes during parsing."""
|
|
577
|
+
from python_agent_harness.prompts import agent_exclude_tools
|
|
578
|
+
|
|
579
|
+
p = self._write_agent(
|
|
580
|
+
"test-exclude-quoted",
|
|
581
|
+
"name: test-exclude-quoted\nexclude_tools:\n - \"Bash\"\n - 'Edit'\n",
|
|
582
|
+
)
|
|
583
|
+
try:
|
|
584
|
+
self.assertEqual(agent_exclude_tools(p), ("Bash", "Edit"))
|
|
585
|
+
finally:
|
|
586
|
+
p.unlink(missing_ok=True)
|
|
587
|
+
|
|
588
|
+
def test_no_frontmatter_at_all(self):
|
|
589
|
+
"""A file with no frontmatter block excludes nothing."""
|
|
590
|
+
from python_agent_harness.prompts import agent_exclude_tools
|
|
591
|
+
|
|
592
|
+
p = self._write_agent("test-no-frontmatter", "")
|
|
593
|
+
p.write_text("You are a simple agent with no frontmatter.", encoding="utf-8")
|
|
594
|
+
try:
|
|
595
|
+
self.assertEqual(agent_exclude_tools(p), ())
|
|
596
|
+
finally:
|
|
597
|
+
p.unlink(missing_ok=True)
|
|
598
|
+
|
|
599
|
+
def test_crlf_line_endings(self):
|
|
600
|
+
"""CRLF line endings must not break frontmatter parsing."""
|
|
601
|
+
from python_agent_harness.prompts import agent_exclude_tools
|
|
602
|
+
|
|
603
|
+
p = AGENTS_DIR / "test-crlf-agent.md"
|
|
604
|
+
p.write_bytes(
|
|
605
|
+
b"---\r\nname: test-crlf-agent\r\nexclude_tools:\r\n - Bash\r\n - Edit\r\n---\r\nYou are a CRLF agent.\r\n"
|
|
606
|
+
)
|
|
607
|
+
try:
|
|
608
|
+
self.assertEqual(agent_exclude_tools(p), ("Bash", "Edit"))
|
|
609
|
+
finally:
|
|
610
|
+
p.unlink(missing_ok=True)
|
|
611
|
+
|
|
612
|
+
|
|
347
613
|
class TestReviewerAgentContent(unittest.TestCase):
|
|
348
614
|
"""Verify the bundled reviewer.md agent has expected content."""
|
|
349
615
|
|
|
@@ -59,6 +59,22 @@ class TestSession(unittest.TestCase):
|
|
|
59
59
|
store = SessionPersistence(project_dir="/tmp/proj", model="m")
|
|
60
60
|
self.assertNotIn("round-times", store.metadata_block())
|
|
61
61
|
|
|
62
|
+
def test_agent_persisted_in_metadata(self):
|
|
63
|
+
"""A session with an active custom agent records it in the
|
|
64
|
+
metadata block so /restore can reapply it."""
|
|
65
|
+
store = SessionPersistence(project_dir="/tmp/proj", model="m", agent="reviewer")
|
|
66
|
+
meta = store.metadata_block()
|
|
67
|
+
self.assertIn("python-agent-harness--agent", meta)
|
|
68
|
+
text = "conversation...\n\n" + meta + "\n"
|
|
69
|
+
parsed = SessionPersistence.parse_metadata(text)
|
|
70
|
+
self.assertEqual(parsed["python-agent-harness--agent"], "reviewer")
|
|
71
|
+
|
|
72
|
+
def test_agent_absent_when_none(self):
|
|
73
|
+
"""No custom agent -> no agent line in the metadata block, so
|
|
74
|
+
old sessions and default-agent sessions parse identically."""
|
|
75
|
+
store = SessionPersistence(project_dir="/tmp/proj", model="m", agent=None)
|
|
76
|
+
self.assertNotIn("python-agent-harness--agent", store.metadata_block())
|
|
77
|
+
|
|
62
78
|
def test_save_and_restore_flow(self):
|
|
63
79
|
import os
|
|
64
80
|
import tempfile
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/diffrender.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/mcp/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/mcp/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/mcp/config.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/mcp/manager.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/models.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/planmode.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/agent.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/plan.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/review.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/prompts/title.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/subagent.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tool_runner.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/base.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/bash.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/bash_win.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/edit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/edit_mac.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/edit_win.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/glob.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/glob_mac.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/glob_win.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/grep.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/grep_mac.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/insert.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/mcp.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/mkdir.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/planexit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/question.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/read.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/skill.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/todo.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tools/write.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/core.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/input.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/python_agent_harness/tui/render.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_concurrent_subagents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.5}/tests/test_subagent_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|