python-agent-harness 1.5.4.2__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.
- {python_agent_harness-1.5.4.2/python_agent_harness.egg-info → python_agent_harness-1.5.4.5}/PKG-INFO +4 -4
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/README.md +3 -3
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/pyproject.toml +1 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/agent.py +1 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/cli.py +3 -4
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/config.py +6 -9
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/manager.py +5 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/persistence.py +0 -3
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/planmode.py +5 -2
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/session.py +1 -8
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/__init__.py +20 -4
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/agent_tool.py +1 -1
- python_agent_harness-1.5.4.5/python_agent_harness/tools/diffapply.py +246 -0
- python_agent_harness-1.5.4.5/python_agent_harness/tools/edit_mac.py +56 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/filesystem.py +4 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/glob.py +4 -1
- python_agent_harness-1.5.4.5/python_agent_harness/tools/glob_mac.py +97 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/grep.py +1 -1
- python_agent_harness-1.5.4.5/python_agent_harness/tools/grep_mac.py +92 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5/python_agent_harness.egg-info}/PKG-INFO +4 -4
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/SOURCES.txt +4 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_agents_md.py +5 -5
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_cli.py +1 -3
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_config.py +16 -6
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_filesystem.py +436 -13
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_invariants.py +26 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_persistence.py +12 -15
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_prompts.py +2 -2
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_scenarios.py +0 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_session.py +16 -10
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_subagent.py +0 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_subagent_isolation.py +1 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/LICENSE +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/client.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/commands.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/setup.cfg +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_client.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_token_estimator.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/tests/test_tools_misc.py +0 -0
{python_agent_harness-1.5.4.2/python_agent_harness.egg-info → python_agent_harness-1.5.4.5}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.4.
|
|
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 `
|
|
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
|
|
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
|
|
|
@@ -248,7 +248,7 @@ python_agent_harness/
|
|
|
248
248
|
|
|
249
249
|
## Development
|
|
250
250
|
|
|
251
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13.
|
|
251
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and macOS.
|
|
252
252
|
|
|
253
253
|
```sh
|
|
254
254
|
make test # unit tests
|
|
@@ -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 `
|
|
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
|
|
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
|
|
|
@@ -225,7 +225,7 @@ python_agent_harness/
|
|
|
225
225
|
|
|
226
226
|
## Development
|
|
227
227
|
|
|
228
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13.
|
|
228
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and macOS.
|
|
229
229
|
|
|
230
230
|
```sh
|
|
231
231
|
make test # unit tests
|
|
@@ -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
|
|
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
|
-
#
|
|
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"
|
|
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']}")
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/config.py
RENAMED
|
@@ -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
|
|
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
|
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/mcp/manager.py
RENAMED
|
@@ -23,6 +23,7 @@ SDK/connection errors that the tool adapter turns into error strings.
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import asyncio
|
|
26
|
+
import concurrent.futures
|
|
26
27
|
import contextlib
|
|
27
28
|
import threading
|
|
28
29
|
import time
|
|
@@ -102,7 +103,10 @@ class _LoopThread:
|
|
|
102
103
|
while True:
|
|
103
104
|
try:
|
|
104
105
|
exc = future.exception(timeout=0.1)
|
|
105
|
-
|
|
106
|
+
# concurrent.futures.TimeoutError is the builtin TimeoutError on
|
|
107
|
+
# 3.11+ but a distinct class on 3.10 — catch the futures one so
|
|
108
|
+
# the poll-timeout branch works on every supported Python.
|
|
109
|
+
except concurrent.futures.TimeoutError:
|
|
106
110
|
# future still running: enforce deadline / cancellation
|
|
107
111
|
if deadline is not None and time.monotonic() >= deadline:
|
|
108
112
|
future.cancel()
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/persistence.py
RENAMED
|
@@ -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),
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/planmode.py
RENAMED
|
@@ -64,8 +64,11 @@ class PlanMode:
|
|
|
64
64
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
65
65
|
if not os.path.exists(path):
|
|
66
66
|
Path(path).write_text("", encoding="utf-8")
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
# Store the canonical path so the plan-mode write guard (which
|
|
68
|
+
# compares against realpath'd tool targets) matches — on macOS
|
|
69
|
+
# TMPDIR (/var) is a symlink to /private/var.
|
|
70
|
+
self.plan_file = os.path.realpath(path)
|
|
71
|
+
return self.plan_file
|
|
69
72
|
|
|
70
73
|
def cleanup_plan_file(self) -> None:
|
|
71
74
|
if not self.plan_file:
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/session.py
RENAMED
|
@@ -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
|
|
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"):
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.5}/python_agent_harness/tools/__init__.py
RENAMED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import sys
|
|
6
|
+
|
|
5
7
|
from .agent_tool import AgentTool
|
|
6
8
|
from .base import PendingToolResult, Registry, Tool, ToolContext
|
|
7
9
|
from .bash import Bash
|
|
8
|
-
from .
|
|
10
|
+
from .edit import Edit
|
|
11
|
+
from .edit_mac import EditMac
|
|
12
|
+
from .filesystem import GlobTool, Grep, Insert, Mkdir, Read, Write
|
|
13
|
+
from .glob_mac import GlobMac
|
|
14
|
+
from .grep_mac import GrepMac
|
|
9
15
|
from .mcp import MCPTool, mcp_tools_from_manager, normalize_mcp_result
|
|
10
16
|
from .planexit import PlanExit
|
|
11
17
|
from .question import Question
|
|
@@ -20,8 +26,11 @@ __all__ = [
|
|
|
20
26
|
"AgentTool",
|
|
21
27
|
"Bash",
|
|
22
28
|
"Edit",
|
|
29
|
+
"EditMac",
|
|
30
|
+
"GlobMac",
|
|
23
31
|
"GlobTool",
|
|
24
32
|
"Grep",
|
|
33
|
+
"GrepMac",
|
|
25
34
|
"Insert",
|
|
26
35
|
"MCPTool",
|
|
27
36
|
"Mkdir",
|
|
@@ -38,14 +47,21 @@ __all__ = [
|
|
|
38
47
|
|
|
39
48
|
def default_registry() -> Registry:
|
|
40
49
|
reg = Registry()
|
|
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()
|
|
41
57
|
for tool in (
|
|
42
58
|
AgentTool(),
|
|
43
59
|
TodoWrite(),
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
glob_tool,
|
|
61
|
+
grep_tool,
|
|
46
62
|
Read(),
|
|
47
63
|
Insert(),
|
|
48
|
-
|
|
64
|
+
edit_tool,
|
|
49
65
|
Write(),
|
|
50
66
|
Mkdir(),
|
|
51
67
|
Bash(),
|
|
@@ -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
|
-
|
|
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.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Pure-Python unified diff applier (macOS Edit backend).
|
|
2
|
+
|
|
3
|
+
macOS ships Apple's old BSD ``/usr/bin/patch`` (patch 2.0-12u11-Apple),
|
|
4
|
+
which rejects well-formed hunks that GNU patch accepts: asymmetric
|
|
5
|
+
context (leading but no trailing context, or vice versa), hunks whose
|
|
6
|
+
last body line is a ``---``-rendered content line, etc. Rather than
|
|
7
|
+
rewriting every diff into a shape that specific patch binary likes, the
|
|
8
|
+
macOS Edit tool applies diffs here, in Python:
|
|
9
|
+
|
|
10
|
+
- the diff is parsed STRUCTURALLY, so hunk-header counts are advisory
|
|
11
|
+
(they are recounted from the body, like ``_fix_patch_headers``) and
|
|
12
|
+
``---``/``+++`` content lines are only file headers when followed by
|
|
13
|
+
their ``+++``/``---`` partner;
|
|
14
|
+
- hunks are matched against the file by content with fuzz (context
|
|
15
|
+
lines may mismatch up to ``FUZZ`` times, removed lines must match
|
|
16
|
+
exactly), preferring the position the header claims;
|
|
17
|
+
- hunks are applied bottom-up so earlier positions stay valid;
|
|
18
|
+
- files are written only when every hunk of the section matched.
|
|
19
|
+
|
|
20
|
+
The applier is platform-independent, so its behavior is covered by
|
|
21
|
+
tests on every OS.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
|
|
29
|
+
_HUNK_HEADER_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
|
30
|
+
_FILE_OLD_RE = re.compile(r"^---[ \t]")
|
|
31
|
+
_FILE_NEW_RE = re.compile(r"^\+\+\+[ \t]")
|
|
32
|
+
_HUNK_BOUNDARY_RE = re.compile(r"^@@ ")
|
|
33
|
+
|
|
34
|
+
# Maximum number of context lines that may mismatch when anchoring a
|
|
35
|
+
# hunk (removed lines must always match exactly).
|
|
36
|
+
FUZZ = 3
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _Hunk:
|
|
40
|
+
__slots__ = ("old_start", "body")
|
|
41
|
+
|
|
42
|
+
def __init__(self, old_start: int, body: list[tuple[str, str, bool]]) -> None:
|
|
43
|
+
# body entries: (kind, content, no_newline); kind in ' ', '-', '+'.
|
|
44
|
+
self.old_start = old_start
|
|
45
|
+
self.body = body
|
|
46
|
+
|
|
47
|
+
def old_seq(self) -> list[tuple[str, str]]:
|
|
48
|
+
"""(kind, content-without-newline) for the lines that must exist
|
|
49
|
+
in the old file (' ' context and '-' removed lines)."""
|
|
50
|
+
return [
|
|
51
|
+
(kind, content.rstrip("\n")) for kind, content, _ in self.body if kind in (" ", "-")
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _Section:
|
|
56
|
+
__slots__ = ("new_path", "hunks")
|
|
57
|
+
|
|
58
|
+
def __init__(self, new_path: str, hunks: list[_Hunk]) -> None:
|
|
59
|
+
self.new_path = new_path
|
|
60
|
+
self.hunks = hunks
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _section_header_at(lines: list[str], idx: int) -> bool:
|
|
64
|
+
"""Whether lines[idx] begins a file section: a ``--- path`` line
|
|
65
|
+
immediately followed by a ``+++ path`` line. Removed/added content
|
|
66
|
+
lines rendered ``---``/``+++`` inside a hunk body are only mistaken
|
|
67
|
+
for headers when the pair shape matches, so the trailing ``+++``
|
|
68
|
+
partner is required (mirrors ``_starts_file_section``)."""
|
|
69
|
+
if idx + 1 >= len(lines):
|
|
70
|
+
return False
|
|
71
|
+
return bool(_FILE_OLD_RE.match(lines[idx]) and _FILE_NEW_RE.match(lines[idx + 1]))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse(diff_text: str) -> list[_Section]:
|
|
75
|
+
"""Parse a unified diff into file sections (paths + hunks)."""
|
|
76
|
+
lines = diff_text.splitlines(keepends=True)
|
|
77
|
+
sections: list[_Section] = []
|
|
78
|
+
i = 0
|
|
79
|
+
n = len(lines)
|
|
80
|
+
while i < n:
|
|
81
|
+
if not _section_header_at(lines, i):
|
|
82
|
+
i += 1
|
|
83
|
+
continue
|
|
84
|
+
new_path = lines[i + 1][len("+++") :].strip()
|
|
85
|
+
i += 2
|
|
86
|
+
hunks: list[_Hunk] = []
|
|
87
|
+
while i < n:
|
|
88
|
+
m = _HUNK_HEADER_RE.match(lines[i])
|
|
89
|
+
if m:
|
|
90
|
+
old_start = int(m.group(1))
|
|
91
|
+
body: list[tuple[str, str, bool]] = []
|
|
92
|
+
i += 1
|
|
93
|
+
while (
|
|
94
|
+
i < n
|
|
95
|
+
and not _HUNK_BOUNDARY_RE.match(lines[i])
|
|
96
|
+
and not _section_header_at(lines, i)
|
|
97
|
+
):
|
|
98
|
+
line = lines[i]
|
|
99
|
+
if line.startswith("\\"): # ""
|
|
100
|
+
if body and body[-1][0] in (" ", "-", "+"):
|
|
101
|
+
kind, content, _ = body[-1]
|
|
102
|
+
body[-1] = (kind, content, True)
|
|
103
|
+
elif line[:1] in (" ", "-", "+"):
|
|
104
|
+
body.append((line[0], line[1:], False))
|
|
105
|
+
i += 1
|
|
106
|
+
hunks.append(_Hunk(old_start, body))
|
|
107
|
+
continue
|
|
108
|
+
if _section_header_at(lines, i):
|
|
109
|
+
break
|
|
110
|
+
i += 1 # stray line (e.g. "diff --git", "index ...") — skip
|
|
111
|
+
sections.append(_Section(new_path, hunks))
|
|
112
|
+
return sections
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _candidate_positions(start: int, limit: int) -> list[int]:
|
|
116
|
+
"""Match offsets around *start* (0-based), closest first, within
|
|
117
|
+
``FUZZ`` lines and bounded by [0, *limit*]."""
|
|
118
|
+
out: list[int] = []
|
|
119
|
+
seen: set[int] = set()
|
|
120
|
+
for d in range(FUZZ + 1):
|
|
121
|
+
for p in (start - d, start + d):
|
|
122
|
+
if 0 <= p <= limit and p not in seen:
|
|
123
|
+
seen.add(p)
|
|
124
|
+
out.append(p)
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _match_hunk(hunk: _Hunk, file_lines: list[str]) -> int | None:
|
|
129
|
+
"""Find the 0-based position where the hunk's old lines match.
|
|
130
|
+
|
|
131
|
+
Removed lines must match exactly; up to ``FUZZ`` context lines may
|
|
132
|
+
mismatch (patch-style fuzz). Pure-insertion hunks (no old lines)
|
|
133
|
+
anchor at ``old_start - 1``.
|
|
134
|
+
"""
|
|
135
|
+
old_seq = hunk.old_seq()
|
|
136
|
+
if not old_seq:
|
|
137
|
+
p = hunk.old_start - 1
|
|
138
|
+
return max(0, min(p, len(file_lines)))
|
|
139
|
+
start = hunk.old_start - 1
|
|
140
|
+
limit = len(file_lines) - len(old_seq)
|
|
141
|
+
for p in _candidate_positions(start, limit):
|
|
142
|
+
mismatches = 0
|
|
143
|
+
ok = True
|
|
144
|
+
for i, (kind, content) in enumerate(old_seq):
|
|
145
|
+
file_content = file_lines[p + i].rstrip("\n")
|
|
146
|
+
if kind == "-":
|
|
147
|
+
if file_content != content:
|
|
148
|
+
ok = False
|
|
149
|
+
break
|
|
150
|
+
elif file_content != content:
|
|
151
|
+
mismatches += 1
|
|
152
|
+
if mismatches > FUZZ:
|
|
153
|
+
ok = False
|
|
154
|
+
break
|
|
155
|
+
if ok:
|
|
156
|
+
return p
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _apply_hunk(hunk: _Hunk, pos: int, file_lines: list[str]) -> None:
|
|
161
|
+
"""Replace the matched region with the hunk's new lines in place."""
|
|
162
|
+
old_count = sum(1 for kind, _, _ in hunk.body if kind in (" ", "-"))
|
|
163
|
+
new_block: list[str] = []
|
|
164
|
+
p = pos
|
|
165
|
+
for kind, content, no_newline in hunk.body:
|
|
166
|
+
if kind == " ":
|
|
167
|
+
new_block.append(file_lines[p])
|
|
168
|
+
p += 1
|
|
169
|
+
elif kind == "-":
|
|
170
|
+
p += 1
|
|
171
|
+
else: # "+"
|
|
172
|
+
if no_newline:
|
|
173
|
+
new_block.append(content.rstrip("\n"))
|
|
174
|
+
elif content.endswith("\n"):
|
|
175
|
+
new_block.append(content)
|
|
176
|
+
else:
|
|
177
|
+
new_block.append(content + "\n")
|
|
178
|
+
file_lines[pos : pos + old_count] = new_block
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _resolve_target(new_path: str, cwd: str, fallback_path: str | None) -> str:
|
|
182
|
+
"""Resolve a diff's ``+++ path`` against *cwd*, with the single-file
|
|
183
|
+
*fallback_path* as backup when the name does not match an existing
|
|
184
|
+
file (e.g. the model wrote a bare basename for a deep path)."""
|
|
185
|
+
name = new_path
|
|
186
|
+
for prefix in ("a/", "b/"):
|
|
187
|
+
if name.startswith(prefix):
|
|
188
|
+
name = name[len(prefix) :]
|
|
189
|
+
break
|
|
190
|
+
if os.path.isabs(name):
|
|
191
|
+
return name
|
|
192
|
+
target = os.path.join(cwd, name)
|
|
193
|
+
if not os.path.isfile(target) and fallback_path and os.path.isfile(fallback_path):
|
|
194
|
+
return fallback_path
|
|
195
|
+
return target
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _apply_section(section: _Section, cwd: str, fallback_path: str | None) -> tuple[bool, str]:
|
|
199
|
+
target = _resolve_target(section.new_path, cwd, fallback_path)
|
|
200
|
+
if not os.path.isfile(target):
|
|
201
|
+
return False, f"target file does not exist: {target}"
|
|
202
|
+
try:
|
|
203
|
+
with open(target, encoding="utf-8", errors="replace") as f:
|
|
204
|
+
file_lines = f.readlines()
|
|
205
|
+
except OSError as e:
|
|
206
|
+
return False, f"cannot read {target}: {e}"
|
|
207
|
+
# Match every hunk against the ORIGINAL content first; only write
|
|
208
|
+
# when all matched (a failed hunk leaves the file untouched).
|
|
209
|
+
plan: list[tuple[_Hunk, int]] = []
|
|
210
|
+
for hunk in section.hunks:
|
|
211
|
+
pos = _match_hunk(hunk, file_lines)
|
|
212
|
+
if pos is None:
|
|
213
|
+
return (
|
|
214
|
+
False,
|
|
215
|
+
f"hunk at line {hunk.old_start} failed to match the file content",
|
|
216
|
+
)
|
|
217
|
+
plan.append((hunk, pos))
|
|
218
|
+
new_lines = list(file_lines)
|
|
219
|
+
for hunk, pos in reversed(plan): # bottom-up: earlier positions stay valid
|
|
220
|
+
_apply_hunk(hunk, pos, new_lines)
|
|
221
|
+
try:
|
|
222
|
+
with open(target, "w", encoding="utf-8") as f:
|
|
223
|
+
f.writelines(new_lines)
|
|
224
|
+
except OSError as e:
|
|
225
|
+
return False, f"cannot write {target}: {e}"
|
|
226
|
+
return True, f"patched {target}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def apply_unified_diff(
|
|
230
|
+
diff_text: str, cwd: str, fallback_path: str | None = None
|
|
231
|
+
) -> tuple[bool, str]:
|
|
232
|
+
"""Apply a unified diff to files under *cwd*.
|
|
233
|
+
|
|
234
|
+
Returns ``(ok, message)``; on failure no file is written. Multi-file
|
|
235
|
+
diffs are applied section by section. *fallback_path* is used when a
|
|
236
|
+
section's target cannot be resolved to an existing file (single-file
|
|
237
|
+
mode).
|
|
238
|
+
"""
|
|
239
|
+
sections = _parse(diff_text)
|
|
240
|
+
if not sections:
|
|
241
|
+
return False, "no file sections found in diff"
|
|
242
|
+
for section in sections:
|
|
243
|
+
ok, msg = _apply_section(section, cwd, fallback_path)
|
|
244
|
+
if not ok:
|
|
245
|
+
return False, msg
|
|
246
|
+
return True, f"applied {len(sections)} file section(s)"
|