python-agent-harness 1.5.5.0__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.0 → python_agent_harness-1.5.5.2}/PKG-INFO +30 -2
- python_agent_harness-1.5.5.0/python_agent_harness.egg-info/PKG-INFO → python_agent_harness-1.5.5.2/README.md +18 -23
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/pyproject.toml +19 -6
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/agent.py +17 -3
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/cli.py +21 -7
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/client.py +15 -2
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/commands.py +0 -5
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/config.py +18 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/persistence.py +1 -7
- python_agent_harness-1.5.5.2/python_agent_harness/prompts/agents/reviewer.md +12 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts.py +57 -2
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/session.py +45 -2
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/commands.py +80 -3
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/core.py +15 -2
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/input.py +1 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/render.py +52 -17
- python_agent_harness-1.5.5.0/README.md → python_agent_harness-1.5.5.2/python_agent_harness.egg-info/PKG-INFO +51 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/SOURCES.txt +2 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_cli.py +1 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_commands.py +0 -1
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_concurrent_subagents.py +2 -0
- python_agent_harness-1.5.5.2/tests/test_custom_agents.py +363 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_invariants.py +1 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_mcp.py +1 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_persistence.py +6 -9
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_planmode.py +1 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_scenarios.py +1 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_session.py +2 -1
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_subagent.py +1 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_subagent_isolation.py +57 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_todos_scope.py +6 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_tools_misc.py +4 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/LICENSE +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/planmode.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/diffapply.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/grep_mac.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/setup.cfg +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_client.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_config.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_filesystem.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/tests/test_token_estimator.py +0 -0
|
@@ -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>
|
|
@@ -99,6 +110,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
|
|
|
99
110
|
- **Focused TUI** — a Rich-based interface with a pinned status bar, Todos panel, inline red/green diff rendering for `Edit` and `Write`, and a `prompt_toolkit` editor with history and completion. `Esc+Enter` submits, `Ctrl-D` quits, and `Ctrl-C` cancels without leaving the application.
|
|
100
111
|
- **MCP support** — optional MCP integration through the `[mcp]` extra. MCP tools become ordinary agent tools such as `mcp__<server>__<tool>`. Supports `stdio`, `streamable-http`, and `sse` transports.
|
|
101
112
|
- **Slash commands** — built-in `/init`, `/review`, `/explain`, and other commands, plus custom commands loaded from `prompts/commands/*.md`.
|
|
113
|
+
- **Custom agents** — switch the main agent's system prompt at runtime with `/agent`. Agent prompt files live in `prompts/agents/*.md`. Use `default_agent` in the config file to start sessions with a specific agent.
|
|
102
114
|
|
|
103
115
|
## Inspired by opencode
|
|
104
116
|
|
|
@@ -160,6 +172,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
160
172
|
"reasoning_effort": null,
|
|
161
173
|
"stream": null
|
|
162
174
|
},
|
|
175
|
+
"default_agent": null,
|
|
163
176
|
"paths": {
|
|
164
177
|
"context_path": null,
|
|
165
178
|
"skill_path": null
|
|
@@ -190,6 +203,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
190
203
|
- **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. `default` restores the main LLM configuration.
|
|
191
204
|
- **`context_windows`** — optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g., `deepseek-v4`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
|
|
192
205
|
- **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
|
|
206
|
+
- **`default_agent`** — name of the agent to use at session start (instead of the built-in `agent.md`). The agent must exist as a `.md` file in the `prompts/agents/` directory. When unset or `null`, the built-in default agent is used. Use `/agent default` in the TUI to switch back to the built-in at any time.
|
|
193
207
|
- **`paths.context_path` / `paths.skill_path`** — locations from which to load context files and skills. When unset, the project-local `<project>/contexts` and `<project>/skills` directories are used.
|
|
194
208
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
195
209
|
- **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
@@ -219,10 +233,24 @@ Launches the interactive TUI agent. If `project-dir` is omitted, the current dir
|
|
|
219
233
|
| `/restore [path\|title\|--latest\|latest]` | Restore a session; title matching uses substring search |
|
|
220
234
|
| `/clear` | Start a fresh conversation |
|
|
221
235
|
| `/model [name]` | Switch LLM profiles; `default` restores the session's original model |
|
|
236
|
+
| `/agent [name]` | Switch agent system prompt; `default` restores the built-in `agent.md` |
|
|
222
237
|
| `/exit` | Quit |
|
|
223
238
|
|
|
224
239
|
Custom commands from `prompts/commands/*.md` are registered as slash commands as well (TUI only).
|
|
225
240
|
|
|
241
|
+
### Custom agents
|
|
242
|
+
|
|
243
|
+
Agent prompt files are markdown files (`.md`) placed in the `prompts/agents/` directory. Each file becomes a switchable agent profile available via the `/agent` TUI command.
|
|
244
|
+
|
|
245
|
+
#### Commands vs Agents
|
|
246
|
+
|
|
247
|
+
| | Commands (`/review`) | Agents (`/agent reviewer`) |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| **Scope** | One-shot (prompt resets after run) | Persistent (stays until next `/agent`) |
|
|
250
|
+
| **Kickoff** | Hardcoded kickoff message | User types their own prompt |
|
|
251
|
+
| **Tools** | Can restrict (`allow_planexit=False`) | All tools |
|
|
252
|
+
| **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
|
|
253
|
+
|
|
226
254
|
## Project layout
|
|
227
255
|
|
|
228
256
|
```text
|
|
@@ -1,26 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.5.0
|
|
4
|
-
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
|
-
Author: huming
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: rich>=13.0
|
|
11
|
-
Requires-Dist: httpx>=0.27
|
|
12
|
-
Requires-Dist: prompt_toolkit>=3.0
|
|
13
|
-
Provides-Extra: mcp
|
|
14
|
-
Requires-Dist: mcp<3,>=2.0; extra == "mcp"
|
|
15
|
-
Provides-Extra: dev
|
|
16
|
-
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
17
|
-
Requires-Dist: pyright>=1.1.380; extra == "dev"
|
|
18
|
-
Requires-Dist: build>=1.2; extra == "dev"
|
|
19
|
-
Requires-Dist: coverage[toml]>=7.0; extra == "dev"
|
|
20
|
-
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
21
|
-
Requires-Dist: mcp<3,>=2.0; extra == "dev"
|
|
22
|
-
Dynamic: license-file
|
|
23
|
-
|
|
24
1
|
<div align="center">
|
|
25
2
|
|
|
26
3
|
# python-agent-harness
|
|
@@ -31,6 +8,7 @@ FSM-driven execution · OpenAI-compatible · built for daily use and easy custom
|
|
|
31
8
|
[](https://github.com/beacoder/python-agent-harness/actions/workflows/ci.yml)
|
|
32
9
|
[](https://pypi.org/project/python-agent-harness/)
|
|
33
10
|
[](https://www.python.org/downloads/)
|
|
11
|
+
[](https://pepy.tech/projects/python-agent-harness)
|
|
34
12
|
[](LICENSE)
|
|
35
13
|
|
|
36
14
|
</div>
|
|
@@ -99,6 +77,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
|
|
|
99
77
|
- **Focused TUI** — a Rich-based interface with a pinned status bar, Todos panel, inline red/green diff rendering for `Edit` and `Write`, and a `prompt_toolkit` editor with history and completion. `Esc+Enter` submits, `Ctrl-D` quits, and `Ctrl-C` cancels without leaving the application.
|
|
100
78
|
- **MCP support** — optional MCP integration through the `[mcp]` extra. MCP tools become ordinary agent tools such as `mcp__<server>__<tool>`. Supports `stdio`, `streamable-http`, and `sse` transports.
|
|
101
79
|
- **Slash commands** — built-in `/init`, `/review`, `/explain`, and other commands, plus custom commands loaded from `prompts/commands/*.md`.
|
|
80
|
+
- **Custom agents** — switch the main agent's system prompt at runtime with `/agent`. Agent prompt files live in `prompts/agents/*.md`. Use `default_agent` in the config file to start sessions with a specific agent.
|
|
102
81
|
|
|
103
82
|
## Inspired by opencode
|
|
104
83
|
|
|
@@ -160,6 +139,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
160
139
|
"reasoning_effort": null,
|
|
161
140
|
"stream": null
|
|
162
141
|
},
|
|
142
|
+
"default_agent": null,
|
|
163
143
|
"paths": {
|
|
164
144
|
"context_path": null,
|
|
165
145
|
"skill_path": null
|
|
@@ -190,6 +170,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
190
170
|
- **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. `default` restores the main LLM configuration.
|
|
191
171
|
- **`context_windows`** — optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g., `deepseek-v4`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
|
|
192
172
|
- **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
|
|
173
|
+
- **`default_agent`** — name of the agent to use at session start (instead of the built-in `agent.md`). The agent must exist as a `.md` file in the `prompts/agents/` directory. When unset or `null`, the built-in default agent is used. Use `/agent default` in the TUI to switch back to the built-in at any time.
|
|
193
174
|
- **`paths.context_path` / `paths.skill_path`** — locations from which to load context files and skills. When unset, the project-local `<project>/contexts` and `<project>/skills` directories are used.
|
|
194
175
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
195
176
|
- **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
@@ -219,10 +200,24 @@ Launches the interactive TUI agent. If `project-dir` is omitted, the current dir
|
|
|
219
200
|
| `/restore [path\|title\|--latest\|latest]` | Restore a session; title matching uses substring search |
|
|
220
201
|
| `/clear` | Start a fresh conversation |
|
|
221
202
|
| `/model [name]` | Switch LLM profiles; `default` restores the session's original model |
|
|
203
|
+
| `/agent [name]` | Switch agent system prompt; `default` restores the built-in `agent.md` |
|
|
222
204
|
| `/exit` | Quit |
|
|
223
205
|
|
|
224
206
|
Custom commands from `prompts/commands/*.md` are registered as slash commands as well (TUI only).
|
|
225
207
|
|
|
208
|
+
### Custom agents
|
|
209
|
+
|
|
210
|
+
Agent prompt files are markdown files (`.md`) placed in the `prompts/agents/` directory. Each file becomes a switchable agent profile available via the `/agent` TUI command.
|
|
211
|
+
|
|
212
|
+
#### Commands vs Agents
|
|
213
|
+
|
|
214
|
+
| | Commands (`/review`) | Agents (`/agent reviewer`) |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| **Scope** | One-shot (prompt resets after run) | Persistent (stays until next `/agent`) |
|
|
217
|
+
| **Kickoff** | Hardcoded kickoff message | User types their own prompt |
|
|
218
|
+
| **Tools** | Can restrict (`allow_planexit=False`) | All tools |
|
|
219
|
+
| **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
|
|
220
|
+
|
|
226
221
|
## Project layout
|
|
227
222
|
|
|
228
223
|
```text
|
|
@@ -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:
|
|
@@ -49,7 +62,7 @@ python-agent-harness = "python_agent_harness.cli:main"
|
|
|
49
62
|
include = ["python_agent_harness*"]
|
|
50
63
|
|
|
51
64
|
[tool.setuptools.package-data]
|
|
52
|
-
python_agent_harness = ["prompts/*.md", "prompts/commands/*.md"]
|
|
65
|
+
python_agent_harness = ["prompts/*.md", "prompts/commands/*.md", "prompts/agents/*.md"]
|
|
53
66
|
|
|
54
67
|
[tool.ruff]
|
|
55
68
|
line-length = 100
|
|
@@ -493,7 +493,10 @@ class AgentLoop:
|
|
|
493
493
|
return # cancelled (Ctrl-C), not an error
|
|
494
494
|
self.error = f"Error: {e}"
|
|
495
495
|
self.info["error"] = self.error
|
|
496
|
-
|
|
496
|
+
# the detail travels with the notification so the TUI can
|
|
497
|
+
# show WHAT failed (e.g. "no quota") in the status bar
|
|
498
|
+
# instead of a bare red "error"
|
|
499
|
+
session.notify("error", self.error)
|
|
497
500
|
return
|
|
498
501
|
|
|
499
502
|
if self._is_cancelled():
|
|
@@ -634,14 +637,25 @@ def run_agent_loop(
|
|
|
634
637
|
|
|
635
638
|
``client`` (when given) overrides the session's client for this
|
|
636
639
|
run — the per-invocation sub-agent clone."""
|
|
637
|
-
|
|
640
|
+
loop = AgentLoop(
|
|
638
641
|
session,
|
|
639
642
|
messages=messages,
|
|
640
643
|
top_level=top_level,
|
|
641
644
|
system=system,
|
|
642
645
|
max_rounds=max_rounds,
|
|
643
646
|
client=client,
|
|
644
|
-
)
|
|
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
|
|
645
659
|
|
|
646
660
|
|
|
647
661
|
class Supervisor:
|
|
@@ -20,6 +20,7 @@ import sys
|
|
|
20
20
|
|
|
21
21
|
from . import config
|
|
22
22
|
from .client import Client
|
|
23
|
+
from .prompts import RESERVED_AGENT_NAME
|
|
23
24
|
from .session import Session
|
|
24
25
|
from .tools import default_registry
|
|
25
26
|
|
|
@@ -91,13 +92,14 @@ def make_session(
|
|
|
91
92
|
# (same precedence as the main agent's stream).
|
|
92
93
|
effective_stream = settings["stream"] if stream is None else stream
|
|
93
94
|
model_profiles = config.load_models_config(config_path)
|
|
95
|
+
default_agent = config.load_default_agent(config_path)
|
|
94
96
|
# Base settings for /model switching: the main llm settings as
|
|
95
97
|
# resolved at session start (incl. the CLI --model/--no-stream
|
|
96
98
|
# overrides above), so a profile's unset keys inherit these
|
|
97
99
|
# instead of values drifted by earlier switches.
|
|
98
100
|
llm_settings = dict(settings)
|
|
99
101
|
llm_settings["stream"] = effective_stream
|
|
100
|
-
|
|
102
|
+
session = Session(
|
|
101
103
|
project_dir=abs_project,
|
|
102
104
|
client=client,
|
|
103
105
|
model=model,
|
|
@@ -117,9 +119,18 @@ def make_session(
|
|
|
117
119
|
skill_path=paths.get("skill_path"),
|
|
118
120
|
mcp=mcp_config,
|
|
119
121
|
model_profiles=model_profiles,
|
|
122
|
+
default_agent=default_agent,
|
|
120
123
|
llm_settings=llm_settings,
|
|
121
124
|
config_path=config_path,
|
|
122
125
|
)
|
|
126
|
+
if default_agent and default_agent != RESERVED_AGENT_NAME:
|
|
127
|
+
ok, msg = session.switch_agent(default_agent)
|
|
128
|
+
if not ok:
|
|
129
|
+
# A typo'd/missing default_agent must not silently start the
|
|
130
|
+
# session with the built-in prompt — record the config error
|
|
131
|
+
# so the TUI can surface it (see Tui.run).
|
|
132
|
+
session.startup_warnings.append(f"default_agent: {msg}")
|
|
133
|
+
return session
|
|
123
134
|
|
|
124
135
|
|
|
125
136
|
def make_session_with_mcp(
|
|
@@ -132,14 +143,14 @@ def make_session_with_mcp(
|
|
|
132
143
|
|
|
133
144
|
Wraps ``make_session``: the session's MCP servers are connected and
|
|
134
145
|
their tools registered before the session is returned (discovery
|
|
135
|
-
happens once, at session start). Per-server failures are
|
|
136
|
-
|
|
137
|
-
keeps working with the
|
|
146
|
+
happens once, at session start). Per-server failures are recorded
|
|
147
|
+
as startup warnings (rendered by the TUI banner) and never prevent
|
|
148
|
+
the session from running — the agent keeps working with the
|
|
149
|
+
built-in tools.
|
|
138
150
|
"""
|
|
139
151
|
session = make_session(project_dir, config_path=config_path, model=model, stream=stream)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
print(f"python-agent-harness: [{server}] {err}", file=sys.stderr)
|
|
152
|
+
for server, err in session.connect_mcp():
|
|
153
|
+
session.startup_warnings.append(f"MCP [{server}]: {err}")
|
|
143
154
|
return session
|
|
144
155
|
|
|
145
156
|
|
|
@@ -228,6 +239,9 @@ def cmd_config(args: argparse.Namespace) -> int:
|
|
|
228
239
|
print(f" {pattern}: {size}")
|
|
229
240
|
else:
|
|
230
241
|
print("context_windows: (none configured — built-in table in config.py applies)")
|
|
242
|
+
# Show default agent
|
|
243
|
+
default_agent = config.load_default_agent(args.path)
|
|
244
|
+
print(f"default_agent: {default_agent or '(default: agent.md)'}")
|
|
231
245
|
return 0
|
|
232
246
|
|
|
233
247
|
|
{python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/client.py
RENAMED
|
@@ -36,6 +36,14 @@ class ApiError(Exception):
|
|
|
36
36
|
"""Raised when the API call itself fails (network/HTTP)."""
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
# Grace between shutdown(SHUT_RDWR) and close() of an in-flight socket
|
|
40
|
+
# on macOS (see _shutdown_and_close): 50ms proved too tight under load
|
|
41
|
+
# (the woken thread can still be parked in select() when the fd is
|
|
42
|
+
# released, losing the wakeup). 200ms keeps Ctrl-C responsive while
|
|
43
|
+
# making the fd-reuse race vanishingly unlikely.
|
|
44
|
+
_ABORT_SHUTDOWN_GRACE_S = 0.2
|
|
45
|
+
|
|
46
|
+
|
|
39
47
|
class RetryableApiError(ApiError):
|
|
40
48
|
"""A transient failure (rate limit, server error) safe to retry.
|
|
41
49
|
|
|
@@ -865,12 +873,17 @@ def _shutdown_and_close(sock: _socket.socket) -> None:
|
|
|
865
873
|
settimeout for the read timeout), so a blocked recv parks in
|
|
866
874
|
select(); closing the fd before that select has processed the
|
|
867
875
|
shutdown wakeup can lose the wakeup and leave the read parked
|
|
868
|
-
forever.
|
|
876
|
+
forever. Worse, ``abort()`` swaps in a fresh httpx client right
|
|
877
|
+
after, so a new socket could reuse the fd number before the
|
|
878
|
+
reader observed the EOF. The grace sleep delays the fd release
|
|
879
|
+
so the woken thread can observe the EOF on the old fd first.
|
|
869
880
|
"""
|
|
870
881
|
with contextlib.suppress(OSError):
|
|
871
882
|
sock.shutdown(_socket.SHUT_RDWR)
|
|
872
883
|
if sys.platform == "darwin":
|
|
873
|
-
|
|
884
|
+
# macOS only; on Linux shutdown() alone reliably wakes the
|
|
885
|
+
# blocked recv (measured: <200ms unblock, no sleeps needed).
|
|
886
|
+
time.sleep(_ABORT_SHUTDOWN_GRACE_S)
|
|
874
887
|
with contextlib.suppress(OSError):
|
|
875
888
|
sock.close()
|
|
876
889
|
|
{python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/commands.py
RENAMED
|
@@ -63,7 +63,6 @@ class SessionCommand:
|
|
|
63
63
|
name: str,
|
|
64
64
|
prompt_file: str,
|
|
65
65
|
kickoff: str,
|
|
66
|
-
buffer_name: str,
|
|
67
66
|
status: str,
|
|
68
67
|
validate_dir: bool = False,
|
|
69
68
|
allow_planexit: bool = True,
|
|
@@ -71,7 +70,6 @@ class SessionCommand:
|
|
|
71
70
|
self.name = name
|
|
72
71
|
self.prompt_file = prompt_file
|
|
73
72
|
self.kickoff = kickoff
|
|
74
|
-
self.buffer_name = buffer_name
|
|
75
73
|
self.status = status
|
|
76
74
|
self.validate_dir = validate_dir
|
|
77
75
|
self.allow_planexit = allow_planexit
|
|
@@ -99,7 +97,6 @@ def initialize_command() -> SessionCommand:
|
|
|
99
97
|
name="initialize",
|
|
100
98
|
prompt_file="initialize.md",
|
|
101
99
|
kickoff="Analyze the repository at ${path} and create/update AGENTS.md.\n",
|
|
102
|
-
buffer_name="*gptel-agent-init:*",
|
|
103
100
|
status=" Initializing...",
|
|
104
101
|
validate_dir=True,
|
|
105
102
|
allow_planexit=False,
|
|
@@ -111,7 +108,6 @@ def review_command() -> SessionCommand:
|
|
|
111
108
|
name="review",
|
|
112
109
|
prompt_file="review.md",
|
|
113
110
|
kickoff="Review the requested code changes.",
|
|
114
|
-
buffer_name="*gptel-agent-review*",
|
|
115
111
|
status=" Reviewing...",
|
|
116
112
|
allow_planexit=False,
|
|
117
113
|
)
|
|
@@ -161,7 +157,6 @@ def load_custom_commands() -> list[SessionCommand]:
|
|
|
161
157
|
name=name,
|
|
162
158
|
prompt_file=str(f.relative_to(PROMPTS_DIR)),
|
|
163
159
|
kickoff="Proceed with the task described in your instructions.\n",
|
|
164
|
-
buffer_name=f"*gptel-agent-{name}*",
|
|
165
160
|
status=f" Running {name}...",
|
|
166
161
|
)
|
|
167
162
|
)
|
{python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/config.py
RENAMED
|
@@ -239,6 +239,7 @@ CONFIG_TEMPLATE = """\
|
|
|
239
239
|
"reasoning_effort": null,
|
|
240
240
|
"stream": null
|
|
241
241
|
}},
|
|
242
|
+
"default_agent": null,
|
|
242
243
|
"paths": {{
|
|
243
244
|
"_comment": "Optional overrides for context and skill directories. Absolute paths or ~ expansion supported.",
|
|
244
245
|
"context_path": null,
|
|
@@ -458,6 +459,23 @@ def load_models_config(path: str | os.PathLike | None = None) -> dict[str, dict]
|
|
|
458
459
|
return profiles
|
|
459
460
|
|
|
460
461
|
|
|
462
|
+
def load_default_agent(path: str | os.PathLike | None = None) -> str | None:
|
|
463
|
+
"""Load the default agent name from the config file's ``default_agent`` key.
|
|
464
|
+
|
|
465
|
+
Returns the agent name (str) when set, or None when unset (meaning
|
|
466
|
+
use the built-in ``default`` agent, i.e. ``prompts/agent.md``).
|
|
467
|
+
"""
|
|
468
|
+
data = _read_config(path)
|
|
469
|
+
val = data.get("default_agent")
|
|
470
|
+
if val is None:
|
|
471
|
+
return None
|
|
472
|
+
if not isinstance(val, str) or not val.strip():
|
|
473
|
+
raise ValueError(
|
|
474
|
+
f"config file {_config_path(path)}: default_agent must be a non-empty string"
|
|
475
|
+
)
|
|
476
|
+
return val.strip()
|
|
477
|
+
|
|
478
|
+
|
|
461
479
|
def mask_secret(value: str | None) -> str:
|
|
462
480
|
return "****" if value else "(unset)"
|
|
463
481
|
|
{python_agent_harness-1.5.5.0 → 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}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Role and Behavior
|
|
2
|
+
You are a code reviewer agent. Your job is to review code changes
|
|
3
|
+
thoroughly and provide constructive feedback.
|
|
4
|
+
|
|
5
|
+
Focus on:
|
|
6
|
+
- Correctness and potential bugs
|
|
7
|
+
- Code style and readability
|
|
8
|
+
- Performance implications
|
|
9
|
+
- Security concerns
|
|
10
|
+
- Test coverage
|
|
11
|
+
|
|
12
|
+
Always provide actionable suggestions with specific file and line references.
|
{python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/prompts.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Prompt loading and assembly.
|
|
2
2
|
|
|
3
3
|
Ported from gptel-agent-harness.el: loads bundled prompt files
|
|
4
|
-
(agent/subagent/commands), strips YAML frontmatter, discovers skills
|
|
4
|
+
(agent/subagent/commands/custom agents), strips YAML frontmatter, discovers skills
|
|
5
5
|
for the {{SKILLS}} placeholder, assembles the effective system prompt
|
|
6
6
|
from project context files + task-completion rules + agent prompt, and
|
|
7
7
|
provides the compaction flow helpers (summarize the conversation with
|
|
@@ -22,10 +22,16 @@ from typing import Any
|
|
|
22
22
|
from . import config
|
|
23
23
|
from .models import Message
|
|
24
24
|
|
|
25
|
+
PROMPTS_DIR = Path(__file__).parent / "prompts"
|
|
26
|
+
AGENTS_DIR = PROMPTS_DIR / "agents"
|
|
27
|
+
# Reserved pseudo-name that restores the built-in agent (prompts/agent.md);
|
|
28
|
+
# custom agent files cannot claim it (see discover_agents).
|
|
29
|
+
RESERVED_AGENT_NAME = "default"
|
|
30
|
+
|
|
25
31
|
|
|
26
32
|
def read_prompt_file(name: str) -> str:
|
|
27
33
|
"""Read a prompt file from the package prompts dir."""
|
|
28
|
-
path =
|
|
34
|
+
path = PROMPTS_DIR / name
|
|
29
35
|
return path.read_text(encoding="utf-8")
|
|
30
36
|
|
|
31
37
|
|
|
@@ -496,3 +502,52 @@ def compacted_messages(summary: str, prompts: list[str]) -> list[Message]:
|
|
|
496
502
|
Message(role="user", content=frame),
|
|
497
503
|
*[Message(role="user", content=p) for p in prompts],
|
|
498
504
|
]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ---- custom agent discovery -----------------------------------------------
|
|
508
|
+
# Agent prompt files are markdown files (``*.md``) in
|
|
509
|
+
# ``prompts/agents/``. Each file becomes a switchable agent profile
|
|
510
|
+
# available via the ``/agent`` TUI command. The file's stem (e.g.
|
|
511
|
+
# ``reviewer.md`` → ``reviewer``) is the agent name; an optional YAML
|
|
512
|
+
# frontmatter ``name:`` can override it. The built-in ``default``
|
|
513
|
+
# agent (``prompts/agent.md``) is always available and cannot be
|
|
514
|
+
# overridden by a file in this directory.
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _agent_name_from_file(path: Path) -> str:
|
|
518
|
+
"""Derive agent name from a file: frontmatter ``name:`` if present,
|
|
519
|
+
otherwise the file stem (lowercased, non-alphanumerics → ``-``)."""
|
|
520
|
+
try:
|
|
521
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
522
|
+
except OSError:
|
|
523
|
+
return ""
|
|
524
|
+
m = _FRONTMATTER_RE.match(text)
|
|
525
|
+
if m:
|
|
526
|
+
for line in m.group(0).splitlines():
|
|
527
|
+
if line.startswith("name:"):
|
|
528
|
+
name = line[len("name:") :].strip()
|
|
529
|
+
if name:
|
|
530
|
+
return name
|
|
531
|
+
base = path.stem.lower()
|
|
532
|
+
base = re.sub(r"[^a-z0-9]+", "-", base)
|
|
533
|
+
return base.strip("-")
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def discover_agents() -> dict[str, str]:
|
|
537
|
+
"""Discover all custom agents in the ``prompts/agents/`` directory.
|
|
538
|
+
|
|
539
|
+
Returns a dict mapping agent name → absolute path to the prompt
|
|
540
|
+
file. Files without a valid name are skipped. The built-in
|
|
541
|
+
``default`` agent is NOT included here — callers add it separately.
|
|
542
|
+
A file claiming the reserved name ``default`` (via stem or
|
|
543
|
+
frontmatter) is skipped: the built-in agent always wins.
|
|
544
|
+
"""
|
|
545
|
+
if not AGENTS_DIR.is_dir():
|
|
546
|
+
return {}
|
|
547
|
+
agents: dict[str, str] = {}
|
|
548
|
+
for f in sorted(AGENTS_DIR.glob("*.md")):
|
|
549
|
+
name = _agent_name_from_file(f)
|
|
550
|
+
if not name or name == RESERVED_AGENT_NAME:
|
|
551
|
+
continue
|
|
552
|
+
agents[name] = str(f.resolve())
|
|
553
|
+
return agents
|
{python_agent_harness-1.5.5.0 → python_agent_harness-1.5.5.2}/python_agent_harness/session.py
RENAMED
|
@@ -22,7 +22,7 @@ from .mcp.manager import MCPManager
|
|
|
22
22
|
from .models import AgentMode
|
|
23
23
|
from .persistence import SessionPersistence, escape_role_headers
|
|
24
24
|
from .planmode import PlanMode
|
|
25
|
-
from .prompts import index_skills
|
|
25
|
+
from .prompts import RESERVED_AGENT_NAME, discover_agents, index_skills
|
|
26
26
|
from .subagent import run_subagent
|
|
27
27
|
from .token_estimator import TokenCalibrator
|
|
28
28
|
from .tools import Registry, ToolContext
|
|
@@ -89,6 +89,7 @@ class Session:
|
|
|
89
89
|
mcp: MCPConfig | None = None,
|
|
90
90
|
mcp_manager: MCPManager | None = None,
|
|
91
91
|
model_profiles: dict[str, dict] | None = None,
|
|
92
|
+
default_agent: str | None = None,
|
|
92
93
|
llm_settings: dict | None = None,
|
|
93
94
|
config_path: str | None = None,
|
|
94
95
|
) -> None:
|
|
@@ -183,6 +184,17 @@ class Session:
|
|
|
183
184
|
# model profile's unset keys inherit these values, so switching
|
|
184
185
|
# between profiles never drifts settings from earlier switches.
|
|
185
186
|
self.llm_settings: dict = dict(llm_settings) if llm_settings else {}
|
|
187
|
+
# Default agent name: when set, the session starts with this
|
|
188
|
+
# agent's prompt instead of the built-in default (agent.md).
|
|
189
|
+
# ``/agent default`` restores the built-in, not this setting.
|
|
190
|
+
self.default_agent = default_agent
|
|
191
|
+
# Non-fatal startup warnings (e.g. a default_agent that could not
|
|
192
|
+
# be applied); the TUI renders these in its banner so they are
|
|
193
|
+
# visible inside the interface rather than on stderr.
|
|
194
|
+
self.startup_warnings: list[str] = []
|
|
195
|
+
# Original system prompt assembled at session start — used by
|
|
196
|
+
# /agent default to restore the original agent.md prompt.
|
|
197
|
+
self._default_system_prompt = system_prompt
|
|
186
198
|
# Monotonic cancel identity: cancel() bumps this counter, so a
|
|
187
199
|
# worker from a cancelled run can tell it was cancelled even
|
|
188
200
|
# after the next run clears the shared event.
|
|
@@ -708,6 +720,37 @@ class Session:
|
|
|
708
720
|
self.stream = merged["stream"]
|
|
709
721
|
return True, f"switched to {name} ({self.model})"
|
|
710
722
|
|
|
723
|
+
def switch_agent(self, name: str) -> tuple[bool, str]:
|
|
724
|
+
"""Switch the main agent's system prompt to a named agent.
|
|
725
|
+
|
|
726
|
+
Agent prompt files are discovered from the ``prompts/agents/`` package
|
|
727
|
+
directory (see ``python_agent_harness.prompts.discover_agents``).
|
|
728
|
+
The pseudo-name ``default`` restores the original system prompt
|
|
729
|
+
saved at session construction. Returns (success, message).
|
|
730
|
+
"""
|
|
731
|
+
if name == RESERVED_AGENT_NAME:
|
|
732
|
+
self.system_prompt = self._default_system_prompt
|
|
733
|
+
self.store.system_prompt = self._default_system_prompt
|
|
734
|
+
return True, "switched to default agent"
|
|
735
|
+
|
|
736
|
+
agents = discover_agents()
|
|
737
|
+
prompt_file = agents.get(name)
|
|
738
|
+
if prompt_file is None:
|
|
739
|
+
available = ", ".join(sorted([RESERVED_AGENT_NAME, *agents.keys()]))
|
|
740
|
+
return False, f"unknown agent: {name} (available: {available})"
|
|
741
|
+
from .prompts import assemble_agent_prompt, load_agent_prompt
|
|
742
|
+
|
|
743
|
+
agent_prompt = load_agent_prompt(prompt_file, skill_dir=self._skill_dir)
|
|
744
|
+
if agent_prompt is None:
|
|
745
|
+
return False, f"agent {name}: prompt file not found or empty: {prompt_file}"
|
|
746
|
+
self.system_prompt = assemble_agent_prompt(
|
|
747
|
+
self.project_dir,
|
|
748
|
+
agent_prompt,
|
|
749
|
+
context_path=self._configured_context_path,
|
|
750
|
+
)
|
|
751
|
+
self.store.system_prompt = self.system_prompt
|
|
752
|
+
return True, f"switched to {name}"
|
|
753
|
+
|
|
711
754
|
# ------------------------------------------------------------------
|
|
712
755
|
# direct commands: compact / summary (no agent loop)
|
|
713
756
|
# ------------------------------------------------------------------
|
|
@@ -752,7 +795,7 @@ class Session:
|
|
|
752
795
|
self.last_messages = compacted_messages(summary, user_prompt_texts(messages))
|
|
753
796
|
self.auto_save(self.last_messages, self.system_prompt)
|
|
754
797
|
self.notify("compact")
|
|
755
|
-
return True, "
|
|
798
|
+
return True, "Session compacted successfully."
|
|
756
799
|
except Exception as e: # noqa: BLE001 - compaction failure is non-fatal
|
|
757
800
|
self.log(f"compaction failed: {e}")
|
|
758
801
|
return False, f"Compaction failed: {e}"
|