python-agent-harness 1.5.4.3__tar.gz → 1.5.4.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.
Files changed (92) hide show
  1. {python_agent_harness-1.5.4.3/python_agent_harness.egg-info → python_agent_harness-1.5.4.5}/PKG-INFO +3 -3
  2. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/README.md +2 -2
  3. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/pyproject.toml +1 -1
  4. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/__init__.py +1 -1
  5. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/agent.py +1 -1
  6. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/cli.py +3 -4
  7. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/config.py +6 -9
  8. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/persistence.py +0 -3
  9. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/session.py +1 -8
  10. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/__init__.py +13 -6
  11. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/agent_tool.py +1 -1
  12. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/filesystem.py +4 -0
  13. python_agent_harness-1.5.4.5/python_agent_harness/tools/glob_mac.py +97 -0
  14. python_agent_harness-1.5.4.5/python_agent_harness/tools/grep_mac.py +92 -0
  15. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5/python_agent_harness.egg-info}/PKG-INFO +3 -3
  16. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/SOURCES.txt +2 -0
  17. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_cli.py +1 -3
  18. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_config.py +16 -6
  19. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_filesystem.py +257 -0
  20. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_persistence.py +12 -15
  21. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_scenarios.py +0 -1
  22. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_session.py +0 -6
  23. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_subagent.py +0 -1
  24. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_subagent_isolation.py +1 -1
  25. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/LICENSE +0 -0
  26. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/__main__.py +0 -0
  27. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/client.py +0 -0
  28. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/commands.py +0 -0
  29. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/context_manager.py +0 -0
  30. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/diffrender.py +0 -0
  31. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/__init__.py +0 -0
  32. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/client.py +0 -0
  33. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/config.py +0 -0
  34. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/manager.py +0 -0
  35. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/models.py +0 -0
  36. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/planmode.py +0 -0
  37. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/agent.md +0 -0
  38. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/build-switch.md +0 -0
  39. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/commands/explain.md +0 -0
  40. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/compact.md +0 -0
  41. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/initialize.md +0 -0
  42. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/plan-mode.md +0 -0
  43. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/plan.md +0 -0
  44. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/review.md +0 -0
  45. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/subagent.md +0 -0
  46. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/summary.md +0 -0
  47. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  48. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/title.md +0 -0
  49. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts.py +0 -0
  50. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/subagent.py +0 -0
  51. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/token_estimator.py +0 -0
  52. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tool_runner.py +0 -0
  53. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/base.py +0 -0
  54. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/bash.py +0 -0
  55. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/diffapply.py +0 -0
  56. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/edit.py +0 -0
  57. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/edit_mac.py +0 -0
  58. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/glob.py +0 -0
  59. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/grep.py +0 -0
  60. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/insert.py +0 -0
  61. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/mcp.py +0 -0
  62. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/mkdir.py +0 -0
  63. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/planexit.py +0 -0
  64. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/question.py +0 -0
  65. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/read.py +0 -0
  66. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/skill.py +0 -0
  67. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/todo.py +0 -0
  68. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/write.py +0 -0
  69. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/__init__.py +0 -0
  70. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/commands.py +0 -0
  71. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/core.py +0 -0
  72. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/input.py +0 -0
  73. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/render.py +0 -0
  74. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  75. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/entry_points.txt +0 -0
  76. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/requires.txt +0 -0
  77. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/top_level.txt +0 -0
  78. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/setup.cfg +0 -0
  79. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_agents_md.py +0 -0
  80. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_client.py +0 -0
  81. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_commands.py +0 -0
  82. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_concurrent_subagents.py +0 -0
  83. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_context_rules.py +0 -0
  84. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_diffrender.py +0 -0
  85. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_invariants.py +0 -0
  86. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_mcp.py +0 -0
  87. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_models.py +0 -0
  88. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_planmode.py +0 -0
  89. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_prompts.py +0 -0
  90. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_todos_scope.py +0 -0
  91. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_token_estimator.py +0 -0
  92. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.5}/tests/test_tools_misc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.4.3
3
+ Version: 1.5.4.5
4
4
  Summary: A lightweight, hackable mini-OpenCode written in Python.
5
5
  Author: huming
6
6
  License-Expression: MIT
