python-agent-harness 1.5.5.1__tar.gz → 1.5.5.2__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.1/python_agent_harness.egg-info → python_agent_harness-1.5.5.2}/PKG-INFO +13 -2
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/README.md +1 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/pyproject.toml +18 -5
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/agent.py +13 -2
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/persistence.py +1 -7
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/commands.py +2 -2
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/core.py +4 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/render.py +52 -17
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2/python_agent_harness.egg-info}/PKG-INFO +13 -2
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_cli.py +1 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_concurrent_subagents.py +2 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_custom_agents.py +4 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_invariants.py +1 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_mcp.py +1 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_persistence.py +6 -9
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_planmode.py +1 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_scenarios.py +1 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_session.py +1 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_subagent.py +1 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_subagent_isolation.py +57 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_todos_scope.py +6 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_tools_misc.py +4 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/LICENSE +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/cli.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/client.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/config.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/planmode.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/agents/reviewer.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/session.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/diffapply.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/grep_mac.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/setup.cfg +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_client.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_config.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_filesystem.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_token_estimator.py +0 -0
{python_agent_harness-1.5.5.1/python_agent_harness.egg-info → python_agent_harness-1.5.5.2}/PKG-INFO
RENAMED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.5.
|
|
3
|
+
Version: 1.5.5.2
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
|
-
Author:
|
|
5
|
+
Author-email: Huming Chen <chenhuming@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/beacoder/python-agent-harness
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
7
17
|
Requires-Python: >=3.11
|
|
8
18
|
Description-Content-Type: text/markdown
|
|
9
19
|
License-File: LICENSE
|
|
@@ -31,6 +41,7 @@ FSM-driven execution · OpenAI-compatible · built for daily use and easy custom
|
|
|
31
41
|
[](https://github.com/beacoder/python-agent-harness/actions/workflows/ci.yml)
|
|
32
42
|
[](https://pypi.org/project/python-agent-harness/)
|
|
33
43
|
[](https://www.python.org/downloads/)
|
|
44
|
+
[](https://pepy.tech/projects/python-agent-harness)
|
|
34
45
|
[](LICENSE)
|
|
35
46
|
|
|
36
47
|
</div>
|
|
@@ -8,6 +8,7 @@ FSM-driven execution · OpenAI-compatible · built for daily use and easy custom
|
|
|
8
8
|
[](https://github.com/beacoder/python-agent-harness/actions/workflows/ci.yml)
|
|
9
9
|
[](https://pypi.org/project/python-agent-harness/)
|
|
10
10
|
[](https://www.python.org/downloads/)
|
|
11
|
+
[](https://pepy.tech/projects/python-agent-harness)
|
|
11
12
|
[](LICENSE)
|
|
12
13
|
|
|
13
14
|
</div>
|
|
@@ -4,21 +4,34 @@ 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.2"
|
|
8
8
|
description = "A lightweight, hackable mini-OpenCode written in Python."
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Huming Chen", email = "chenhuming@gmail.com"}
|
|
11
|
+
]
|
|
12
|
+
license = "MIT"
|
|
9
13
|
readme = "README.md"
|
|
10
14
|
requires-python = ">=3.11"
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14"
|
|
13
25
|
]
|
|
14
|
-
license = 'MIT'
|
|
15
|
-
license-files = ["LICENSE"]
|
|
16
26
|
dependencies = [
|
|
17
27
|
"rich>=13.0",
|
|
18
28
|
"httpx>=0.27",
|
|
19
29
|
"prompt_toolkit>=3.0",
|
|
20
30
|
]
|
|
21
31
|
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/beacoder/python-agent-harness"
|
|
34
|
+
|
|
22
35
|
[project.optional-dependencies]
|
|
23
36
|
# MCP (Model Context Protocol) client support: exposes MCP server tools to
|
|
24
37
|
# the agent as namespaced mcp__<server>__<tool> tools. Install with:
|
|
@@ -637,14 +637,25 @@ def run_agent_loop(
|
|
|
637
637
|
|
|
638
638
|
``client`` (when given) overrides the session's client for this
|
|
639
639
|
run — the per-invocation sub-agent clone."""
|
|
640
|
-
|
|
640
|
+
loop = AgentLoop(
|
|
641
641
|
session,
|
|
642
642
|
messages=messages,
|
|
643
643
|
top_level=top_level,
|
|
644
644
|
system=system,
|
|
645
645
|
max_rounds=max_rounds,
|
|
646
646
|
client=client,
|
|
647
|
-
)
|
|
647
|
+
)
|
|
648
|
+
result = loop.run()
|
|
649
|
+
# Notify run_done only for a successfully completed top-level run:
|
|
650
|
+
# - top_level: sub-agents share the parent's session (notify_fn
|
|
651
|
+
# included) and must not mark the parent run finished while it is
|
|
652
|
+
# still executing tools
|
|
653
|
+
# - DONE only: ERRS already notifies "error" with the detail, and
|
|
654
|
+
# ABRT is cancellation/supersession (TUI sets that status). DONE
|
|
655
|
+
# also implies not cancelled — the FSM routes cancelled to ABRT.
|
|
656
|
+
if loop.top_level and loop.state == AgentLoop.DONE:
|
|
657
|
+
session.notify("run_done")
|
|
658
|
+
return result
|
|
648
659
|
|
|
649
660
|
|
|
650
661
|
class Supervisor:
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/persistence.py
RENAMED
|
@@ -170,18 +170,12 @@ class SessionPersistence:
|
|
|
170
170
|
lines = [";; Local Variables:"]
|
|
171
171
|
pairs = [
|
|
172
172
|
("python-agent-harness--project-dir", self.project_dir),
|
|
173
|
-
("
|
|
174
|
-
("gptel-system-prompt", self.system_prompt),
|
|
175
|
-
("gptel-temperature", self.temperature),
|
|
176
|
-
("gptel-max-tokens", self.max_tokens),
|
|
173
|
+
("python-agent-harness--model", self.model),
|
|
177
174
|
]
|
|
178
175
|
for name, value in pairs:
|
|
179
176
|
if value is None:
|
|
180
177
|
continue
|
|
181
178
|
lines.append(f";; {name}: {value!r}")
|
|
182
|
-
if self.tool_names:
|
|
183
|
-
names = " ".join(f'"{n}"' for n in self.tool_names)
|
|
184
|
-
lines.append(f";; gptel--tool-names: ({names})")
|
|
185
179
|
if self.round_times:
|
|
186
180
|
stamps = " ".join(repr(float(t)) for t in self.round_times)
|
|
187
181
|
lines.append(f";; python-agent-harness--round-times: {stamps}")
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/commands.py
RENAMED
|
@@ -578,7 +578,7 @@ class CommandMixin:
|
|
|
578
578
|
continue
|
|
579
579
|
meta = SessionPersistence.parse_metadata(text)
|
|
580
580
|
basename = os.path.basename(f)
|
|
581
|
-
model = meta.get("
|
|
581
|
+
model = meta.get("python-agent-harness--model", "?")
|
|
582
582
|
project = meta.get("python-agent-harness--project-dir", "?")
|
|
583
583
|
self.console.print(f" {basename:50s} model={model:20s} project={project}")
|
|
584
584
|
|
|
@@ -645,7 +645,7 @@ class CommandMixin:
|
|
|
645
645
|
self.session.last_messages = list(messages)
|
|
646
646
|
self.session.clear_todos()
|
|
647
647
|
self._history_dirty = True
|
|
648
|
-
model = meta.get("
|
|
648
|
+
model = meta.get("python-agent-harness--model", "?")
|
|
649
649
|
project = meta.get("python-agent-harness--project-dir", "?")
|
|
650
650
|
self.console.print(
|
|
651
651
|
f"[green]restored:[/green] {os.path.basename(path)} "
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/core.py
RENAMED
|
@@ -153,6 +153,10 @@ class Tui(RenderMixin, InputMixin, CommandMixin):
|
|
|
153
153
|
elif kind == "save-error":
|
|
154
154
|
with self.lock:
|
|
155
155
|
self.status = " auto-save failed"
|
|
156
|
+
elif kind == "run_done":
|
|
157
|
+
with self.lock:
|
|
158
|
+
self.status = " done"
|
|
159
|
+
self._history_dirty = True
|
|
156
160
|
else:
|
|
157
161
|
with self.lock:
|
|
158
162
|
self.status = " running"
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/render.py
RENAMED
|
@@ -450,9 +450,9 @@ class RenderMixin:
|
|
|
450
450
|
height = getattr(self.console, "height", None)
|
|
451
451
|
if not height or height <= 0:
|
|
452
452
|
return 60
|
|
453
|
-
# reserve: status bar (
|
|
453
|
+
# reserve: status bar (up to 2 lines) + input prompt (1)
|
|
454
454
|
# + the pinned Todos section when visible (its title line + rows)
|
|
455
|
-
reserved =
|
|
455
|
+
reserved = 3
|
|
456
456
|
if self.session.todos:
|
|
457
457
|
reserved += min(len(self.session.todos), 8) + 1
|
|
458
458
|
return max(5, height - reserved)
|
|
@@ -521,23 +521,58 @@ class RenderMixin:
|
|
|
521
521
|
return t
|
|
522
522
|
|
|
523
523
|
def _fit_status(self, used: int, msg: str) -> str:
|
|
524
|
-
"""Fit the status message on the status
|
|
525
|
-
|
|
526
|
-
|
|
524
|
+
"""Fit the status message on the status bar: up to two lines.
|
|
525
|
+
|
|
526
|
+
Line 1 shares its row with the mode/context prefix (``used``
|
|
527
|
+
cells); line 2 gets the full terminal width. Newlines in the
|
|
528
|
+
message are flattened, words break at spaces where possible,
|
|
529
|
+
and anything beyond two lines is truncated with an ellipsis.
|
|
530
|
+
"""
|
|
527
531
|
msg = " ".join(msg.splitlines())
|
|
528
532
|
width = self.console.width or 80
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
533
|
+
avail1 = max(1, width - used - 1)
|
|
534
|
+
avail2 = max(1, width - 2)
|
|
535
|
+
line1, rest = self._wrap_line(msg, avail1)
|
|
536
|
+
if not rest:
|
|
537
|
+
return line1
|
|
538
|
+
line2, rest2 = self._wrap_line(rest, avail2)
|
|
539
|
+
if rest2:
|
|
540
|
+
# re-wrap reserving a cell so the ellipsis fits the line
|
|
541
|
+
line2, _ = self._wrap_line(rest, max(1, avail2 - 1))
|
|
542
|
+
return f"{line1}\n{line2}…"
|
|
543
|
+
return f"{line1}\n{line2}"
|
|
544
|
+
|
|
545
|
+
@staticmethod
|
|
546
|
+
def _wrap_line(text: str, avail: int) -> tuple[str, str]:
|
|
547
|
+
"""Split ``text`` into ``(line, remainder)`` where ``line`` is the
|
|
548
|
+
longest prefix fitting ``avail`` cells.
|
|
549
|
+
|
|
550
|
+
Breaks at a space so words stay whole; a single word wider than
|
|
551
|
+
the line (long JSON tokens, URLs) is hard-broken at the cell
|
|
552
|
+
boundary instead of dropped.
|
|
553
|
+
"""
|
|
554
|
+
if cell_len(text) <= avail:
|
|
555
|
+
return text, ""
|
|
556
|
+
words = text.split(" ")
|
|
557
|
+
line = ""
|
|
558
|
+
for idx, word in enumerate(words):
|
|
559
|
+
candidate = word if not line else f"{line} {word}"
|
|
560
|
+
if cell_len(candidate) <= avail:
|
|
561
|
+
line = candidate
|
|
562
|
+
continue
|
|
563
|
+
if not line:
|
|
564
|
+
# one word wider than the whole line: cut mid-word
|
|
565
|
+
take = ""
|
|
566
|
+
for ch in word:
|
|
567
|
+
if cell_len(take) + cell_len(ch) > avail:
|
|
568
|
+
break
|
|
569
|
+
take += ch
|
|
570
|
+
rest_word = word[len(take) :]
|
|
571
|
+
tail = words[idx + 1 :]
|
|
572
|
+
remainder = " ".join([rest_word, *tail]) if rest_word else " ".join(tail)
|
|
573
|
+
return take, remainder
|
|
574
|
+
return line, " ".join(words[idx:])
|
|
575
|
+
return line, ""
|
|
541
576
|
|
|
542
577
|
def _status_style(self) -> str:
|
|
543
578
|
"""Status-bar color by state: errors red, activity cyan, idle dim."""
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2/python_agent_harness.egg-info}/PKG-INFO
RENAMED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.5.
|
|
3
|
+
Version: 1.5.5.2
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
|
-
Author:
|
|
5
|
+
Author-email: Huming Chen <chenhuming@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/beacoder/python-agent-harness
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
7
17
|
Requires-Python: >=3.11
|
|
8
18
|
Description-Content-Type: text/markdown
|
|
9
19
|
License-File: LICENSE
|
|
@@ -31,6 +41,7 @@ FSM-driven execution · OpenAI-compatible · built for daily use and easy custom
|
|
|
31
41
|
[](https://github.com/beacoder/python-agent-harness/actions/workflows/ci.yml)
|
|
32
42
|
[](https://pypi.org/project/python-agent-harness/)
|
|
33
43
|
[](https://www.python.org/downloads/)
|
|
44
|
+
[](https://pepy.tech/projects/python-agent-harness)
|
|
34
45
|
[](LICENSE)
|
|
35
46
|
|
|
36
47
|
</div>
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
12
12
|
|
|
13
13
|
import plan_cleanup # noqa: F401,E402 (side-effect: auto-remove /tmp plan dirs)
|
|
14
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
14
15
|
|
|
15
16
|
from python_agent_harness import cli, config
|
|
16
17
|
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_concurrent_subagents.py
RENAMED
|
@@ -28,6 +28,8 @@ import time
|
|
|
28
28
|
import unittest
|
|
29
29
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
30
30
|
|
|
31
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
32
|
+
|
|
31
33
|
from python_agent_harness.client import Client
|
|
32
34
|
from python_agent_harness.session import Session
|
|
33
35
|
from python_agent_harness.tools import default_registry
|
|
@@ -11,6 +11,10 @@ import unittest
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from unittest import mock
|
|
13
13
|
|
|
14
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
15
|
+
|
|
16
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
17
|
+
|
|
14
18
|
from python_agent_harness import config
|
|
15
19
|
from python_agent_harness.client import Client
|
|
16
20
|
from python_agent_harness.prompts import AGENTS_DIR, discover_agents
|
|
@@ -54,6 +54,7 @@ from unittest import mock
|
|
|
54
54
|
sys.path.insert(0, os.path.dirname(__file__)) # sibling test helpers
|
|
55
55
|
|
|
56
56
|
import plan_cleanup # noqa: F401,E402 (side-effect: auto-remove /tmp plan dirs)
|
|
57
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
57
58
|
from agent.agent_test_utils import ParallelToolSession, RecordingSession, agent_call # noqa: E402
|
|
58
59
|
|
|
59
60
|
from python_agent_harness import config # noqa: E402
|
|
@@ -37,8 +37,7 @@ class TestSession(unittest.TestCase):
|
|
|
37
37
|
text = "conversation...\n\n" + meta + "\n"
|
|
38
38
|
parsed = SessionPersistence.parse_metadata(text)
|
|
39
39
|
self.assertEqual(parsed["python-agent-harness--project-dir"], "/tmp/proj")
|
|
40
|
-
self.assertEqual(parsed["
|
|
41
|
-
self.assertIn("glob", parsed["gptel--tool-names"])
|
|
40
|
+
self.assertEqual(parsed["python-agent-harness--model"], "deepseek-v4")
|
|
42
41
|
stripped = SessionPersistence.strip_metadata(text)
|
|
43
42
|
self.assertEqual(stripped.strip(), "conversation...")
|
|
44
43
|
|
|
@@ -252,22 +251,20 @@ class TestSession(unittest.TestCase):
|
|
|
252
251
|
";; Local Variables:\n"
|
|
253
252
|
"plain line without marker\n"
|
|
254
253
|
";; no-colon-here\n"
|
|
255
|
-
";;
|
|
256
|
-
";;
|
|
257
|
-
";; gptel-system-prompt: unquoted-value\n"
|
|
254
|
+
";; python-agent-harness--project-dir: '/tmp/proj'\n"
|
|
255
|
+
";; python-agent-harness--model: 'deepseek-v4'\n"
|
|
258
256
|
";; End:\n"
|
|
259
257
|
)
|
|
260
258
|
parsed = SessionPersistence.parse_metadata(text)
|
|
261
|
-
self.assertEqual(parsed["
|
|
262
|
-
self.assertEqual(parsed["
|
|
263
|
-
self.assertEqual(parsed["gptel-system-prompt"], "unquoted-value")
|
|
259
|
+
self.assertEqual(parsed["python-agent-harness--project-dir"], "/tmp/proj")
|
|
260
|
+
self.assertEqual(parsed["python-agent-harness--model"], "deepseek-v4")
|
|
264
261
|
self.assertNotIn("plain line without marker", parsed)
|
|
265
262
|
|
|
266
263
|
def test_strip_metadata_without_block(self):
|
|
267
264
|
self.assertEqual(SessionPersistence.strip_metadata("plain"), "plain")
|
|
268
265
|
|
|
269
266
|
def test_strip_metadata_without_end_marker(self):
|
|
270
|
-
text = "conversation\n;; Local Variables:\n;;
|
|
267
|
+
text = "conversation\n;; Local Variables:\n;; python-agent-harness--model: 'm'\n"
|
|
271
268
|
self.assertEqual(SessionPersistence.strip_metadata(text), "conversation")
|
|
272
269
|
|
|
273
270
|
def test_latest_session_missing_dir(self):
|
|
@@ -8,6 +8,7 @@ from unittest import mock
|
|
|
8
8
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
9
9
|
|
|
10
10
|
import plan_cleanup # noqa: F401,E402 (side-effect: auto-remove /tmp plan dirs)
|
|
11
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
11
12
|
|
|
12
13
|
from python_agent_harness.models import AgentMode
|
|
13
14
|
from python_agent_harness.planmode import PlanMode, _plan_temp_dir
|
|
@@ -23,6 +23,7 @@ from unittest import mock
|
|
|
23
23
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
24
24
|
|
|
25
25
|
import plan_cleanup # noqa: F401,E402 (side-effect: auto-remove /tmp plan dirs)
|
|
26
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
26
27
|
|
|
27
28
|
from python_agent_harness import config
|
|
28
29
|
from python_agent_harness.agent import AgentLoop
|
|
@@ -10,6 +10,7 @@ from unittest import mock
|
|
|
10
10
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
11
11
|
|
|
12
12
|
import plan_cleanup # noqa: F401,E402 (side-effect: auto-remove /tmp plan dirs)
|
|
13
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
13
14
|
from agent.agent_test_utils import FakeClient, RecordingSession
|
|
14
15
|
|
|
15
16
|
from python_agent_harness import config
|
|
@@ -10,6 +10,7 @@ import unittest
|
|
|
10
10
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
11
11
|
|
|
12
12
|
import plan_cleanup # noqa: F401,E402 (side-effect: auto-remove /tmp plan dirs)
|
|
13
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
13
14
|
|
|
14
15
|
from python_agent_harness.models import Message, Usage
|
|
15
16
|
from python_agent_harness.persistence import SessionPersistence
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/tests/test_subagent_isolation.py
RENAMED
|
@@ -4,8 +4,14 @@ session, and must never clobber the parent's history.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
7
9
|
import unittest
|
|
8
10
|
|
|
11
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
12
|
+
|
|
13
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
14
|
+
|
|
9
15
|
from python_agent_harness import config
|
|
10
16
|
from python_agent_harness.agent import run_agent_loop
|
|
11
17
|
from python_agent_harness.models import Message, ToolCall, Usage
|
|
@@ -127,6 +133,57 @@ class TestSubagentIsolation(unittest.TestCase):
|
|
|
127
133
|
self.assertIn(1, client.streamed) # parent's Agent-call turn
|
|
128
134
|
self.assertIn(4, client.streamed) # parent's reply turn
|
|
129
135
|
|
|
136
|
+
def test_run_done_notified_once_for_parent_not_subagent(self):
|
|
137
|
+
"""'run_done' fires exactly once for the top-level run reaching
|
|
138
|
+
DONE — the inner sub-agent loop (which shares the parent's
|
|
139
|
+
session) must not fire it mid-parent-run, and the sub-agent's
|
|
140
|
+
own DONE must not double-fire after the parent already notified."""
|
|
141
|
+
client = RecClient(
|
|
142
|
+
[
|
|
143
|
+
("", [ToolCall(id="p1", name="Agent", arguments=AGENT_CALL)]),
|
|
144
|
+
("", [ToolCall(id="s1", name="Read", arguments="{}")]),
|
|
145
|
+
"sub done", # sub-agent reaches DONE
|
|
146
|
+
"parent done", # parent reaches DONE
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
s = make_session(client)
|
|
150
|
+
kinds = []
|
|
151
|
+
s.notify_fn = lambda kind, data=None: kinds.append(kind)
|
|
152
|
+
run_agent_loop(s, messages=[Message(role="user", content="delegate")], top_level=True)
|
|
153
|
+
self.assertEqual(kinds.count("run_done"), 1)
|
|
154
|
+
|
|
155
|
+
def test_run_done_not_sent_on_error_or_cancel(self):
|
|
156
|
+
"""ERRS (API error) and ABRT (Ctrl-C) must not notify run_done —
|
|
157
|
+
those states have their own status paths ('error', cancel)."""
|
|
158
|
+
|
|
159
|
+
# ERRS: the client raises mid-request
|
|
160
|
+
class BoomClient(RecClient):
|
|
161
|
+
def chat(self, *a, **k):
|
|
162
|
+
raise RuntimeError("boom")
|
|
163
|
+
|
|
164
|
+
s = make_session(BoomClient([]))
|
|
165
|
+
kinds = []
|
|
166
|
+
s.notify_fn = lambda kind, data=None: kinds.append(kind)
|
|
167
|
+
run_agent_loop(s, messages=[Message(role="user", content="go")], top_level=True)
|
|
168
|
+
self.assertNotIn("run_done", kinds)
|
|
169
|
+
|
|
170
|
+
# ABRT: cancel before the run; the loop routes to ABRT
|
|
171
|
+
s2 = make_session(RecClient(["done"]))
|
|
172
|
+
kinds2 = []
|
|
173
|
+
s2.notify_fn = lambda kind, data=None: kinds2.append(kind)
|
|
174
|
+
s2.cancel()
|
|
175
|
+
run_agent_loop(s2, messages=[Message(role="user", content="go")], top_level=True)
|
|
176
|
+
self.assertNotIn("run_done", kinds2)
|
|
177
|
+
|
|
178
|
+
def test_run_done_without_notify_fn_is_noop(self):
|
|
179
|
+
"""A session without a TUI (notify_fn None) must not crash when
|
|
180
|
+
the run completes (Session.notify no-ops)."""
|
|
181
|
+
client = RecClient(["done"])
|
|
182
|
+
s = make_session(client)
|
|
183
|
+
run_agent_loop(
|
|
184
|
+
s, messages=[Message(role="user", content="go")], top_level=True
|
|
185
|
+
) # must not raise
|
|
186
|
+
|
|
130
187
|
def test_subagent_uses_separate_client_when_configured(self):
|
|
131
188
|
"""A session with a dedicated sub-agent client (subagent_llm in
|
|
132
189
|
the config) routes sub-agent requests through it with its own
|
|
@@ -4,8 +4,14 @@ todo list.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
7
9
|
import unittest
|
|
8
10
|
|
|
11
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
12
|
+
|
|
13
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
14
|
+
|
|
9
15
|
from python_agent_harness.agent import AgentLoop
|
|
10
16
|
from python_agent_harness.models import Message, ToolCall, Usage
|
|
11
17
|
from python_agent_harness.session import Session
|
|
@@ -18,6 +18,10 @@ import time
|
|
|
18
18
|
import unittest
|
|
19
19
|
from unittest import mock
|
|
20
20
|
|
|
21
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
22
|
+
|
|
23
|
+
import session_sandbox # noqa: F401,E402 (side-effect: redirect SESSION_DIR)
|
|
24
|
+
|
|
21
25
|
from python_agent_harness.tools.agent_tool import AgentTool
|
|
22
26
|
from python_agent_harness.tools.base import PendingToolResult, Registry, ToolContext
|
|
23
27
|
from python_agent_harness.tools.planexit import PlanExit
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/diffrender.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/config.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/manager.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/models.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/planmode.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/agent.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/plan.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/review.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/title.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/session.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/subagent.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tool_runner.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/base.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/bash.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/edit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/edit_mac.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/glob.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/glob_mac.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/grep.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/grep_mac.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/insert.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/mcp.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/mkdir.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/planexit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/question.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/read.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/skill.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/todo.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/write.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.1 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/input.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|