python-agent-harness 1.5.5.2__tar.gz → 1.5.5.4__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.2/python_agent_harness.egg-info → python_agent_harness-1.5.5.4}/PKG-INFO +3 -2
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/README.md +1 -1
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/pyproject.toml +2 -1
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/cli.py +8 -4
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/client.py +16 -4
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/config.py +14 -4
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/persistence.py +3 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/session.py +8 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/__init__.py +17 -4
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/bash.py +5 -3
- python_agent_harness-1.5.5.4/python_agent_harness/tools/bash_win.py +232 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/diffapply.py +2 -2
- python_agent_harness-1.5.5.4/python_agent_harness/tools/edit_win.py +56 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob.py +2 -2
- python_agent_harness-1.5.5.4/python_agent_harness/tools/glob_win.py +90 -0
- python_agent_harness-1.5.5.4/python_agent_harness/tools/grep_win.py +141 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/commands.py +78 -17
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/input.py +61 -11
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4/python_agent_harness.egg-info}/PKG-INFO +3 -2
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/SOURCES.txt +4 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_client.py +7 -3
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_commands.py +2 -1
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_concurrent_subagents.py +4 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_custom_agents.py +43 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_filesystem.py +346 -18
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_invariants.py +4 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_persistence.py +16 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_planmode.py +5 -1
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_scenarios.py +4 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_session.py +18 -7
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_tools_misc.py +1 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/LICENSE +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/planmode.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/agents/reviewer.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/grep_mac.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/setup.cfg +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_config.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/tests/test_token_estimator.py +0 -0
{python_agent_harness-1.5.5.2/python_agent_harness.egg-info → python_agent_harness-1.5.5.4}/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.4
|
|
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
|
|
@@ -9,6 +9,7 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
9
9
|
Classifier: Environment :: Console
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
13
|
Classifier: Operating System :: POSIX :: Linux
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -276,7 +277,7 @@ python_agent_harness/
|
|
|
276
277
|
|
|
277
278
|
## Development
|
|
278
279
|
|
|
279
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and
|
|
280
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux, MacOS and Windows.
|
|
280
281
|
|
|
281
282
|
```sh
|
|
282
283
|
make test # unit tests
|
|
@@ -243,7 +243,7 @@ python_agent_harness/
|
|
|
243
243
|
|
|
244
244
|
## Development
|
|
245
245
|
|
|
246
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and
|
|
246
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux, MacOS and Windows.
|
|
247
247
|
|
|
248
248
|
```sh
|
|
249
249
|
make test # unit tests
|
|
@@ -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.4"
|
|
8
8
|
description = "A lightweight, hackable mini-OpenCode written in Python."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Huming Chen", email = "chenhuming@gmail.com"}
|
|
@@ -17,6 +17,7 @@ classifiers = [
|
|
|
17
17
|
"Environment :: Console",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: Microsoft :: Windows",
|
|
20
21
|
"Operating System :: POSIX :: Linux",
|
|
21
22
|
"Programming Language :: Python :: 3.11",
|
|
22
23
|
"Programming Language :: Python :: 3.12",
|
|
@@ -9,7 +9,7 @@ sessions, restore and summary/explain — are TUI slash commands only;
|
|
|
9
9
|
they are NOT registered as CLI subcommands.
|
|
10
10
|
|
|
11
11
|
Configuration (LLM etc.) is read from a JSON file, by default
|
|
12
|
-
|
|
12
|
+
from the OS config directory; see `config --init`.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
@@ -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).
|
|
@@ -175,7 +179,7 @@ def cmd_config(args: argparse.Namespace) -> int:
|
|
|
175
179
|
if path.exists() and not args.force:
|
|
176
180
|
print(f"config already exists: {path} (use --force to overwrite)")
|
|
177
181
|
return 1
|
|
178
|
-
template = config.CONFIG_TEMPLATE.format(path=path)
|
|
182
|
+
template = config.CONFIG_TEMPLATE.format(path=str(path).replace("\\", "\\\\"))
|
|
179
183
|
path.write_text(template, encoding="utf-8")
|
|
180
184
|
print(f"wrote config template: {path}")
|
|
181
185
|
return 0
|
|
@@ -250,7 +254,7 @@ def _add_config_arg(parser: argparse.ArgumentParser, suppress: bool = False) ->
|
|
|
250
254
|
"--config",
|
|
251
255
|
metavar="PATH",
|
|
252
256
|
default=argparse.SUPPRESS if suppress else None,
|
|
253
|
-
help="path to config.json (default:
|
|
257
|
+
help="path to config.json (default: OS config dir / python-agent-harness / config.json)",
|
|
254
258
|
)
|
|
255
259
|
|
|
256
260
|
|
{python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/client.py
RENAMED
|
@@ -13,6 +13,7 @@ import os
|
|
|
13
13
|
import random
|
|
14
14
|
import socket as _socket
|
|
15
15
|
import sys
|
|
16
|
+
import tempfile
|
|
16
17
|
import threading
|
|
17
18
|
import time
|
|
18
19
|
import uuid
|
|
@@ -128,7 +129,7 @@ def _llm_log_path() -> Path:
|
|
|
128
129
|
d = Path(log_dir)
|
|
129
130
|
d.mkdir(parents=True, exist_ok=True)
|
|
130
131
|
return d / f"python-agent-harness-{date_str}-{session_id}.json"
|
|
131
|
-
return Path(f"
|
|
132
|
+
return Path(tempfile.gettempdir()) / f"python-agent-harness-{date_str}-{session_id}.json"
|
|
132
133
|
|
|
133
134
|
|
|
134
135
|
def _log_llm_interaction(
|
|
@@ -196,8 +197,12 @@ def _resolve_ca_bundle() -> str | bool:
|
|
|
196
197
|
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
197
198
|
"/etc/ssl/certs/ca-certificates.crt",
|
|
198
199
|
"/etc/ssl/ca-bundle.pem",
|
|
200
|
+
# Windows (Git for Windows, conda, etc.)
|
|
201
|
+
os.path.join(os.environ.get("PROGRAMFILES", ""), "Git", "mingw64", "ssl", "cert.pem"),
|
|
202
|
+
os.path.join(os.environ.get("PROGRAMFILES(X86)", ""), "Git", "mingw64", "ssl", "cert.pem"),
|
|
203
|
+
os.path.join(os.environ.get("CONDA_PREFIX", ""), "ssl", "cert.pem"),
|
|
199
204
|
):
|
|
200
|
-
if os.path.isfile(cand):
|
|
205
|
+
if cand and os.path.isfile(cand):
|
|
201
206
|
return cand
|
|
202
207
|
return True
|
|
203
208
|
|
|
@@ -877,9 +882,16 @@ def _shutdown_and_close(sock: _socket.socket) -> None:
|
|
|
877
882
|
after, so a new socket could reuse the fd number before the
|
|
878
883
|
reader observed the EOF. The grace sleep delays the fd release
|
|
879
884
|
so the woken thread can observe the EOF on the old fd first.
|
|
885
|
+
|
|
886
|
+
On Windows, ``shutdown(SHUT_RDWR)`` on the client socket causes
|
|
887
|
+
``WinError 10058`` on the server side when it tries to read,
|
|
888
|
+
producing noisy BrokenPipeError tracebacks. Skipping shutdown
|
|
889
|
+
and just closing the fd is sufficient — Windows ``close()``
|
|
890
|
+
triggers a TCP RST that reliably wakes the blocked read.
|
|
880
891
|
"""
|
|
881
|
-
|
|
882
|
-
|
|
892
|
+
if sys.platform != "win32":
|
|
893
|
+
with contextlib.suppress(OSError):
|
|
894
|
+
sock.shutdown(_socket.SHUT_RDWR)
|
|
883
895
|
if sys.platform == "darwin":
|
|
884
896
|
# macOS only; on Linux shutdown() alone reliably wakes the
|
|
885
897
|
# blocked recv (measured: <200ms unblock, no sleeps needed).
|
{python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/config.py
RENAMED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from .mcp.config import MCPConfig
|
|
@@ -50,7 +51,10 @@ CALIBRATION_MIN = 0.5
|
|
|
50
51
|
CALIBRATION_MAX = 3.0
|
|
51
52
|
|
|
52
53
|
# ---- sessions ---------------------------------------------------------------
|
|
53
|
-
|
|
54
|
+
if sys.platform == "win32":
|
|
55
|
+
SESSION_DIR = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
56
|
+
else:
|
|
57
|
+
SESSION_DIR = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
54
58
|
SESSION_SUBDIR = "python-agent-harness/sessions"
|
|
55
59
|
AUTO_SAVE_SESSION = True
|
|
56
60
|
|
|
@@ -164,9 +168,15 @@ DEFAULT_AGENT_PROMPT_FILE = PROMPTS_DIR / "agent.md"
|
|
|
164
168
|
DEFAULT_SUBAGENT_PROMPT_FILE = PROMPTS_DIR / "subagent.md"
|
|
165
169
|
|
|
166
170
|
# ---- configuration file ---------------------------------------------------------
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
171
|
+
if sys.platform == "win32":
|
|
172
|
+
CONFIG_DIR = (
|
|
173
|
+
Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
|
174
|
+
/ "python-agent-harness"
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
CONFIG_DIR = (
|
|
178
|
+
Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "python-agent-harness"
|
|
179
|
+
)
|
|
170
180
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
171
181
|
|
|
172
182
|
DEFAULT_LLM: dict = {
|
{python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/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.2 → python_agent_harness-1.5.5.4}/python_agent_harness/session.py
RENAMED
|
@@ -162,6 +162,12 @@ class Session:
|
|
|
162
162
|
# cache build simultaneously — double-checked locking prevents
|
|
163
163
|
# redundant filesystem scans and the data race on the cache tuple
|
|
164
164
|
self._skill_index_lock = threading.Lock()
|
|
165
|
+
# NOTE: the store does NOT record ``default_agent`` here: the
|
|
166
|
+
# store's agent must reflect the agent actually applied, and
|
|
167
|
+
# only a successful switch_agent() can guarantee that (a typo'd
|
|
168
|
+
# default_agent is reported as a startup warning and the session
|
|
169
|
+
# keeps the built-in prompt). cli.make_session applies the
|
|
170
|
+
# default_agent right after construction, which sets it.
|
|
165
171
|
self.store = SessionPersistence(
|
|
166
172
|
project_dir=project_dir,
|
|
167
173
|
model=model,
|
|
@@ -731,6 +737,7 @@ class Session:
|
|
|
731
737
|
if name == RESERVED_AGENT_NAME:
|
|
732
738
|
self.system_prompt = self._default_system_prompt
|
|
733
739
|
self.store.system_prompt = self._default_system_prompt
|
|
740
|
+
self.store.agent = None
|
|
734
741
|
return True, "switched to default agent"
|
|
735
742
|
|
|
736
743
|
agents = discover_agents()
|
|
@@ -749,6 +756,7 @@ class Session:
|
|
|
749
756
|
context_path=self._configured_context_path,
|
|
750
757
|
)
|
|
751
758
|
self.store.system_prompt = self.system_prompt
|
|
759
|
+
self.store.agent = name
|
|
752
760
|
return True, f"switched to {name}"
|
|
753
761
|
|
|
754
762
|
# ------------------------------------------------------------------
|
{python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/__init__.py
RENAMED
|
@@ -7,11 +7,15 @@ import sys
|
|
|
7
7
|
from .agent_tool import AgentTool
|
|
8
8
|
from .base import PendingToolResult, Registry, Tool, ToolContext
|
|
9
9
|
from .bash import Bash
|
|
10
|
+
from .bash_win import BashWindows
|
|
10
11
|
from .edit import Edit
|
|
11
12
|
from .edit_mac import EditMac
|
|
13
|
+
from .edit_win import EditWindows
|
|
12
14
|
from .filesystem import GlobTool, Grep, Insert, Mkdir, Read, Write
|
|
13
15
|
from .glob_mac import GlobMac
|
|
16
|
+
from .glob_win import GlobWindows
|
|
14
17
|
from .grep_mac import GrepMac
|
|
18
|
+
from .grep_win import GrepWindows
|
|
15
19
|
from .mcp import MCPTool, mcp_tools_from_manager, normalize_mcp_result
|
|
16
20
|
from .planexit import PlanExit
|
|
17
21
|
from .question import Question
|
|
@@ -25,12 +29,16 @@ __all__ = [
|
|
|
25
29
|
"ToolContext",
|
|
26
30
|
"AgentTool",
|
|
27
31
|
"Bash",
|
|
32
|
+
"BashWindows",
|
|
28
33
|
"Edit",
|
|
29
34
|
"EditMac",
|
|
35
|
+
"EditWindows",
|
|
30
36
|
"GlobMac",
|
|
31
37
|
"GlobTool",
|
|
38
|
+
"GlobWindows",
|
|
32
39
|
"Grep",
|
|
33
40
|
"GrepMac",
|
|
41
|
+
"GrepWindows",
|
|
34
42
|
"Insert",
|
|
35
43
|
"MCPTool",
|
|
36
44
|
"Mkdir",
|
|
@@ -50,10 +58,15 @@ def default_registry() -> Registry:
|
|
|
50
58
|
# macOS lacks GNU `patch`, `tree`, and PCRE-enabled git — use
|
|
51
59
|
# pure-Python / POSIX-compatible Mac variants that register under
|
|
52
60
|
# the same tool names so callers are platform-independent.
|
|
61
|
+
# Windows lacks process groups, select on pipes, SIGKILL, `patch`,
|
|
62
|
+
# `tree`, `find`, and `grep` — Windows variants use pure-Python
|
|
63
|
+
# fallbacks and CREATE_NEW_PROCESS_GROUP + taskkill instead.
|
|
53
64
|
_mac = sys.platform == "darwin"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
_win = sys.platform == "win32"
|
|
66
|
+
edit_tool = EditWindows() if _win else EditMac() if _mac else Edit()
|
|
67
|
+
glob_tool = GlobWindows() if _win else GlobMac() if _mac else GlobTool()
|
|
68
|
+
grep_tool = GrepWindows() if _win else GrepMac() if _mac else Grep()
|
|
69
|
+
bash_tool = BashWindows() if _win else Bash()
|
|
57
70
|
for tool in (
|
|
58
71
|
AgentTool(),
|
|
59
72
|
TodoWrite(),
|
|
@@ -64,7 +77,7 @@ def default_registry() -> Registry:
|
|
|
64
77
|
edit_tool,
|
|
65
78
|
Write(),
|
|
66
79
|
Mkdir(),
|
|
67
|
-
|
|
80
|
+
bash_tool,
|
|
68
81
|
Skill(),
|
|
69
82
|
Question(),
|
|
70
83
|
):
|
{python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/bash.py
RENAMED
|
@@ -30,6 +30,7 @@ import os
|
|
|
30
30
|
import select
|
|
31
31
|
import signal
|
|
32
32
|
import subprocess
|
|
33
|
+
import sys
|
|
33
34
|
import threading
|
|
34
35
|
import time
|
|
35
36
|
from collections import deque
|
|
@@ -56,7 +57,7 @@ def _kill_pgid(pgid: int) -> None:
|
|
|
56
57
|
stdout pipe open is still running in the group.
|
|
57
58
|
"""
|
|
58
59
|
with contextlib.suppress(ProcessLookupError, PermissionError, OSError):
|
|
59
|
-
os.killpg(pgid, signal.SIGKILL)
|
|
60
|
+
os.killpg(pgid, signal.SIGKILL) # pyright: ignore[reportAttributeAccessIssue]
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
def _kill_graceful(pgid: int, proc: subprocess.Popen) -> None:
|
|
@@ -66,7 +67,7 @@ def _kill_graceful(pgid: int, proc: subprocess.Popen) -> None:
|
|
|
66
67
|
children before the hard kill.
|
|
67
68
|
"""
|
|
68
69
|
with contextlib.suppress(ProcessLookupError, PermissionError, OSError):
|
|
69
|
-
os.killpg(pgid, signal.SIGTERM)
|
|
70
|
+
os.killpg(pgid, signal.SIGTERM) # pyright: ignore[reportAttributeAccessIssue]
|
|
70
71
|
try:
|
|
71
72
|
proc.wait(timeout=2)
|
|
72
73
|
except subprocess.TimeoutExpired:
|
|
@@ -144,7 +145,8 @@ def _collect_output(proc: subprocess.Popen, cancel: threading.Event | None) -> t
|
|
|
144
145
|
if stdout is None: # unreachable (stdout=PIPE), kept for the type checker
|
|
145
146
|
return "", "ok"
|
|
146
147
|
fd = stdout.fileno()
|
|
147
|
-
|
|
148
|
+
if sys.platform != "win32":
|
|
149
|
+
os.set_blocking(fd, False) # pyright: ignore[reportAttributeAccessIssue]
|
|
148
150
|
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
149
151
|
head: list[str] = []
|
|
150
152
|
head_len = 0
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Windows Bash tool: process management for win32.
|
|
2
|
+
|
|
3
|
+
Windows lacks ``os.killpg``, ``os.set_blocking``, ``select.select``
|
|
4
|
+
on pipe fds, and ``start_new_session``. This subclass overrides
|
|
5
|
+
``_execute`` to use Windows-native process creation and termination
|
|
6
|
+
while inheriting the shared output-truncation, timeout, and exit-code
|
|
7
|
+
helpers from :class:`Bash`.
|
|
8
|
+
|
|
9
|
+
Process creation uses ``CREATE_NEW_PROCESS_GROUP`` so the child is
|
|
10
|
+
isolated from console Ctrl-C signals (the TUI handles cancel itself).
|
|
11
|
+
Process termination uses ``taskkill /F /T /PID`` to kill the process
|
|
12
|
+
and all its children (the Windows equivalent of killing a process
|
|
13
|
+
group). Output is collected via a reader thread + queue, since
|
|
14
|
+
``select.select`` cannot poll Windows pipes.
|
|
15
|
+
|
|
16
|
+
Only the process-creation and output-collection strategy differs;
|
|
17
|
+
the tool name, description, parameters, timeout semantics, and
|
|
18
|
+
exit-code reporting are inherited so callers, the tool registry,
|
|
19
|
+
and the plan-mode write guard are platform-independent.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import codecs
|
|
25
|
+
import contextlib
|
|
26
|
+
import os
|
|
27
|
+
import queue
|
|
28
|
+
import subprocess
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
from collections import deque
|
|
32
|
+
|
|
33
|
+
from ..config import BASH_TIMEOUT_MAX, BASH_TIMEOUT_SILENCE
|
|
34
|
+
from ..config import MAX_OUTPUT_CHARS as _MAX_OUTPUT
|
|
35
|
+
from .base import PendingToolResult, ToolContext
|
|
36
|
+
from .bash import (
|
|
37
|
+
_DRAIN_GRACE,
|
|
38
|
+
_POLL_INTERVAL,
|
|
39
|
+
_READ_CHUNK,
|
|
40
|
+
_TAIL_LINES,
|
|
41
|
+
Bash,
|
|
42
|
+
_append_exit_code,
|
|
43
|
+
_assemble_truncated,
|
|
44
|
+
_timeout_message,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _kill_process_tree(pid: int) -> None:
|
|
49
|
+
"""Kill the process PID and all its children (Windows equivalent of
|
|
50
|
+
``os.killpg`` with ``SIGKILL``).
|
|
51
|
+
|
|
52
|
+
Uses ``taskkill /F /T /PID`` which recursively terminates the
|
|
53
|
+
process tree. Errors are suppressed (already gone, etc.).
|
|
54
|
+
"""
|
|
55
|
+
with contextlib.suppress(Exception):
|
|
56
|
+
subprocess.run(
|
|
57
|
+
["taskkill", "/F", "/T", "/PID", str(pid)],
|
|
58
|
+
stdin=subprocess.DEVNULL,
|
|
59
|
+
stdout=subprocess.DEVNULL,
|
|
60
|
+
stderr=subprocess.DEVNULL,
|
|
61
|
+
timeout=10,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _kill_process_tree_graceful(pid: int, proc: subprocess.Popen) -> None:
|
|
66
|
+
"""Graceful kill: terminate, then taskkill /F /T if still alive.
|
|
67
|
+
|
|
68
|
+
Windows has no SIGTERM equivalent for process trees;
|
|
69
|
+
``proc.terminate()`` calls ``TerminateProcess`` on the immediate
|
|
70
|
+
child. If it doesn't die within 2s, ``taskkill /F /T`` force-kills
|
|
71
|
+
the whole tree (children included).
|
|
72
|
+
"""
|
|
73
|
+
with contextlib.suppress(ProcessLookupError, OSError):
|
|
74
|
+
proc.terminate()
|
|
75
|
+
try:
|
|
76
|
+
proc.wait(timeout=2)
|
|
77
|
+
except subprocess.TimeoutExpired:
|
|
78
|
+
_kill_process_tree(pid)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _collect_output_win(proc: subprocess.Popen, cancel: threading.Event | None) -> tuple[str, str]:
|
|
82
|
+
"""Read PROC's merged output on Windows; return (text, status).
|
|
83
|
+
|
|
84
|
+
Windows lacks ``select.select`` on pipe fds and ``os.set_blocking``,
|
|
85
|
+
so a reader thread does blocking ``os.read`` calls and feeds chunks
|
|
86
|
+
into a ``queue.Queue``. The collector polls the queue with
|
|
87
|
+
``_POLL_INTERVAL``, checking cancel/timeout conditions between
|
|
88
|
+
reads — mirroring the Unix ``_collect_output`` logic exactly.
|
|
89
|
+
|
|
90
|
+
Status is one of ``"ok"``, ``"cancelled"``, ``"timeout_silence"``,
|
|
91
|
+
``"timeout_max"``. Keeps the head (first ``_MAX_OUTPUT`` chars)
|
|
92
|
+
and the tail (last ``_TAIL_LINES`` lines) and discards the middle,
|
|
93
|
+
so memory stays bounded no matter how much the process writes.
|
|
94
|
+
"""
|
|
95
|
+
stdout = proc.stdout
|
|
96
|
+
if stdout is None:
|
|
97
|
+
return "", "ok"
|
|
98
|
+
|
|
99
|
+
chunk_queue: queue.Queue[bytes | None] = queue.Queue()
|
|
100
|
+
|
|
101
|
+
def _reader() -> None:
|
|
102
|
+
"""Blocking reader thread: reads raw bytes and feeds the queue."""
|
|
103
|
+
try:
|
|
104
|
+
fd = stdout.fileno()
|
|
105
|
+
while True:
|
|
106
|
+
raw = os.read(fd, _READ_CHUNK)
|
|
107
|
+
if not raw:
|
|
108
|
+
break
|
|
109
|
+
chunk_queue.put(raw)
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
finally:
|
|
113
|
+
chunk_queue.put(None)
|
|
114
|
+
|
|
115
|
+
threading.Thread(target=_reader, daemon=True).start()
|
|
116
|
+
|
|
117
|
+
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
118
|
+
head: list[str] = []
|
|
119
|
+
head_len = 0
|
|
120
|
+
tail: deque[str] = deque(maxlen=_TAIL_LINES)
|
|
121
|
+
pending_line = ""
|
|
122
|
+
total = 0
|
|
123
|
+
exited = False
|
|
124
|
+
drain_until: float | None = None
|
|
125
|
+
start = time.monotonic()
|
|
126
|
+
last_output = start
|
|
127
|
+
|
|
128
|
+
def finish() -> str:
|
|
129
|
+
nonlocal pending_line
|
|
130
|
+
if pending_line:
|
|
131
|
+
if len(pending_line) > _MAX_OUTPUT:
|
|
132
|
+
pending_line = pending_line[:_MAX_OUTPUT]
|
|
133
|
+
tail.append(pending_line)
|
|
134
|
+
head_text = "".join(head)
|
|
135
|
+
if total > _MAX_OUTPUT:
|
|
136
|
+
return _assemble_truncated(head_text, tail)
|
|
137
|
+
return head_text
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
if cancel is not None and cancel.is_set():
|
|
141
|
+
return "", "cancelled"
|
|
142
|
+
now = time.monotonic()
|
|
143
|
+
if not exited:
|
|
144
|
+
if BASH_TIMEOUT_SILENCE is not None and now - last_output >= BASH_TIMEOUT_SILENCE:
|
|
145
|
+
return finish(), "timeout_silence"
|
|
146
|
+
if BASH_TIMEOUT_MAX is not None and now - start >= BASH_TIMEOUT_MAX:
|
|
147
|
+
return finish(), "timeout_max"
|
|
148
|
+
if exited and drain_until is not None and now >= drain_until:
|
|
149
|
+
break
|
|
150
|
+
try:
|
|
151
|
+
raw = chunk_queue.get(timeout=_POLL_INTERVAL)
|
|
152
|
+
except queue.Empty:
|
|
153
|
+
if not exited and proc.poll() is not None:
|
|
154
|
+
exited = True
|
|
155
|
+
drain_until = time.monotonic() + _DRAIN_GRACE
|
|
156
|
+
continue
|
|
157
|
+
if raw is None:
|
|
158
|
+
break
|
|
159
|
+
chunk = decoder.decode(raw).replace("\r\n", "\n")
|
|
160
|
+
total += len(chunk)
|
|
161
|
+
last_output = time.monotonic()
|
|
162
|
+
if head_len < _MAX_OUTPUT:
|
|
163
|
+
take = chunk[: _MAX_OUTPUT - head_len]
|
|
164
|
+
head.append(take)
|
|
165
|
+
head_len += len(take)
|
|
166
|
+
parts = chunk.split("\n")
|
|
167
|
+
parts[0] = pending_line + parts[0]
|
|
168
|
+
pending_line = parts.pop()
|
|
169
|
+
for line in parts:
|
|
170
|
+
if len(line) > _MAX_OUTPUT:
|
|
171
|
+
line = line[:_MAX_OUTPUT]
|
|
172
|
+
tail.append(line)
|
|
173
|
+
return finish(), "ok"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class BashWindows(Bash):
|
|
177
|
+
"""Bash for Windows: ``CREATE_NEW_PROCESS_GROUP`` + ``taskkill``.
|
|
178
|
+
|
|
179
|
+
Overrides :meth:`_execute` to use Windows-native process creation
|
|
180
|
+
(``CREATE_NEW_PROCESS_GROUP``) and termination (``taskkill /F /T``).
|
|
181
|
+
All shared logic — timeout messages, output truncation, exit-code
|
|
182
|
+
reporting, the ``PendingToolResult`` async contract — is inherited
|
|
183
|
+
unchanged from :class:`Bash`.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def _execute(self, command: str, ctx: ToolContext) -> str | PendingToolResult:
|
|
187
|
+
cancel = ctx.cancel_event
|
|
188
|
+
if cancel is not None and cancel.is_set():
|
|
189
|
+
return "Error: Bash command cancelled."
|
|
190
|
+
try:
|
|
191
|
+
proc = subprocess.Popen(
|
|
192
|
+
command,
|
|
193
|
+
shell=True,
|
|
194
|
+
stdin=subprocess.DEVNULL,
|
|
195
|
+
stdout=subprocess.PIPE,
|
|
196
|
+
stderr=subprocess.STDOUT,
|
|
197
|
+
bufsize=0,
|
|
198
|
+
cwd=ctx.cwd,
|
|
199
|
+
creationflags=getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0),
|
|
200
|
+
)
|
|
201
|
+
except OSError as e:
|
|
202
|
+
return f"Error: {e}"
|
|
203
|
+
|
|
204
|
+
pid = proc.pid
|
|
205
|
+
pending = PendingToolResult()
|
|
206
|
+
|
|
207
|
+
def deliverer() -> None:
|
|
208
|
+
try:
|
|
209
|
+
out, status = _collect_output_win(proc, cancel)
|
|
210
|
+
except Exception as e: # noqa: BLE001 - delivered as an error string
|
|
211
|
+
out = f"Error: Bash failed — {e}"
|
|
212
|
+
else:
|
|
213
|
+
if status == "cancelled":
|
|
214
|
+
_kill_process_tree(pid)
|
|
215
|
+
out = "Error: Bash command cancelled."
|
|
216
|
+
elif status == "timeout_silence":
|
|
217
|
+
_kill_process_tree_graceful(pid, proc)
|
|
218
|
+
out = _timeout_message(out, silence=True)
|
|
219
|
+
elif status == "timeout_max":
|
|
220
|
+
_kill_process_tree_graceful(pid, proc)
|
|
221
|
+
out = _timeout_message(out, silence=False)
|
|
222
|
+
elif status == "ok":
|
|
223
|
+
out = _append_exit_code(out, proc)
|
|
224
|
+
pending.deliver(out)
|
|
225
|
+
with contextlib.suppress(Exception):
|
|
226
|
+
if proc.stdout is not None:
|
|
227
|
+
proc.stdout.close()
|
|
228
|
+
with contextlib.suppress(Exception):
|
|
229
|
+
proc.wait(timeout=2)
|
|
230
|
+
|
|
231
|
+
threading.Thread(target=deliverer, daemon=True).start()
|
|
232
|
+
return pending
|
|
@@ -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
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Windows Edit tool: pure-Python diff applier (no ``patch`` binary).
|
|
2
|
+
|
|
3
|
+
Windows does not ship ``patch.exe``, so the diff/patch mode of
|
|
4
|
+
:class:`Edit` (which shells out to ``patch --forward``) fails on a
|
|
5
|
+
stock Windows install. The pure-Python applier (``diffapply.py``)
|
|
6
|
+
parses the diff structurally and matches hunks with fuzz, so the same
|
|
7
|
+
diffs apply on Windows, macOS, and Linux.
|
|
8
|
+
|
|
9
|
+
Only the diff-application strategy differs; the tool name stays
|
|
10
|
+
``Edit`` so callers, the tool registry and the plan-mode write guard
|
|
11
|
+
are platform-independent.
|
|
12
|
+
|
|
13
|
+
The implementation is identical to :class:`EditMac` — both platforms
|
|
14
|
+
lack a reliable ``patch`` binary and use the same Python applier.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from ..diffrender import unified_diff
|
|
22
|
+
from .base import ToolContext
|
|
23
|
+
from .diffapply import apply_unified_diff
|
|
24
|
+
from .edit import Edit, _strip_diff_fence
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EditWindows(Edit):
|
|
28
|
+
"""Edit with a pure-Python diff/patch backend for Windows."""
|
|
29
|
+
|
|
30
|
+
def _apply_patch(self, path: str, cwd: str, diff: str, ctx: ToolContext) -> str:
|
|
31
|
+
text = diff if diff.endswith("\n") else diff + "\n"
|
|
32
|
+
text = _strip_diff_fence(text)
|
|
33
|
+
is_file = os.path.isfile(path)
|
|
34
|
+
old_content = None
|
|
35
|
+
if is_file:
|
|
36
|
+
try:
|
|
37
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
38
|
+
old_content = f.read()
|
|
39
|
+
except OSError:
|
|
40
|
+
old_content = None
|
|
41
|
+
ok, msg = apply_unified_diff(text, cwd, path if is_file else None)
|
|
42
|
+
if not ok:
|
|
43
|
+
return f"Error: Failed to apply diff to {path}.\n{msg}"
|
|
44
|
+
if old_content is not None and os.path.isfile(path):
|
|
45
|
+
try:
|
|
46
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
47
|
+
new_content = f.read()
|
|
48
|
+
diff_text = unified_diff(old_content, new_content, path)
|
|
49
|
+
if diff_text:
|
|
50
|
+
ctx.record_diff(diff_text)
|
|
51
|
+
except OSError:
|
|
52
|
+
pass
|
|
53
|
+
return (
|
|
54
|
+
f"Diff successfully applied to {path}.\n"
|
|
55
|
+
f"Applied with the built-in Python diff applier.\n{msg}"
|
|
56
|
+
)
|
{python_agent_harness-1.5.5.2 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob.py
RENAMED
|
@@ -156,10 +156,10 @@ def _git_glob_results(raw: str, git_root: str, base: str, depth: object) -> str:
|
|
|
156
156
|
"""
|
|
157
157
|
lines = [line for line in raw.split("\0") if line]
|
|
158
158
|
if _natnump(depth):
|
|
159
|
-
rel_base = os.path.relpath(base, git_root)
|
|
159
|
+
rel_base = os.path.relpath(base, git_root).replace(os.sep, "/")
|
|
160
160
|
base_depth = 0 if rel_base == "." else 1 + rel_base.count("/")
|
|
161
161
|
lines = [line for line in lines if line.count("/") < base_depth + depth]
|
|
162
|
-
out = "\n".join(os.path.join(git_root, line) for line in lines)
|
|
162
|
+
out = "\n".join(os.path.join(git_root, line).replace(os.sep, "/") for line in lines)
|
|
163
163
|
if not out:
|
|
164
164
|
return ""
|
|
165
165
|
return _spool(out + "\n", "glob")
|