@@ -186,13 +186,13 @@ All LLM settings live in a single JSON configuration file. Environment variables
186
186
 
187
187
  ### Configuration options
188
188
 
189
- - **`llm`** — main LLM configuration. Optional keys include `backend`, `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
189
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
190
190
  - **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. `default` restores the main LLM configuration.
191
191
  - **`context_windows`** — optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g., `deepseek-v4`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
192
192
  - **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
193
193
  - **`paths.context_path` / `paths.skill_path`** — locations from which to load context files and skills. When unset, the project-local `<project>/contexts` and `<project>/skills` directories are used.
194
194
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
195
- - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`, `_BACKEND`).
195
+ - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
196
196
  - **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
197
197
  - **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
198
198
 
@@ -163,13 +163,13 @@ All LLM settings live in a single JSON configuration file. Environment variables
163
163
 
164
164
  ### Configuration options
165
165
 
166
- - **`llm`** — main LLM configuration. Optional keys include `backend`, `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
166
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
167
167
  - **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. `default` restores the main LLM configuration.
168
168
  - **`context_windows`** — optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g., `deepseek-v4`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
169
169
  - **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
170
170
  - **`paths.context_path` / `paths.skill_path`** — locations from which to load context files and skills. When unset, the project-local `<project>/contexts` and `<project>/skills` directories are used.
171
171
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
172
- - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`, `_BACKEND`).
172
+ - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
173
173
  - **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
174
174
  - **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
175
175
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-agent-harness"
7
- version = "1.5.4.3"
7
+ version = "1.5.4.5"
8
8
  description = "A lightweight, hackable mini-OpenCode written in Python."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -5,7 +5,7 @@ from .mcp.manager import MCPManager
5
5
  from .models import AgentMode, Message, ToolCall, ToolSpec
6
6
  from .session import Session
7
7
 
8
- __version__ = "1.5.4.3"
8
+ __version__ = "1.5.4.5"
9
9
 
10
10
  __all__ = [
11
11
  "Session",
@@ -452,7 +452,7 @@ class AgentLoop:
452
452
  )
453
453
  # sub-agent runs use their own LLM when one is configured
454
454
  # (a per-invocation clone of session.subagent_client,
455
- # mirroring gptel-agent-harness-subagent-model/-backend);
455
+ # mirroring gptel-agent-harness-subagent-model);
456
456
  # everything unset inherits the main agent's settings, so
457
457
  # the sub-agent path is identical when no separate LLM is
458
458
  # configured
@@ -54,8 +54,8 @@ def make_session(
54
54
  config_path=config_path,
55
55
  )
56
56
  # A separate client for sub-agent requests only when a different
57
- # LLM is configured (mirrors gptel-agent-harness-subagent-model /
58
- # -backend); otherwise the sub-agent shares the main client.
57
+ # LLM is configured (mirrors gptel-agent-harness-subagent-model);
58
+ # otherwise the sub-agent shares the main client.
59
59
  subagent_client = None
