python-agent-harness 1.5.5.3__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.3/python_agent_harness.egg-info → python_agent_harness-1.5.5.4}/PKG-INFO +1 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/pyproject.toml +1 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/cli.py +5 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/persistence.py +3 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/session.py +8 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/diffapply.py +2 -2
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/grep_win.py +12 -7
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/commands.py +71 -15
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4/python_agent_harness.egg-info}/PKG-INFO +1 -1
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_custom_agents.py +43 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_persistence.py +16 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/LICENSE +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/README.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/client.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/config.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/planmode.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/agents/reviewer.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/bash_win.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/edit_win.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob_win.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/grep_mac.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/setup.cfg +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_client.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_config.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_filesystem.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_scenarios.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_session.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_token_estimator.py +0 -0
- {python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_tools_misc.py +0 -0
|
@@ -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"}
|
|
@@ -125,7 +125,11 @@ def make_session(
|
|
|
125
125
|
)
|
|
126
126
|
if default_agent and default_agent != RESERVED_AGENT_NAME:
|
|
127
127
|
ok, msg = session.switch_agent(default_agent)
|
|
128
|
-
if
|
|
128
|
+
if ok:
|
|
129
|
+
# record the applied agent so saved sessions restore it;
|
|
130
|
+
# switch_agent("default") clears it again
|
|
131
|
+
session.store.agent = default_agent
|
|
132
|
+
else:
|
|
129
133
|
# A typo'd/missing default_agent must not silently start the
|
|
130
134
|
# session with the built-in prompt — record the config error
|
|
131
135
|
# so the TUI can surface it (see Tui.run).
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.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.3 → 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
|
# ------------------------------------------------------------------
|
|
@@ -48,7 +48,7 @@ class _Hunk:
|
|
|
48
48
|
"""(kind, content-without-newline) for the lines that must exist
|
|
49
49
|
in the old file (' ' context and '-' removed lines)."""
|
|
50
50
|
return [
|
|
51
|
-
(kind, content.rstrip("\n")) for kind, content, _ in self.body if kind in (" ", "-")
|
|
51
|
+
(kind, content.rstrip("\r\n")) for kind, content, _ in self.body if kind in (" ", "-")
|
|
52
52
|
]
|
|
53
53
|
|
|
54
54
|
|
|
@@ -142,7 +142,7 @@ def _match_hunk(hunk: _Hunk, file_lines: list[str]) -> int | None:
|
|
|
142
142
|
mismatches = 0
|
|
143
143
|
ok = True
|
|
144
144
|
for i, (kind, content) in enumerate(old_seq):
|
|
145
|
-
file_content = file_lines[p + i].rstrip("\n")
|
|
145
|
+
file_content = file_lines[p + i].rstrip("\r\n")
|
|
146
146
|
if kind == "-":
|
|
147
147
|
if file_content != content:
|
|
148
148
|
ok = False
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/grep_win.py
RENAMED
|
@@ -98,12 +98,17 @@ class GrepWindows(Grep):
|
|
|
98
98
|
files = [Path(path)]
|
|
99
99
|
else:
|
|
100
100
|
root = Path(path)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for p in root.rglob("*")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
collected: list[Path] = []
|
|
102
|
+
try:
|
|
103
|
+
for p in root.rglob("*"):
|
|
104
|
+
if not p.is_file():
|
|
105
|
+
continue
|
|
106
|
+
if any(part.startswith(".") for part in p.relative_to(root).parts[:-1]):
|
|
107
|
+
continue
|
|
108
|
+
collected.append(p)
|
|
109
|
+
except OSError:
|
|
110
|
+
pass
|
|
111
|
+
files = collected
|
|
107
112
|
|
|
108
113
|
for file_path in files:
|
|
109
114
|
if match_count >= max_matches:
|
|
@@ -124,7 +129,7 @@ class GrepWindows(Grep):
|
|
|
124
129
|
start = max(0, i - 1 - context)
|
|
125
130
|
end = min(len(lines), i + context)
|
|
126
131
|
for j in range(start, end):
|
|
127
|
-
marker = "
|
|
132
|
+
marker = "-" if j + 1 != i else ":"
|
|
128
133
|
results.append(f"{file_path}:{j + 1}{marker}{lines[j].rstrip()}")
|
|
129
134
|
results.append("") # blank line between matches with context
|
|
130
135
|
else:
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/commands.py
RENAMED
|
@@ -119,7 +119,7 @@ class CommandMixin:
|
|
|
119
119
|
"/review [project] [commit|branch|PR] review code changes\n"
|
|
120
120
|
"/explain [project] [target] explain code\n"
|
|
121
121
|
"/sessions list saved sessions\n"
|
|
122
|
-
"/restore [path | title | --latest | latest] restore a saved session\n"
|
|
122
|
+
"/restore [path | title | --latest | latest] restore a saved session (conversation + agent + model)\n"
|
|
123
123
|
"/model [name] switch LLM model profile\n"
|
|
124
124
|
"/agent [name] switch agent system prompt\n"
|
|
125
125
|
"Ctrl-C cancels the current execution (app stays open); "
|
|
@@ -642,6 +642,59 @@ class CommandMixin:
|
|
|
642
642
|
title = title_from_filename(path)
|
|
643
643
|
if title:
|
|
644
644
|
self.session.store.title = title
|
|
645
|
+
# Restore agent if saved
|
|
646
|
+
agent = meta.get("python-agent-harness--agent")
|
|
647
|
+
if agent:
|
|
648
|
+
try:
|
|
649
|
+
success, msg = self.session.switch_agent(agent)
|
|
650
|
+
if success:
|
|
651
|
+
self.console.print(f"[green]{msg}[/green]")
|
|
652
|
+
else:
|
|
653
|
+
self.console.print(f"[yellow]warning: {msg}[/yellow]")
|
|
654
|
+
except Exception as e:
|
|
655
|
+
self.console.print(f"[yellow]warning: could not restore agent: {e}[/yellow]")
|
|
656
|
+
# Restore model if saved: /model's profile switching is the
|
|
657
|
+
# canonical path — it also fixes base_url/api_key/temperature
|
|
658
|
+
# and other settings that a bare model-name change would leave
|
|
659
|
+
# pointing at the wrong endpoint. The metadata stores the raw
|
|
660
|
+
# model name, so first try it as a profile name, then fall back
|
|
661
|
+
# to a profile whose model matches; without a matching profile
|
|
662
|
+
# the current model stays active (profile names are not saved).
|
|
663
|
+
model = meta.get("python-agent-harness--model")
|
|
664
|
+
if model:
|
|
665
|
+
try:
|
|
666
|
+
if model == self.session.model:
|
|
667
|
+
# Already the active model: nothing to do, no noise.
|
|
668
|
+
pass
|
|
669
|
+
elif model == self.session.llm_settings.get("model"):
|
|
670
|
+
# The saved model is the session-start default: the
|
|
671
|
+
# "default" pseudo-profile restores exactly those
|
|
672
|
+
# original llm settings (profile names were not saved,
|
|
673
|
+
# so there is no better match).
|
|
674
|
+
success, msg = self.session.switch_model("default")
|
|
675
|
+
self.console.print(f"[green]{msg}[/green]")
|
|
676
|
+
else:
|
|
677
|
+
success, msg = self.session.switch_model(model)
|
|
678
|
+
if not success:
|
|
679
|
+
profile = next(
|
|
680
|
+
(
|
|
681
|
+
name
|
|
682
|
+
for name, p in self.session.model_profiles.items()
|
|
683
|
+
if p.get("model") == model
|
|
684
|
+
),
|
|
685
|
+
None,
|
|
686
|
+
)
|
|
687
|
+
if profile:
|
|
688
|
+
success, msg = self.session.switch_model(profile)
|
|
689
|
+
if success:
|
|
690
|
+
self.console.print(f"[green]{msg}[/green]")
|
|
691
|
+
else:
|
|
692
|
+
self.console.print(
|
|
693
|
+
f"[yellow]warning: model {model!r} has no matching profile, "
|
|
694
|
+
f"keeping current model[/yellow]"
|
|
695
|
+
)
|
|
696
|
+
except Exception as e:
|
|
697
|
+
self.console.print(f"[yellow]warning: could not restore model: {e}[/yellow]")
|
|
645
698
|
# Replace conversation history: a new generation. Invalidate any
|
|
646
699
|
# worker still winding down from a cancelled run so its salvaged
|
|
647
700
|
# history can't clobber the restored session.
|
|
@@ -650,11 +703,11 @@ class CommandMixin:
|
|
|
650
703
|
self.session.last_messages = list(messages)
|
|
651
704
|
self.session.clear_todos()
|
|
652
705
|
self._history_dirty = True
|
|
653
|
-
model = meta.get("python-agent-harness--model", "?")
|
|
654
706
|
project = meta.get("python-agent-harness--project-dir", "?")
|
|
707
|
+
agent_display = meta.get("python-agent-harness--agent", "default")
|
|
655
708
|
self.console.print(
|
|
656
709
|
f"[green]restored:[/green] {os.path.basename(path)} "
|
|
657
|
-
f"(model={model}, project={project}, {len(messages)} messages)"
|
|
710
|
+
f"(model={model}, agent={agent_display}, project={project}, {len(messages)} messages)"
|
|
658
711
|
)
|
|
659
712
|
|
|
660
713
|
@staticmethod
|
|
@@ -664,13 +717,15 @@ class CommandMixin:
|
|
|
664
717
|
The save format is markdown with **role**: content blocks
|
|
665
718
|
separated by blank lines.
|
|
666
719
|
|
|
667
|
-
``tool`` blocks are dropped: the saved markdown
|
|
668
|
-
``tool_call_id``/``name`` (assistant tool calls are
|
|
669
|
-
plain text), so a restored ``role="tool"`` message
|
|
670
|
-
API-invalid payload (a tool message with no
|
|
671
|
-
``tool_calls``).
|
|
672
|
-
|
|
673
|
-
|
|
720
|
+
``tool`` and ``system`` blocks are dropped: the saved markdown
|
|
721
|
+
does not keep ``tool_call_id``/``name`` (assistant tool calls are
|
|
722
|
+
flattened to plain text), so a restored ``role="tool"`` message
|
|
723
|
+
would form an API-invalid payload (a tool message with no
|
|
724
|
+
preceding assistant ``tool_calls``). A restored ``system``
|
|
725
|
+
message would duplicate the live system prompt the client
|
|
726
|
+
prepends on every request. The following assistant reply
|
|
727
|
+
already summarizes the results, so dropping them loses no
|
|
728
|
+
essential context.
|
|
674
729
|
|
|
675
730
|
Body lines that merely look like a block header are escaped by
|
|
676
731
|
the renderer (see `escape_role_headers`) and unescaped here, so
|
|
@@ -687,9 +742,10 @@ class CommandMixin:
|
|
|
687
742
|
header = split_role_header(line)
|
|
688
743
|
if header is not None:
|
|
689
744
|
role, rest = header
|
|
690
|
-
# Save the previous block (tool blocks
|
|
691
|
-
#
|
|
692
|
-
|
|
745
|
+
# Save the previous block (tool blocks lose their
|
|
746
|
+
# tool_call_id/name; system blocks would duplicate the
|
|
747
|
+
# live system prompt the client prepends per request)
|
|
748
|
+
if current_role is not None and current_role not in ("tool", "system"):
|
|
693
749
|
content = "\n".join(current_lines).strip()
|
|
694
750
|
if content:
|
|
695
751
|
messages.append(Message(role=current_role, content=content))
|
|
@@ -698,8 +754,8 @@ class CommandMixin:
|
|
|
698
754
|
continue
|
|
699
755
|
current_lines.append(unescape_role_header(line))
|
|
700
756
|
|
|
701
|
-
# Don't forget the last block (tool blocks
|
|
702
|
-
if current_role is not None and current_role
|
|
757
|
+
# Don't forget the last block (tool/system blocks dropped, see above)
|
|
758
|
+
if current_role is not None and current_role not in ("tool", "system"):
|
|
703
759
|
content = "\n".join(current_lines).strip()
|
|
704
760
|
if content:
|
|
705
761
|
messages.append(Message(role=current_role, content=content))
|
|
@@ -163,6 +163,25 @@ class TestSwitchAgent(unittest.TestCase):
|
|
|
163
163
|
session.switch_agent("default")
|
|
164
164
|
self.assertEqual(session.store.system_prompt, "original prompt")
|
|
165
165
|
|
|
166
|
+
def test_switch_updates_store_agent(self):
|
|
167
|
+
"""switch_agent must keep store.agent in sync so every auto-save
|
|
168
|
+
records the currently active agent in the metadata block."""
|
|
169
|
+
session = _make_session(system_prompt="original prompt")
|
|
170
|
+
self.assertIsNone(session.store.agent) # no custom agent yet
|
|
171
|
+
session.switch_agent("reviewer")
|
|
172
|
+
self.assertEqual(session.store.agent, "reviewer")
|
|
173
|
+
session.switch_agent("default")
|
|
174
|
+
self.assertIsNone(session.store.agent) # default = no custom agent
|
|
175
|
+
|
|
176
|
+
def test_failed_switch_leaves_store_agent_untouched(self):
|
|
177
|
+
"""A failed switch (unknown name) must not change store.agent:
|
|
178
|
+
the metadata must never record an agent that is not active."""
|
|
179
|
+
session = _make_session(system_prompt="original prompt")
|
|
180
|
+
session.switch_agent("reviewer")
|
|
181
|
+
ok, _ = session.switch_agent("nonexistent")
|
|
182
|
+
self.assertFalse(ok)
|
|
183
|
+
self.assertEqual(session.store.agent, "reviewer")
|
|
184
|
+
|
|
166
185
|
|
|
167
186
|
class TestDefaultAgentConfig(unittest.TestCase):
|
|
168
187
|
"""config.load_default_agent: reading the default_agent setting."""
|
|
@@ -286,6 +305,30 @@ class TestMakeSessionWithDefaultAgent(unittest.TestCase):
|
|
|
286
305
|
p.write_text(json.dumps({"default_agent": "reviewer"}), encoding="utf-8")
|
|
287
306
|
session = make_session("/tmp", config_path=str(p))
|
|
288
307
|
self.assertEqual(session.startup_warnings, [])
|
|
308
|
+
session.close()
|
|
309
|
+
|
|
310
|
+
def test_valid_default_agent_recorded_in_store(self):
|
|
311
|
+
"""A successfully applied default_agent is recorded in store.agent,
|
|
312
|
+
so saved sessions restore it. A typo'd one must NOT be recorded:
|
|
313
|
+
the metadata must never name an agent that is not active."""
|
|
314
|
+
from python_agent_harness.cli import make_session
|
|
315
|
+
|
|
316
|
+
with tempfile.TemporaryDirectory() as d:
|
|
317
|
+
p = Path(d) / "config.json"
|
|
318
|
+
p.write_text(json.dumps({"default_agent": "reviewer"}), encoding="utf-8")
|
|
319
|
+
session = make_session("/tmp", config_path=str(p))
|
|
320
|
+
try:
|
|
321
|
+
self.assertEqual(session.store.agent, "reviewer")
|
|
322
|
+
finally:
|
|
323
|
+
session.close()
|
|
324
|
+
with tempfile.TemporaryDirectory() as d:
|
|
325
|
+
p = Path(d) / "config.json"
|
|
326
|
+
p.write_text(json.dumps({"default_agent": "no-such-agent"}), encoding="utf-8")
|
|
327
|
+
session = make_session("/tmp", config_path=str(p))
|
|
328
|
+
try:
|
|
329
|
+
self.assertIsNone(session.store.agent)
|
|
330
|
+
finally:
|
|
331
|
+
session.close()
|
|
289
332
|
|
|
290
333
|
|
|
291
334
|
class TestMcpFailuresAsStartupWarnings(unittest.TestCase):
|
|
@@ -59,6 +59,22 @@ class TestSession(unittest.TestCase):
|
|
|
59
59
|
store = SessionPersistence(project_dir="/tmp/proj", model="m")
|
|
60
60
|
self.assertNotIn("round-times", store.metadata_block())
|
|
61
61
|
|
|
62
|
+
def test_agent_persisted_in_metadata(self):
|
|
63
|
+
"""A session with an active custom agent records it in the
|
|
64
|
+
metadata block so /restore can reapply it."""
|
|
65
|
+
store = SessionPersistence(project_dir="/tmp/proj", model="m", agent="reviewer")
|
|
66
|
+
meta = store.metadata_block()
|
|
67
|
+
self.assertIn("python-agent-harness--agent", meta)
|
|
68
|
+
text = "conversation...\n\n" + meta + "\n"
|
|
69
|
+
parsed = SessionPersistence.parse_metadata(text)
|
|
70
|
+
self.assertEqual(parsed["python-agent-harness--agent"], "reviewer")
|
|
71
|
+
|
|
72
|
+
def test_agent_absent_when_none(self):
|
|
73
|
+
"""No custom agent -> no agent line in the metadata block, so
|
|
74
|
+
old sessions and default-agent sessions parse identically."""
|
|
75
|
+
store = SessionPersistence(project_dir="/tmp/proj", model="m", agent=None)
|
|
76
|
+
self.assertNotIn("python-agent-harness--agent", store.metadata_block())
|
|
77
|
+
|
|
62
78
|
def test_save_and_restore_flow(self):
|
|
63
79
|
import os
|
|
64
80
|
import tempfile
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/diffrender.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/config.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/mcp/manager.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/models.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/planmode.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/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.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/plan.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/review.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts/title.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/prompts.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/subagent.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tool_runner.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/base.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/bash.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/bash_win.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/edit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/edit_mac.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/edit_win.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob_mac.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/glob_win.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/grep.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/grep_mac.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/insert.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/mcp.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/mkdir.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/planexit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/question.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/read.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/skill.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/todo.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tools/write.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/core.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/input.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/python_agent_harness/tui/render.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_concurrent_subagents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.5.3 → python_agent_harness-1.5.5.4}/tests/test_subagent_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|