60
60
  if any(
61
61
  subagent_settings[k] != settings[k] for k in ("base_url", "api_key", "model", "timeout")
@@ -101,7 +101,6 @@ def make_session(
101
101
  project_dir=abs_project,
102
102
  client=client,
103
103
  model=model,
104
- backend=settings["backend"],
105
104
  system_prompt=system_prompt,
106
105
  subagent_system_prompt=subagent_system_prompt,
107
106
  temperature=settings["temperature"],
@@ -176,7 +175,7 @@ def cmd_config(args: argparse.Namespace) -> int:
176
175
  print(f"config file: {path}")
177
176
  if not path.exists():
178
177
  print("(file does not exist yet — run `python-agent-harness config --init` to create it)")
179
- for key in ("base_url", "model", "backend"):
178
+ for key in ("base_url", "model"):
180
179
  print(f"{key}: {settings[key]}")
181
180
  print(f"api_key: {config.mask_secret(settings['api_key'])}")
182
181
  print(f"temperature: {settings['temperature']}")
@@ -168,7 +168,6 @@ DEFAULT_LLM: dict = {
168
168
  "base_url": "https://api.openai.com/v1",
169
169
  "api_key": None,
170
170
  "model": "gpt-5-mini",
171
- "backend": "OpenAI-compatible",
172
171
  "temperature": TEMPERATURE,
173
172
  "max_tokens": MAX_TOKENS,
174
173
  "timeout": 600.0,
@@ -182,8 +181,7 @@ DEFAULT_PATHS: dict = {
182
181
  }
183
182
 
184
183
  # Sub-agent LLM overrides: every key defaults to None, meaning "inherit
185
- # the main LLM setting" (mirrors gptel-agent-harness-subagent-model /
186
- # -backend). Only the keys the user actually sets differ from the main
184
+ # the main LLM setting" (mirrors gptel-agent-harness-subagent-model).
187
185
  # agent's LLM. ``profile`` references a named profile from the
188
186
  # ``models`` section: its settings are applied on top of any explicit
189
187
  # subagent_llm keys (profile wins), and unset keys still inherit the
@@ -193,7 +191,6 @@ DEFAULT_SUBAGENT_LLM: dict = {
193
191
  "base_url": None,
194
192
  "api_key": None,
195
193
  "model": None,
196
- "backend": None,
197
194
  "temperature": None,
198
195
  "max_tokens": None,
199
196
  "timeout": None,
@@ -264,14 +261,12 @@ _ENV_OVERRIDES = {
264
261
  "base_url": "OPENAI_BASE_URL",
265
262
  "api_key": "OPENAI_API_KEY",
266
263
  "model": "OPENAI_MODEL",
267
- "backend": "OPENAI_BACKEND",
268
264
  }
269
265
 
270
266
  _SUBAGENT_ENV_OVERRIDES = {
271
267
  "base_url": "OPENAI_SUBAGENT_BASE_URL",
272
268
  "api_key": "OPENAI_SUBAGENT_API_KEY",
273
269
  "model": "OPENAI_SUBAGENT_MODEL",
274
- "backend": "OPENAI_SUBAGENT_BACKEND",
275
270
  }
276
271
 
277
272
 
@@ -320,7 +315,6 @@ def load_llm_config(path: str | os.PathLike | None = None) -> dict:
320
315
  "base_url",
321
316
  "api_key",
322
317
  "model",
323
- "backend",
324
318
  "temperature",
325
319
  "max_tokens",
326
320
  "timeout",
@@ -342,7 +336,7 @@ def load_subagent_llm_config(
342
336
  ) -> dict:
343
337
  """Resolve sub-agent LLM settings; unset keys inherit ``main``.
344
338
 
345
- Mirrors gptel-agent-harness-subagent-model/-backend: sub-agents
339
+ Mirrors gptel-agent-harness-subagent-model: sub-agents
346
340
  (the Agent tool) use their own LLM when configured, otherwise the
347
341
  main agent's. Precedence: ``main`` settings < config file
348
342
  ``subagent_llm`` object < referenced ``models`` profile (when
@@ -379,7 +373,6 @@ def load_subagent_llm_config(
379
373
  "base_url",
380
374
  "api_key",
381
375
  "model",
382
- "backend",
383
376
  "temperature",
384
377
  "max_tokens",
385
378
  "timeout",
@@ -507,6 +500,10 @@ def load_context_windows_config(
507
500
  raise ValueError(
508
501
  f"config file {_config_path(path)}: context_windows.{pattern} must be an integer"
509
502
  )
503
+ if size <= 0:
504
+ raise ValueError(
505
+ f"config file {_config_path(path)}: context_windows.{pattern} must be positive"
506
+ )
510
507
  entries.append((pattern, size))
511
508
  return entries
512
509
 
@@ -107,7 +107,6 @@ class SessionPersistence:
107
107
  self,
108
108
  project_dir: str,
109
109
  model: str,
110
- backend: str,
111
110
  system_prompt: str | None = None,
112
111
  temperature: float | None = None,
113
112
  max_tokens: int | None = None,
@@ -116,7 +115,6 @@ class SessionPersistence:
116
115
  ) -> None:
117
116
  self.project_dir = project_dir
118
117
  self.model = model
119
- self.backend = backend
120
118
  self.system_prompt = system_prompt
121
119
  self.temperature = temperature
122
120
  self.max_tokens = max_tokens
@@ -173,7 +171,6 @@ class SessionPersistence:
173
171
  pairs = [
174
172
  ("python-agent-harness--project-dir", self.project_dir),
175
173
  ("gptel-model", self.model),
176
- ("gptel--backend-name", self.backend),
177
174
  ("gptel-system-prompt", self.system_prompt),
178
175
  ("gptel-temperature", self.temperature),
179
176
  ("gptel-max-tokens", self.max_tokens),
@@ -71,7 +71,6 @@ class Session:
71
71
  project_dir: str,
72
72
  client: Client,
73
73
  model: str,
74
- backend: str = "OpenAI-compatible",
75
74
  system_prompt: str | None = None,
76
75
  subagent_system_prompt: str | None = None,
77
76
  temperature: float = config.TEMPERATURE,
@@ -96,7 +95,6 @@ class Session:
96
95
  self.project_dir = project_dir
97
96
  self.client = client
98
97
  self.model = model
99
- self.backend = backend
100
98
  self.system_prompt = system_prompt
101
99
  self.subagent_system_prompt = subagent_system_prompt
102
100
  self.temperature = temperature
@@ -113,7 +111,7 @@ class Session:
113
111
  # Sub-agent LLM: a dedicated client (base_url/api_key/model/
114
112
  # timeout) and per-request options when a different LLM is
115
113
  # configured for sub-agents (mirrors gptel-agent-harness-
116
- # subagent-model/-backend); every unset option inherits the
114
+ # subagent-model); every unset option inherits the
117
115
  # main agent's value. The sub-agent loop never uses this
118
116
  # client directly — each Agent tool invocation clones it
119
117
  # (see run_subagent) so concurrent sub-agents never share a
@@ -161,7 +159,6 @@ class Session:
161
159
  self.store = SessionPersistence(
162
160
  project_dir=project_dir,
163
161
  model=model,
164
- backend=backend,
165
162
  system_prompt=system_prompt,
166
163
  temperature=temperature,
167
164
  max_tokens=max_tokens,
@@ -659,7 +656,6 @@ class Session:
659
656
  "base_url": self.client.base_url,
660
657
  "api_key": self.client.api_key,
661
658
  "model": self.model,
662
- "backend": self.backend,
663
659
  "temperature": self.temperature,
664
660
  "max_tokens": self.max_tokens,
665
661
  "timeout": self.client.timeout,
@@ -673,7 +669,6 @@ class Session:
673
669
  "base_url",
674
670
  "api_key",
675
671
  "model",
676
- "backend",
677
672
  "temperature",
678
673
  "max_tokens",
679
674
  "timeout",
@@ -690,8 +685,6 @@ class Session:
690
685
  # the calibration factor is tokenizer-specific: a factor tuned
691
686
  # to the previous model must not skew estimates for the new one
692
687
  self.calibrator.reset()
693
- self.backend = merged["backend"]
694
- self.store.backend = merged["backend"]
695
688
  self.temperature = merged["temperature"]
696
689
  self.max_tokens = merged["max_tokens"]
697
690
  if hasattr(self.client, "set_timeout"):
@@ -10,6 +10,8 @@ from .bash import Bash
10
10
  from .edit import Edit
11
11
  from .edit_mac import EditMac
12
12
  from .filesystem import GlobTool, Grep, Insert, Mkdir, Read, Write
13
+ from .glob_mac import GlobMac
14
+ from .grep_mac import GrepMac
13
15
  from .mcp import MCPTool, mcp_tools_from_manager, normalize_mcp_result
14
16
  from .planexit import PlanExit
15
17
  from .question import Question
@@ -25,8 +27,10 @@ __all__ = [
25
27
  "Bash",
26
28
  "Edit",
27
29
  "EditMac",
30
+ "GlobMac",
28
31
  "GlobTool",
29
32
  "Grep",
33
+ "GrepMac",
30
34
  "Insert",
31
35
  "MCPTool",
32
36
  "Mkdir",
@@ -43,15 +47,18 @@ __all__ = [
43
47
 
44
48
  def default_registry() -> Registry:
45
49
  reg = Registry()
46
- # macOS's BSD patch rejects well-formed hunks that GNU patch accepts,
47
- # so macOS uses EditMac (pure-Python diff applier); Linux keeps the
48
- # patch-binary Edit. Both register under the name "Edit".
49
- edit_tool = EditMac() if sys.platform == "darwin" else Edit()
50
+ # macOS lacks GNU `patch`, `tree`, and PCRE-enabled git use
51
+ # pure-Python / POSIX-compatible Mac variants that register under
52
+ # the same tool names so callers are platform-independent.
53
+ _mac = sys.platform == "darwin"
54
+ edit_tool = EditMac() if _mac else Edit()
55
+ glob_tool = GlobMac() if _mac else GlobTool()
56
+ grep_tool = GrepMac() if _mac else Grep()
50
57
  for tool in (
51
58
  AgentTool(),
52
59
  TodoWrite(),
53
- GlobTool(),
54
- Grep(),
60
+ glob_tool,
61
+ grep_tool,
55
62
  Read(),
56
63
  Insert(),
57
64
  edit_tool,
@@ -6,7 +6,7 @@ sub-agent loop, delivering the result string when it finishes — a
6
6
  long-running sub-agent never blocks the parent's sequential tool loop.
7
7
 
8
8
  Sub-agents run the same agent loop with a fresh loop instance; their
9
- backend/model can be overridden (see config). Results flow back to the
9
+ model can be overridden (see config). Results flow back to the
10
10
  parent as a single tool result string. Errors are contained: an
11
11
  unexpected sub-agent response becomes an error string fed to the parent,
12
12
  never a crash.
@@ -148,7 +148,9 @@ def _git_root(path: str) -> str | None:
148
148
  # isort: off
149
149
  from .edit import Edit, _fix_patch_headers, _strip_diff_fence # noqa: E402
150
150
  from .glob import GlobTool, _git_glob_results # noqa: E402
151
+ from .glob_mac import GlobMac # noqa: E402
151
152
  from .grep import Grep, _grep_out # noqa: E402
153
+ from .grep_mac import GrepMac # noqa: E402
152
154
  from .insert import Insert # noqa: E402
153
155
  from .mkdir import Mkdir # noqa: E402
154
156
  from .read import Read # noqa: E402
@@ -157,8 +159,10 @@ from .write import Write # noqa: E402
157
159
 
158
160
  __all__ = [
159
161
  "Edit",
162
+ "GlobMac",
160
163
  "GlobTool",
161
164
  "Grep",
165
+ "GrepMac",
162
166
  "Insert",
163
167
  "Mkdir",
164
168
  "Read",
@@ -0,0 +1,97 @@
1
+ """macOS Glob tool: ``git ls-files`` inside git repos, ``pathlib`` outside.
2
+
3
+ Apple ships neither GNU ``tree`` nor a compatible equivalent, so the
4
+ non-git fallback in :class:`GlobTool` (which shells out to ``tree``)
5
+ fails on a stock macOS install. ``GlobMac`` replaces that fallback
6
+ with :meth:`pathlib.Path.rglob`, which is pure-Python, requires no
7
+ external binary, and produces identical results.
8
+
9
+ Only the non-git fallback differs; the git path, the tool name, and
10
+ the result format are inherited unchanged so callers, the tool
11
+ registry, and the plan-mode write guard are platform-independent.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import fnmatch
17
+ import os
18
+ from pathlib import Path
19
+
20
+ from .base import ToolContext
21
+ from .filesystem import _natnump, _spool
22
+ from .glob import GlobTool
23
+
24
+
25
+ class GlobMac(GlobTool):
26
+ """Glob with a pure-Python non-git fallback for macOS."""
27
+
28
+ def _tree_fallback(self, pattern: str, base: str, depth: object) -> str:
29
+ """Walk *base* with :func:`pathlib.Path.rglob` instead of ``tree``.
30
+
31
+ Results are absolute paths sorted by modification time (newest
32
+ first), matching the ``tree --sort=mtime`` order of the Linux
33
+ fallback. Hidden directories (``.git``, etc.) are skipped.
34
+ """
35
+ root = Path(base)
36
+ matches: list[tuple[float, str]] = []
37
+ if _natnump(depth):
38
+ # Depth-limited: walk manually so we can count levels.
39
+ for dirpath, dirnames, filenames in os.walk(base):
40
+ # Skip hidden directories (mirrors tree's -I .git and
41
+ # the general expectation that dotfiles are excluded).
42
+ dirnames[:] = [d for d in dirnames if not d.startswith(".")]
43
+ rel = os.path.relpath(dirpath, base)
44
+ level = 0 if rel == "." else rel.count(os.sep) + 1
45
+ if level >= depth:
46
+ dirnames.clear()
47
+ continue
48
+ for name in filenames:
49
+ if fnmatch.fnmatch(name.lower(), pattern.lower()):
50
+ full = os.path.join(dirpath, name)
51
+ try:
52
+ mtime = os.path.getmtime(full)
53
+ except OSError:
54
+ mtime = 0.0
55
+ matches.append((mtime, full))
56
+ else:
57
+ # Unlimited depth: rglob is simpler.
58
+ for p in root.rglob("*"):
59
+ if any(part.startswith(".") for part in p.relative_to(root).parts):
60
+ continue
61
+ if p.is_file() and fnmatch.fnmatch(p.name.lower(), pattern.lower()):
62
+ try:
63
+ mtime = p.stat().st_mtime
64
+ except OSError:
65
+ mtime = 0.0
66
+ matches.append((mtime, str(p)))
67
+
68
+ # Sort newest-first (like tree --sort=mtime).
69
+ matches.sort(key=lambda t: t[0], reverse=True)
70
+ out = "\n".join(path for _, path in matches)
71
+ if not out:
72
+ return ""
73
+ return _spool(out + "\n", "glob")
74
+
75
+ def run(self, args: dict, ctx: ToolContext) -> str:
76
+ pattern = args.get("pattern") or ""
77
+ if not pattern:
78
+ return "Error: pattern must not be empty"
79
+ path = args.get("path")
80
+ if path:
81
+ if not (os.path.isdir(path) and os.access(path, os.R_OK)):
82
+ return f"Error: path {path} is not readable"
83
+ else:
84
+ path = ctx.cwd
85
+ base = os.path.realpath(path)
86
+ depth = args.get("depth")
87
+
88
+ from .filesystem import _git_root
89
+
90
+ git_root = _git_root(base)
91
+
92
+ if git_root:
93
+ # The git path is identical to the parent — delegate.
94
+ return super().run(args, ctx)
95
+
96
+ # Non-git: pure-Python fallback instead of `tree`.
97
+ return self._tree_fallback(pattern, base, depth)
@@ -0,0 +1,92 @@
1
+ """macOS Grep tool: ``git grep -E`` instead of ``git grep -P``.
2
+
3
+ Apple's default Git (installed via Xcode Command Line Tools) is built
4
+ without PCRE support, so ``git grep -P`` (Perl-compatible regex) fails
5
+ with::
6
+
7
+ fatal: cannot use Perl-compatible regexes when not compiled with USE_LIBPCRE
8
+
9
+ The Linux :class:`Grep` tool uses ``-P`` as the first-choice backend;
10
+ when that fails on macOS the fallback chain (``rg`` → ``grep``) still
11
+ works, but:
12
+
13
+ - ``rg`` (ripgrep) is not installed by default on macOS.
14
+ - BSD ``grep`` supports only POSIX ERE, losing Perl features
15
+ (``\\d``, ``\\w``, lookaheads).
16
+ - Two unnecessary failed attempts add latency before reaching a
17
+ working backend.
18
+
19
+ ``GrepMac`` replaces ``-P`` with ``-E`` (POSIX extended regex) in the
20
+ ``git grep`` command, so the git path — the fastest and most common
21
+ backend — works on stock macOS. The rest of the fallback chain (rg,
22
+ grep) is inherited unchanged.
23
+
24
+ Only the ``git grep`` regex flag differs; the tool name and result
25
+ format are inherited so callers, the tool registry, and the plan-mode
26
+ write guard are platform-independent.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import os
32
+ import subprocess
33
+
34
+ from .base import ToolContext
35
+ from .filesystem import _git_root
36
+ from .grep import Grep, _grep_out
37
+
38
+
39
+ class GrepMac(Grep):
40
+ """Grep with ``git grep -E`` for macOS (no PCRE dependency)."""
41
+
42
+ def run(self, args: dict, ctx: ToolContext) -> str:
43
+ regex = args["regex"]
44
+ path = os.path.realpath(args["path"])
45
+ if not os.path.isdir(path) and not os.path.isfile(path):
46
+ return f"Error: path {args['path']} is not readable"
47
+ glob = args.get("glob")
48
+ context = args.get("context_lines")
49
+ if context is not None:
50
+ context = max(0, min(15, int(context)))
51
+
52
+ git_root = _git_root(path) if os.path.isdir(path) else None
53
+ if git_root:
54
+ rel = os.path.relpath(path, git_root)
55
+ pathspec = rel
56
+ if glob and os.path.isdir(path):
57
+ pathspec = os.path.join(rel, glob).replace(os.sep, "/")
58
+ # -E (extended regex) instead of -P (Perl regex): Apple's
59
+ # Git is built without PCRE, so -P fails on stock macOS.
60
+ cmd = [
61
+ "git",
62
+ "grep",
63
+ "--line-number",
64
+ "--no-color",
65
+ "--max-count=1000",
66
+ "--untracked",
67
+ "-E",
68
+ "-e",
69
+ regex,
70
+ "--",
71
+ pathspec,
72
+ ]
73
+ if context:
74
+ cmd = cmd[:3] + [f"-C{context}"] + cmd[3:]
75
+ try:
76
+ proc = subprocess.run(
77
+ cmd,
78
+ cwd=git_root,
79
+ capture_output=True,
80
+ text=True,
81
+ encoding="utf-8",
82
+ errors="replace",
83
+ timeout=60,
84
+ )
85
+ except (OSError, subprocess.TimeoutExpired):
86
+ proc = None
87
+ if proc is not None and proc.returncode in (0, 1):
88
+ return _grep_out(proc, "git")
89
+
90
+ # Non-git or git grep failed: delegate to the parent's rg/grep
91
+ # fallback chain (identical on both platforms).
92
+ return super().run(args, ctx)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.4.3
3
+ Version: 1.5.4.5
4
4
  Summary: A lightweight, hackable mini-OpenCode written in Python.
5
5
  Author: huming
6
6
  License-Expression: MIT
@@ -186,13 +186,13 @@ All LLM settings live in a single JSON configuration file. Environment variables
186
186
 
187
187
  ### Configuration options
188
188
 
189
- - **`llm`** — main LLM configuration. Optional keys include `backend`, `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
189
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
190
190
  - **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. `default` restores the main LLM configuration.
191
191
  - **`context_windows`** — optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g., `deepseek-v4`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
192
192
  - **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
193
193
  - **`paths.context_path` / `paths.skill_path`** — locations from which to load context files and skills. When unset, the project-local `<project>/contexts` and `<project>/skills` directories are used.
194
194
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
195
- - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`, `_BACKEND`).
195
+ - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
196
196
  - **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
197
197
  - **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
198
198
 
@@ -49,7 +49,9 @@ python_agent_harness/tools/edit.py
49
49
  python_agent_harness/tools/edit_mac.py
50
50
  python_agent_harness/tools/filesystem.py
51
51
  python_agent_harness/tools/glob.py
52
+ python_agent_harness/tools/glob_mac.py
52
53
  python_agent_harness/tools/grep.py
54
+ python_agent_harness/tools/grep_mac.py
53
55
  python_agent_harness/tools/insert.py
54
56
  python_agent_harness/tools/mcp.py
55
57
  python_agent_harness/tools/mkdir.py
@@ -20,11 +20,9 @@ class TestMakeSessionPromptDefaults(unittest.TestCase):
20
20
  "OPENAI_BASE_URL",
21
21
  "OPENAI_API_KEY",
22
22
  "OPENAI_MODEL",
23
- "OPENAI_BACKEND",
24
23
  "OPENAI_SUBAGENT_BASE_URL",
25
24
  "OPENAI_SUBAGENT_API_KEY",
26
25
  "OPENAI_SUBAGENT_MODEL",
27
- "OPENAI_SUBAGENT_BACKEND",
28
26
  ]
29
27
 
30
28
  def setUp(self):
@@ -178,7 +176,7 @@ class TestMakeSessionPromptDefaults(unittest.TestCase):
178
176
 
179
177
  def test_subagent_llm_inherits_main_client_by_default(self):
180
178
  """Without subagent_llm overrides, the sub-agent shares the
181
- main client (mirrors gptel-agent-harness: nil backend/model
179
+ main client (mirrors gptel-agent-harness: nil model overrides
182
180
  inherit the main agent's)."""
183
181
  session = cli.make_session(self._tmp.name, config_path=self._config_path)
184
182
  try:
@@ -11,12 +11,10 @@ ENV_KEYS = [
11
11
  "OPENAI_BASE_URL",
12
12
  "OPENAI_API_KEY",
13
13
  "OPENAI_MODEL",
14
- "OPENAI_BACKEND",
15
14
  "PYTHON_AGENT_HARNESS_CONFIG",
16
15
  "OPENAI_SUBAGENT_BASE_URL",
17
16
  "OPENAI_SUBAGENT_API_KEY",
18
17
  "OPENAI_SUBAGENT_MODEL",
19
- "OPENAI_SUBAGENT_BACKEND",
20
18
  ]
21
19
 
22
20
 
@@ -49,7 +47,6 @@ class TestConfigFile(unittest.TestCase):
49
47
  '"base_url": "https://api.example.com/v1", '
50
48
  '"api_key": "sk-test", '
51
49
  '"model": "custom-model", '
52
- '"backend": "Example", '
53
50
  '"temperature": 0.2, '
54
51
  '"max_tokens": 2048, '
55
52
  '"timeout": 30.0, '
@@ -60,7 +57,6 @@ class TestConfigFile(unittest.TestCase):
60
57
  self.assertEqual(settings["base_url"], "https://api.example.com/v1")
61
58
  self.assertEqual(settings["api_key"], "sk-test")
62
59
  self.assertEqual(settings["model"], "custom-model")
63
- self.assertEqual(settings["backend"], "Example")
64
60
  self.assertEqual(settings["temperature"], 0.2)
65
61
  self.assertEqual(settings["max_tokens"], 2048)
66
62
  self.assertEqual(settings["timeout"], 30.0)
@@ -257,6 +253,21 @@ class TestContextWindowsConfig(unittest.TestCase):
257
253
  with self.assertRaises(ValueError):
258
254
  config.load_context_windows_config(p)
259
255
 
256
+ def test_context_windows_zero_rejected(self):
257
+ """A zero context window would cause a division-by-zero at runtime."""
258
+ with tempfile.TemporaryDirectory() as d:
259
+ p = Path(d) / "config.json"
260
+ p.write_text('{"context_windows": {"m": 0}}', encoding="utf-8")
261
+ with self.assertRaises(ValueError):
262
+ config.load_context_windows_config(p)
263
+
264
+ def test_context_windows_negative_rejected(self):
265
+ with tempfile.TemporaryDirectory() as d:
266
+ p = Path(d) / "config.json"
267
+ p.write_text('{"context_windows": {"m": -1}}', encoding="utf-8")
268
+ with self.assertRaises(ValueError):
269
+ config.load_context_windows_config(p)
270
+
260
271
  def test_get_context_window_precedence(self):
261
272
  """Config-file override -> CONTEXT_WINDOWS -> default."""
262
273
  with tempfile.TemporaryDirectory() as d:
@@ -327,7 +338,7 @@ class TestContextWindowsConfig(unittest.TestCase):
327
338
 
328
339
  class TestSubagentLlmConfig(unittest.TestCase):
329
340
  """Sub-agent LLM settings: unset keys inherit the main settings
330
- (mirrors gptel-agent-harness-subagent-model/-backend)."""
341
+ (mirrors gptel-agent-harness-subagent-model)."""
331
342
 
332
343
  def setUp(self):
333
344
  self._saved = {k: os.environ.get(k) for k in ENV_KEYS}
@@ -345,7 +356,6 @@ class TestSubagentLlmConfig(unittest.TestCase):
345
356
  "base_url": "https://api.main.example/v1",
346
357
  "api_key": "sk-main",
347
358
  "model": "big-model",
348
- "backend": "Main",
349
359
  "temperature": 0.0,
350
360
  "max_tokens": None,
351
361
  "timeout": 600.0,